scron-noxz

[fork] simple cron daemon
git clone https://noxz.tech/git/scron-noxz.git
Log | Files | README | LICENSE

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