diff --git a/config.mk b/config.mk index 38a137d7bfd8116ced429a4b90f96de652a7dbdb..ca0485d00a3e63bfe961e4e37f3f90c32d65bd08 100644 --- a/config.mk +++ b/config.mk @@ -9,3 +9,4 @@ MODULES += coroutine MODULES += mqtt MODULES += terminal MODULES += main +MODULES += timer diff --git a/examples/timer/Makefile b/examples/timer/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..31374b4729edded3f2e0f72ac1912092891f0e39 --- /dev/null +++ b/examples/timer/Makefile @@ -0,0 +1,6 @@ +all test clean distclean: + @for i in $(shell ls) ; do \ + if [ -d $$i ]; then \ + $(MAKE) -C $$i $@ || exit $$? ; \ + fi \ + done diff --git a/examples/timer/weekly_timer/Makefile b/examples/timer/weekly_timer/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..caed9d75185ca4c18327040ff0ddc93633f732a6 --- /dev/null +++ b/examples/timer/weekly_timer/Makefile @@ -0,0 +1,11 @@ +EXE_NAME := example/timer/weekly_timer + +CPP_SRC_FILES := main.cpp + +CXXFLAGS := -DLOG_MODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) +LDFLAGS += \ + -ltbox_timer \ + -ltbox_event \ + -ltbox_base \ + +include ${TOP_DIR}/tools/exe_common.mk diff --git a/examples/timer/weekly_timer/main.cpp b/examples/timer/weekly_timer/main.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9d4fdfc132aa29011695186c927b521c7234cd79 --- /dev/null +++ b/examples/timer/weekly_timer/main.cpp @@ -0,0 +1,54 @@ +#include + +#include +#include +#include + +#include +#include +#include + +using namespace std; +using namespace tbox; +using namespace tbox::event; + +void PrintUsage(const char *process_name) { + cout << "Usage:" << process_name << " seconds_from_00 week_mask timezone_offset_minutes" << endl; +} + +int main(int argc, char *argv[]) { + if (argc < 4) { + PrintUsage(argv[0]); + return 0; + } + + int seconds_from_00 = 0; + std::string week_mask; + int timezone_offset_minutes = 0; + + try { + seconds_from_00 = std::stoi(argv[1]); + week_mask = argv[2]; + timezone_offset_minutes = std::stoi(argv[3]); + } catch (const std::exception &e) { + PrintUsage(argv[0]); + return 0; + } + + LogOutput_Initialize(argv[0]); + LogInfo("second:%d, week_mask:%s, timezone:%d", seconds_from_00, + week_mask.c_str(), timezone_offset_minutes); + + Loop* sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + timer::WeeklyTimer tmr(sp_loop); + tmr.initialize(seconds_from_00, week_mask, timezone_offset_minutes); + tmr.setCallback([]{ LogInfo("time is up"); }); + tmr.enable(); + + sp_loop->runLoop(Loop::Mode::kForever); + + LogOutput_Cleanup(); + return 0; +} diff --git a/modules/eventx/Makefile b/modules/eventx/Makefile index 1836f2315c99c03359cd188d70d70fb83dfe8447..fd537b48f5dd7d73b14bcad48ec215cf5d8be99a 100644 --- a/modules/eventx/Makefile +++ b/modules/eventx/Makefile @@ -22,7 +22,7 @@ TEST_CPP_SRC_FILES = \ timeout_monitor_test.cpp \ request_pool_test.cpp \ -TEST_LDFLAGS := $(LDFLAGS) -ltbox_event -ltbox_base -levent_core -lev +TEST_LDFLAGS := $(LDFLAGS) -ltbox_event -ltbox_base ENABLE_SHARED_LIB = no include ../../tools/lib_common.mk diff --git a/modules/timer/Makefile b/modules/timer/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..e93e0e67a494706ec2f74d66abd8952cad45dfe2 --- /dev/null +++ b/modules/timer/Makefile @@ -0,0 +1,22 @@ +LIB_NAME = timer +LIB_VERSION_X = 0 +LIB_VERSION_Y = 0 +LIB_VERSION_Z = 1 + +HEAD_FILES = \ + weekly_timer.h \ + crontab_timer.h \ + +CPP_SRC_FILES = \ + weekly_timer.cpp \ + crontab_timer.cpp \ + ccronexpr.cpp \ + +CXXFLAGS := -DLOG_MODULE_ID='"timer"' $(CXXFLAGS) + +TEST_CPP_SRC_FILES = + +TEST_LDFLAGS := $(LDFLAGS) -ltbox_event -ltbox_base +ENABLE_SHARED_LIB = no + +include ../../tools/lib_common.mk diff --git a/modules/timer/ccronexpr.cpp b/modules/timer/ccronexpr.cpp new file mode 100644 index 0000000000000000000000000000000000000000..09f69bcce06191c68358766a32743730040fddc7 --- /dev/null +++ b/modules/timer/ccronexpr.cpp @@ -0,0 +1,1227 @@ +/* + * Copyright 2015, alex at staticlibs.net + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * File: ccronexpr.c + * Author: alex + * + * Created on February 24, 2015, 9:35 AM + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "ccronexpr.h" + +#define CRON_MAX_SECONDS 60 +#define CRON_MAX_MINUTES 60 +#define CRON_MAX_HOURS 24 +#define CRON_MAX_DAYS_OF_WEEK 8 +#define CRON_MAX_DAYS_OF_MONTH 32 +#define CRON_MAX_MONTHS 12 +#define CRON_MAX_YEARS_DIFF 4 + +#define CRON_CF_SECOND 0 +#define CRON_CF_MINUTE 1 +#define CRON_CF_HOUR_OF_DAY 2 +#define CRON_CF_DAY_OF_WEEK 3 +#define CRON_CF_DAY_OF_MONTH 4 +#define CRON_CF_MONTH 5 +#define CRON_CF_YEAR 6 + +#define CRON_CF_ARR_LEN 7 + +#define CRON_INVALID_INSTANT ((time_t) -1) + +static const char* const DAYS_ARR[] = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; +#define CRON_DAYS_ARR_LEN 7 +static const char* const MONTHS_ARR[] = { "FOO", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; +#define CRON_MONTHS_ARR_LEN 13 + +#define CRON_MAX_STR_LEN_TO_SPLIT 256 +#define CRON_MAX_NUM_TO_SRING 1000000000 +/* computes number of digits in decimal number */ +#define CRON_NUM_OF_DIGITS(num) (abs(num) < 10 ? 1 : \ + (abs(num) < 100 ? 2 : \ + (abs(num) < 1000 ? 3 : \ + (abs(num) < 10000 ? 4 : \ + (abs(num) < 100000 ? 5 : \ + (abs(num) < 1000000 ? 6 : \ + (abs(num) < 10000000 ? 7 : \ + (abs(num) < 100000000 ? 8 : \ + (abs(num) < 1000000000 ? 9 : 10))))))))) + +#ifndef _WIN32 +struct tm *gmtime_r(const time_t *timep, struct tm *result); +#ifdef CRON_USE_LOCAL_TIME +struct tm *localtime_r(const time_t *timep, struct tm *result); +#endif /* CRON_USE_LOCAL_TIME */ +#endif /* _WIN32 */ + +#ifndef CRON_TEST_MALLOC +#define cron_malloc(x) malloc(x); +#define cron_free(x) free(x); +#else /* CRON_TEST_MALLOC */ +void* cron_malloc(size_t n); +void cron_free(void* p); +#endif /* CRON_TEST_MALLOC */ + +#ifdef __MINGW32__ +/* To avoid warning when building with mingw */ +time_t _mkgmtime(struct tm* tm); +#endif /* __MINGW32__ */ + +/* Defining 'cron_mktime' to use use UTC (default) or local time */ +#ifndef CRON_USE_LOCAL_TIME + +/* http://stackoverflow.com/a/22557778 */ +#ifdef _WIN32 +time_t cron_mktime(struct tm* tm) { + return _mkgmtime(tm); +} +#else /* !_WIN32 */ +#ifndef ANDROID +/* can be hidden in time.h */ +time_t timegm(struct tm* __tp); +#endif /* ANDROID */ +time_t cron_mktime(struct tm* tm) { +#ifndef ANDROID + return timegm(tm); +#else /* ANDROID */ + /* https://github.com/adobe/chromium/blob/cfe5bf0b51b1f6b9fe239c2a3c2f2364da9967d7/base/os_compat_android.cc#L20 */ + static const time_t kTimeMax = ~(1L << (sizeof (time_t) * CHAR_BIT - 1)); + static const time_t kTimeMin = (1L << (sizeof (time_t) * CHAR_BIT - 1)); + time64_t result = timegm64(tm); + if (result < kTimeMin || result > kTimeMax) return -1; + return result; +#endif /* ANDROID */ +} +#endif /* _WIN32 */ + +struct tm* cron_time(time_t* date, struct tm* out) { +#ifdef __MINGW32__ + (void)(out); /* To avoid unused warning */ + return gmtime(date); +#else /* !__MINGW32__ */ +#ifdef _WIN32 + errno_t err = gmtime_s(out, date); + return 0 == err ? out : NULL; +#else /* !_WIN32 */ + return gmtime_r(date, out); +#endif /* _WIN32 */ +#endif /* __MINGW32__ */ +} + +#else /* CRON_USE_LOCAL_TIME */ + +time_t cron_mktime(struct tm* tm) { + tm->tm_isdst = -1; + return mktime(tm); +} + +struct tm* cron_time(time_t* date, struct tm* out) { +#ifdef _WIN32 + errno_t err = localtime_s(out, date); + return 0 == err ? out : NULL; +#else /* _WIN32 */ + return localtime_r(date, out); +#endif /* _WIN32 */ +} + +#endif /* CRON_USE_LOCAL_TIME */ + +void cron_set_bit(uint8_t* rbyte, int idx) { + uint8_t j = (uint8_t) (idx / 8); + uint8_t k = (uint8_t) (idx % 8); + + rbyte[j] |= (1 << k); +} + +void cron_del_bit(uint8_t* rbyte, int idx) { + uint8_t j = (uint8_t) (idx / 8); + uint8_t k = (uint8_t) (idx % 8); + + rbyte[j] &= ~(1 << k); +} + +uint8_t cron_get_bit(uint8_t* rbyte, int idx) { + uint8_t j = (uint8_t) (idx / 8); + uint8_t k = (uint8_t) (idx % 8); + + if (rbyte[j] & (1 << k)) { + return 1; + } else { + return 0; + } +} + +static void free_splitted(char** splitted, size_t len) { + size_t i; + if (!splitted) return; + for (i = 0; i < len; i++) { + if (splitted[i]) { + cron_free(splitted[i]); + } + } + cron_free(splitted); +} + +static char* strdupl(const char* str, size_t len) { + if (!str) return NULL; + char* res = (char*) cron_malloc(len + 1); + if (!res) return NULL; + memset(res, 0, len + 1); + memcpy(res, str, len); + return res; +} + +static unsigned int next_set_bit(uint8_t* bits, unsigned int max, unsigned int from_index, int* notfound) { + unsigned int i; + if (!bits) { + *notfound = 1; + return 0; + } + for (i = from_index; i < max; i++) { + if (cron_get_bit(bits, i)) return i; + } + *notfound = 1; + return 0; +} + +static void push_to_fields_arr(int* arr, int fi) { + int i; + if (!arr || -1 == fi) { + return; + } + for (i = 0; i < CRON_CF_ARR_LEN; i++) { + if (arr[i] == fi) return; + } + for (i = 0; i < CRON_CF_ARR_LEN; i++) { + if (-1 == arr[i]) { + arr[i] = fi; + return; + } + } +} + +static int add_to_field(struct tm* calendar, int field, int val) { + if (!calendar || -1 == field) { + return 1; + } + switch (field) { + case CRON_CF_SECOND: + calendar->tm_sec = calendar->tm_sec + val; + break; + case CRON_CF_MINUTE: + calendar->tm_min = calendar->tm_min + val; + break; + case CRON_CF_HOUR_OF_DAY: + calendar->tm_hour = calendar->tm_hour + val; + break; + case CRON_CF_DAY_OF_WEEK: /* mkgmtime ignores this field */ + case CRON_CF_DAY_OF_MONTH: + calendar->tm_mday = calendar->tm_mday + val; + break; + case CRON_CF_MONTH: + calendar->tm_mon = calendar->tm_mon + val; + break; + case CRON_CF_YEAR: + calendar->tm_year = calendar->tm_year + val; + break; + default: + return 1; /* unknown field */ + } + time_t res = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == res) { + return 1; + } + return 0; +} + +/** + * Reset the calendar setting all the fields provided to zero. + */ +static int reset_min(struct tm* calendar, int field) { + if (!calendar || -1 == field) { + return 1; + } + switch (field) { + case CRON_CF_SECOND: + calendar->tm_sec = 0; + break; + case CRON_CF_MINUTE: + calendar->tm_min = 0; + break; + case CRON_CF_HOUR_OF_DAY: + calendar->tm_hour = 0; + break; + case CRON_CF_DAY_OF_WEEK: + calendar->tm_wday = 0; + break; + case CRON_CF_DAY_OF_MONTH: + calendar->tm_mday = 1; + break; + case CRON_CF_MONTH: + calendar->tm_mon = 0; + break; + case CRON_CF_YEAR: + calendar->tm_year = 0; + break; + default: + return 1; /* unknown field */ + } + time_t res = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == res) { + return 1; + } + return 0; +} + +static int reset_all_min(struct tm* calendar, int* fields) { + int i; + int res = 0; + if (!calendar || !fields) { + return 1; + } + for (i = 0; i < CRON_CF_ARR_LEN; i++) { + if (-1 != fields[i]) { + res = reset_min(calendar, fields[i]); + if (0 != res) return res; + } + } + return 0; +} + +static int set_field(struct tm* calendar, int field, int val) { + if (!calendar || -1 == field) { + return 1; + } + switch (field) { + case CRON_CF_SECOND: + calendar->tm_sec = val; + break; + case CRON_CF_MINUTE: + calendar->tm_min = val; + break; + case CRON_CF_HOUR_OF_DAY: + calendar->tm_hour = val; + break; + case CRON_CF_DAY_OF_WEEK: + calendar->tm_wday = val; + break; + case CRON_CF_DAY_OF_MONTH: + calendar->tm_mday = val; + break; + case CRON_CF_MONTH: + calendar->tm_mon = val; + break; + case CRON_CF_YEAR: + calendar->tm_year = val; + break; + default: + return 1; /* unknown field */ + } + time_t res = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == res) { + return 1; + } + return 0; +} + +/** + * Search the bits provided for the next set bit after the value provided, + * and reset the calendar. + */ +static unsigned int find_next(uint8_t* bits, unsigned int max, unsigned int value, struct tm* calendar, unsigned int field, unsigned int nextField, int* lower_orders, int* res_out) { + int notfound = 0; + int err = 0; + unsigned int next_value = next_set_bit(bits, max, value, ¬found); + /* roll over if needed */ + if (notfound) { + err = add_to_field(calendar, nextField, 1); + if (err) goto return_error; + err = reset_min(calendar, field); + if (err) goto return_error; + notfound = 0; + next_value = next_set_bit(bits, max, 0, ¬found); + } + if (notfound || next_value != value) { + err = set_field(calendar, field, next_value); + if (err) goto return_error; + err = reset_all_min(calendar, lower_orders); + if (err) goto return_error; + } + return next_value; + + return_error: + *res_out = 1; + return 0; +} + +static unsigned int find_next_day(struct tm* calendar, uint8_t* days_of_month, unsigned int day_of_month, uint8_t* days_of_week, unsigned int day_of_week, int* resets, int* res_out) { + int err; + unsigned int count = 0; + unsigned int max = 366; + while ((!cron_get_bit(days_of_month, day_of_month) || !cron_get_bit(days_of_week, day_of_week)) && count++ < max) { + err = add_to_field(calendar, CRON_CF_DAY_OF_MONTH, 1); + + if (err) goto return_error; + day_of_month = calendar->tm_mday; + day_of_week = calendar->tm_wday; + reset_all_min(calendar, resets); + } + return day_of_month; + + return_error: + *res_out = 1; + return 0; +} + +static int do_next(cron_expr* expr, struct tm* calendar, unsigned int dot) { + int i; + int res = 0; + int* resets = NULL; + int* empty_list = NULL; + unsigned int second = 0; + unsigned int update_second = 0; + unsigned int minute = 0; + unsigned int update_minute = 0; + unsigned int hour = 0; + unsigned int update_hour = 0; + unsigned int day_of_week = 0; + unsigned int day_of_month = 0; + unsigned int update_day_of_month = 0; + unsigned int month = 0; + unsigned int update_month = 0; + + resets = (int*) cron_malloc(CRON_CF_ARR_LEN * sizeof(int)); + if (!resets) goto return_result; + empty_list = (int*) cron_malloc(CRON_CF_ARR_LEN * sizeof(int)); + if (!empty_list) goto return_result; + for (i = 0; i < CRON_CF_ARR_LEN; i++) { + resets[i] = -1; + empty_list[i] = -1; + } + + second = calendar->tm_sec; + update_second = find_next(expr->seconds, CRON_MAX_SECONDS, second, calendar, CRON_CF_SECOND, CRON_CF_MINUTE, empty_list, &res); + if (0 != res) goto return_result; + if (second == update_second) { + push_to_fields_arr(resets, CRON_CF_SECOND); + } + + minute = calendar->tm_min; + update_minute = find_next(expr->minutes, CRON_MAX_MINUTES, minute, calendar, CRON_CF_MINUTE, CRON_CF_HOUR_OF_DAY, resets, &res); + if (0 != res) goto return_result; + if (minute == update_minute) { + push_to_fields_arr(resets, CRON_CF_MINUTE); + } else { + res = do_next(expr, calendar, dot); + if (0 != res) goto return_result; + } + + hour = calendar->tm_hour; + update_hour = find_next(expr->hours, CRON_MAX_HOURS, hour, calendar, CRON_CF_HOUR_OF_DAY, CRON_CF_DAY_OF_WEEK, resets, &res); + if (0 != res) goto return_result; + if (hour == update_hour) { + push_to_fields_arr(resets, CRON_CF_HOUR_OF_DAY); + } else { + res = do_next(expr, calendar, dot); + if (0 != res) goto return_result; + } + + day_of_week = calendar->tm_wday; + day_of_month = calendar->tm_mday; + update_day_of_month = find_next_day(calendar, expr->days_of_month, day_of_month, expr->days_of_week, day_of_week, resets, &res); + if (0 != res) goto return_result; + if (day_of_month == update_day_of_month) { + push_to_fields_arr(resets, CRON_CF_DAY_OF_MONTH); + } else { + res = do_next(expr, calendar, dot); + if (0 != res) goto return_result; + } + + month = calendar->tm_mon; /*day already adds one if no day in same month is found*/ + update_month = find_next(expr->months, CRON_MAX_MONTHS, month, calendar, CRON_CF_MONTH, CRON_CF_YEAR, resets, &res); + if (0 != res) goto return_result; + if (month != update_month) { + if (calendar->tm_year - dot > 4) { + res = -1; + goto return_result; + } + res = do_next(expr, calendar, dot); + if (0 != res) goto return_result; + } + goto return_result; + + return_result: + if (!resets || !empty_list) { + res = -1; + } + if (resets) { + cron_free(resets); + } + if (empty_list) { + cron_free(empty_list); + } + return res; +} + +static int to_upper(char* str) { + if (!str) return 1; + int i; + for (i = 0; '\0' != str[i]; i++) { + int c = (int)str[i]; + str[i] = (char) toupper(c); + } + return 0; +} + +static char* to_string(int num) { + if (abs(num) >= CRON_MAX_NUM_TO_SRING) return NULL; + char* str = (char*) cron_malloc(CRON_NUM_OF_DIGITS(num) + 1); + if (!str) return NULL; + int res = sprintf(str, "%d", num); + if (res < 0) { + cron_free(str); + return NULL; + } + return str; +} + +static char* str_replace(char *orig, const char *rep, const char *with) { + char *result; /* the return string */ + char *ins; /* the next insert point */ + char *tmp; /* varies */ + size_t len_rep; /* length of rep */ + size_t len_with; /* length of with */ + size_t len_front; /* distance between rep and end of last rep */ + int count; /* number of replacements */ + if (!orig) return NULL; + if (!rep) rep = ""; + if (!with) with = ""; + len_rep = strlen(rep); + len_with = strlen(with); + + ins = orig; + for (count = 0; NULL != (tmp = strstr(ins, rep)); ++count) { + ins = tmp + len_rep; + } + + /* first time through the loop, all the variable are set correctly + from here on, + tmp points to the end of the result string + ins points to the next occurrence of rep in orig + orig points to the remainder of orig after "end of rep" + */ + tmp = result = (char*) cron_malloc(strlen(orig) + (len_with - len_rep) * count + 1); + if (!result) return NULL; + + while (count--) { + ins = strstr(orig, rep); + len_front = ins - orig; + tmp = strncpy(tmp, orig, len_front) + len_front; + tmp = strcpy(tmp, with) + len_with; + orig += len_front + len_rep; /* move to next "end of rep" */ + } + strcpy(tmp, orig); + return result; +} + +static unsigned int parse_uint(const char* str, int* errcode) { + char* endptr; + errno = 0; + long int l = strtol(str, &endptr, 0); + if (errno == ERANGE || *endptr != '\0' || l < 0 || l > INT_MAX) { + *errcode = 1; + return 0; + } else { + *errcode = 0; + return (unsigned int) l; + } +} + +static char** split_str(const char* str, char del, size_t* len_out) { + size_t i; + size_t stlen = 0; + size_t len = 0; + int accum = 0; + char* buf = NULL; + char** res = NULL; + size_t bi = 0; + size_t ri = 0; + char* tmp; + + if (!str) goto return_error; + for (i = 0; '\0' != str[i]; i++) { + stlen += 1; + if (stlen >= CRON_MAX_STR_LEN_TO_SPLIT) goto return_error; + } + + for (i = 0; i < stlen; i++) { + int c = str[i]; + if (del == str[i]) { + if (accum > 0) { + len += 1; + accum = 0; + } + } else if (!isspace(c)) { + accum += 1; + } + } + /* tail */ + if (accum > 0) { + len += 1; + } + if (0 == len) return NULL; + + buf = (char*) cron_malloc(stlen + 1); + if (!buf) goto return_error; + memset(buf, 0, stlen + 1); + res = (char**) cron_malloc(len * sizeof(char*)); + if (!res) goto return_error; + memset(res, 0, len * sizeof(char*)); + + for (i = 0; i < stlen; i++) { + int c = str[i]; + if (del == str[i]) { + if (bi > 0) { + tmp = strdupl(buf, bi); + if (!tmp) goto return_error; + res[ri++] = tmp; + memset(buf, 0, stlen + 1); + bi = 0; + } + } else if (!isspace(c)) { + buf[bi++] = str[i]; + } + } + /* tail */ + if (bi > 0) { + tmp = strdupl(buf, bi); + if (!tmp) goto return_error; + res[ri++] = tmp; + } + cron_free(buf); + *len_out = len; + return res; + + return_error: + if (buf) { + cron_free(buf); + } + free_splitted(res, len); + *len_out = 0; + return NULL; +} + +static char* replace_ordinals(char* value, const char* const * arr, size_t arr_len) { + size_t i; + char* cur = value; + char* res = NULL; + int first = 1; + for (i = 0; i < arr_len; i++) { + char* strnum = to_string((int) i); + if (!strnum) { + if (!first) { + cron_free(cur); + } + return NULL; + } + res = str_replace(cur, arr[i], strnum); + cron_free(strnum); + if (!first) { + cron_free(cur); + } + if (!res) { + return NULL; + } + cur = res; + if (first) { + first = 0; + } + } + return res; +} + +static int has_char(char* str, char ch) { + size_t i; + size_t len = 0; + if (!str) return 0; + len = strlen(str); + for (i = 0; i < len; i++) { + if (str[i] == ch) return 1; + } + return 0; +} + +static unsigned int* get_range(char* field, unsigned int min, unsigned int max, const char** error) { + + char** parts = NULL; + size_t len = 0; + unsigned int* res = (unsigned int*) cron_malloc(2 * sizeof(unsigned int)); + if (!res) goto return_error; + + res[0] = 0; + res[1] = 0; + if (1 == strlen(field) && '*' == field[0]) { + res[0] = min; + res[1] = max - 1; + } else if (!has_char(field, '-')) { + int err = 0; + unsigned int val = parse_uint(field, &err); + if (err) { + *error = "Unsigned integer parse error 1"; + goto return_error; + } + + res[0] = val; + res[1] = val; + } else { + parts = split_str(field, '-', &len); + if (2 != len) { + *error = "Specified range requires two fields"; + goto return_error; + } + int err = 0; + res[0] = parse_uint(parts[0], &err); + if (err) { + *error = "Unsigned integer parse error 2"; + goto return_error; + } + res[1] = parse_uint(parts[1], &err); + if (err) { + *error = "Unsigned integer parse error 3"; + goto return_error; + } + } + if (res[0] >= max || res[1] >= max) { + *error = "Specified range exceeds maximum"; + goto return_error; + } + if (res[0] < min || res[1] < min) { + *error = "Specified range is less than minimum"; + goto return_error; + } + if (res[0] > res[1]) { + *error = "Specified range start exceeds range end"; + goto return_error; + } + + free_splitted(parts, len); + *error = NULL; + return res; + + return_error: + free_splitted(parts, len); + if (res) { + cron_free(res); + } + + return NULL; +} + +static void set_number_hits(const char* value, uint8_t* target, unsigned int min, unsigned int max, const char** error) { + size_t i; + unsigned int i1; + size_t len = 0; + + char** fields = split_str(value, ',', &len); + if (!fields) { + *error = "Comma split error"; + goto return_result; + } + + for (i = 0; i < len; i++) { + if (!has_char(fields[i], '/')) { + /* Not an incrementer so it must be a range (possibly empty) */ + + unsigned int* range = get_range(fields[i], min, max, error); + + if (*error) { + if (range) { + cron_free(range); + } + goto return_result; + + } + + for (i1 = range[0]; i1 <= range[1]; i1++) { + cron_set_bit(target, i1); + + } + cron_free(range); + + } else { + size_t len2 = 0; + char** split = split_str(fields[i], '/', &len2); + if (2 != len2) { + *error = "Incrementer must have two fields"; + free_splitted(split, len2); + goto return_result; + } + unsigned int* range = get_range(split[0], min, max, error); + if (*error) { + if (range) { + cron_free(range); + } + free_splitted(split, len2); + goto return_result; + } + if (!has_char(split[0], '-')) { + range[1] = max - 1; + } + int err = 0; + unsigned int delta = parse_uint(split[1], &err); + if (err) { + *error = "Unsigned integer parse error 4"; + cron_free(range); + free_splitted(split, len2); + goto return_result; + } + if (0 == delta) { + *error = "Incrementer may not be zero"; + cron_free(range); + free_splitted(split, len2); + goto return_result; + } + for (i1 = range[0]; i1 <= range[1]; i1 += delta) { + cron_set_bit(target, i1); + } + free_splitted(split, len2); + cron_free(range); + + } + } + goto return_result; + + return_result: + free_splitted(fields, len); + +} + +static void set_months(char* value, uint8_t* targ, const char** error) { + unsigned int i; + unsigned int max = 12; + + char* replaced = NULL; + + to_upper(value); + replaced = replace_ordinals(value, MONTHS_ARR, CRON_MONTHS_ARR_LEN); + if (!replaced) { + *error = "Invalid month format"; + return; + } + set_number_hits(replaced, targ, 1, max + 1, error); + cron_free(replaced); + + /* ... and then rotate it to the front of the months */ + for (i = 1; i <= max; i++) { + if (cron_get_bit(targ, i)) { + cron_set_bit(targ, i - 1); + cron_del_bit(targ, i); + } + } +} + +static void set_days_of_week(char* field, uint8_t* targ, const char** error) { + unsigned int max = 7; + char* replaced = NULL; + + if (1 == strlen(field) && '?' == field[0]) { + field[0] = '*'; + } + to_upper(field); + replaced = replace_ordinals(field, DAYS_ARR, CRON_DAYS_ARR_LEN); + if (!replaced) { + *error = "Invalid day format"; + return; + } + set_number_hits(replaced, targ, 0, max + 1, error); + cron_free(replaced); + if (cron_get_bit(targ, 7)) { + /* Sunday can be represented as 0 or 7*/ + cron_set_bit(targ, 0); + cron_del_bit(targ, 7); + } +} + +static void set_days_of_month(char* field, uint8_t* targ, const char** error) { + /* Days of month start with 1 (in Cron and Calendar) so add one */ + if (1 == strlen(field) && '?' == field[0]) { + field[0] = '*'; + } + set_number_hits(field, targ, 1, CRON_MAX_DAYS_OF_MONTH, error); +} + +void cron_parse_expr(const char* expression, cron_expr* target, const char** error) { + const char* err_local; + size_t len = 0; + char** fields = NULL; + if (!error) { + error = &err_local; + } + *error = NULL; + if (!expression) { + *error = "Invalid NULL expression"; + goto return_res; + } + if (!target) { + *error = "Invalid NULL target"; + goto return_res; + } + + fields = split_str(expression, ' ', &len); + if (len != 6) { + *error = "Invalid number of fields, expression must consist of 6 fields"; + goto return_res; + } + memset(target, 0, sizeof(*target)); + set_number_hits(fields[0], target->seconds, 0, 60, error); + if (*error) goto return_res; + set_number_hits(fields[1], target->minutes, 0, 60, error); + if (*error) goto return_res; + set_number_hits(fields[2], target->hours, 0, 24, error); + if (*error) goto return_res; + set_days_of_month(fields[3], target->days_of_month, error); + if (*error) goto return_res; + set_months(fields[4], target->months, error); + if (*error) goto return_res; + set_days_of_week(fields[5], target->days_of_week, error); + if (*error) goto return_res; + + goto return_res; + + return_res: + free_splitted(fields, len); +} + +time_t cron_next(cron_expr* expr, time_t date) { + /* + The plan: + + 1 Round up to the next whole second + + 2 If seconds match move on, otherwise find the next match: + 2.1 If next match is in the next minute then roll forwards + + 3 If minute matches move on, otherwise find the next match + 3.1 If next match is in the next hour then roll forwards + 3.2 Reset the seconds and go to 2 + + 4 If hour matches move on, otherwise find the next match + 4.1 If next match is in the next day then roll forwards, + 4.2 Reset the minutes and seconds and go to 2 + + ... + */ + if (!expr) return CRON_INVALID_INSTANT; + struct tm calval; + memset(&calval, 0, sizeof(struct tm)); + struct tm* calendar = cron_time(&date, &calval); + if (!calendar) return CRON_INVALID_INSTANT; + time_t original = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == original) return CRON_INVALID_INSTANT; + + int res = do_next(expr, calendar, calendar->tm_year); + if (0 != res) return CRON_INVALID_INSTANT; + + time_t calculated = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == calculated) return CRON_INVALID_INSTANT; + if (calculated == original) { + /* We arrived at the original timestamp - round up to the next whole second and try again... */ + res = add_to_field(calendar, CRON_CF_SECOND, 1); + if (0 != res) return CRON_INVALID_INSTANT; + res = do_next(expr, calendar, calendar->tm_year); + if (0 != res) return CRON_INVALID_INSTANT; + } + + return cron_mktime(calendar); +} + + +/* https://github.com/staticlibs/ccronexpr/pull/8 */ + +static unsigned int prev_set_bit(uint8_t* bits, int from_index, int to_index, int* notfound) { + int i; + if (!bits) { + *notfound = 1; + return 0; + } + for (i = from_index; i >= to_index; i--) { + if (cron_get_bit(bits, i)) return i; + } + *notfound = 1; + return 0; +} + +static int last_day_of_month(int month, int year) { + struct tm cal; + time_t t; + memset(&cal,0,sizeof(cal)); + cal.tm_sec=0; + cal.tm_min=0; + cal.tm_hour=0; + cal.tm_mon = month+1; + cal.tm_mday = 0; + cal.tm_year=year; + t=mktime(&cal); + return gmtime(&t)->tm_mday; +} + +/** + * Reset the calendar setting all the fields provided to zero. + */ +static int reset_max(struct tm* calendar, int field) { + if (!calendar || -1 == field) { + return 1; + } + switch (field) { + case CRON_CF_SECOND: + calendar->tm_sec = 59; + break; + case CRON_CF_MINUTE: + calendar->tm_min = 59; + break; + case CRON_CF_HOUR_OF_DAY: + calendar->tm_hour = 23; + break; + case CRON_CF_DAY_OF_WEEK: + calendar->tm_wday = 6; + break; + case CRON_CF_DAY_OF_MONTH: + calendar->tm_mday = last_day_of_month(calendar->tm_mon, calendar->tm_year); + break; + case CRON_CF_MONTH: + calendar->tm_mon = 11; + break; + case CRON_CF_YEAR: + /* I don't think this is supposed to happen ... */ + fprintf(stderr, "reset CRON_CF_YEAR\n"); + break; + default: + return 1; /* unknown field */ + } + time_t res = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == res) { + return 1; + } + return 0; +} + +static int reset_all_max(struct tm* calendar, int* fields) { + int i; + int res = 0; + if (!calendar || !fields) { + return 1; + } + for (i = 0; i < CRON_CF_ARR_LEN; i++) { + if (-1 != fields[i]) { + res = reset_max(calendar, fields[i]); + if (0 != res) return res; + } + } + return 0; +} + +/** + * Search the bits provided for the next set bit after the value provided, + * and reset the calendar. + */ +static unsigned int find_prev(uint8_t* bits, unsigned int max, unsigned int value, struct tm* calendar, unsigned int field, unsigned int nextField, int* lower_orders, int* res_out) { + int notfound = 0; + int err = 0; + unsigned int next_value = prev_set_bit(bits, value, 0, ¬found); + /* roll under if needed */ + if (notfound) { + err = add_to_field(calendar, nextField, -1); + if (err) goto return_error; + err = reset_max(calendar, field); + if (err) goto return_error; + notfound = 0; + next_value = prev_set_bit(bits, max - 1, value, ¬found); + } + if (notfound || next_value != value) { + err = set_field(calendar, field, next_value); + if (err) goto return_error; + err = reset_all_max(calendar, lower_orders); + if (err) goto return_error; + } + return next_value; + + return_error: + *res_out = 1; + return 0; +} + +static unsigned int find_prev_day(struct tm* calendar, uint8_t* days_of_month, unsigned int day_of_month, uint8_t* days_of_week, unsigned int day_of_week, int* resets, int* res_out) { + int err; + unsigned int count = 0; + unsigned int max = 366; + while ((!cron_get_bit(days_of_month, day_of_month) || !cron_get_bit(days_of_week, day_of_week)) && count++ < max) { + err = add_to_field(calendar, CRON_CF_DAY_OF_MONTH, -1); + + if (err) goto return_error; + day_of_month = calendar->tm_mday; + day_of_week = calendar->tm_wday; + reset_all_max(calendar, resets); + } + return day_of_month; + + return_error: + *res_out = 1; + return 0; +} + +static int do_prev(cron_expr* expr, struct tm* calendar, unsigned int dot) { + int i; + int res = 0; + int* resets = NULL; + int* empty_list = NULL; + unsigned int second = 0; + unsigned int update_second = 0; + unsigned int minute = 0; + unsigned int update_minute = 0; + unsigned int hour = 0; + unsigned int update_hour = 0; + unsigned int day_of_week = 0; + unsigned int day_of_month = 0; + unsigned int update_day_of_month = 0; + unsigned int month = 0; + unsigned int update_month = 0; + + resets = (int*) cron_malloc(CRON_CF_ARR_LEN * sizeof(int)); + if (!resets) goto return_result; + empty_list = (int*) cron_malloc(CRON_CF_ARR_LEN * sizeof(int)); + if (!empty_list) goto return_result; + for (i = 0; i < CRON_CF_ARR_LEN; i++) { + resets[i] = -1; + empty_list[i] = -1; + } + + second = calendar->tm_sec; + update_second = find_prev(expr->seconds, CRON_MAX_SECONDS, second, calendar, CRON_CF_SECOND, CRON_CF_MINUTE, empty_list, &res); + if (0 != res) goto return_result; + if (second == update_second) { + push_to_fields_arr(resets, CRON_CF_SECOND); + } + + minute = calendar->tm_min; + update_minute = find_prev(expr->minutes, CRON_MAX_MINUTES, minute, calendar, CRON_CF_MINUTE, CRON_CF_HOUR_OF_DAY, resets, &res); + if (0 != res) goto return_result; + if (minute == update_minute) { + push_to_fields_arr(resets, CRON_CF_MINUTE); + } else { + res = do_prev(expr, calendar, dot); + if (0 != res) goto return_result; + } + + hour = calendar->tm_hour; + update_hour = find_prev(expr->hours, CRON_MAX_HOURS, hour, calendar, CRON_CF_HOUR_OF_DAY, CRON_CF_DAY_OF_WEEK, resets, &res); + if (0 != res) goto return_result; + if (hour == update_hour) { + push_to_fields_arr(resets, CRON_CF_HOUR_OF_DAY); + } else { + res = do_prev(expr, calendar, dot); + if (0 != res) goto return_result; + } + + day_of_week = calendar->tm_wday; + day_of_month = calendar->tm_mday; + update_day_of_month = find_prev_day(calendar, expr->days_of_month, day_of_month, expr->days_of_week, day_of_week, resets, &res); + if (0 != res) goto return_result; + if (day_of_month == update_day_of_month) { + push_to_fields_arr(resets, CRON_CF_DAY_OF_MONTH); + } else { + res = do_prev(expr, calendar, dot); + if (0 != res) goto return_result; + } + + month = calendar->tm_mon; /*day already adds one if no day in same month is found*/ + update_month = find_prev(expr->months, CRON_MAX_MONTHS, month, calendar, CRON_CF_MONTH, CRON_CF_YEAR, resets, &res); + if (0 != res) goto return_result; + if (month != update_month) { + if (dot - calendar->tm_year > CRON_MAX_YEARS_DIFF) { + res = -1; + goto return_result; + } + res = do_prev(expr, calendar, dot); + if (0 != res) goto return_result; + } + goto return_result; + + return_result: + if (!resets || !empty_list) { + res = -1; + } + if (resets) { + cron_free(resets); + } + if (empty_list) { + cron_free(empty_list); + } + return res; +} + +time_t cron_prev(cron_expr* expr, time_t date) { + /* + The plan: + + 1 Round down to a whole second + + 2 If seconds match move on, otherwise find the next match: + 2.1 If next match is in the next minute then roll forwards + + 3 If minute matches move on, otherwise find the next match + 3.1 If next match is in the next hour then roll forwards + 3.2 Reset the seconds and go to 2 + + 4 If hour matches move on, otherwise find the next match + 4.1 If next match is in the next day then roll forwards, + 4.2 Reset the minutes and seconds and go to 2 + + ... + */ + if (!expr) return CRON_INVALID_INSTANT; + struct tm calval; + memset(&calval, 0, sizeof(struct tm)); + struct tm* calendar = cron_time(&date, &calval); + if (!calendar) return CRON_INVALID_INSTANT; + time_t original = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == original) return CRON_INVALID_INSTANT; + + /* calculate the previous occurrence */ + int res = do_prev(expr, calendar, calendar->tm_year); + if (0 != res) return CRON_INVALID_INSTANT; + + /* check for a match, try from the next second if one wasn't found */ + time_t calculated = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == calculated) return CRON_INVALID_INSTANT; + if (calculated == original) { + /* We arrived at the original timestamp - round up to the next whole second and try again... */ + res = add_to_field(calendar, CRON_CF_SECOND, -1); + if (0 != res) return CRON_INVALID_INSTANT; + res = do_prev(expr, calendar, calendar->tm_year); + if (0 != res) return CRON_INVALID_INSTANT; + } + + return cron_mktime(calendar); +} diff --git a/modules/timer/ccronexpr.h b/modules/timer/ccronexpr.h new file mode 100644 index 0000000000000000000000000000000000000000..afa8ab398131101be97b79b072cd407a7c3dccc1 --- /dev/null +++ b/modules/timer/ccronexpr.h @@ -0,0 +1,93 @@ +/* + * Copyright 2015, alex at staticlibs.net + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * File: ccronexpr.h + * Author: alex + * + * Created on February 24, 2015, 9:35 AM + */ + +#ifndef CCRONEXPR_H +#define CCRONEXPR_H + +#if defined(__cplusplus) && !defined(CRON_COMPILE_AS_CXX) +extern "C" { +#endif + +#ifndef ANDROID +#include +#else /* ANDROID */ +#include +#endif /* ANDROID */ + +#include /*added for use if uint*_t data types*/ + +/** + * Parsed cron expression + */ +typedef struct { + uint8_t seconds[8]; + uint8_t minutes[8]; + uint8_t hours[3]; + uint8_t days_of_week[1]; + uint8_t days_of_month[4]; + uint8_t months[2]; +} cron_expr; + +/** + * Parses specified cron expression. + * + * @param expression cron expression as nul-terminated string, + * should be no longer that 256 bytes + * @param pointer to cron expression structure, it's client code responsibility + * to free/destroy it afterwards + * @param error output error message, will be set to string literal + * error message in case of error. Will be set to NULL on success. + * The error message should NOT be freed by client. + */ +void cron_parse_expr(const char* expression, cron_expr* target, const char** error); + +/** + * Uses the specified expression to calculate the next 'fire' date after + * the specified date. All dates are processed as UTC (GMT) dates + * without timezones information. To use local dates (current system timezone) + * instead of GMT compile with '-DCRON_USE_LOCAL_TIME' + * + * @param expr parsed cron expression to use in next date calculation + * @param date start date to start calculation from + * @return next 'fire' date in case of success, '((time_t) -1)' in case of error. + */ +time_t cron_next(cron_expr* expr, time_t date); + +/** + * Uses the specified expression to calculate the previous 'fire' date after + * the specified date. All dates are processed as UTC (GMT) dates + * without timezones information. To use local dates (current system timezone) + * instead of GMT compile with '-DCRON_USE_LOCAL_TIME' + * + * @param expr parsed cron expression to use in previous date calculation + * @param date start date to start calculation from + * @return previous 'fire' date in case of success, '((time_t) -1)' in case of error. + */ +time_t cron_prev(cron_expr* expr, time_t date); + + +#if defined(__cplusplus) && !defined(CRON_COMPILE_AS_CXX) +} /* extern "C"*/ +#endif + +#endif /* CCRONEXPR_H */ diff --git a/modules/timer/crontab_timer.cpp b/modules/timer/crontab_timer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..999388be40faa15cb747efdfc10a287a36df186c --- /dev/null +++ b/modules/timer/crontab_timer.cpp @@ -0,0 +1,96 @@ +#include "cron_timer.h" + +#include +#include +#include +#include +#include + +#include +#include +#include "ccronexpr.h" + +namespace tbox { +namespace timer { + +CrontabTimer::CrontabTimer(event::Loop *loop) : + wp_loop_(loop), + sp_timer_ev_(loop->newTimerEvent()) +{ + assert(sp_timer_ev_ != nullptr); + sp_cron_expr_ = malloc(sizeof(cron_expr)); + assert(sp_cron_expr_ != nullptr); + memset(sp_cron_expr_, 0, sizeof(cron_expr)); +} + +CrontabTimer::~CrontabTimer() +{ + free(sp_cron_expr_); + delete sp_timer_ev_; +} + +bool CrontabTimer::isEnabled() const +{ + return sp_timer_ev_->isEnabled(); +} + +bool CrontabTimer::enable() +{ + return setNextAlarm(); +} + +bool CrontabTimer::disable() +{ + return sp_timer_ev_->disable(); +} + +bool CrontabTimer::initialize(const std::string &crontab_str, Callback cb) +{ + const char *error_str = nullptr; + memset(sp_cron_expr_, 0, sizeof(cron_expr)); + + // check validity of crontab str + cron_parse_expr(crontab_str.c_str(), static_cast(sp_cron_expr_), &error_str); + if (error_str != nullptr) { // Invalid expression. + return false; + } + + cb_ = cb; + sp_timer_ev_->setCallback([this] { this->onTimeExpired(); }); + + return true; +} + +void CrontabTimer::cleanup() +{ + cb_ = nullptr; +} + +void CrontabTimer::refresh() +{ + if (sp_timer_ev_->isEnabled()) + setNextAlarm(); +} + +void CrontabTimer::onTimeExpired() +{ + setNextAlarm(); + + if (cb_) + cb_(); +} + +bool CrontabTimer::setNextAlarm() +{ + struct timeval now_tv; + gettimeofday(&now_tv, nullptr); + auto next_ts = cron_next(static_cast(sp_cron_expr_), now_tv.tv_sec); + auto duration_s = next_ts - now_tv.tv_sec; + auto duration_ms = duration_s * 1000 - now_tv.tv_usec / 1000; + + sp_timer_ev_->initialize(std::chrono::milliseconds(duration_ms), event::Event::Mode::kOneshot); + return sp_timer_ev_->enable(); +} + +} +} diff --git a/modules/timer/crontab_timer.h b/modules/timer/crontab_timer.h new file mode 100644 index 0000000000000000000000000000000000000000..c65aed4db90f6f8533d71c69b69aed84c477bbbc --- /dev/null +++ b/modules/timer/crontab_timer.h @@ -0,0 +1,73 @@ +#ifndef TBOX_TIMER_CRON_TIMER_H +#define TBOX_TIMER_CRON_TIMER_H + +#include + +#include +#include + +namespace tbox { +namespace timer { +/* + * @brief The linux crontab timer. + * + * CrontabTimer allow the user to make plans by linux crontab expression. + * Linux-cron tab expression + * * * * * * + - - - - - - + | | | | | | + | | | | | +----- day of week (0 - 7) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat + | | | | +---------- month (1 - 12) OR jan,feb,mar,apr ... + | | | +--------------- day of month (1 - 31) + | | +-------------------- hour (0 - 23) + | +------------------------- minute (0 - 59) + +------------------------------ second (0 - 59) + + * + * code example: + * + * Loop *loop = Loop::New(); + * CrontabTimer *tmr = new CrontabTimer(loop); + * tmr->initialize("18 28 14 * * *", [] { std::cout << "timeout" << std::endl; }); // every day at 14:28:18 + * tmr->enable(); + * loop->runLoop(); + */ + +class CrontabTimer +{ + public: + CrontabTimer(event::Loop *wp_loop); + virtual ~CrontabTimer(); + + using Callback = std::function; + bool initialize(const std::string &crontab_str, Callback cb); + + bool isEnabled() const; + bool enable(); + bool disable(); + + /* + * @brief refresh the internal timer. + * + * In order not to affect the accuracy of the timer, + * you need to call this function to refresh the internal timer + * when the system clock was changed. + */ + void refresh(); + void cleanup(); + + private: + void onTimeExpired(); + bool setNextAlarm(); + + private: + event::Loop *wp_loop_; + event::TimerEvent *sp_timer_ev_; + Callback cb_; + void *sp_cron_expr_; +}; + +} +} + +#endif //TBOX_TIMER_CRON_TIMER_H diff --git a/modules/timer/weekly_timer.cpp b/modules/timer/weekly_timer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ec97fd1d41b2c81fa06b8a9b79e2cd8994efdda6 --- /dev/null +++ b/modules/timer/weekly_timer.cpp @@ -0,0 +1,149 @@ +#include "weekly_timer.h" + +#include +#include + +#include +#include +#include + +namespace tbox { +namespace timer { + +namespace { +constexpr auto kSecondsOfDay = 60 * 60 * 24; +constexpr auto kSecondsOfWeek = kSecondsOfDay * 7; +constexpr auto kMaxTimezoneOffsetMinutes = 60 * 12; +} + +WeeklyTimer::WeeklyTimer(event::Loop *wp_loop) : + wp_loop_(wp_loop), + sp_timer_ev_(wp_loop->newTimerEvent()) +{ + sp_timer_ev_->setCallback( + [this] { + state_ = State::kInited; + if (!week_mask_.empty()) + activeTimer(); + + ++cb_level_; + if (cb_) + cb_(); + --cb_level_; + } + ); +} + +WeeklyTimer::~WeeklyTimer() { + assert(cb_level_ == 0); + cleanup(); + delete sp_timer_ev_; +} + +bool WeeklyTimer::initialize(int seconds_of_day, const std::string &week_mask, int timezone_offset_minutes) { + if (state_ == State::kRunning) { + LogWarn("timer is running state, disable first"); + return false; + } + + if (seconds_of_day < 0 || seconds_of_day >= kSecondsOfDay) { + LogWarn("seconds_of_day:%d, out of range: [0,%d)", seconds_of_day, kSecondsOfDay); + return false; + } + + if (week_mask.size() != 7 && !week_mask.empty()) { + LogWarn("week_mask:%s, length either 0 or 7", week_mask.c_str()); + return false; + } + + week_mask_ = week_mask; + seconds_of_day_ = seconds_of_day; + timezone_offset_seconds_ = timezone_offset_minutes * 60; + state_ = State::kInited; + + return true; +} + +bool WeeklyTimer::isEnabled() const { + return state_ == State::kRunning; +} + +bool WeeklyTimer::enable() { + if (state_ == State::kInited) + return activeTimer(); + + LogWarn("should initialize first"); + return false; +} + +bool WeeklyTimer::disable() { + if (state_ == State::kRunning) { + state_ = State::kInited; + return sp_timer_ev_->disable(); + } + return false; +} + +void WeeklyTimer::cleanup() { + if (state_ < State::kInited) + return; + + disable(); + + week_mask_.clear(); + seconds_of_day_ = 0; + timezone_offset_seconds_ = 0; + cb_ = nullptr; + state_ = State::kNone; +} + +int WeeklyTimer::calculateWaitSeconds(uint32_t curr_utc_ts) { + uint32_t curr_local_ts = curr_utc_ts + timezone_offset_seconds_; + + int curr_week = (((curr_local_ts % kSecondsOfWeek) / kSecondsOfDay) + 4) % 7; + int curr_seconds = curr_local_ts % kSecondsOfDay; + +#if 0 + LogTrace("curr_week:%d, curr_seconds:%d", curr_week, curr_seconds); +#endif + + int wait_seconds = seconds_of_day_ - curr_seconds; + if (week_mask_.empty()) { + if (wait_seconds <= 0) + wait_seconds += kSecondsOfDay; + return wait_seconds; + } else { + for (int i = 0; i < 7; ++i) { + if (wait_seconds > 0 && week_mask_.at((i + curr_week) % 7) == '1') + return wait_seconds; + wait_seconds += kSecondsOfDay; + } + return -1; + } +} + +bool WeeklyTimer::activeTimer() { + struct timeval tv; + int ret = gettimeofday(&tv, nullptr); + if (ret != 0) { + LogWarn("gettimeofday() fail, ret:%d", ret); + return false; + } + + auto wait_seconds = calculateWaitSeconds(tv.tv_sec); +#if 0 + LogTrace("wait_seconds:%d", wait_seconds); +#endif + if (wait_seconds < 0) + return false; + + auto wait_milliseconds = wait_seconds * 1000 - tv.tv_usec / 1000; + sp_timer_ev_->initialize(std::chrono::milliseconds(wait_milliseconds), event::Event::Mode::kOneshot); + sp_timer_ev_->enable(); + + state_ = State::kRunning; + return true; +} + +} +} diff --git a/modules/timer/weekly_timer.h b/modules/timer/weekly_timer.h new file mode 100644 index 0000000000000000000000000000000000000000..0d11a69b4483dcf457ec6c0a9f29a9d1155e6b0b --- /dev/null +++ b/modules/timer/weekly_timer.h @@ -0,0 +1,69 @@ +#ifndef TBOX_TIMER_WEEKLY_TIMER_H_20221019 +#define TBOX_TIMER_WEEKLY_TIMER_H_20221019 + +#include + +#include +#include + +namespace tbox { +namespace timer { +/* + * @brief The linux weekly timer. + * + * WeeklyTimer allow the user to make plans weekly + * + * code example: + * + * Loop *loop = Loop::New(); + * WeeklyTimer *tmr = new WeeklyTimer(loop); + * tmr->initialize((8 * 60 * 60), "1111111", 0); // everyday at 08:00 in 0 timezone + * tmr->setCallback([] { std::cout << "time is up" << endl;}); + * tmr->enable(); + * loop->runLoop(); + */ + +class WeeklyTimer +{ + public: + explicit WeeklyTimer(event::Loop *wp_loop); + virtual ~WeeklyTimer(); + + bool initialize(int seconds_of_day, const std::string &week_mask, int timezone_offset_minutes = 0); + + using Callback = std::function; + void setCallback(const Callback &cb) { cb_ = cb; } + + bool isEnabled() const; + bool enable(); + bool disable(); + + void cleanup(); + + protected: + int calculateWaitSeconds(uint32_t curr_utc_ts); + bool activeTimer(); + + private: + event::Loop *wp_loop_; + event::TimerEvent *sp_timer_ev_; + + int seconds_of_day_ = 0; + std::string week_mask_; + int timezone_offset_seconds_ = 0; + + int cb_level_ = 0; + Callback cb_; + + enum class State { + kNone, + kInited, + kRunning + }; + State state_ = State::kNone; +}; + +} +} + +#endif //TBOX_TIMER_WEEKLY_TIMER_H_20221019