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