Initial commit

Signed-off-by: David Oberhollenzer <david.oberhollenzer@tele2.at>
This commit is contained in:
David Oberhollenzer 2018-11-18 21:24:39 +01:00
commit d2b3a983e1
15 changed files with 992 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
*.o
.deps
.dirstamp
Makefile
Makefile.in
config.*
aclocal.m4
autom4te.cache/
compile
configure
depcomp
install-sh
missing
stamp-h1
gcrond

13
LICENSE Normal file
View File

@ -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.

13
Makefile.am Normal file
View File

@ -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

122
README.md Normal file
View File

@ -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.

3
autogen.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
autoreconf --force --install --symlink

38
configure.ac Normal file
View File

@ -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])

56
cronscan.c Normal file
View File

@ -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;
}

61
crontab.c Normal file
View File

@ -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);
}

7
crontab/0-example Normal file
View File

@ -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

128
gcrond.c Normal file
View File

@ -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;
}

66
gcrond.h Normal file
View File

@ -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 */

35
m4/ac_define_dir.m4 Normal file
View File

@ -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
])

40
m4/compiler.m4 Normal file
View File

@ -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"])])
])

318
rdcron.c Normal file
View File

@ -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;
}

77
rdline.c Normal file
View File

@ -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);
}