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