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