osd-contiki/apps/time/cron.c
2016-02-26 17:30:16 +01:00

489 lines
14 KiB
C

/**
* \file
* cron: Cron-like functionality
* \author
* Ralf Schlatterbeck <rsc@runtux.com>
*
* \brief cron-like command scheduler
*
* Inspired by vixie's cron by Paul Vixie which is
* Copyright 1988,1990,1993,1994 by Paul Vixie
*
* Distribute freely, except: don't remove my name from the source or
* documentation (don't take credit for my work), mark your changes (don't
* get me blamed for your possible bugs), don't alter or remove this
* notice. May be sold if buildable source is provided to buyer. No
* warrantee of any kind, express or implied, is included with this
* software; use at your own risk, responsibility for damages (if any) to
* anyone resulting from the use of this software rests entirely with the
* user.
*
* Changes to make this work on a microcontroller by Ralf Schlatterbeck
* In fact this is mostly a rewrite but keeps central algorithms and
* some data structures of the original.
* The syntax is simplified, we don't support the @ notation (e.g.
* @hourly) and named entities (e.g. weekday names instead of numbers).
* Furthermore we can't send email and don't support environment
* variables. The called commands are functions registered via a
* registration mechanism before runtime.
* Copyright 2016 Ralf Schlatterbeck, distribute with the conditions
* given above.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <assert.h>
#include "contiki.h"
#include "cron.h"
#include "time.h"
#undef DEBUG
static struct cron_entry cron_entries [MAX_CRON_ENTRIES];
static struct cron_cmd cron_commands [MAX_CRON_COMMANDS];
static size_t cron_registered_entries = 0;
static size_t cron_registered_commands = 0;
static struct cron_entry *entry_freelist = NULL;
static struct cron_entry *entry_list = NULL;
typedef int64_t minute_t;
static time_t start_time;
static minute_t cron_clock_time;
/* Register a new cron command
* A command consists of a name (which must be unique, uniqueness is not
* enforced currently as registrations are done once at initialization
* time), a function to be called and a parameter.
*/
int cron_register_command
(const char *name, void (*function)(void *), void * parameter)
{
if (cron_registered_commands >= MAX_CRON_COMMANDS) {
return -1;
}
cron_commands [cron_registered_commands].name = name;
cron_commands [cron_registered_commands].function = function;
cron_commands [cron_registered_commands].parameter = parameter;
cron_registered_commands++;
return 0;
}
/*
* Allocate a new crontab entry.
* This allocates from the freelist and returns NULL if no more entries
* are available.
* Implementation note: If cron_registered_entries is 0 we first
* initialize the entry_freelist and the entry_list.
*/
struct cron_entry *allocate_cron_entry (void)
{
size_t n;
struct cron_entry *e;
if (cron_registered_entries == 0) {
entry_list = NULL;
entry_freelist = &cron_entries [0];
for (n=0; n < (MAX_CRON_ENTRIES - 1); n++) {
cron_entries [n].next = &cron_entries [n+1];
}
cron_entries [MAX_CRON_ENTRIES - 1].next = NULL;
}
if (cron_registered_entries >= MAX_CRON_ENTRIES) {
return NULL;
}
e = entry_freelist;
entry_freelist = e->next;
e->next = entry_list;
entry_list = e;
cron_registered_entries++;
return e;
}
void free_cron_entry (struct cron_entry *obsolete)
{
struct cron_entry *e;
assert (cron_registered_entries);
if (entry_list == obsolete) {
entry_list = obsolete->next;
obsolete->next = entry_freelist;
entry_freelist = obsolete;
cron_registered_entries--;
return;
}
for (e=entry_list; e; e=e->next) {
if (e->next == obsolete) {
e->next = obsolete->next;
obsolete->next = entry_freelist;
entry_freelist = obsolete;
cron_registered_entries--;
break;
}
}
assert (e != NULL);
}
struct cron_entry *get_cron_entry (size_t idx)
{
if (idx >= MAX_CRON_ENTRIES) {
return NULL;
}
return &cron_entries [idx];
}
/* Set the number in bits, return 0 on success -1 on error */
static int set_element (bitstr_t *bits, int low, int high, int n)
{
if (n < low || n > high) {
return -1;
}
bit_set (bits, (n - low));
return 0;
}
static const char *get_number (int *np, int high, const char *s)
{
char *endptr;
long l = strtol (s, &endptr, 10);
if (endptr == s || errno == ERANGE || l > high) {
return NULL;
}
*np = l;
return endptr;
}
static const char *get_range (bitstr_t *bits, int low, int high, const char *s)
{
int i;
int n1, n2, n3;
if (*s == '*') {
/* '*' means "first-last" but can be modified by /step */
n1 = low;
n2 = high;
if (*++s == '\0') {
return NULL;
}
} else {
s = get_number (&n1, high, s);
if (s == NULL || *s == '\0') {
return NULL;
}
if (*s != '-') {
if (set_element (bits, low, high, n1) < 0) {
return NULL;
}
return s;
} else {
if (*++s == '\0') {
return NULL;
}
s = get_number (&n2, high, s);
if (*s == '\0') {
return NULL;
}
}
}
if (*s == '/') {
if (*++s == '\0') {
return NULL;
}
s = get_number (&n3, high, s);
if (*s == '\0') {
return NULL;
}
} else {
n3 = 1;
}
/* Explicit check for sane values */
if (n1 < low || n1 > high || n2 < low || n2 > high) {
return NULL;
}
for (i=n1; i<=n2; i+=n3) {
if (set_element (bits, low, high, i) < 0) {
return NULL;
}
}
return s;
}
static const char *get_list (bitstr_t *bits, int low, int high, const char *s)
{
int done = 0;
bit_nclear (bits, 0, (high - low + 1));
while (!done) {
s = get_range (bits, low, high, s);
if (NULL == s) {
return NULL;
}
if (*s == ',') {
s++;
} else {
done = 1;
}
}
/* Skip white space */
while (s && isspace (*s)) {
s++;
}
return s;
}
/*
* Parse a single crontab entry on a single line.
* We get the line via CoAP.
* Returns 0 if parsed successfully, -1 on error.
* On error the err pointer is set to the error message.
*/
int parse_crontab_line
(const char *line, struct cron_entry *e, const char **err)
{
size_t n;
const char *s = line;
size_t cmd_len = 0;
e->flags &= ~VALID;
if (*s == '*') {
e->flags |= MIN_STAR;
}
s = get_list (e->minute, FIRST_MINUTE, LAST_MINUTE, s);
if (NULL == s || *s == '\0') {
*err = "minute";
return -1;
}
if (*s == '*') {
e->flags |= HR_STAR;
}
s = get_list (e->hour, FIRST_HOUR, LAST_HOUR, s);
if (NULL == s || *s == '\0') {
*err = "hour";
return -1;
}
if (*s == '*') {
e->flags |= DOM_STAR;
}
s = get_list (e->dom, FIRST_DOM, LAST_DOM, s);
if (NULL == s || *s == '\0') {
*err = "dom";
return -1;
}
s = get_list (e->month, FIRST_MONTH, LAST_MONTH, s);
if (NULL == s || *s == '\0') {
*err = "month";
return -1;
}
if (*s == '*') {
e->flags |= DOW_STAR;
}
s = get_list (e->dow, FIRST_DOW, LAST_DOW, s);
if (NULL == s || *s == '\0') {
*err = "dow";
return -1;
}
/* Make sundays equivalent */
if (bit_test (e->dow, 0) || bit_test (e->dow, 7)) {
bit_set (e->dow, 0);
bit_set (e->dow, 7);
}
/* strip whitespace at *end* of command
* by getting length without whitespace
*/
for (n=0; s [n]; n++) {
if (!isspace (s [n])) {
cmd_len = n + 1;
}
}
for (n=0; n<cron_registered_commands; n++) {
if ( cmd_len == strlen (cron_commands [n].name)
&& !strncmp (cron_commands [n].name, s, cmd_len)
)
{
e->cmd = &cron_commands [n];
break;
}
}
if (n == cron_registered_commands) {
*err = "command";
return -1;
}
e->flags |= VALID;
return 0;
}
static void set_time (void)
{
struct tm *tm;
struct timeval tv;
gettimeofday (&tv, NULL);
start_time = tv.tv_sec;
tm = localtime (&start_time);
/* We adjust the time to GMT so we can catch DST changes */
cron_clock_time = (start_time + tm->tm_gmtoff) / (time_t)SECONDS_PER_MINUTE;
}
static void find_jobs (minute_t vtime, int do_wild, int do_nonwild)
{
time_t virtual_second = vtime * SECONDS_PER_MINUTE;
struct tm *tm = gmtime (&virtual_second);
int minute, hour, dom, month, dow;
struct cron_entry *e;
/* make 0-based values out of these so we can use them as indicies */
minute = tm->tm_min -FIRST_MINUTE;
hour = tm->tm_hour -FIRST_HOUR;
dom = tm->tm_mday -FIRST_DOM;
month = tm->tm_mon +1 /* 0..11 -> 1..12 */ -FIRST_MONTH;
dow = tm->tm_wday -FIRST_DOW;
#ifdef DEBUG
printf ("%d %d %d %d %d\n", minute, hour, dom, month, dow);
#endif
/* the dom/dow situation is odd. '* * 1,15 * Sun' will run on the
* first and fifteenth AND every Sunday; '* * * * Sun' will run *only*
* on Sundays; '* * 1,15 * *' will run *only* the 1st and 15th. this
* is why we keep 'e->dow_star' and 'e->dom_star'. yes, it's bizarre.
* like many bizarre things, it's the standard.
*/
for (e = entry_list; e; e = e->next) {
#ifdef DEBUG
if (e->flags & VALID) {
printf ("Checking entry %s\n", e->cmd->name);
}
printf ("valid: %d\n", (e->flags & VALID));
printf ("minute: %d\n", (bit_test (e->minute, minute)));
printf ("hour: %d\n", (bit_test (e->hour, hour)));
printf ("month %d\n", (bit_test (e->month, month)));
printf ("dom* %d\n", (e->flags & DOM_STAR));
printf ("dow* %d\n", (e->flags & DOW_STAR));
printf ("dow %d\n", (bit_test (e->dow, dow)));
printf ("dom %d\n", (bit_test (e->dom, dom)));
printf ("min* %d\n", (e->flags & MIN_STAR));
printf ("hr* %d\n", (e->flags & HR_STAR));
printf ("nonwild %d\n", (do_nonwild));
#endif
if ( (e->flags & VALID)
&& bit_test (e->minute, minute)
&& bit_test (e->hour, hour)
&& bit_test (e->month, month)
&& ( ((e->flags & DOM_STAR) || (e->flags & DOW_STAR))
? (bit_test (e->dow, dow) && bit_test (e->dom, dom))
: (bit_test (e->dow, dow) || bit_test (e->dom, dom))
)
)
{
if ( (do_nonwild && !(e->flags & (MIN_STAR | HR_STAR)))
|| (do_wild && (e->flags & (MIN_STAR | HR_STAR)))
)
{
printf ("Cron: calling \"%s\"\n", e->cmd->name);
e->cmd->function (e->cmd->parameter);
}
}
}
}
/*
* The cron callback function must be run regularly, at least once per
* minute.
* Implementation:
* Clocks are in minutes since the epoch (time() / 60).
* virtual_time is the time it *would* be if we are called promptly and
* nobody ever changed the clock. It is monotonically increasing...
* unless a timejump happens.
* time_running is the time we were last called. We determine
* initialization by checking time_running for -1.
* cron_clock_time is the current time for this run.
*/
void cron (void)
{
static minute_t time_running = -1;
static minute_t virtual_time = -1;
minute_t time_diff;
/*
* ... calculate how the current time differs from
* our virtual clock. Classify the change into one
* of 4 cases
*/
set_time ();
if (time_running == cron_clock_time) {
#ifdef DEBUG
printf ("time not reached\n");
#endif
return;
}
time_running = cron_clock_time;
time_diff = time_running - virtual_time;
#ifdef DEBUG
printf ("time_diff: %ld\n", (long)time_diff);
#endif
/* shortcut for the most common case */
if (time_diff == 1) {
virtual_time = time_running;
find_jobs (virtual_time, 1, 1);
} else {
int wakeup_kind = -1;
if (time_diff > -(3*MINUTE_COUNT)) {
wakeup_kind = 0;
}
if (time_diff > 0) {
wakeup_kind = 1;
}
if (time_diff > 5) {
wakeup_kind = 2;
}
if (time_diff > (3*MINUTE_COUNT)) {
wakeup_kind = 3;
}
#ifdef DEBUG
printf ("wakeup_kind: %d\n", wakeup_kind);
#endif
switch (wakeup_kind) {
/* time_diff is a small positive number (wokeup late)
* run jobs for each virtual minute until caught up.
*/
case 1:
do {
virtual_time++;
find_jobs (virtual_time, 1, 1);
} while (virtual_time < time_running);
break;
/* time_diff is a medium-sized positive number, for example
* because we went to DST. Run wildcard jobs once, then run any
* fixed-time jobs that would otherwise be skipped. If we use up
* our minute (possible, if there are a lot of jobs to run) go
* around the loop again so that wildcard jobs have a chance to
* run, and we do our housekeeping.
*/
case 2:
/* run wildcard jobs for current minute */
find_jobs (time_running, 1, 0);
/* run fixed-time jobs for each minute missed */
do {
virtual_time++;
find_jobs (virtual_time, 0, 1);
set_time ();
} while ( virtual_time < time_running
&& cron_clock_time == time_running
);
break;
/* time_diff is a small or medium-sized negative num, eg.
* because of DST ending. Just run the wildcard jobs. The
* fixed-time jobs probably have already run, and should not be
* repeated. virtual_time does not change until we are caught up.
*/
case 0:
find_jobs (time_running, 1, 0);
break;
/* Other: time has changed a *lot*, jump virtual time, and run
* everything
*/
default:
virtual_time = time_running;
find_jobs (time_running, 1, 1);
break;
}
}
}