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 }