commit 8d2c5dd9a33fd240aaf24d21ad6954a2e8d4accd
parent 296af80dbae381898dcee762ecbe36a36079fa66
Author: sin <sin@2f30.org>
Date: Tue, 9 Dec 2014 16:04:14 +0000
Import crond from http://git.2f30.org/scron/tree/
Diffstat:
M | LICENSE | | | 1 | + |
M | Makefile | | | 1 | + |
A | crond.c | | | 530 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
3 files changed, 532 insertions(+), 0 deletions(-)
diff --git a/LICENSE b/LICENSE
@@ -30,6 +30,7 @@ MIT/X Consortium License
© 2014 Jeffrey Picard <jeff@jeffreypicard.com>
© 2014 Evan Gates <evan.gates@gmail.com>
© 2014 Michael Forney <mforney@mforney.org>
+© 2014 Ari Malinen <ari.malinen@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
diff --git a/Makefile b/Makefile
@@ -69,6 +69,7 @@ BIN =\
cols\
comm\
cp\
+ crond\
cut\
date\
dirname\
diff --git a/crond.c b/crond.c
@@ -0,0 +1,530 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <errno.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <syslog.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "arg.h"
+#include "queue.h"
+
+#define VERSION "0.3.2"
+
+#define LEN(x) (sizeof (x) / sizeof *(x))
+
+struct field {
+ /* [low, high] */
+ long low;
+ long high;
+ /* for every `div' units */
+ long div;
+};
+
+struct ctabentry {
+ struct field min;
+ struct field hour;
+ struct field mday;
+ struct field mon;
+ struct field wday;
+ char *cmd;
+ TAILQ_ENTRY(ctabentry) entry;
+};
+
+struct jobentry {
+ char *cmd;
+ pid_t pid;
+ TAILQ_ENTRY(jobentry) entry;
+};
+
+char *argv0;
+static sig_atomic_t chldreap;
+static sig_atomic_t reload;
+static sig_atomic_t quit;
+static TAILQ_HEAD(, ctabentry) ctabhead = TAILQ_HEAD_INITIALIZER(ctabhead);
+static TAILQ_HEAD(, jobentry) jobhead = TAILQ_HEAD_INITIALIZER(jobhead);
+static char *config = "/etc/crontab";
+static char *pidfile = "/var/run/crond.pid";
+static int nflag;
+
+static void
+loginfo(const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ if (nflag == 0)
+ vsyslog(LOG_INFO, fmt, ap);
+ else
+ vfprintf(stdout, fmt, ap);
+ fflush(stdout);
+ va_end(ap);
+}
+
+static void
+logwarn(const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ if (nflag == 0)
+ vsyslog(LOG_WARNING, fmt, ap);
+ else
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+}
+
+static void
+logerr(const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ if (nflag == 0)
+ vsyslog(LOG_ERR, fmt, ap);
+ else
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+}
+
+static void *
+emalloc(size_t size)
+{
+ void *p;
+ p = malloc(size);
+ if (!p) {
+ logerr("error: out of memory\n");
+ if (nflag == 0)
+ unlink(pidfile);
+ exit(EXIT_FAILURE);
+ }
+ return p;
+}
+
+static char *
+estrdup(const char *s)
+{
+ char *p;
+
+ p = strdup(s);
+ if (!p) {
+ logerr("error: out of memory\n");
+ if (nflag == 0)
+ unlink(pidfile);
+ exit(EXIT_FAILURE);
+ }
+ return p;
+}
+
+static void
+runjob(char *cmd)
+{
+ struct jobentry *je;
+ time_t t;
+ pid_t pid;
+
+ t = time(NULL);
+
+ /* If command is already running, skip it */
+ TAILQ_FOREACH(je, &jobhead, entry) {
+ if (strcmp(je->cmd, cmd) == 0) {
+ loginfo("already running %s pid: %d at %s",
+ je->cmd, je->pid, ctime(&t));
+ return;
+ }
+ }
+
+ pid = fork();
+ if (pid < 0) {
+ logerr("error: failed to fork job: %s time: %s",
+ cmd, ctime(&t));
+ return;
+ } else if (pid == 0) {
+ setsid();
+ loginfo("run: %s pid: %d at %s",
+ cmd, getpid(), ctime(&t));
+ execl("/bin/sh", "/bin/sh", "-c", cmd, (char *)NULL);
+ logerr("error: failed to execute job: %s time: %s",
+ cmd, ctime(&t));
+ _exit(EXIT_FAILURE);
+ } else {
+ je = emalloc(sizeof(*je));
+ je->cmd = estrdup(cmd);
+ je->pid = pid;
+ TAILQ_INSERT_TAIL(&jobhead, je, entry);
+ }
+}
+
+static void
+waitjob(void)
+{
+ struct jobentry *je, *tmp;
+ int status;
+ time_t t;
+ pid_t pid;
+
+ t = time(NULL);
+
+ while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
+ je = NULL;
+ TAILQ_FOREACH(tmp, &jobhead, entry) {
+ if (tmp->pid == pid) {
+ je = tmp;
+ break;
+ }
+ }
+ if (je) {
+ TAILQ_REMOVE(&jobhead, je, entry);
+ free(je->cmd);
+ free(je);
+ }
+ if (WIFEXITED(status) == 1)
+ loginfo("complete: pid: %d returned: %d time: %s",
+ pid, WEXITSTATUS(status), ctime(&t));
+ else if (WIFSIGNALED(status) == 1)
+ loginfo("complete: pid: %d terminated by signal: %s time: %s",
+ pid, strsignal(WTERMSIG(status)), ctime(&t));
+ else if (WIFSTOPPED(status) == 1)
+ loginfo("complete: pid: %d stopped by signal: %s time: %s",
+ pid, strsignal(WSTOPSIG(status)), ctime(&t));
+ }
+}
+
+static int
+isleap(int year)
+{
+ if (year % 400 == 0)
+ return 1;
+ if (year % 100 == 0)
+ return 0;
+ return (year % 4 == 0);
+}
+
+static int
+daysinmon(int mon, int year)
+{
+ int days[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
+ if (year < 1900)
+ year += 1900;
+ if (isleap(year))
+ days[1] = 29;
+ return days[mon];
+}
+
+static int
+matchentry(struct ctabentry *cte, struct tm *tm)
+{
+ struct {
+ struct field *f;
+ int tm;
+ int len;
+ } matchtbl[] = {
+ { .f = &cte->min, .tm = tm->tm_min, .len = 60 },
+ { .f = &cte->hour, .tm = tm->tm_hour, .len = 24 },
+ { .f = &cte->mday, .tm = tm->tm_mday, .len = daysinmon(tm->tm_mon, tm->tm_year) },
+ { .f = &cte->mon, .tm = tm->tm_mon, .len = 12 },
+ { .f = &cte->wday, .tm = tm->tm_wday, .len = 7 },
+ };
+ size_t i;
+
+ for (i = 0; i < LEN(matchtbl); i++) {
+ if (matchtbl[i].f->high == -1) {
+ if (matchtbl[i].f->low == -1) {
+ continue;
+ } else if (matchtbl[i].f->div > 0) {
+ if (matchtbl[i].tm > 0) {
+ if (matchtbl[i].tm % matchtbl[i].f->div == 0)
+ continue;
+ } else {
+ if (matchtbl[i].len % matchtbl[i].f->div == 0)
+ continue;
+ }
+ } else if (matchtbl[i].f->low == matchtbl[i].tm) {
+ continue;
+ }
+ } else if (matchtbl[i].f->low <= matchtbl[i].tm &&
+ matchtbl[i].f->high >= matchtbl[i].tm) {
+ continue;
+ }
+ break;
+ }
+ if (i != LEN(matchtbl))
+ return 0;
+ return 1;
+}
+
+static int
+parsefield(const char *field, long low, long high, struct field *f)
+{
+ long min, max, div;
+ char *e1, *e2;
+
+ if (strcmp(field, "*") == 0) {
+ f->low = -1;
+ f->high = -1;
+ return 0;
+ }
+
+ div = -1;
+ max = -1;
+ min = strtol(field, &e1, 10);
+
+ switch (e1[0]) {
+ case '-':
+ e1++;
+ errno = 0;
+ max = strtol(e1, &e2, 10);
+ if (e2[0] != '\0' || errno != 0)
+ return -1;
+ break;
+ case '*':
+ e1++;
+ if (e1[0] != '/')
+ return -1;
+ e1++;
+ errno = 0;
+ div = strtol(e1, &e2, 10);
+ if (e2[0] != '\0' || errno != 0)
+ return -1;
+ break;
+ case '\0':
+ break;
+ default:
+ return -1;
+ }
+
+ if (min < low || min > high)
+ return -1;
+ if (max != -1)
+ if (max < low || max > high)
+ return -1;
+ if (div != -1)
+ if (div < low || div > high)
+ return -1;
+
+ f->low = min;
+ f->high = max;
+ f->div = div;
+ return 0;
+}
+
+static void
+unloadentries(void)
+{
+ struct ctabentry *cte, *tmp;
+
+ for (cte = TAILQ_FIRST(&ctabhead); cte; cte = tmp) {
+ tmp = TAILQ_NEXT(cte, entry);
+ TAILQ_REMOVE(&ctabhead, cte, entry);
+ free(cte->cmd);
+ free(cte);
+ }
+}
+
+static int
+loadentries(void)
+{
+ struct ctabentry *cte;
+ FILE *fp;
+ char *line = NULL, *p, *col;
+ int r = 0, y;
+ size_t size = 0;
+ ssize_t len;
+
+ if ((fp = fopen(config, "r")) == NULL) {
+ logerr("error: can't open %s: %s\n", config, strerror(errno));
+ return -1;
+ }
+
+ for (y = 0; (len = getline(&line, &size, fp)) != -1; y++) {
+ p = line;
+ if (line[0] == '#' || line[0] == '\n' || line[0] == '\0')
+ continue;
+
+ cte = emalloc(sizeof(*cte));
+
+ col = strsep(&p, "\t");
+ if (!col || parsefield(col, 0, 59, &cte->min) < 0) {
+ logerr("error: failed to parse `min' field on line %d\n",
+ y + 1);
+ free(cte);
+ r = -1;
+ break;
+ }
+
+ col = strsep(&p, "\t");
+ if (!col || parsefield(col, 0, 23, &cte->hour) < 0) {
+ logerr("error: failed to parse `hour' field on line %d\n",
+ y + 1);
+ free(cte);
+ r = -1;
+ break;
+ }
+
+ col = strsep(&p, "\t");
+ if (!col || parsefield(col, 1, 31, &cte->mday) < 0) {
+ logerr("error: failed to parse `mday' field on line %d\n",
+ y + 1);
+ free(cte);
+ r = -1;
+ break;
+ }
+
+ col = strsep(&p, "\t");
+ if (!col || parsefield(col, 1, 12, &cte->mon) < 0) {
+ logerr("error: failed to parse `mon' field on line %d\n",
+ y + 1);
+ free(cte);
+ r = -1;
+ break;
+ }
+
+ col = strsep(&p, "\t");
+ if (!col || parsefield(col, 0, 6, &cte->wday) < 0) {
+ logerr("error: failed to parse `wday' field on line %d\n",
+ y + 1);
+ free(cte);
+ r = -1;
+ break;
+ }
+
+ col = strsep(&p, "\n");
+ if (!col) {
+ logerr("error: missing `cmd' field on line %d\n",
+ y + 1);
+ free(cte);
+ r = -1;
+ break;
+ }
+ cte->cmd = estrdup(col);
+
+ TAILQ_INSERT_TAIL(&ctabhead, cte, entry);
+ }
+
+ if (r < 0)
+ unloadentries();
+
+ free(line);
+ fclose(fp);
+
+ return r;
+}
+
+static void
+reloadentries(void)
+{
+ unloadentries();
+ if (loadentries() < 0)
+ logwarn("warning: discarding old crontab entries\n");
+}
+
+static void
+sighandler(int sig)
+{
+ switch (sig) {
+ case SIGCHLD:
+ chldreap = 1;
+ break;
+ case SIGHUP:
+ reload = 1;
+ break;
+ case SIGTERM:
+ quit = 1;
+ break;
+ }
+}
+
+static void
+usage(void)
+{
+ fprintf(stderr, "usage: %s [-f file] [-n]\n", argv0);
+ fprintf(stderr, " -f config file\n");
+ fprintf(stderr, " -n do not daemonize\n");
+ exit(EXIT_FAILURE);
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp;
+ struct ctabentry *cte;
+ time_t t;
+ struct tm *tm;
+ struct sigaction sa;
+
+ ARGBEGIN {
+ case 'n':
+ nflag = 1;
+ break;
+ case 'f':
+ config = EARGF(usage());
+ break;
+ default:
+ usage();
+ } ARGEND;
+
+ if (argc > 0)
+ usage();
+
+ if (nflag == 0) {
+ openlog(argv[0], LOG_CONS | LOG_PID, LOG_CRON);
+ if (daemon(1, 0) < 0) {
+ logerr("error: failed to daemonize %s\n", strerror(errno));
+ return EXIT_FAILURE;
+ }
+ if ((fp = fopen(pidfile, "w"))) {
+ fprintf(fp, "%d\n", getpid());
+ fclose(fp);
+ }
+ }
+
+ sa.sa_handler = sighandler;
+ sigfillset(&sa.sa_mask);
+ sa.sa_flags = SA_RESTART;
+ sigaction(SIGCHLD, &sa, NULL);
+ sigaction(SIGHUP, &sa, NULL);
+ sigaction(SIGTERM, &sa, NULL);
+
+ loadentries();
+
+ while (1) {
+ t = time(NULL);
+ sleep(60 - t % 60);
+
+ if (quit == 1) {
+ if (nflag == 0)
+ unlink(pidfile);
+ unloadentries();
+ /* Don't wait or kill forked processes, just exit */
+ break;
+ }
+
+ if (reload == 1 || chldreap == 1) {
+ if (reload == 1) {
+ reloadentries();
+ reload = 0;
+ }
+ if (chldreap == 1) {
+ waitjob();
+ chldreap = 0;
+ }
+ continue;
+ }
+
+ TAILQ_FOREACH(cte, &ctabhead, entry) {
+ t = time(NULL);
+ tm = localtime(&t);
+ if (matchentry(cte, tm) == 1)
+ runjob(cte->cmd);
+ }
+ }
+
+ if (nflag == 0)
+ closelog();
+
+ return EXIT_SUCCESS;
+}