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