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