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