cron.c - sbase - suckless unix tools | |
git clone git://git.suckless.org/sbase | |
Log | |
Files | |
Refs | |
README | |
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(ctabhea… | |
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:… | |
162 pid, strsignal(WTERMSIG(status)), ctime(… | |
163 else if (WIFSTOPPED(status) == 1) | |
164 loginfo("complete: pid: %d stopped by signal: %s… | |
165 pid, strsignal(WSTOPSIG(status)), ctime(… | |
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(t… | |
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]… | |
218 continue; | |
219 break; | |
220 case REPEAT: | |
221 if (matchtbl[i].tm > 0) { | |
222 if (matchtbl[i].tm % matchtbl[i].f->val[… | |
223 continue; | |
224 } else { | |
225 if (matchtbl[i].len % matchtbl[i].f->val… | |
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]… | |
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] == … | |
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(er… | |
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]… | |
420 logerr("error: failed to parse `%s' fiel… | |
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", strerr… | |
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 exi… | |
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 } |