diff --git a/Makefile b/Makefile index 20e8ff58..2f1d995e 100644 --- a/Makefile +++ b/Makefile @@ -114,6 +114,7 @@ SRCS = src/version.c \ src/input.c \ src/http/http_client.c \ src/fsmonitor.c \ + src/cron.c \ SRCS += \ src/api.c \ diff --git a/src/cron.c b/src/cron.c new file mode 100644 index 00000000..0dbcbbfd --- /dev/null +++ b/src/cron.c @@ -0,0 +1,329 @@ +/* + * Tvheadend - cron routines + * + * Copyright (C) 2014 Adam Sutton + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "cron.h" + +#include +#include +#include + +/* + * Parse value + */ +static int +cron_parse_val ( const char *str, const char **key, int *v ) +{ + int i = 0; + if (key) { + while (key[i]) { + if (!strncasecmp(str, key[i], strlen(key[i]))) { + *v = i; + return 0; + } + i++; + } + } + + return sscanf(str, "%d", v) == 1 ? 0 : 1; +} + +/* + * Parse individual field in cron spec + */ +static int +cron_parse_field + ( const char **istr, uint64_t *field, uint64_t mask, int bits, int off, + const char **key ) +{ + int sn = -1, en = -1, mn = -1; + const char *str = *istr; + const char *beg = str; + uint64_t val = 0; + while ( 1 ) { + if ( *str == '*' ) { + sn = 0; + en = bits - 1; + beg = NULL; + } else if ( *str == ',' || *str == ' ' || *str == '\0' ) { + if (beg) + if (cron_parse_val(beg, key, en == -1 ? (sn == -1 ? &sn : &en) : &mn)) + return 1; + if ((sn - off) >= bits || (en - off) >= bits || mn > bits) + return 1; + if (en < 0) en = sn; + if (mn < 0) mn = 1; + while (sn <= en) { + if ( (sn % mn) == 0 ) + val |= (0x1LL << (sn - off)); + sn++; + } + if (*str != ',') break; + sn = en = mn = -1; + beg = (str + 1); + } else if ( *str == '/' ) { + if (beg) + if (en == -1 || cron_parse_val(beg, key, sn == -1 ? &sn : &en)) + return 1; + beg = (str + 1); + } else if ( *str == '-' ) { + if (sn != -1 || cron_parse_val(beg, key, &sn)) + return 1; + beg = (str + 1); + } + str++; + } + if (*str == ' ') str++; + *istr = str; + *field = (val | ((val >> bits) & 0x1)) & mask; + return 0; +} + +/* + * Set value + */ +int +cron_set ( cron_t *c, const char *str ) +{ + uint64_t ho, mi, mo, dm, dw; + static const char *days[] = { + "sun", "mon", "tue", "wed", "thu", "fri", "sat" + }; + static const char *months[] = { + "ignore", + "jan", "feb", "mar", "apr", "may", "jun", + "jul", "aug", "sep", "oct", "nov", "dec" + }; + + /* Daily (01:01) */ + if ( !strcmp(str, "@daily") ) { + c->c_min = 1; + c->c_hour = 1; + c->c_mday = CRON_MDAY_MASK; + c->c_mon = CRON_MON_MASK; + c->c_wday = CRON_WDAY_MASK; + + /* Hourly (XX:02) */ + } else if ( !strcmp(str, "@hourly") ) { + c->c_min = 2; + c->c_hour = CRON_HOUR_MASK; + c->c_mday = CRON_MDAY_MASK; + c->c_mon = CRON_MON_MASK; + c->c_wday = CRON_WDAY_MASK; + + /* Standard */ + } else { + if (cron_parse_field(&str, &mi, CRON_MIN_MASK, 60, 0, NULL) || !mi) + return 1; + if (cron_parse_field(&str, &ho, CRON_HOUR_MASK, 24, 0, NULL) || !ho) + return 1; + if (cron_parse_field(&str, &dm, CRON_MDAY_MASK, 31, 1, NULL) || !dm) + return 1; + if (cron_parse_field(&str, &mo, CRON_MON_MASK, 12, 1, months) || !mo) + return 1; + if (cron_parse_field(&str, &dw, CRON_WDAY_MASK, 7, 0, days) || !dw) + return 1; + c->c_min = mi; + c->c_hour = ho; + c->c_mday = dm; + c->c_mon = mo; + c->c_wday = dw; + } + + return 0; +} + +/* + * Check for leap year + */ +static int +is_leep_year ( int year ) +{ + if (!(year % 400)) + return 1; + if (!(year % 100)) + return 0; + return (year % 4) ? 0 : 1; +} + +/* + * Check for days in month + */ +static int +days_in_month ( int year, int mon ) +{ + int d; + if (mon == 2) + d = 28 + is_leep_year(year); + else + d = 30 + ((0x15AA >> mon) & 0x1); + return d; +} + +/* + * Find the next time (starting from now) that the cron should fire + */ +int +cron_next ( cron_t *c, const time_t now, time_t *ret ) +{ + struct tm nxt; + int endyear; + localtime_r(&now, &nxt); + endyear = nxt.tm_year + 10; + + /* Invalid day */ + if (!(c->c_mday & (0x1LL << (nxt.tm_mday-1))) || + !(c->c_wday & (0x1LL << (nxt.tm_wday))) || + !(c->c_mon & (0x1LL << (nxt.tm_mon))) ) { + nxt.tm_min = 0; + nxt.tm_hour = 0; + + /* Invalid hour */ + } else if (!(c->c_hour & (0x1LL << nxt.tm_hour))) { + nxt.tm_min = 0; + + /* Increment */ + } else { + ++nxt.tm_min; + } + + /* Minute */ + while (!(c->c_min & (0x1LL << nxt.tm_min))) { + if (nxt.tm_min == 60) { + ++nxt.tm_hour; + nxt.tm_min = 0; + } else + nxt.tm_min++; + } + + /* Hour */ + while (!(c->c_hour & (0x1LL << nxt.tm_hour))) { + if (nxt.tm_hour == 24) { + ++nxt.tm_mday; + ++nxt.tm_wday; + nxt.tm_hour = 0; + } else + ++nxt.tm_hour; + } + + /* Date */ + if (nxt.tm_wday == 7) + nxt.tm_wday = 0; + if (nxt.tm_mday == days_in_month(nxt.tm_year+1900, nxt.tm_mon+1)) { + nxt.tm_mday = 1; + nxt.tm_mon++; + if (nxt.tm_mon == 12) { + nxt.tm_mon = 0; + ++nxt.tm_year; + } + } + while (!(c->c_mday & (0x1LL << (nxt.tm_mday-1))) || + !(c->c_wday & (0x1LL << (nxt.tm_wday))) || + !(c->c_mon & (0x1LL << (nxt.tm_mon))) ) { + + /* Stop possible infinite loop on invalid request */ + if (nxt.tm_year >= endyear) + return -1; + + /* Increment day of week */ + if (++nxt.tm_wday == 7) + nxt.tm_wday = 0; + + /* Increment day */ + if (++nxt.tm_mday > days_in_month(nxt.tm_year+1900, nxt.tm_mon+1)) { + nxt.tm_mday = 1; + if (++nxt.tm_mon == 12) { + nxt.tm_mon = 0; + ++nxt.tm_year; + } + } + + /* Shortcut the month */ + while (!(c->c_mon & (0x1LL << nxt.tm_mon))) { + nxt.tm_wday + += 1 + (days_in_month(nxt.tm_year+1900, nxt.tm_mon+1) - nxt.tm_mday); + nxt.tm_mday = 1; + if (++nxt.tm_mon >= 12) { + nxt.tm_mon = 0; + ++nxt.tm_year; + } + } + nxt.tm_wday %= 7; + } + + /* Create time */ + // TODO: not sure this will provide the correct time with respect to DST! + nxt.tm_isdst = 0; + *ret = mktime(&nxt); + return 0; +} + +/* + * Testing + */ +#if 0 +static +void print_bits ( uint64_t b, int n ) +{ + while (n) { + printf("%d", (int)(b & 0x1)); + b >>= 1; + n--; + } +} + +int +main ( int argc, char **argv ) +{ + cron_t c; + time_t n; + struct tm tm; + char buf[128]; + + time(&n); + if (cron_set(&c, argv[1])) + printf("INVALID CRON: %s\n", argv[1]); + else { + printf("min = "); print_bits(c.c_min, 60); printf("\n"); + printf("hour = "); print_bits(c.c_hour, 24); printf("\n"); + printf("mday = "); print_bits(c.c_mday, 31); printf("\n"); + printf("mon = "); print_bits(c.c_mon, 12); printf("\n"); + printf("wday = "); print_bits(c.c_wday, 7); printf("\n"); + + localtime_r(&n, &tm); + strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", &tm); + printf("NOW: %s\n", buf); + + if (cron_next(&c, n, &n)) { + printf("FAILED to find NEXT\n"); + return 1; + } + localtime_r(&n, &tm); + strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", &tm); + printf("NXT: %s\n", buf); + + } + return 0; +} +#endif + +/****************************************************************************** + * Editor Configuration + * + * vim:sts=2:ts=2:sw=2:et + *****************************************************************************/ diff --git a/src/cron.h b/src/cron.h new file mode 100644 index 00000000..155afa17 --- /dev/null +++ b/src/cron.h @@ -0,0 +1,68 @@ +/* + * Tvheadend - cron routines + * + * Copyright (C) 2014 Adam Sutton + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef __TVH_CRON_H__ +#define __TVH_CRON_H__ + +#include +#include + +#define CRON_MIN_MASK (0x0FFFFFFFFFFFFFFFLL) // 60 bits +#define CRON_HOUR_MASK (0x00FFFFFF) // 24 bits +#define CRON_MDAY_MASK (0x7FFFFFFF) // 31 bits +#define CRON_MON_MASK (0x0FFF) // 12 bits +#define CRON_WDAY_MASK (0x7F) // 7 bits + +typedef struct cron +{ + uint64_t c_min; ///< Minute mask + uint32_t c_hour; ///< Hour mask + uint32_t c_mday; ///< Day of the Month mask + uint16_t c_mon; ///< Month mask + uint8_t c_wday; ///< Day of the Week mask +} cron_t; + +/** + * Initialise from a string + * + * @param c The cron instance to update + * @param str String representation of the cron + * + * @return 0 if OK, 1 if failed to parse + */ +int cron_set ( cron_t *c, const char *str ); + +/** + * Determine the next time a cron will run (from cur) + * + * @param c The cron to check + * @param now The current time + * @param nxt The next time to execute + * + * @return 0 if next time was found + */ +int cron_next ( cron_t *c, const time_t cur, time_t *nxt ); + +#endif /* __TVH_CRON_H__ */ + +/****************************************************************************** + * Editor Configuration + * + * vim:sts=2:ts=2:sw=2:et + *****************************************************************************/