From 59e1d376db325c495bdb2369e7fa0404e5d330e8 Mon Sep 17 00:00:00 2001 From: Joachim Nilsson Date: Sun, 30 Oct 2016 14:51:52 +0100 Subject: [PATCH] cron: New API for absolute timers This patch adds support for at/cron-like jobs that need an absolute timer. The API supports waking up with an UEV_HUP callback event if the wall clock changed due to NTP or user input while waiting. - uev_cron_init() - uev_cron_set() - uev_cron_start() - uev_cron_stop() A very simple cronrun example/test program is also included. Signed-off-by: Joachim Nilsson --- .gitignore | 3 - README.md | 30 +++++--- cron.c | 165 +++++++++++++++++++++++++++++++++++++++++++ examples/.gitignore | 4 ++ examples/Makefile.am | 2 +- examples/cronrun.c | 68 ++++++++++++++++++ private.h | 13 +++- uev.c | 25 +++++++ uev.h | 6 ++ 9 files changed, 301 insertions(+), 15 deletions(-) create mode 100644 cron.c create mode 100644 examples/.gitignore create mode 100644 examples/cronrun.c diff --git a/.gitignore b/.gitignore index 3ecf72d..adf0623 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,6 @@ *.html .deps .libs -examples/joystick -examples/signal compile config.* configure @@ -26,7 +24,6 @@ aclocal.m4 ar-lib autom4te.cache bench -test libuev*.tar.* libuev.pc !libuev.pc.in diff --git a/README.md b/README.md index 7bdac22..05f053d 100644 --- a/README.md +++ b/README.md @@ -35,17 +35,25 @@ For details, see [this article][4] at [lwn.net](http://lwn.net). API --- -The C interface to [libuEv][] is listed in `uev.h`. It handles three -different types of events: I/O (files, sockets, message queues, etc.), -timers, and signals. With a slight caveat on signals detailed below -in the [Summary](#summary). +The C API to [libuEv][], listed in `uev/uev.h`, handles three different +types of events: I/O (files, sockets, message queues, etc.), timers, and +signals. The [Summary](#summary) details a slight caveat on signals. + +Timers can be either relative, timeout in milliseconds, or absolute with +a time given in `time_t`, see `mktime()` et al. Absolute timers are +called cron timers and their callbacks get an `UEV_HUP` error event if +the wall clock changes, either via NTP or user input. + +**NOTE:** On some systems, embedded systems in particular, `time_t` is a +32-bit integer that wraps around in the year 2038. [libuEv][] cannot +protect you against this problem, unfortunately. ```C /* - * Callback example, arg is passed from watcher's *_init() - * w->fd holds the file descriptor, events is set by libuEv - * to indicate if any of UEV_READ and/or UEV_WRITE is ready. + * Callback example, arg comes from the watcher's *_init() function, + * w->fd holds the file descriptor or socket, and events is set by + * libuEv to indicate status: UEV_HUP, UEV_READ and/or UEV_WRITE */ void callback (uev_t *w, void *arg, int events); @@ -60,12 +68,18 @@ in the [Summary](#summary). int uev_io_start (uev_t *w); int uev_io_stop (uev_t *w); - /* Timer watcher: timeout and period in milliseconds */ + /* Timer watcher: schedule a relative timer, timeout and period in milliseconds */ int uev_timer_init (uev_ctx_t *ctx, uev_t *w, uev_cb_t *cb, void *arg, int timeout, int period); int uev_timer_set (uev_t *w, int timeout, int period); /* Change timeout or period */ int uev_timer_start (uev_t *w); /* Restart a stopped timer */ int uev_timer_stop (uev_t *w); /* Stop a timer */ + /* Cron watcher: schedule an absolute timer, when and period in time_t seconds */ + int uev_cron_init (uev_ctx_t *ctx, uev_t *w, uev_cb_t *cb, void *arg, time_t when, time_t period); + int uev_cron_set (uev_t *w, int when, time_t period); /* Change when or period */ + int uev_cron_start (uev_t *w); /* Restart a stopped cron */ + int uev_cron_stop (uev_t *w); /* Stop a cron */ + /* Signal watcher: signo is the signal to wait for, e.g., SIGTERM */ int uev_signal_init (uev_ctx_t *ctx, uev_t *w, uev_cb_t *cb, void *arg, int signo); int uev_signal_set (uev_t *w, int signo); /* Change signal to wait for */ diff --git a/cron.c b/cron.c new file mode 100644 index 0000000..1343bdc --- /dev/null +++ b/cron.c @@ -0,0 +1,165 @@ +/* libuEv - Micro event loop library + * + * Copyright (c) 2012 Flemming Madsen + * Copyright (c) 2013-2016 Joachim Nilsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include /* memset() */ +#include +#include /* close(), read() */ + +#include "uev.h" + +/* Missing defines in GLIBC <= 2.24 */ +#ifndef TFD_TIMER_CANCEL_ON_SET +#define TFD_TIMER_CANCEL_ON_SET (1 << 1) +#endif +#ifndef TFD_SETTIME_FLAGS +#define TFD_SETTIME_FLAGS (TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET) +#endif + + +/** + * Create and start an at/cron job watcher + * @param ctx A valid libuEv context + * @param w Pointer to an uev_t watcher + * @param cb Callback function for cron job + * @param arg Optional callback argument + * @param when First point in time to call @param cb + * @param interval For an at job this is zero, for cron the offset interval + * + * For at jobs set @param interval to zero and only use @param when. For + * cron jobs, set @param interval to the offset. E.g., if the job should + * run every five minutes set the @param tm_min of struct tm to five. + * + * Use mktime() to create the time_t arguments. The special value zero + * may be used for @param when to denote 'now', where 'now' is when the + * event loop is started. You can also treat time_t simply as a signed + * integer. E.g., set @param interval to 3600 to create a cron job that + * runs every hour. + * + * @return POSIX OK(0) or non-zero with @param errno set on error. + */ +int uev_cron_init(uev_ctx_t *ctx, uev_t *w, uev_cb_t *cb, void *arg, time_t when, time_t interval) +{ + int fd; + + if (when < 0 || interval < 0) { + errno = ERANGE; + return -1; + } + + fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK | TFD_CLOEXEC); + if (fd < 0) + return -1; + + if (uev_watcher_init(ctx, w, UEV_CRON_TYPE, cb, arg, fd, UEV_READ)) + goto exit; + + if (uev_cron_set(w, when, interval)) { + uev_watcher_stop(w); + exit: + close(fd); + return -1; + } + + return 0; +} + +/** + * Reset an at/cron job watcher + * @param w Watcher to reset + * @param when First point in time to call @param cb + * @param interval For an at job this is zero, for cron the offset interval + * + * @return POSIX OK(0) or non-zero with @param errno set on error. + */ +int uev_cron_set(uev_t *w, time_t when, time_t interval) +{ + /* Every watcher must be registered to a context */ + if (!w || !w->ctx) { + errno = EINVAL; + return -1; + } + + if (when < 0 || interval < 0) { + errno = ERANGE; + return -1; + } + + /* Handle stopped timers */ + if (w->fd < 0) { + /* Timer already stopped */ + if (!when && !interval) + return 0; + + /* Remove from internal list */ + LIST_REMOVE(w, link); + + if (uev_cron_init(w->ctx, w, (uev_cb_t *)w->cb, w->arg, when, interval)) + return -1; + } + + w->u.c.when = when; + w->u.c.interval = interval; + + if (w->ctx->running) { + struct itimerspec time; + + memset(&time, 0, sizeof(time)); + time.it_value.tv_sec = when; + time.it_interval.tv_sec = interval; + if (timerfd_settime(w->fd, TFD_SETTIME_FLAGS, &time, NULL) < 0) + return 1; + } + + return uev_watcher_start(w); +} + +/** + * Start a stopped at/cron job watcher + * @param w Watcher to start (again) + * + * @return POSIX OK(0) or non-zero with @param errno set on error. + */ +int uev_cron_start(uev_t *w) +{ + return uev_timer_start(w); +} + +/** + * Stop and unregister an at/cron job watcher + * @param w Watcher to stop + * + * @return POSIX OK(0) or non-zero with @param errno set on error. + */ +int uev_cron_stop(uev_t *w) +{ + return uev_timer_stop(w); +} + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..1822c3c --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,4 @@ +joystick +signal +cronrun +test diff --git a/examples/Makefile.am b/examples/Makefile.am index b2be953..eaad9ca 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -1,4 +1,4 @@ -noinst_PROGRAMS = signal test joystick +noinst_PROGRAMS = signal test joystick cronrun AM_CPPFLAGS = -I$(top_srcdir) AM_LDFLAGS = -L$(top_srcdir) LIBS = $(LTLIBINTL) diff --git a/examples/cronrun.c b/examples/cronrun.c new file mode 100644 index 0000000..a9281ff --- /dev/null +++ b/examples/cronrun.c @@ -0,0 +1,68 @@ +/* Test cron API in libuEv event library + * + * Copyright (c) 2016 Joachim Nilsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include +#include +#include "uev.h" + +#define UNUSED(arg) arg __attribute__ ((unused)) + + +static void cron_job(uev_t *w, void *arg, int events) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + printf("Cron job HELO %s", ctime(&tv.tv_sec)); +} + +int main(void) +{ + uev_t cron_watcher; + uev_ctx_t ctx; + time_t when, interval; + struct timeval tv; + + /* Initialize libuEv */ + uev_init(&ctx); + + /* Cron job */ + gettimeofday(&tv, NULL); + when = tv.tv_sec + 30; + interval = 30; + uev_cron_init(&ctx, &cron_watcher, cron_job, NULL, when, interval); + + /* Start event loop */ + printf("Start of test %s", ctime(&tv.tv_sec)); + printf("Expected cron %s", ctime(&when)); + + return uev_run(&ctx, 0); +} + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/private.h b/private.h index bfc3e42..fecb51e 100644 --- a/private.h +++ b/private.h @@ -31,13 +31,14 @@ /* I/O, timer, or signal watcher */ typedef enum { - UEV_IO_TYPE = 1, - UEV_TIMER_TYPE, + UEV_CRON_TYPE = 1, + UEV_IO_TYPE, UEV_SIGNAL_TYPE, + UEV_TIMER_TYPE, } uev_type_t; /* Event mask, used internally only. */ -#define UEV_EVENT_MASK (UEV_READ | UEV_WRITE | UEV_PRI) +#define UEV_EVENT_MASK (UEV_READ | UEV_WRITE | UEV_PRI | UEV_HUP) /* Main libuEv context type */ typedef struct { @@ -63,6 +64,12 @@ struct uev; \ /* Arguments for different watchers */ \ union { \ + /* Cron watchers */ \ + struct { \ + time_t when; \ + time_t interval; \ + } c; \ + \ /* Timer watchers, time in milliseconds */ \ struct { \ int timeout; \ diff --git a/uev.c b/uev.c index ba91bb1..99cf505 100644 --- a/uev.c +++ b/uev.c @@ -166,6 +166,7 @@ int uev_exit(uev_ctx_t *ctx) continue; switch (w->type) { + case UEV_CRON_TYPE: case UEV_TIMER_TYPE: uev_timer_stop(w); break; @@ -219,6 +220,8 @@ int uev_run(uev_ctx_t *ctx, int flags) /* Start all dormant timers */ LIST_FOREACH(w, &ctx->watchers, link) { + if (UEV_CRON_TYPE == w->type) + uev_cron_set(w, w->u.c.when, w->u.c.interval); if (UEV_TIMER_TYPE == w->type) uev_timer_set(w, w->u.t.timeout, w->u.t.period); } @@ -274,6 +277,24 @@ int uev_run(uev_ctx_t *ctx, int flags) } } + if (UEV_CRON_TYPE == w->type) { + uint64_t exp; + + if (read(w->fd, &exp, sizeof(exp)) != sizeof(exp)) { + if (errno != ECANCELED) { + uev_exit(ctx); + return -3; + } + + events[i].events = UEV_HUP; + } + + if (!w->u.c.interval) + w->u.c.when = 0; + else + w->u.c.when += w->u.c.interval; + } + if (UEV_TIMER_TYPE == w->type) { uint64_t exp; @@ -299,6 +320,10 @@ int uev_run(uev_ctx_t *ctx, int flags) if (w->cb) w->cb(w, w->arg, events[i].events & UEV_EVENT_MASK); + if (UEV_CRON_TYPE == w->type) { + if (!w->u.c.when) + uev_timer_stop(w); + } if (UEV_TIMER_TYPE == w->type) { if (!w->u.t.timeout) uev_timer_stop(w); diff --git a/uev.h b/uev.h index 8b79efa..80ebdb7 100644 --- a/uev.h +++ b/uev.h @@ -35,6 +35,7 @@ #define UEV_READ EPOLLIN #define UEV_WRITE EPOLLOUT #define UEV_PRI EPOLLPRI +#define UEV_HUP EPOLLHUP /* Run flags */ #define UEV_ONCE 1 @@ -69,6 +70,11 @@ int uev_timer_set (uev_t *w, int timeout, int period); int uev_timer_start (uev_t *w); int uev_timer_stop (uev_t *w); +int uev_cron_init (uev_ctx_t *ctx, uev_t *w, uev_cb_t *cb, void *arg, time_t when, time_t interval); +int uev_cron_set (uev_t *w, time_t when, time_t interval); +int uev_cron_start (uev_t *w); +int uev_cron_stop (uev_t *w); + int uev_signal_init (uev_ctx_t *ctx, uev_t *w, uev_cb_t *cb, void *arg, int signo); int uev_signal_set (uev_t *w, int signo); int uev_signal_start (uev_t *w);