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