1 /* $OpenBSD: cron.c,v 1.40 2009/01/29 22:50:16 sobrado Exp $ */ 2 3 /* Copyright 1988,1990,1993,1994 by Paul Vixie 4 * All rights reserved 5 */ 6 7 /* 8 * Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC") 9 * Copyright (c) 1997,2000 by Internet Software Consortium, Inc. 10 * 11 * Permission to use, copy, modify, and distribute this software for any 12 * purpose with or without fee is hereby granted, provided that the above 13 * copyright notice and this permission notice appear in all copies. 14 * 15 * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES 16 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 17 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR 18 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 19 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 20 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 21 * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 22 */ 23 24 #if !defined(lint) && !defined(LINT) 25 static const char rcsid[] = "$OpenBSD: cron.c,v 1.40 2009/01/29 22:50:16 sobrado Exp $"; 26 #endif 27 28 #define MAIN_PROGRAM 29 30 #include "cron.h" 31 32 enum timejump { negative, small, medium, large }; 33 34 static void usage(void), 35 run_reboot_jobs(cron_db *), 36 find_jobs(int, cron_db *, int, int), 37 set_time(int), 38 cron_sleep(int), 39 sigchld_handler(int), 40 sighup_handler(int), 41 sigchld_reaper(void), 42 quit(int), 43 parse_args(int c, char *v[]); 44 45 static volatile sig_atomic_t got_sighup, got_sigchld; 46 static int timeRunning, virtualTime, clockTime, cronSock; 47 static long GMToff; 48 static cron_db database; 49 static at_db at_database; 50 static double batch_maxload = BATCH_MAXLOAD; 51 52 static void 53 usage(void) { 54 #if DEBUGGING 55 const char **dflags; 56 #endif 57 58 fprintf(stderr, "usage: %s [-n] [-l load_avg] [-x [", ProgramName); 59 #if DEBUGGING 60 for (dflags = DebugFlagNames; *dflags; dflags++) 61 fprintf(stderr, "%s%s", *dflags, dflags[1] ? "," : "]"); 62 #else 63 fprintf(stderr, "debugging flags (none supported in this build)]"); 64 #endif 65 fprintf(stderr, "]\n"); 66 exit(ERROR_EXIT); 67 } 68 69 int 70 main(int argc, char *argv[]) { 71 struct sigaction sact; 72 int fd; 73 74 ProgramName = argv[0]; 75 76 setlocale(LC_ALL, ""); 77 78 #if defined(BSD) 79 setlinebuf(stdout); 80 setlinebuf(stderr); 81 #endif 82 83 NoFork = 0; 84 parse_args(argc, argv); 85 86 bzero((char *)&sact, sizeof sact); 87 sigemptyset(&sact.sa_mask); 88 sact.sa_flags = 0; 89 #ifdef SA_RESTART 90 sact.sa_flags |= SA_RESTART; 91 #endif 92 sact.sa_handler = sigchld_handler; 93 (void) sigaction(SIGCHLD, &sact, NULL); 94 sact.sa_handler = sighup_handler; 95 (void) sigaction(SIGHUP, &sact, NULL); 96 sact.sa_handler = quit; 97 (void) sigaction(SIGINT, &sact, NULL); 98 (void) sigaction(SIGTERM, &sact, NULL); 99 sact.sa_handler = SIG_IGN; 100 (void) sigaction(SIGPIPE, &sact, NULL); 101 (void) sigaction(SIGUSR1, &sact, NULL); /* XXX */ 102 103 acquire_daemonlock(0); 104 set_cron_uid(); 105 set_cron_cwd(); 106 107 if (putenv("PATH="_PATH_DEFPATH) < 0) { 108 log_it("CRON", getpid(), "DEATH", "can't malloc"); 109 exit(1); 110 } 111 112 /* if there are no debug flags turned on, fork as a daemon should. 113 */ 114 if (DebugFlags) { 115 #if DEBUGGING 116 (void) fprintf(stderr, "[%ld] cron started\n", (long)getpid()); 117 #endif 118 } else if (NoFork == 0) { 119 switch (fork()) { 120 case -1: 121 log_it("CRON",getpid(),"DEATH","can't fork"); 122 exit(0); 123 break; 124 case 0: 125 /* child process */ 126 (void) setsid(); 127 if ((fd = open(_PATH_DEVNULL, O_RDWR, 0)) >= 0) { 128 (void) dup2(fd, STDIN); 129 (void) dup2(fd, STDOUT); 130 (void) dup2(fd, STDERR); 131 if (fd != STDERR) 132 (void) close(fd); 133 } 134 log_it("CRON",getpid(),"STARTUP",CRON_VERSION); 135 break; 136 default: 137 /* parent process should just die */ 138 _exit(0); 139 } 140 } 141 142 acquire_daemonlock(0); 143 cronSock = open_socket(); 144 database.head = NULL; 145 database.tail = NULL; 146 database.mtime = (time_t) 0; 147 load_database(&database); 148 at_database.head = NULL; 149 at_database.tail = NULL; 150 at_database.mtime = (time_t) 0; 151 scan_atjobs(&at_database, NULL); 152 set_time(TRUE); 153 run_reboot_jobs(&database); 154 timeRunning = virtualTime = clockTime; 155 156 /* 157 * Too many clocks, not enough time (Al. Einstein) 158 * These clocks are in minutes since the epoch, adjusted for timezone. 159 * virtualTime: is the time it *would* be if we woke up 160 * promptly and nobody ever changed the clock. It is 161 * monotonically increasing... unless a timejump happens. 162 * At the top of the loop, all jobs for 'virtualTime' have run. 163 * timeRunning: is the time we last awakened. 164 * clockTime: is the time when set_time was last called. 165 */ 166 while (TRUE) { 167 int timeDiff; 168 enum timejump wakeupKind; 169 170 /* ... wait for the time (in minutes) to change ... */ 171 do { 172 cron_sleep(timeRunning + 1); 173 set_time(FALSE); 174 } while (clockTime == timeRunning); 175 timeRunning = clockTime; 176 177 /* 178 * Calculate how the current time differs from our virtual 179 * clock. Classify the change into one of 4 cases. 180 */ 181 timeDiff = timeRunning - virtualTime; 182 183 /* shortcut for the most common case */ 184 if (timeDiff == 1) { 185 virtualTime = timeRunning; 186 find_jobs(virtualTime, &database, TRUE, TRUE); 187 } else { 188 if (timeDiff > (3*MINUTE_COUNT) || 189 timeDiff < -(3*MINUTE_COUNT)) 190 wakeupKind = large; 191 else if (timeDiff > 5) 192 wakeupKind = medium; 193 else if (timeDiff > 0) 194 wakeupKind = small; 195 else 196 wakeupKind = negative; 197 198 switch (wakeupKind) { 199 case small: 200 /* 201 * case 1: timeDiff is a small positive number 202 * (wokeup late) run jobs for each virtual 203 * minute until caught up. 204 */ 205 Debug(DSCH, ("[%ld], normal case %d minutes to go\n", 206 (long)getpid(), timeDiff)) 207 do { 208 if (job_runqueue()) 209 sleep(10); 210 virtualTime++; 211 find_jobs(virtualTime, &database, 212 TRUE, TRUE); 213 } while (virtualTime < timeRunning); 214 break; 215 216 case medium: 217 /* 218 * case 2: timeDiff is a medium-sized positive 219 * number, for example because we went to DST 220 * run wildcard jobs once, then run any 221 * fixed-time jobs that would otherwise be 222 * skipped if we use up our minute (possible, 223 * if there are a lot of jobs to run) go 224 * around the loop again so that wildcard jobs 225 * have a chance to run, and we do our 226 * housekeeping. 227 */ 228 Debug(DSCH, ("[%ld], DST begins %d minutes to go\n", 229 (long)getpid(), timeDiff)) 230 /* run wildcard jobs for current minute */ 231 find_jobs(timeRunning, &database, TRUE, FALSE); 232 233 /* run fixed-time jobs for each minute missed */ 234 do { 235 if (job_runqueue()) 236 sleep(10); 237 virtualTime++; 238 find_jobs(virtualTime, &database, 239 FALSE, TRUE); 240 set_time(FALSE); 241 } while (virtualTime< timeRunning && 242 clockTime == timeRunning); 243 break; 244 245 case negative: 246 /* 247 * case 3: timeDiff is a small or medium-sized 248 * negative num, eg. because of DST ending. 249 * Just run the wildcard jobs. The fixed-time 250 * jobs probably have already run, and should 251 * not be repeated. Virtual time does not 252 * change until we are caught up. 253 */ 254 Debug(DSCH, ("[%ld], DST ends %d minutes to go\n", 255 (long)getpid(), timeDiff)) 256 find_jobs(timeRunning, &database, TRUE, FALSE); 257 break; 258 default: 259 /* 260 * other: time has changed a *lot*, 261 * jump virtual time, and run everything 262 */ 263 Debug(DSCH, ("[%ld], clock jumped\n", 264 (long)getpid())) 265 virtualTime = timeRunning; 266 find_jobs(timeRunning, &database, TRUE, TRUE); 267 } 268 } 269 270 /* Jobs to be run (if any) are loaded; clear the queue. */ 271 job_runqueue(); 272 273 /* Run any jobs in the at queue. */ 274 atrun(&at_database, batch_maxload, 275 timeRunning * SECONDS_PER_MINUTE - GMToff); 276 277 /* Check to see if we received a signal while running jobs. */ 278 if (got_sighup) { 279 got_sighup = 0; 280 log_close(); 281 } 282 if (got_sigchld) { 283 got_sigchld = 0; 284 sigchld_reaper(); 285 } 286 load_database(&database); 287 scan_atjobs(&at_database, NULL); 288 } 289 } 290 291 static void 292 run_reboot_jobs(cron_db *db) { 293 user *u; 294 entry *e; 295 296 for (u = db->head; u != NULL; u = u->next) { 297 for (e = u->crontab; e != NULL; e = e->next) { 298 if (e->flags & WHEN_REBOOT) 299 job_add(e, u); 300 } 301 } 302 (void) job_runqueue(); 303 } 304 305 static void 306 find_jobs(int vtime, cron_db *db, int doWild, int doNonWild) { 307 time_t virtualSecond = vtime * SECONDS_PER_MINUTE; 308 struct tm *tm = gmtime(&virtualSecond); 309 int minute, hour, dom, month, dow; 310 user *u; 311 entry *e; 312 313 /* make 0-based values out of these so we can use them as indices 314 */ 315 minute = tm->tm_min -FIRST_MINUTE; 316 hour = tm->tm_hour -FIRST_HOUR; 317 dom = tm->tm_mday -FIRST_DOM; 318 month = tm->tm_mon +1 /* 0..11 -> 1..12 */ -FIRST_MONTH; 319 dow = tm->tm_wday -FIRST_DOW; 320 321 Debug(DSCH, ("[%ld] tick(%d,%d,%d,%d,%d) %s %s\n", 322 (long)getpid(), minute, hour, dom, month, dow, 323 doWild?" ":"No wildcard",doNonWild?" ":"Wildcard only")) 324 325 /* the dom/dow situation is odd. '* * 1,15 * Sun' will run on the 326 * first and fifteenth AND every Sunday; '* * * * Sun' will run *only* 327 * on Sundays; '* * 1,15 * *' will run *only* the 1st and 15th. this 328 * is why we keep 'e->dow_star' and 'e->dom_star'. yes, it's bizarre. 329 * like many bizarre things, it's the standard. 330 */ 331 for (u = db->head; u != NULL; u = u->next) { 332 for (e = u->crontab; e != NULL; e = e->next) { 333 Debug(DSCH|DEXT, ("user [%s:%lu:%lu:...] cmd=\"%s\"\n", 334 e->pwd->pw_name, (unsigned long)e->pwd->pw_uid, 335 (unsigned long)e->pwd->pw_gid, e->cmd)) 336 if (bit_test(e->minute, minute) && 337 bit_test(e->hour, hour) && 338 bit_test(e->month, month) && 339 ( ((e->flags & DOM_STAR) || (e->flags & DOW_STAR)) 340 ? (bit_test(e->dow,dow) && bit_test(e->dom,dom)) 341 : (bit_test(e->dow,dow) || bit_test(e->dom,dom)) 342 ) 343 ) { 344 if ((doNonWild && 345 !(e->flags & (MIN_STAR|HR_STAR))) || 346 (doWild && (e->flags & (MIN_STAR|HR_STAR)))) 347 job_add(e, u); 348 } 349 } 350 } 351 } 352 353 /* 354 * Set StartTime and clockTime to the current time. 355 * These are used for computing what time it really is right now. 356 * Note that clockTime is a unix wallclock time converted to minutes. 357 */ 358 static void 359 set_time(int initialize) { 360 struct tm tm; 361 static int isdst; 362 363 StartTime = time(NULL); 364 365 /* We adjust the time to GMT so we can catch DST changes. */ 366 tm = *localtime(&StartTime); 367 if (initialize || tm.tm_isdst != isdst) { 368 isdst = tm.tm_isdst; 369 GMToff = get_gmtoff(&StartTime, &tm); 370 Debug(DSCH, ("[%ld] GMToff=%ld\n", 371 (long)getpid(), (long)GMToff)) 372 } 373 clockTime = (StartTime + GMToff) / (time_t)SECONDS_PER_MINUTE; 374 } 375 376 /* 377 * Try to just hit the next minute. 378 */ 379 static void 380 cron_sleep(int target) { 381 int fd, nfds; 382 unsigned char poke; 383 struct timeval t1, t2, tv; 384 struct sockaddr_un s_un; 385 socklen_t sunlen; 386 static fd_set *fdsr; 387 388 gettimeofday(&t1, NULL); 389 t1.tv_sec += GMToff; 390 tv.tv_sec = (target * SECONDS_PER_MINUTE - t1.tv_sec) + 1; 391 tv.tv_usec = 0; 392 393 if (fdsr == NULL) { 394 fdsr = (fd_set *)calloc(howmany(cronSock + 1, NFDBITS), 395 sizeof(fd_mask)); 396 } 397 398 while (timerisset(&tv) && tv.tv_sec < 65) { 399 Debug(DSCH, ("[%ld] Target time=%ld, sec-to-wait=%ld\n", 400 (long)getpid(), (long)target*SECONDS_PER_MINUTE, tv.tv_sec)) 401 402 poke = RELOAD_CRON | RELOAD_AT; 403 if (fdsr) 404 FD_SET(cronSock, fdsr); 405 /* Sleep until we time out, get a poke, or get a signal. */ 406 nfds = select(cronSock + 1, fdsr, NULL, NULL, &tv); 407 if (nfds == 0) 408 break; /* timer expired */ 409 if (nfds == -1 && errno != EINTR) 410 break; /* an error occurred */ 411 if (nfds > 0) { 412 Debug(DSCH, ("[%ld] Got a poke on the socket\n", 413 (long)getpid())) 414 sunlen = sizeof(s_un); 415 fd = accept(cronSock, (struct sockaddr *)&s_un, &sunlen); 416 if (fd >= 0 && fcntl(fd, F_SETFL, O_NONBLOCK) == 0) { 417 (void) read(fd, &poke, 1); 418 close(fd); 419 if (poke & RELOAD_CRON) { 420 database.mtime = (time_t)0; 421 load_database(&database); 422 } 423 if (poke & RELOAD_AT) { 424 /* 425 * We run any pending at jobs right 426 * away so that "at now" really runs 427 * jobs immediately. 428 */ 429 gettimeofday(&t2, NULL); 430 at_database.mtime = (time_t)0; 431 if (scan_atjobs(&at_database, &t2)) 432 atrun(&at_database, 433 batch_maxload, t2.tv_sec); 434 } 435 } 436 } else { 437 /* Interrupted by a signal. */ 438 if (got_sighup) { 439 got_sighup = 0; 440 log_close(); 441 } 442 if (got_sigchld) { 443 got_sigchld = 0; 444 sigchld_reaper(); 445 } 446 } 447 448 /* Adjust tv and continue where we left off. */ 449 gettimeofday(&t2, NULL); 450 t2.tv_sec += GMToff; 451 timersub(&t2, &t1, &t1); 452 timersub(&tv, &t1, &tv); 453 memcpy(&t1, &t2, sizeof(t1)); 454 if (tv.tv_sec < 0) 455 tv.tv_sec = 0; 456 if (tv.tv_usec < 0) 457 tv.tv_usec = 0; 458 } 459 } 460 461 static void 462 sighup_handler(int x) { 463 got_sighup = 1; 464 } 465 466 static void 467 sigchld_handler(int x) { 468 got_sigchld = 1; 469 } 470 471 static void 472 quit(int x) { 473 (void) unlink(_PATH_CRON_PID); 474 _exit(0); 475 } 476 477 static void 478 sigchld_reaper(void) { 479 WAIT_T waiter; 480 PID_T pid; 481 482 do { 483 pid = waitpid(-1, &waiter, WNOHANG); 484 switch (pid) { 485 case -1: 486 if (errno == EINTR) 487 continue; 488 Debug(DPROC, 489 ("[%ld] sigchld...no children\n", 490 (long)getpid())) 491 break; 492 case 0: 493 Debug(DPROC, 494 ("[%ld] sigchld...no dead kids\n", 495 (long)getpid())) 496 break; 497 default: 498 Debug(DPROC, 499 ("[%ld] sigchld...pid #%ld died, stat=%d\n", 500 (long)getpid(), (long)pid, WEXITSTATUS(waiter))) 501 break; 502 } 503 } while (pid > 0); 504 } 505 506 static void 507 parse_args(int argc, char *argv[]) { 508 int argch; 509 char *ep; 510 511 while (-1 != (argch = getopt(argc, argv, "l:nx:"))) { 512 switch (argch) { 513 case 'l': 514 errno = 0; 515 batch_maxload = strtod(optarg, &ep); 516 if (*ep != '\0' || ep == optarg || errno == ERANGE || 517 batch_maxload < 0) { 518 fprintf(stderr, "Illegal load average: %s\n", 519 optarg); 520 usage(); 521 } 522 break; 523 case 'n': 524 NoFork = 1; 525 break; 526 case 'x': 527 if (!set_debug_flags(optarg)) 528 usage(); 529 break; 530 default: 531 usage(); 532 } 533 } 534 } 535