cron.c - sbase - suckless unix tools
 (HTM) git clone git://git.suckless.org/sbase
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) 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 }