mirror of https://github.com/pygos/cron
commit
d2b3a983e1
15 changed files with 992 additions and 0 deletions
@ -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. |
@ -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
|
@ -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. |
@ -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]) |
@ -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; |
||||
} |
@ -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); |
||||
} |
@ -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 |
@ -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; |
||||
} |
@ -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 */ |
@ -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 |
||||
]) |
||||
|
@ -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"])]) |
||||
]) |
||||
|
@ -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; |
||||
} |
@ -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); |
||||
} |
Loading…
Reference in new issue