diff options
author | David Oberhollenzer <david.oberhollenzer@tele2.at> | 2018-11-18 21:24:39 +0100 |
---|---|---|
committer | David Oberhollenzer <david.oberhollenzer@tele2.at> | 2018-11-23 02:01:21 +0100 |
commit | d2b3a983e16f15d636619ebd61a3fa08c889b080 (patch) | |
tree | 54740409709b76c7045bcb634b5b79377f7798c3 |
Initial commit
Signed-off-by: David Oberhollenzer <david.oberhollenzer@tele2.at>
-rw-r--r-- | .gitignore | 15 | ||||
-rw-r--r-- | LICENSE | 13 | ||||
-rw-r--r-- | Makefile.am | 13 | ||||
-rw-r--r-- | README.md | 122 | ||||
-rwxr-xr-x | autogen.sh | 3 | ||||
-rw-r--r-- | configure.ac | 38 | ||||
-rw-r--r-- | cronscan.c | 56 | ||||
-rw-r--r-- | crontab.c | 61 | ||||
-rw-r--r-- | crontab/0-example | 7 | ||||
-rw-r--r-- | gcrond.c | 128 | ||||
-rw-r--r-- | gcrond.h | 66 | ||||
-rw-r--r-- | m4/ac_define_dir.m4 | 35 | ||||
-rw-r--r-- | m4/compiler.m4 | 40 | ||||
-rw-r--r-- | rdcron.c | 318 | ||||
-rw-r--r-- | rdline.c | 77 |
15 files changed, 992 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c0eaa3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.o +.deps +.dirstamp +Makefile +Makefile.in +config.* +aclocal.m4 +autom4te.cache/ +compile +configure +depcomp +install-sh +missing +stamp-h1 +gcrond @@ -0,0 +1,13 @@ +Copyright (c) 2018 David Oberhollenzer <david.oberhollenzer@tele2.at> + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..6f72aea --- /dev/null +++ b/Makefile.am @@ -0,0 +1,13 @@ +ACLOCAL_AMFLAGS = -I m4 + +AM_CPPFLAGS = -D_GNU_SOURCE +AM_CFLAGS = $(WARN_CFLAGS) + +sbin_PROGRAMS = gcrond + +gcrond_SOURCES = gcrond.c gcrond.h rdcron.c crontab.c cronscan.c rdline.c + +crontabdir = @GCRONDIR@ +crontab_DATA = crontab/0-example + +EXTRA_DIST = crontab/0-example LICENSE README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..94f1b7c --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# About + +This package contains a small cron implementation called `gcrond`. + +It was written due to a perceived lack of a proper, simple cron +implementation. All other cron implementation I came across were either decade +old, abandoned pieces of horror ("Cool, I didn't even know that C syntax +allows this!") or hopelessly integrated into other, much larger projects (e.g. +absorbed by SystemD or in the case of OpenBSD cron, married to special OpenBSD +syscalls). + +It was a fun little exercise and it seems to work so far. No idea about +standards compliance tough, the implementation was mostly written against +the Wikipedia article about Cron. + +## License + +The source code in this package is provided under the OpenBSD flavored ISC +license. So you can practically do as you wish, as long as you retain the +original copyright notice. The software is provided "as is" (as usual) with +no warranty whatsoever (e.g. it might actually do what it was designed for, +but it could just as well set your carpet on fire). + +The sub directory `m4` contains third party macro files used by the build +system which may be subject to their own, respective licenses. + + +## Portability + +The program in this package has been written for and tested on a GNU/Linux +system, so there may be some GNU-isms in there in addition to Linux specific +code. Depending on your target platform, some minor porting effort may be +required. + + +# Building and installing + +This package uses autotools. If you downloaded a distribution tar ball, simply +run the `configure` script and then `make` after the Makefile has been +generated. A list of possible `configure` options can be viewed by running +`configure --help`. + +If you really wish to do so, run `make install` to install the program on your +system. + +When working with the git tree, run the `autogen.sh` script to generate the +configure script and friends. + + +# Crontab File Format + +The cron daemon reads its configuration from all files it can find +in `/etc/crontab.d/` (exact path can be configured). + +The files are read line by line. Empty lines or lines starting with '#' are +skipped. + +Each non-empty line consists of the typical cron fields: + +1. The `minute` field. Legal values are from 0 to 59. +2. The `hour` field. Legal values are from 0 to 23. +3. The `day of month` field. Legal values are from 1 to 31 (or fewer, depending + on the month. +4. The `month` field. Legal values are from 1 to 12 (January to December) + or the mnemonics `JAN`, `FEB`, `MAR`, `APR`, ... +5. The `day of week` field. Legal values are from 0 to 6 (Sunday to Saturday) + or the mnemonics `SUN`, `MON`, `TUE`, `WED`, ... +6. The command to execute. + + +The fields are separated by spaces. For the time matching fields, multiple +comma separated values can be specified (e.g. `MON,WED,FRI` for a job that +should run on Mondays, Wednesdays and Fridays). + +The wild-card character `*` matches any legal value. An stepping can be +specified by appending `/` and then a stepping (e.g. for the minute field, +`*/5` would let a job run every five minutes). + +A range of values can also be specified as `<lower>-<upper>`, for instance +`MON-FRI` would match every day from Monday to Friday (equivalent to `1-5`). + +Intervals and specific values can be combined, for instance a day of month +field `*/7,13,25` would trigger once a week, starting from the first of the +month (1,7,14,21,28), but additionally include the 13th and the 25th. The +same could be expressed as `1-31/7,13,25`. + + +Instead of specifying a terse cron matching expression, the first five fields +can be replaced with one of the following mnemonics: + +- `@yearly` or `@anually` is equivalent to `0 0 1 1 *`, i.e. 1st of January + at midnight +- `@monthly` is equivalent to `0 0 1 * *`, i.e. 1st of every month at midnight +- `@weekly` is equivalent to `0 0 * * 0`, i.e. every Sunday at midnight +- `@daily` is equivalent to `0 0 * * *`, i.e. every day at midnight +- `@hourly` is equivalent to `0 * * * *`, i.e. every first minute of the hour + +Lastly, the command field is not broken down but passed to `/bin/sh -c` +*as is*. + + +# Security Considerations + +The cron daemon currently has no means of specifying a user to run the jobs as, +so if cron runs as root, the jobs it starts do as well. Since by default it +reads its configuration from `/etc` which by default is only writable by root, +this shouldn't be too much of a problem when using cron for typical system +administration tasks. + +If a job should run as another user, tools such as `su`, `runuser`, `setpriv` +et cetera need to be used. + +# Possible Future Directions + +The following things would be nice to have: + +- decent logging for cron and the output of the jobs. +- cron jobs per user, e.g. scan `~/.crontab.d` or similar and run the collected + jobs as the respective user. +- timezone handling +- some usable strategy for handling time jumps, e.g. caused by a job that + syncs time with an NTP server on a system without RTC. diff --git a/autogen.sh b/autogen.sh new file mode 100755 index 0000000..c08fadf --- /dev/null +++ b/autogen.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +autoreconf --force --install --symlink diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..1d9c210 --- /dev/null +++ b/configure.ac @@ -0,0 +1,38 @@ +AC_PREREQ([2.60]) + +AC_INIT([gcron], [0.1], [david.oberhollenzer@tele2.at], gcron) +AC_CONFIG_MACRO_DIR([m4]) +AM_INIT_AUTOMAKE([foreign subdir-objects dist-xz]) +AM_SILENT_RULES([yes]) +AC_PROG_CC +AC_PROG_CC_C99 +AC_PROG_INSTALL +AC_PROG_RANLIB + +UL_WARN_ADD([-Wall]) +UL_WARN_ADD([-Wextra]) +UL_WARN_ADD([-Wunused]) +UL_WARN_ADD([-Wmissing-prototypes]) +UL_WARN_ADD([-Wmissing-declarations]) +UL_WARN_ADD([-Wwrite-strings]) +UL_WARN_ADD([-Wjump-misses-init]) +UL_WARN_ADD([-Wuninitialized]) +UL_WARN_ADD([-Winit-self]) +UL_WARN_ADD([-Wlogical-op]) +UL_WARN_ADD([-Wunused-but-set-parameter]) +UL_WARN_ADD([-Wunused-but-set-variable]) +UL_WARN_ADD([-Wunused-parameter]) +UL_WARN_ADD([-Wunused-result]) +UL_WARN_ADD([-Wunused-variable]) +UL_WARN_ADD([-Wduplicated-cond]) +UL_WARN_ADD([-Wduplicated-branches]) +UL_WARN_ADD([-Wrestrict]) +UL_WARN_ADD([-Wnull-dereference]) +UL_WARN_ADD([-pedantic]) + +AC_SUBST([WARN_CFLAGS]) + +AC_CONFIG_HEADERS([config.h]) +AC_DEFINE_DIR(GCRONDIR, sysconfdir/crontab.d, [crontab source directory]) + +AC_OUTPUT([Makefile]) diff --git a/cronscan.c b/cronscan.c new file mode 100644 index 0000000..16ebb09 --- /dev/null +++ b/cronscan.c @@ -0,0 +1,56 @@ +/* SPDX-License-Identifier: ISC */ +#include "gcrond.h" + +int cronscan(const char *directory, crontab_t **list) +{ + crontab_t *cron, *tail = NULL; + struct dirent *ent; + int dfd, ret = 0; + DIR *dir; + + dir = opendir(directory); + if (dir == NULL) { + perror(directory); + return -1; + } + + dfd = dirfd(dir); + if (dfd < 0) { + perror(directory); + closedir(dir); + return -1; + } + + for (;;) { + errno = 0; + ent = readdir(dir); + + if (ent == NULL) { + if (errno != 0) { + perror(directory); + ret = -1; + } + break; + } + + if (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) + continue; + + cron = rdcron(dfd, ent->d_name); + if (cron == NULL) + continue; + + if (tail == NULL) { + *list = cron; + tail = cron; + } else { + tail->next = cron; + } + + while (tail->next != NULL) + tail = tail->next; + } + + closedir(dir); + return ret; +} diff --git a/crontab.c b/crontab.c new file mode 100644 index 0000000..2b26ebf --- /dev/null +++ b/crontab.c @@ -0,0 +1,61 @@ +/* SPDX-License-Identifier: ISC */ +#include "gcrond.h" + +void cron_tm_to_mask(crontab_t *out, struct tm *t) +{ + memset(out, 0, sizeof(*out)); + out->minute = 1UL << ((unsigned long)t->tm_min); + out->hour = 1 << t->tm_hour; + out->dayofmonth = 1 << (t->tm_mday - 1); + out->month = 1 << t->tm_mon; + out->dayofweek = 1 << t->tm_wday; +} + +bool cron_should_run(const crontab_t *t, const crontab_t *mask) +{ + if ((t->minute & mask->minute) == 0) + return false; + + if ((t->hour & mask->hour) == 0) + return false; + + if ((t->dayofmonth & mask->dayofmonth) == 0) + return false; + + if ((t->month & mask->month) == 0) + return false; + + if ((t->dayofweek & mask->dayofweek) == 0) + return false; + + return true; +} + +void delcron(crontab_t *cron) +{ + if (cron != NULL) { + free(cron->exec); + free(cron); + } +} + +int runjob(crontab_t *tab) +{ + pid_t pid; + + if (tab->exec == NULL) + return 0; + + pid = fork(); + if (pid == -1) { + perror("fork"); + return -1; + } + + if (pid != 0) + return 0; + + execl("/bin/sh", "sh", "-c", tab->exec, (char *) 0); + perror("runnig shell interpreter"); + exit(EXIT_FAILURE); +} diff --git a/crontab/0-example b/crontab/0-example new file mode 100644 index 0000000..fcca906 --- /dev/null +++ b/crontab/0-example @@ -0,0 +1,7 @@ +# +-------- minute (0 - 59) +# | +------ hour (0 - 23) +# | | +---- day of month (1 - 31) +# | | | +-- month (1 - 12) +# | | | | +-- day of week (0 - 6) +# | | | | | +# * * * * * command to execute diff --git a/gcrond.c b/gcrond.c new file mode 100644 index 0000000..0e28277 --- /dev/null +++ b/gcrond.c @@ -0,0 +1,128 @@ +/* SPDX-License-Identifier: ISC */ +#include "gcrond.h" + +static crontab_t *jobs; +static sig_atomic_t run = 1; +static sig_atomic_t rescan = 1; + +static void read_config(void) +{ + if (cronscan(GCRONDIR, &jobs)) { + fputs("Error reading configuration. Continuing anyway.\n", + stderr); + } +} + +static void cleanup_config(void) +{ + crontab_t *t; + + while (jobs != NULL) { + t = jobs; + jobs = jobs->next; + delcron(t); + } +} + +static int timeout_minutes(int minutes) +{ + time_t now = time(NULL); + struct tm t; + + localtime_r(&now, &t); + return minutes * 60 + 30 - t.tm_sec; +} + +static int calc_timeout(void) +{ + time_t now = time(NULL), future; + struct tm tmstruct; + crontab_t mask, *t; + int minutes; + + for (minutes = 0; minutes < 120; ++minutes) { + future = now + minutes * 60; + + localtime_r(&future, &tmstruct); + cron_tm_to_mask(&mask, &tmstruct); + + for (t = jobs; t != NULL; t = t->next) { + if (cron_should_run(t, &mask)) + goto out; + } + } +out: + return timeout_minutes(minutes ? minutes : 1); +} + +static void runjobs(void) +{ + time_t now = time(NULL); + struct tm tmstruct; + crontab_t mask, *t; + + localtime_r(&now, &tmstruct); + cron_tm_to_mask(&mask, &tmstruct); + + for (t = jobs; t != NULL; t = t->next) { + if (cron_should_run(t, &mask)) + runjob(t); + } +} + +static void sighandler(int signo) +{ + pid_t pid; + + switch (signo) { + case SIGINT: + case SIGTERM: + run = 0; + break; + case SIGHUP: + rescan = 1; + break; + case SIGCHLD: + while ((pid = waitpid(-1, NULL, WNOHANG)) != -1) + ; + break; + } +} + +int main(void) +{ + struct timespec stime; + struct sigaction act; + int timeout; + + memset(&act, 0, sizeof(act)); + act.sa_handler = sighandler; + sigaction(SIGINT, &act, NULL); + sigaction(SIGTERM, &act, NULL); + sigaction(SIGHUP, &act, NULL); + sigaction(SIGCHLD, &act, NULL); + + while (run) { + if (rescan == 1) { + cleanup_config(); + read_config(); + timeout = timeout_minutes(1); + rescan = 0; + } else { + runjobs(); + timeout = calc_timeout(); + } + + stime.tv_sec = timeout; + stime.tv_nsec = 0; + + while (nanosleep(&stime, &stime) != 0 && run && !rescan) { + if (errno != EINTR) { + perror("nanosleep"); + break; + } + } + } + + return EXIT_SUCCESS; +} diff --git a/gcrond.h b/gcrond.h new file mode 100644 index 0000000..c6f3071 --- /dev/null +++ b/gcrond.h @@ -0,0 +1,66 @@ +/* SPDX-License-Identifier: ISC */ +#ifndef GCROND_H +#define GCROND_H + +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <stdbool.h> +#include <stddef.h> +#include <string.h> +#include <stdlib.h> +#include <stdint.h> +#include <unistd.h> +#include <limits.h> +#include <signal.h> +#include <dirent.h> +#include <stdarg.h> +#include <ctype.h> +#include <stdio.h> +#include <errno.h> +#include <fcntl.h> +#include <time.h> + +#include "config.h" + +#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) + +typedef struct crontab_t { + struct crontab_t *next; + char *exec; + + uint64_t minute; + uint32_t hour; + uint32_t dayofmonth; + uint16_t month; + uint8_t dayofweek; +} crontab_t; + +typedef struct { + const char *filename; /* input file name */ + size_t lineno; /* current line number */ + FILE *fp; + char *line; +} rdline_t; + +int rdline_init(rdline_t *t, int dirfd, const char *filename); + +void rdline_complain(rdline_t *t, const char *msg, ...); + +void rdline_cleanup(rdline_t *t); + +int rdline(rdline_t *t); + +crontab_t *rdcron(int dirfd, const char *filename); + +void delcron(crontab_t *cron); + +int cronscan(const char *directory, crontab_t **list); + +void cron_tm_to_mask(crontab_t *out, struct tm *t); + +bool cron_should_run(const crontab_t *t, const crontab_t *mask); + +int runjob(crontab_t *tab); + +#endif /* GCROND_H */ diff --git a/m4/ac_define_dir.m4 b/m4/ac_define_dir.m4 new file mode 100644 index 0000000..3b48c8b --- /dev/null +++ b/m4/ac_define_dir.m4 @@ -0,0 +1,35 @@ +dnl @synopsis AC_DEFINE_DIR(VARNAME, DIR [, DESCRIPTION]) +dnl +dnl This macro sets VARNAME to the expansion of the DIR variable, +dnl taking care of fixing up ${prefix} and such. +dnl +dnl VARNAME is then offered as both an output variable and a C +dnl preprocessor symbol. +dnl +dnl Example: +dnl +dnl AC_DEFINE_DIR([DATADIR], [datadir], [Where data are placed to.]) +dnl +dnl @category Misc +dnl @author Stepan Kasal <kasal@ucw.cz> +dnl @author Andreas Schwab <schwab@suse.de> +dnl @author Guido U. Draheim <guidod@gmx.de> +dnl @author Alexandre Oliva +dnl @version 2006-10-13 +dnl @license AllPermissive + +AC_DEFUN([AC_DEFINE_DIR], [ + prefix_NONE= + exec_prefix_NONE= + test "x$prefix" = xNONE && prefix_NONE=yes && prefix=$ac_default_prefix + test "x$exec_prefix" = xNONE && exec_prefix_NONE=yes && exec_prefix=$prefix +dnl In Autoconf 2.60, ${datadir} refers to ${datarootdir}, which in turn +dnl refers to ${prefix}. Thus we have to use `eval' twice. + eval ac_define_dir="\"[$]$2\"" + eval ac_define_dir="\"$ac_define_dir\"" + AC_SUBST($1, "$ac_define_dir") + AC_DEFINE_UNQUOTED($1, "$ac_define_dir", [$3]) + test "$prefix_NONE" && prefix=NONE + test "$exec_prefix_NONE" && exec_prefix=NONE +]) + diff --git a/m4/compiler.m4 b/m4/compiler.m4 new file mode 100644 index 0000000..058c73f --- /dev/null +++ b/m4/compiler.m4 @@ -0,0 +1,40 @@ +dnl Copyright (C) 2008-2011 Free Software Foundation, Inc. +dnl This file is free software; the Free Software Foundation +dnl gives unlimited permission to copy and/or distribute it, +dnl with or without modifications, as long as this notice is preserved. + +dnl From Simon Josefsson +dnl -- derivated from coreutils m4/warnings.m4 + +# UL_AS_VAR_APPEND(VAR, VALUE) +# ---------------------------- +# Provide the functionality of AS_VAR_APPEND if Autoconf does not have it. +m4_ifdef([AS_VAR_APPEND], +[m4_copy([AS_VAR_APPEND], [UL_AS_VAR_APPEND])], +[m4_define([UL_AS_VAR_APPEND], +[AS_VAR_SET([$1], [AS_VAR_GET([$1])$2])])]) + +# UL_ADD_WARN(COMPILER_OPTION [, VARNAME]) +# ------------------------ +# Adds parameter to WARN_CFLAGS (or to $VARNAME) if the compiler supports it. +AC_DEFUN([UL_WARN_ADD], [ + m4_define([warnvarname], m4_default([$2],WARN_CFLAGS)) + AS_VAR_PUSHDEF([ul_Warn], [ul_cv_warn_$1])dnl + AC_CACHE_CHECK([whether compiler handles $1], m4_defn([ul_Warn]), [ + # store AC_LANG_WERROR status, then turn it on + save_ac_[]_AC_LANG_ABBREV[]_werror_flag="${ac_[]_AC_LANG_ABBREV[]_werror_flag}" + AC_LANG_WERROR + + ul_save_CPPFLAGS="$CPPFLAGS" + CPPFLAGS="-Werror ${CPPFLAGS} $1" + AC_PREPROC_IFELSE([AC_LANG_PROGRAM([])], + [AS_VAR_SET(ul_Warn, [yes])], + [AS_VAR_SET(ul_Warn, [no])]) + # restore AC_LANG_WERROR + ac_[]_AC_LANG_ABBREV[]_werror_flag="${save_ac_[]_AC_LANG_ABBREV[]_werror_flag}" + + CPPFLAGS="$ul_save_CPPFLAGS" + ]) + AS_VAR_IF(ul_Warn, [yes], [UL_AS_VAR_APPEND(warnvarname, [" $1"])]) +]) + diff --git a/rdcron.c b/rdcron.c new file mode 100644 index 0000000..7e39763 --- /dev/null +++ b/rdcron.c @@ -0,0 +1,318 @@ +/* SPDX-License-Identifier: ISC */ +#include "gcrond.h" + +typedef struct { + const char *name; + int value; +} enum_map_t; + +static const enum_map_t weekday[] = { + { "MON", 1 }, + { "TUE", 2 }, + { "WED", 3 }, + { "THU", 4 }, + { "FRI", 5 }, + { "SAT", 6 }, + { "SUN", 0 }, + { NULL, 0 }, +}; + +static const enum_map_t month[] = { + { "JAN", 1 }, + { "FEB", 2 }, + { "MAR", 3 }, + { "APR", 4 }, + { "MAY", 5 }, + { "JUN", 6 }, + { "JUL", 7 }, + { "AUG", 8 }, + { "SEP", 9 }, + { "OCT", 10 }, + { "NOV", 11 }, + { "DEC", 12 }, + { NULL, 0 }, +}; + +static const struct { + const char *macro; + crontab_t tab; +} intervals[] = { + { + .macro = "yearly", + .tab = { + .minute = 0x01, + .hour = 0x01, + .dayofmonth = 0x01, + .month = 0x01, + .dayofweek = 0xFF + }, + }, { + .macro = "annually", + .tab = { + .minute = 0x01, + .hour = 0x01, + .dayofmonth = 0x01, + .month = 0x01, + .dayofweek = 0xFF + }, + }, { + .macro = "monthly", + .tab = { + .minute = 0x01, + .hour = 0x01, + .dayofmonth = 0x01, + .month = 0xFFFF, + .dayofweek = 0xFF + }, + }, { + .macro = "weekly", + .tab = { + .minute = 0x01, + .hour = 0x01, + .dayofmonth = 0xFFFFFFFF, + .month = 0xFFFF, + .dayofweek = 0x01 + }, + }, { + .macro = "daily", + .tab = { + .minute = 0x01, + .hour = 0x01, + .dayofmonth = 0xFFFFFFFF, + .month = 0xFFFF, + .dayofweek = 0xFF + }, + }, { + .macro = "hourly", + .tab = { + .minute = 0x01, + .hour = 0xFFFFFFFF, + .dayofmonth = 0xFFFFFFFF, + .month = 0xFFFF, + .dayofweek = 0xFF + }, + }, +}; + +/*****************************************************************************/ + +static char *readnum(char *line, int *out, int minval, int maxval, + const enum_map_t *mnemonic, rdline_t *rd) +{ + int i, temp, value = 0; + const enum_map_t *ev; + + if (!isdigit(*line)) { + if (!mnemonic) + goto fail_mn; + + for (i = 0; isalnum(line[i]); ++i) + ; + if (i == 0) + goto fail_mn; + + temp = line[i]; + line[i] = '\0'; + + for (ev = mnemonic; ev->name != NULL; ++ev) { + if (!strcmp(line, mnemonic->name) == 0) + break; + } + + if (ev->name == NULL) { + rdline_complain(rd, "unexpected '%s'", line); + return NULL; + } + line[i] = temp; + *out = ev->value; + return line + i; + } + + while (isdigit(*line)) { + i = ((*(line++)) - '0'); + if (value > (maxval - i) / 10) + goto fail_of; + value = value * 10 + i; + } + + if (value < minval) + goto fail_uf; + + *out = value; + return line; +fail_of: + rdline_complain(rd, "value exceeds maximum (%d > %d)", value, maxval); + return NULL; +fail_uf: + rdline_complain(rd, "value too small (%d < %d)", value, minval); + return NULL; +fail_mn: + rdline_complain(rd, "expected numeric value"); + return NULL; +} + +static char *readfield(char *line, uint64_t *out, int minval, int maxval, + const enum_map_t *mnemonic, rdline_t *rd) +{ + int value, endvalue, step; + uint64_t v = 0; +next: + if (*line == '*') { + ++line; + value = minval; + endvalue = maxval; + } else { + line = readnum(line, &value, minval, maxval, mnemonic, rd); + if (!line) + goto fail; + + if (*line == '-') { + line = readnum(line + 1, &endvalue, minval, maxval, + mnemonic, rd); + if (!line) + goto fail; + } else { + endvalue = value; + } + + if (endvalue < value) + goto fail; + } + + if (*line == '/') { + line = readnum(line + 1, &step, 1, maxval + 1, NULL, rd); + if (!line) + goto fail; + } else { + step = 1; + } + + while (value <= endvalue) { + v |= 1UL << (unsigned long)(value - minval); + value += step; + } + + if (*line == ',') { + ++line; + goto next; + } + + if (*line != '\0' && !isspace(*line)) + goto fail; + while (isspace(*line)) + ++line; + + *out = v; + return line; +fail: + rdline_complain(rd, "invalid time range expression"); + return NULL; +} + +/*****************************************************************************/ + +static char *cron_interval(crontab_t *cron, rdline_t *rd) +{ + char *arg = rd->line; + size_t i, j; + + if (*(arg++) != '@') + goto fail; + for (j = 0; isalpha(arg[j]); ++j) + ; + if (j == 0 || !isspace(arg[j])) + goto fail; + + for (i = 0; i < ARRAY_SIZE(intervals); ++i) { + if (strlen(intervals[i].macro) != j) + continue; + if (strncmp(intervals[i].macro, arg, j) == 0) + break; + } + + if (i == ARRAY_SIZE(intervals)) + goto fail; + + cron->minute = intervals[i].tab.minute; + cron->hour = intervals[i].tab.hour; + cron->dayofmonth = intervals[i].tab.dayofmonth; + cron->month = intervals[i].tab.month; + cron->dayofweek = intervals[i].tab.dayofweek; + return arg + j; +fail: + rdline_complain(rd, "unknown interval '%s'", arg); + return NULL; +} + +static char *cron_fields(crontab_t *cron, rdline_t *rd) +{ + char *arg = rd->line; + uint64_t value; + + if ((arg = readfield(arg, &value, 0, 59, NULL, rd)) == NULL) + return NULL; + cron->minute = value; + + if ((arg = readfield(arg, &value, 0, 23, NULL, rd)) == NULL) + return NULL; + cron->hour = value; + + if ((arg = readfield(arg, &value, 1, 31, NULL, rd)) == NULL) + return NULL; + cron->dayofmonth = value; + + if ((arg = readfield(arg, &value, 1, 12, month, rd)) == NULL) + return NULL; + cron->month = value; + + if ((arg = readfield(arg, &value, 0, 6, weekday, rd)) == NULL) + return NULL; + cron->dayofweek = value; + + return arg; +} + +crontab_t *rdcron(int dirfd, const char *filename) +{ + crontab_t *cron, *list = NULL; + rdline_t rd; + char *ptr; + + if (rdline_init(&rd, dirfd, filename)) + return NULL; + + while (rdline(&rd) == 0) { + cron = calloc(1, sizeof(*cron)); + if (cron == NULL) { + rdline_complain(&rd, strerror(errno)); + break; + } + + if (rd.line[0] == '@') { + ptr = cron_interval(cron, &rd); + } else { + ptr = cron_fields(cron, &rd); + } + + if (ptr == NULL) { + free(cron); + continue; + } + + while (isspace(*ptr)) + ++ptr; + + cron->exec = strdup(ptr); + if (cron->exec == NULL) { + rdline_complain(&rd, strerror(errno)); + free(cron); + continue; + } + + cron->next = list; + list = cron; + } + + rdline_cleanup(&rd); + return list; +} diff --git a/rdline.c b/rdline.c new file mode 100644 index 0000000..99b4f4e --- /dev/null +++ b/rdline.c @@ -0,0 +1,77 @@ +/* SPDX-License-Identifier: ISC */ +#include "gcrond.h" + +int rdline(rdline_t *t) +{ + size_t i, len; + + do { + free(t->line); + t->line = NULL; + errno = 0; + len = 0; + + if (getline(&t->line, &len, t->fp) < 0) { + if (errno) { + rdline_complain(t, strerror(errno)); + return -1; + } + return 1; + } + + t->lineno += 1; + + for (i = 0; isspace(t->line[i]); ++i) + ; + + if (t->line[i] == '\0' || t->line[i] == '#') { + t->line[0] = '\0'; + } else if (i) { + memmove(t->line, t->line + i, len - i + 1); + } + } while (t->line[0] == '\0'); + + return 0; +} + +void rdline_complain(rdline_t *t, const char *msg, ...) +{ + va_list ap; + + fprintf(stderr, "%s: %zu: ", t->filename, t->lineno); + + va_start(ap, msg); + vfprintf(stderr, msg, ap); + va_end(ap); + + fputc('\n', stderr); +} + +int rdline_init(rdline_t *t, int dirfd, const char *filename) +{ + int fd; + + memset(t, 0, sizeof(*t)); + + fd = openat(dirfd, filename, O_RDONLY); + if (fd == -1) { + perror(filename); + return -1; + } + + t->fp = fdopen(fd, "r"); + if (t->fp == NULL) { + perror("fdopen"); + close(fd); + return -1; + } + + t->filename = filename; + return 0; +} + +void rdline_cleanup(rdline_t *t) +{ + free(t->line); + fclose(t->fp); +} |