| 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 } |