1/* See LICENSE file for copyright and license details. */
2
3#include <sys/types.h>
4#include <sys/wait.h>
5
6#include <ctype.h>
7#include <errno.h>
8#include <limits.h>
9#include <signal.h>
10#include <stdarg.h>
11#include <stdio.h>
12#include <stdlib.h>
13#include <string.h>
14#include <syslog.h>
15#include <time.h>
16#include <unistd.h>
17
18#include "paths.h"
19#include "queue.h"
20#include "util.h"
21#include "wexec.h"
22
23struct field {
24 enum { ERROR, WILDCARD, NUMBER, RANGE, REPEAT, LIST } type;
25 long *val;
26 int len;
27};
28
29struct ctabentry {
30 struct field min;
31 struct field hour;
32 struct field mday;
33 struct field mon;
34 struct field wday;
35 char *cmd;
36 TAILQ_ENTRY(ctabentry) entry;
37};
38
39struct jobentry {
40 char *cmd;
41 pid_t pid;
42 TAILQ_ENTRY(jobentry) entry;
43};
44
45static sig_atomic_t chldreap;
46static sig_atomic_t reload;
47static sig_atomic_t quit;
48static TAILQ_HEAD(, ctabentry) ctabhead = TAILQ_HEAD_INITIALIZER(ctabhead);
49static TAILQ_HEAD(, jobentry) jobhead = TAILQ_HEAD_INITIALIZER(jobhead);
50static char *config = "/etc/crontab";
51static char *pidfile = "/var/run/crond.pid";
52static int nflag;
53
54static void
55loginfo(const char *fmt, ...)
56{
57 va_list ap;
58 va_start(ap, fmt);
59 if (nflag == 0)
60 vsyslog(LOG_INFO, fmt, ap);
61 else
62 vfprintf(stdout, fmt, ap);
63 fflush(stdout);
64 va_end(ap);
65}
66
67static void
68logwarn(const char *fmt, ...)
69{
70 va_list ap;
71 va_start(ap, fmt);
72 if (nflag == 0)
73 vsyslog(LOG_WARNING, fmt, ap);
74 else
75 vfprintf(stderr, fmt, ap);
76 va_end(ap);
77}
78
79static void
80logerr(const char *fmt, ...)
81{
82 va_list ap;
83 va_start(ap, fmt);
84 if (nflag == 0)
85 vsyslog(LOG_ERR, fmt, ap);
86 else
87 vfprintf(stderr, fmt, ap);
88 va_end(ap);
89}
90
91static void
92runjob(char *cmd)
93{
94 struct jobentry *je;
95 time_t t;
96 pid_t pid;
97
98 t = time(NULL);
99
100 /* If command is already running, skip it */
101 TAILQ_FOREACH(je, &jobhead, entry)
102 {
103 if (strcmp(je->cmd, cmd) == 0) {
104 loginfo("already running %s pid: %d at %s", je->cmd, je->pid, ctime(&t));
105 return;
106 }
107 }
108
109 switch ((pid = fork())) {
110 case -1:
111 logerr("error: failed to fork job: %s time: %s", cmd, ctime(&t));
112 return;
113 case 0:
114 setsid();
115 loginfo("run: %s pid: %d at %s", cmd, getpid(), ctime(&t));
116 wexecv_self(ARUU_PATH_BIN "/sh", (char *const[]){"sh", "-c", cmd, NULL});
117 logerr("error: failed to execute job: %s time: %s", cmd, ctime(&t));
118 _exit(1);
119 default:
120 je = emalloc(sizeof(*je));
121 je->cmd = estrdup(cmd);
122 je->pid = pid;
123 TAILQ_INSERT_TAIL(&jobhead, je, entry);
124 }
125}
126
127static void
128waitjob(void)
129{
130 struct jobentry *je, *tmp;
131 int status;
132 time_t t;
133 pid_t pid;
134
135 t = time(NULL);
136
137 while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
138 je = NULL;
139 TAILQ_FOREACH(tmp, &jobhead, entry)
140 {
141 if (tmp->pid == pid) {
142 je = tmp;
143 break;
144 }
145 }
146 if (je) {
147 TAILQ_REMOVE(&jobhead, je, entry);
148 free(je->cmd);
149 free(je);
150 }
151 if (WIFEXITED(status) == 1)
152 loginfo("complete: pid: %d returned: %d time: %s", pid, WEXITSTATUS(status), ctime(&t));
153 else if (WIFSIGNALED(status) == 1)
154 loginfo(
155 "complete: pid: %d terminated by signal: %s "
156 "time: %s",
157 pid,
158 strsignal(WTERMSIG(status)),
159 ctime(&t)
160 );
161 else if (WIFSTOPPED(status) == 1)
162 loginfo(
163 "complete: pid: %d stopped by signal: %s time: "
164 "%s",
165 pid,
166 strsignal(WSTOPSIG(status)),
167 ctime(&t)
168 );
169 }
170}
171
172static int
173isleap(int year)
174{
175 if (year % 400 == 0)
176 return 1;
177 if (year % 100 == 0)
178 return 0;
179 return (year % 4 == 0);
180}
181
182static int
183daysinmon(int mon, int year)
184{
185 int days[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
186 if (year < 1900)
187 year += 1900;
188 if (isleap(year))
189 days[1] = 29;
190 return days[mon];
191}
192
193static int
194matchentry(struct ctabentry *cte, struct tm *tm)
195{
196 struct {
197 struct field *f;
198 int tm;
199 int len;
200 } matchtbl[] = {
201 {.f = &cte->min, .tm = tm->tm_min, .len = 60},
202 {.f = &cte->hour, .tm = tm->tm_hour, .len = 24},
203 {.f = &cte->mday, .tm = tm->tm_mday, .len = daysinmon(tm->tm_mon, tm->tm_year)},
204 {.f = &cte->mon, .tm = tm->tm_mon, .len = 12},
205 {.f = &cte->wday, .tm = tm->tm_wday, .len = 7},
206 };
207 size_t i;
208 int j;
209
210 for (i = 0; i < LEN(matchtbl); i++) {
211 switch (matchtbl[i].f->type) {
212 case WILDCARD:
213 continue;
214 case NUMBER:
215 if (matchtbl[i].f->val[0] == matchtbl[i].tm)
216 continue;
217 break;
218 case RANGE:
219 if (matchtbl[i].f->val[0] <= matchtbl[i].tm)
220 if (matchtbl[i].f->val[1] >= matchtbl[i].tm)
221 continue;
222 break;
223 case REPEAT:
224 if (matchtbl[i].tm > 0) {
225 if (matchtbl[i].tm % matchtbl[i].f->val[0] == 0)
226 continue;
227 } else {
228 if (matchtbl[i].len % matchtbl[i].f->val[0] == 0)
229 continue;
230 }
231 break;
232 case LIST:
233 for (j = 0; j < matchtbl[i].f->len; j++)
234 if (matchtbl[i].f->val[j] == matchtbl[i].tm)
235 break;
236 if (j < matchtbl[i].f->len)
237 continue;
238 break;
239 default:
240 break;
241 }
242 break;
243 }
244 if (i != LEN(matchtbl))
245 return 0;
246 return 1;
247}
248
249static int
250parsefield(const char *field, long low, long high, struct field *f)
251{
252 int i;
253 char *e1, *e2;
254 const char *p;
255
256 p = field;
257 while (isdigit(*p))
258 p++;
259
260 f->type = ERROR;
261
262 switch (*p) {
263 case '*':
264 if (strcmp(field, "*") == 0) {
265 f->val = NULL;
266 f->len = 0;
267 f->type = WILDCARD;
268 } else if (strncmp(field, "*/", 2) == 0) {
269 f->val = emalloc(sizeof(*f->val));
270 f->len = 1;
271
272 errno = 0;
273 f->val[0] = strtol(field + 2, &e1, 10);
274 if (e1[0] != '\0' || errno != 0 || f->val[0] == 0)
275 break;
276
277 f->type = REPEAT;
278 }
279 break;
280 case '\0':
281 f->val = emalloc(sizeof(*f->val));
282 f->len = 1;
283
284 errno = 0;
285 f->val[0] = strtol(field, &e1, 10);
286 if (e1[0] != '\0' || errno != 0)
287 break;
288
289 f->type = NUMBER;
290 break;
291 case '-':
292 f->val = emalloc(2 * sizeof(*f->val));
293 f->len = 2;
294
295 errno = 0;
296 f->val[0] = strtol(field, &e1, 10);
297 if (e1[0] != '-' || errno != 0)
298 break;
299
300 errno = 0;
301 f->val[1] = strtol(e1 + 1, &e2, 10);
302 if (e2[0] != '\0' || errno != 0)
303 break;
304
305 f->type = RANGE;
306 break;
307 case ',':
308 for (i = 1; isdigit(*p) || *p == ','; p++)
309 if (*p == ',')
310 i++;
311 f->val = emalloc(i * sizeof(*f->val));
312 f->len = i;
313
314 errno = 0;
315 f->val[0] = strtol(field, &e1, 10);
316 if (f->val[0] < low || f->val[0] > high)
317 break;
318
319 for (i = 1; *e1 == ',' && errno == 0; i++) {
320 errno = 0;
321 f->val[i] = strtol(e1 + 1, &e2, 10);
322 e1 = e2;
323 }
324 if (e1[0] != '\0' || errno != 0)
325 break;
326
327 f->type = LIST;
328 break;
329 default:
330 return -1;
331 }
332
333 for (i = 0; i < f->len; i++)
334 if (f->val[i] < low || f->val[i] > high)
335 f->type = ERROR;
336
337 if (f->type == ERROR) {
338 free(f->val);
339 return -1;
340 }
341
342 return 0;
343}
344
345static void
346freecte(struct ctabentry *cte, int nfields)
347{
348 switch (nfields) {
349 case 6:
350 free(cte->cmd);
351 /* fallthrough */
352 case 5:
353 free(cte->wday.val);
354 /* fallthrough */
355 case 4:
356 free(cte->mon.val);
357 /* fallthrough */
358 case 3:
359 free(cte->mday.val);
360 /* fallthrough */
361 case 2:
362 free(cte->hour.val);
363 /* fallthrough */
364 case 1:
365 free(cte->min.val);
366 }
367 free(cte);
368}
369
370static void
371unloadentries(void)
372{
373 struct ctabentry *cte, *tmp;
374
375 for (cte = TAILQ_FIRST(&ctabhead); cte; cte = tmp) {
376 tmp = TAILQ_NEXT(cte, entry);
377 TAILQ_REMOVE(&ctabhead, cte, entry);
378 freecte(cte, 6);
379 }
380}
381
382static int
383loadentries(void)
384{
385 struct ctabentry *cte;
386 FILE *fp;
387 char *line = NULL, *p, *col;
388 int r = 0, y;
389 size_t size = 0;
390 ssize_t len;
391 struct fieldlimits {
392 char *name;
393 long min;
394 long max;
395 struct field *f;
396 } flim[] = {
397 {"min", 0, 59, NULL},
398 {"hour", 0, 23, NULL},
399 {"mday", 1, 31, NULL},
400 {"mon", 1, 12, NULL},
401 {"wday", 0, 6, NULL}
402 };
403 size_t x;
404
405 if ((fp = fopen(config, "r")) == NULL) {
406 logerr("error: can't open %s: %s\n", config, strerror(errno));
407 return -1;
408 }
409
410 for (y = 0; (len = getline(&line, &size, fp)) != -1; y++) {
411 p = line;
412 if (line[0] == '#' || line[0] == '\n' || line[0] == '\0')
413 continue;
414
415 cte = emalloc(sizeof(*cte));
416 flim[0].f = &cte->min;
417 flim[1].f = &cte->hour;
418 flim[2].f = &cte->mday;
419 flim[3].f = &cte->mon;
420 flim[4].f = &cte->wday;
421
422 for (x = 0; x < LEN(flim); x++) {
423 do
424 col = strsep(&p, "\t\n ");
425 while (col && col[0] == '\0');
426
427 if (!col || parsefield(col, flim[x].min, flim[x].max, flim[x].f) < 0) {
428 logerr(
429 "error: failed to parse `%s' field on "
430 "line %d\n",
431 flim[x].name,
432 y + 1
433 );
434 freecte(cte, x);
435 r = -1;
436 break;
437 }
438 }
439
440 if (r == -1)
441 break;
442
443 col = strsep(&p, "\n");
444 if (col)
445 while (col[0] == '\t' || col[0] == ' ')
446 col++;
447 if (!col || col[0] == '\0') {
448 logerr("error: missing `cmd' field on line %d\n", y + 1);
449 freecte(cte, 5);
450 r = -1;
451 break;
452 }
453 cte->cmd = estrdup(col);
454
455 TAILQ_INSERT_TAIL(&ctabhead, cte, entry);
456 }
457
458 if (r < 0)
459 unloadentries();
460
461 free(line);
462 fclose(fp);
463
464 return r;
465}
466
467static void
468reloadentries(void)
469{
470 unloadentries();
471 if (loadentries() < 0)
472 logwarn("warning: discarding old crontab entries\n");
473}
474
475static void
476sighandler(int sig)
477{
478 switch (sig) {
479 case SIGCHLD:
480 chldreap = 1;
481 break;
482 case SIGHUP:
483 reload = 1;
484 break;
485 case SIGTERM:
486 quit = 1;
487 break;
488 }
489}
490
491static void
492usage(void)
493{
494 eprintf("usage: %s [-f file] [-n]\n", argv0);
495}
496
497// ?man cron: cron daemon
498// ?man daemon to run scheduled background commands
499int
500main(int argc, char *argv[])
501{
502 FILE *fp;
503 struct ctabentry *cte;
504 time_t t;
505 struct tm *tm;
506 struct sigaction sa;
507
508 ARGBEGIN
509 {
510 // ?man -n: print line numbers or counts
511 case 'n':
512 nflag = 1;
513 break;
514 // ?man -f:str: force the operation
515 case 'f':
516 config = EARGF(usage());
517 break;
518 default:
519 usage();
520 }
521 ARGEND
522
523 if (argc > 0)
524 usage();
525
526 if (nflag == 0) {
527 openlog(argv[0], LOG_CONS | LOG_PID, LOG_CRON);
528 if (daemon(1, 0) < 0) {
529 logerr("error: failed to daemonize %s\n", strerror(errno));
530 return 1;
531 }
532 if ((fp = fopen(pidfile, "w"))) {
533 fprintf(fp, "%d\n", getpid());
534 fclose(fp);
535 }
536 }
537
538 sa.sa_handler = sighandler;
539 sigfillset(&sa.sa_mask);
540 sa.sa_flags = SA_RESTART;
541 sigaction(SIGCHLD, &sa, NULL);
542 sigaction(SIGHUP, &sa, NULL);
543 sigaction(SIGTERM, &sa, NULL);
544
545 loadentries();
546
547 while (1) {
548 t = time(NULL);
549 sleep(60 - t % 60);
550
551 if (quit == 1) {
552 if (nflag == 0)
553 unlink(pidfile);
554 unloadentries();
555 /* Don't wait or kill forked processes, just exit */
556 break;
557 }
558
559 if (reload == 1 || chldreap == 1) {
560 if (reload == 1) {
561 reloadentries();
562 reload = 0;
563 }
564 if (chldreap == 1) {
565 waitjob();
566 chldreap = 0;
567 }
568 continue;
569 }
570
571 TAILQ_FOREACH(cte, &ctabhead, entry)
572 {
573 t = time(NULL);
574 tm = localtime(&t);
575 if (matchentry(cte, tm) == 1)
576 runjob(cte->cmd);
577 }
578 }
579
580 if (nflag == 0)
581 closelog();
582
583 return 0;
584}