sbase

suckless unix tools
git clone git://git.suckless.org/sbase
Log | Files | Refs | README | LICENSE

cron.c (10177B)


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