scron

simple cron daemon
git clone git://git.2f30.org/scron
Log | Files | Refs | README | LICENSE

crond.c (10838B)


      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 <ctype.h>
     12 #include <string.h>
     13 #include <syslog.h>
     14 #include <time.h>
     15 #include <unistd.h>
     16 
     17 #include "arg.h"
     18 #include "queue.h"
     19 
     20 #define VERSION "0.4"
     21 
     22 #define LEN(x) (sizeof (x) / sizeof *(x))
     23 
     24 struct field {
     25 	enum {
     26 		ERROR,
     27 		WILDCARD,
     28 		NUMBER,
     29 		RANGE,
     30 		REPEAT,
     31 		LIST
     32 	} type;
     33 	long *val;
     34 	int len;
     35 };
     36 
     37 struct ctabentry {
     38 	struct field min;
     39 	struct field hour;
     40 	struct field mday;
     41 	struct field mon;
     42 	struct field wday;
     43 	char *cmd;
     44 	TAILQ_ENTRY(ctabentry) entry;
     45 };
     46 
     47 struct jobentry {
     48 	char *cmd;
     49 	pid_t pid;
     50 	TAILQ_ENTRY(jobentry) entry;
     51 };
     52 
     53 char *argv0;
     54 static sig_atomic_t chldreap;
     55 static sig_atomic_t reload;
     56 static sig_atomic_t quit;
     57 static TAILQ_HEAD(, ctabentry) ctabhead = TAILQ_HEAD_INITIALIZER(ctabhead);
     58 static TAILQ_HEAD(, jobentry) jobhead = TAILQ_HEAD_INITIALIZER(jobhead);
     59 static char *config = "/etc/crontab";
     60 static char *pidfile = "/var/run/crond.pid";
     61 static int nflag;
     62 
     63 static void
     64 loginfo(const char *fmt, ...)
     65 {
     66 	va_list ap;
     67 	va_start(ap, fmt);
     68 	if (nflag == 0)
     69 		vsyslog(LOG_INFO, fmt, ap);
     70 	else
     71 		vfprintf(stdout, fmt, ap);
     72 	fflush(stdout);
     73 	va_end(ap);
     74 }
     75 
     76 static void
     77 logwarn(const char *fmt, ...)
     78 {
     79 	va_list ap;
     80 	va_start(ap, fmt);
     81 	if (nflag == 0)
     82 		vsyslog(LOG_WARNING, fmt, ap);
     83 	else
     84 		vfprintf(stderr, fmt, ap);
     85 	va_end(ap);
     86 }
     87 
     88 static void
     89 logerr(const char *fmt, ...)
     90 {
     91 	va_list ap;
     92 	va_start(ap, fmt);
     93 	if (nflag == 0)
     94 		vsyslog(LOG_ERR, fmt, ap);
     95 	else
     96 		vfprintf(stderr, fmt, ap);
     97 	va_end(ap);
     98 }
     99 
    100 static void *
    101 emalloc(size_t size)
    102 {
    103 	void *p;
    104 	p = malloc(size);
    105 	if (!p) {
    106 		logerr("error: out of memory\n");
    107 		if (nflag == 0)
    108 			unlink(pidfile);
    109 		exit(EXIT_FAILURE);
    110 	}
    111 	return p;
    112 }
    113 
    114 static char *
    115 estrdup(const char *s)
    116 {
    117 	char *p;
    118 
    119 	p = strdup(s);
    120 	if (!p) {
    121 		logerr("error: out of memory\n");
    122 		if (nflag == 0)
    123 			unlink(pidfile);
    124 		exit(EXIT_FAILURE);
    125 	}
    126 	return p;
    127 }
    128 
    129 static void
    130 runjob(char *cmd)
    131 {
    132 	struct jobentry *je;
    133 	time_t t;
    134 	pid_t pid;
    135 
    136 	t = time(NULL);
    137 
    138 	/* If command is already running, skip it */
    139 	TAILQ_FOREACH(je, &jobhead, entry) {
    140 		if (strcmp(je->cmd, cmd) == 0) {
    141 			loginfo("already running %s pid: %d at %s",
    142 				je->cmd, je->pid, ctime(&t));
    143 			return;
    144 		}
    145 	}
    146 
    147 	pid = fork();
    148 	if (pid < 0) {
    149 		logerr("error: failed to fork job: %s time: %s",
    150 		       cmd, ctime(&t));
    151 		return;
    152 	} else if (pid == 0) {
    153 		setsid();
    154 		loginfo("run: %s pid: %d at %s",
    155 			cmd, getpid(), ctime(&t));
    156 		execl("/bin/sh", "/bin/sh", "-c", cmd, (char *)NULL);
    157 		logerr("error: failed to execute job: %s time: %s",
    158 		       cmd, ctime(&t));
    159 		_exit(EXIT_FAILURE);
    160 	} else {
    161 		je = emalloc(sizeof(*je));
    162 		je->cmd = estrdup(cmd);
    163 		je->pid = pid;
    164 		TAILQ_INSERT_TAIL(&jobhead, je, entry);
    165 	}
    166 }
    167 
    168 static void
    169 waitjob(void)
    170 {
    171 	struct jobentry *je, *tmp;
    172 	int status;
    173 	time_t t;
    174 	pid_t pid;
    175 
    176 	t = time(NULL);
    177 
    178 	while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
    179 		je = NULL;
    180 		TAILQ_FOREACH(tmp, &jobhead, entry) {
    181 			if (tmp->pid == pid) {
    182 				je = tmp;
    183 				break;
    184 			}
    185 		}
    186 		if (je) {
    187 			TAILQ_REMOVE(&jobhead, je, entry);
    188 			free(je->cmd);
    189 			free(je);
    190 		}
    191 		if (WIFEXITED(status) == 1)
    192 			loginfo("complete: pid: %d returned: %d time: %s",
    193 				pid, WEXITSTATUS(status), ctime(&t));
    194 		else if (WIFSIGNALED(status) == 1)
    195 			loginfo("complete: pid: %d terminated by signal: %s time: %s",
    196 				pid, strsignal(WTERMSIG(status)), ctime(&t));
    197 		else if (WIFSTOPPED(status) == 1)
    198 			loginfo("complete: pid: %d stopped by signal: %s time: %s",
    199 				pid, strsignal(WSTOPSIG(status)), ctime(&t));
    200 	}
    201 }
    202 
    203 static int
    204 isleap(int year)
    205 {
    206 	if (year % 400 == 0)
    207 		return 1;
    208 	if (year % 100 == 0)
    209 		return 0;
    210 	return (year % 4 == 0);
    211 }
    212 
    213 static int
    214 daysinmon(int mon, int year)
    215 {
    216 	int days[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
    217 	if (year < 1900)
    218 		year += 1900;
    219 	if (isleap(year))
    220 		days[1] = 29;
    221 	return days[mon];
    222 }
    223 
    224 static int
    225 matchentry(struct ctabentry *cte, struct tm *tm)
    226 {
    227 	struct {
    228 		struct field *f;
    229 		int tm;
    230 		int len;
    231 	} matchtbl[] = {
    232 		{ .f = &cte->min,  .tm = tm->tm_min,  .len = 60 },
    233 		{ .f = &cte->hour, .tm = tm->tm_hour, .len = 24 },
    234 		{ .f = &cte->mday, .tm = tm->tm_mday, .len = daysinmon(tm->tm_mon, tm->tm_year) },
    235 		{ .f = &cte->mon,  .tm = tm->tm_mon,  .len = 12 },
    236 		{ .f = &cte->wday, .tm = tm->tm_wday, .len = 7  },
    237 	};
    238 	size_t i;
    239 	int j;
    240 
    241 	for (i = 0; i < LEN(matchtbl); i++) {
    242 		switch (matchtbl[i].f->type) {
    243 		case WILDCARD:
    244 			continue;
    245 		case NUMBER:
    246 			if (matchtbl[i].f->val[0] == matchtbl[i].tm)
    247 				continue;
    248 			break;
    249 		case RANGE:
    250 			if (matchtbl[i].f->val[0] <= matchtbl[i].tm)
    251 				if (matchtbl[i].f->val[1] >= matchtbl[i].tm)
    252 					continue;
    253 			break;
    254 		case REPEAT:
    255 			if (matchtbl[i].tm > 0) {
    256 				if (matchtbl[i].tm % matchtbl[i].f->val[0] == 0)
    257 					continue;
    258 			} else {
    259 				if (matchtbl[i].len % matchtbl[i].f->val[0] == 0)
    260 					continue;
    261 			}
    262 			break;
    263 		case LIST:
    264 			for (j = 0; j < matchtbl[i].f->len; j++)
    265 				if (matchtbl[i].f->val[j] == matchtbl[i].tm)
    266 					break;
    267 			if (j < matchtbl[i].f->len)
    268 				continue;
    269 			break;
    270 		default:
    271 			break;
    272 		}
    273 		break;
    274 	}
    275 	if (i != LEN(matchtbl))
    276 		return 0;
    277 	return 1;
    278 }
    279 
    280 static int
    281 parsefield(const char *field, long low, long high, struct field *f)
    282 {
    283 	int i;
    284 	char *e1, *e2;
    285 	const char *p;
    286 
    287 	p = field;
    288 	while (isdigit(*p))
    289 		p++;
    290 
    291 	f->type = ERROR;
    292 
    293 	switch (*p) {
    294 	case '*':
    295 		if (strcmp(field, "*") == 0) {
    296 			f->val = NULL;
    297 			f->len = 0;
    298 			f->type = WILDCARD;
    299 		} else if (strncmp(field, "*/", 2) == 0) {
    300 			f->val = emalloc(sizeof(*f->val));
    301 			f->len = 1;
    302 
    303 			errno = 0;
    304 			f->val[0] = strtol(field + 2, &e1, 10);
    305 			if (e1[0] != '\0' || errno != 0 || f->val[0] == 0)
    306 				break;
    307 
    308 			f->type = REPEAT;
    309 		}
    310 		break;
    311 	case '\0':
    312 		f->val = emalloc(sizeof(*f->val));
    313 		f->len = 1;
    314 
    315 		errno = 0;
    316 		f->val[0] = strtol(field, &e1, 10);
    317 		if (e1[0] != '\0' || errno != 0)
    318 			break;
    319 
    320 		f->type = NUMBER;
    321 		break;
    322 	case '-':
    323 		f->val = emalloc(2 * sizeof(*f->val));
    324 		f->len = 2;
    325 
    326 		errno = 0;
    327 		f->val[0] = strtol(field, &e1, 10);
    328 		if (e1[0] != '-' || errno != 0)
    329 			break;
    330 
    331 		errno = 0;
    332 		f->val[1] = strtol(e1 + 1, &e2, 10);
    333 		if (e2[0] != '\0' || errno != 0)
    334 			break;
    335 
    336 		f->type = RANGE;
    337 		break;
    338 	case ',':
    339 		for (i = 1; isdigit(*p) || *p == ','; p++)
    340 			if (*p == ',')
    341 				i++;
    342 		f->val = emalloc(i * sizeof(*f->val));
    343 		f->len = i;
    344 
    345 		errno = 0;
    346 		f->val[0] = strtol(field, &e1, 10);
    347 		if (f->val[0] < low || f->val[0] > high)
    348 			break;
    349 
    350 		for (i = 1; *e1 == ',' && errno == 0; i++) {
    351 			errno = 0;
    352 			f->val[i] = strtol(e1 + 1, &e2, 10);
    353 			e1 = e2;
    354 		}
    355 		if (e1[0] != '\0' || errno != 0)
    356 			break;
    357 
    358 		f->type = LIST;
    359 		break;
    360 	default:
    361 		return -1;
    362 	}
    363 
    364 	for (i = 0; i < f->len; i++)
    365 		if (f->val[i] < low || f->val[i] > high)
    366 			f->type = ERROR;
    367 
    368 	if (f->type == ERROR) {
    369 		free(f->val);
    370 		return -1;
    371 	}
    372 
    373 	return 0;
    374 }
    375 
    376 static void
    377 freecte(struct ctabentry *cte, int nfields)
    378 {
    379 	switch (nfields) {
    380 	case 6:
    381 		free(cte->cmd);
    382 	case 5:
    383 		free(cte->wday.val);
    384 	case 4:
    385 		free(cte->mon.val);
    386 	case 3:
    387 		free(cte->mday.val);
    388 	case 2:
    389 		free(cte->hour.val);
    390 	case 1:
    391 		free(cte->min.val);
    392 	}
    393 	free(cte);
    394 }
    395 
    396 static void
    397 unloadentries(void)
    398 {
    399 	struct ctabentry *cte, *tmp;
    400 
    401 	for (cte = TAILQ_FIRST(&ctabhead); cte; cte = tmp) {
    402 		tmp = TAILQ_NEXT(cte, entry);
    403 		TAILQ_REMOVE(&ctabhead, cte, entry);
    404 		freecte(cte, 6);
    405 	}
    406 }
    407 
    408 static int
    409 loadentries(void)
    410 {
    411 	struct ctabentry *cte;
    412 	FILE *fp;
    413 	char *line = NULL, *p, *col;
    414 	int r = 0, y;
    415 	size_t size = 0;
    416 	ssize_t len;
    417 	struct fieldlimits {
    418 		char *name;
    419 		long min;
    420 		long max;
    421 		struct field *f;
    422 	} flim[] = {
    423 		{ "min",  0, 59, NULL },
    424 		{ "hour", 0, 23, NULL },
    425 		{ "mday", 1, 31, NULL },
    426 		{ "mon",  1, 12, NULL },
    427 		{ "wday", 0, 6,  NULL }
    428 	};
    429 	size_t x;
    430 
    431 	if ((fp = fopen(config, "r")) == NULL) {
    432 		logerr("error: can't open %s: %s\n", config, strerror(errno));
    433 		return -1;
    434 	}
    435 
    436 	for (y = 0; (len = getline(&line, &size, fp)) != -1; y++) {
    437 		p = line;
    438 		if (line[0] == '#' || line[0] == '\n' || line[0] == '\0')
    439 			continue;
    440 
    441 		cte = emalloc(sizeof(*cte));
    442 		flim[0].f = &cte->min;
    443 		flim[1].f = &cte->hour;
    444 		flim[2].f = &cte->mday;
    445 		flim[3].f = &cte->mon;
    446 		flim[4].f = &cte->wday;
    447 
    448 		for (x = 0; x < LEN(flim); x++) {
    449 			do
    450 				col = strsep(&p, "\t\n ");
    451 			while (col && col[0] == '\0');
    452 
    453 			if (!col || parsefield(col, flim[x].min, flim[x].max, flim[x].f) < 0) {
    454 				logerr("error: failed to parse `%s' field on line %d\n",
    455 						flim[x].name, y + 1);
    456 				freecte(cte, x);
    457 				r = -1;
    458 				break;
    459 			}
    460 		}
    461 
    462 		if (r == -1)
    463 			break;
    464 
    465 		col = strsep(&p, "\n");
    466 		if (col)
    467 			while (col[0] == '\t' || col[0] == ' ')
    468 				col++;
    469 		if (!col || col[0] == '\0') {
    470 			logerr("error: missing `cmd' field on line %d\n",
    471 			       y + 1);
    472 			freecte(cte, 5);
    473 			r = -1;
    474 			break;
    475 		}
    476 		cte->cmd = estrdup(col);
    477 
    478 		TAILQ_INSERT_TAIL(&ctabhead, cte, entry);
    479 	}
    480 
    481 	if (r < 0)
    482 		unloadentries();
    483 
    484 	free(line);
    485 	fclose(fp);
    486 
    487 	return r;
    488 }
    489 
    490 static void
    491 reloadentries(void)
    492 {
    493 	unloadentries();
    494 	if (loadentries() < 0)
    495 		logwarn("warning: discarding old crontab entries\n");
    496 }
    497 
    498 static void
    499 sighandler(int sig)
    500 {
    501 	switch (sig) {
    502 	case SIGCHLD:
    503 		chldreap = 1;
    504 		break;
    505 	case SIGHUP:
    506 		reload = 1;
    507 		break;
    508 	case SIGTERM:
    509 		quit = 1;
    510 		break;
    511 	}
    512 }
    513 
    514 static void
    515 usage(void)
    516 {
    517 	fprintf(stderr, VERSION " (c) 2014-2015\n");
    518 	fprintf(stderr, "usage: %s [-f file] [-n]\n", argv0);
    519 	fprintf(stderr, "  -f	config file\n");
    520 	fprintf(stderr, "  -n	do not daemonize\n");
    521 	exit(EXIT_FAILURE);
    522 }
    523 
    524 int
    525 main(int argc, char *argv[])
    526 {
    527 	FILE *fp;
    528 	struct ctabentry *cte;
    529 	time_t t;
    530 	struct tm *tm;
    531 	struct sigaction sa;
    532 
    533 	ARGBEGIN {
    534 	case 'n':
    535 		nflag = 1;
    536 		break;
    537 	case 'f':
    538 		config = EARGF(usage());
    539 		break;
    540 	default:
    541 		usage();
    542 	} ARGEND;
    543 
    544 	if (argc > 0)
    545 		usage();
    546 
    547 	if (nflag == 0) {
    548 		openlog(argv[0], LOG_CONS | LOG_PID, LOG_CRON);
    549 		if (daemon(1, 0) < 0) {
    550 			logerr("error: failed to daemonize %s\n", strerror(errno));
    551 			return EXIT_FAILURE;
    552 		}
    553 		if ((fp = fopen(pidfile, "w"))) {
    554 			fprintf(fp, "%d\n", getpid());
    555 			fclose(fp);
    556 		}
    557 	}
    558 
    559 	sa.sa_handler = sighandler;
    560 	sigfillset(&sa.sa_mask);
    561 	sa.sa_flags = SA_RESTART;
    562 	sigaction(SIGCHLD, &sa, NULL);
    563 	sigaction(SIGHUP, &sa, NULL);
    564 	sigaction(SIGTERM, &sa, NULL);
    565 
    566 	loadentries();
    567 
    568 	while (1) {
    569 		t = time(NULL);
    570 		sleep(60 - t % 60);
    571 
    572 		if (quit == 1) {
    573 			if (nflag == 0)
    574 				unlink(pidfile);
    575 			unloadentries();
    576 			/* Don't wait or kill forked processes, just exit */
    577 			break;
    578 		}
    579 
    580 		if (reload == 1 || chldreap == 1) {
    581 			if (reload == 1) {
    582 				reloadentries();
    583 				reload = 0;
    584 			}
    585 			if (chldreap == 1) {
    586 				waitjob();
    587 				chldreap = 0;
    588 			}
    589 			continue;
    590 		}
    591 
    592 		TAILQ_FOREACH(cte, &ctabhead, entry) {
    593 			t = time(NULL);
    594 			tm = localtime(&t);
    595 			if (matchentry(cte, tm) == 1)
    596 				runjob(cte->cmd);
    597 		}
    598 	}
    599 
    600 	if (nflag == 0)
    601 		closelog();
    602 
    603 	return EXIT_SUCCESS;
    604 }