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