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