morpheus-base

morpheus base system
git clone git://git.2f30.org/morpheus-base
Log | Files | Refs

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 }