xref: /openbsd-src/usr.sbin/cron/cron.c (revision a28daedfc357b214be5c701aa8ba8adb29a7f1c2)
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