diff --git a/Makefile b/Makefile index f56fb78dd..735d7034a 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,7 @@ DISCORD_OBJS = $(SRC_DIR)/concord-once.o \ $(SRC_DIR)/discord-adapter_ratelimit.o \ $(SRC_DIR)/discord-adapter_refcount.o \ $(SRC_DIR)/discord-client.o \ + $(SRC_DIR)/discord-loop.o \ $(SRC_DIR)/discord-gateway.o \ $(SRC_DIR)/discord-timer.o \ $(SRC_DIR)/discord-misc.o \ diff --git a/cog-utils/cog-utils.c b/cog-utils/cog-utils.c index 2c0ce902a..7c8db3bfe 100644 --- a/cog-utils/cog-utils.c +++ b/cog-utils/cog-utils.c @@ -215,6 +215,40 @@ cog_sleep_ms(const long tms) return ret; } +int +cog_sleep_us(const long tms) +{ + int ret; + +#if _POSIX_C_SOURCE >= 199309L + struct timespec ts; + + if (tms < 0) { + errno = EINVAL; + return -1; + } + + ts.tv_sec = tms / 1000000; + ts.tv_nsec = (tms % 1000000) * 1000; + + do { + ret = nanosleep(&ts, &ts); + } while (ret && errno == EINTR); +#else + struct timeval timeout; + long _tms = tms; + + timeout.tv_sec = _tms / 1000000L; + _tms = tms % 1000000L; + timeout.tv_usec = (int)_tms; + select(0, NULL, NULL, NULL, &timeout); + + ret = 0; +#endif + + return ret; +} + /* returns current timestamp in milliseconds */ uint64_t cog_timestamp_ms(void) diff --git a/cog-utils/cog-utils.h b/cog-utils/cog-utils.h index 0eed6f06a..5ddc84c0a 100644 --- a/cog-utils/cog-utils.h +++ b/cog-utils/cog-utils.h @@ -141,13 +141,22 @@ size_t cog_strndup(const char src[], size_t len, char **p_dest); size_t cog_asprintf(char **strp, const char fmt[], ...); /** - * @brief Sleep for milliseconds amount + * @brief Sleep for amount of milliseconds * - * @param tms milliseconds amount to sleep for + * @param tms amount of milliseconds to sleep for * @return 0 on success, -1 on error with an `errno` set to indicate the error */ int cog_sleep_ms(const long tms); + +/** + * @brief Sleep for amount of microseconds + * + * @param tms amount of microseconds to sleep for + * @return 0 on success, -1 on error with an `errno` set to indicate the error + */ +int cog_sleep_us(const long tms); + /** * @brief Get the current timestamp in milliseconds * diff --git a/examples/timers.c b/examples/timers.c index bff5a03b5..c1c25d6f8 100644 --- a/examples/timers.c +++ b/examples/timers.c @@ -2,70 +2,76 @@ #include #include #include -#include #include "discord.h" -static int interrupted = 0; - static void -on_sigint(int sig) { - interrupted = 1; +print_timer_info(struct discord_timer *timer) +{ + printf("Timer id:%u flags:%i " + "delay:%" PRIi64 " interval:%" PRIi64 " repeat:%" PRIi64 "\n", + timer->id, timer->flags, timer->delay, timer->interval, + timer->repeat); } static void -one_shot_timer_cb(struct discord *client, struct discord_timer *timer) { - printf("one_shot_timer_cb %u triggered with flags %i\n", - timer->id, timer->flags); +one_shot_timer_cb(struct discord *client, struct discord_timer *timer) +{ + print_timer_info(timer); + if (~timer->flags & DISCORD_TIMER_CANCELED) { + // if timer is not canceled + puts(timer->data); - //DO NOT IGNORE CANCELATION - if (timer->flags & DISCORD_TIMER_CANCELED) { - puts("Timer has been canceled"); - return; - } - if (interrupted) { - puts("Shutdown Canceled"); - return; - } - puts(timer->data); - discord_shutdown(client); + // timers can be updated in the callback (see below) + if (0) { + timer->interval += 100; + timer->repeat = 1; + return; // skip free(timer->data); + } + } + else { + puts("ONE SHOT TIMER CANCELED"); + } + free(timer->data); } static void -repeating_timer_cb(struct discord *client, struct discord_timer *timer) { - printf("repeating_timer_cb %u triggered with flags %i\n", - timer->id, timer->flags); - printf("%"PRIi64", %"PRIi64"\n", timer->interval, timer->repeat); - if (timer->repeat == 0) - puts("Shutting down soon, press ctrl + c to cancel"); +repeating_timer_cb(struct discord *client, struct discord_timer *timer) +{ + print_timer_info(timer); + if (timer->flags & DISCORD_TIMER_CANCELED) { + printf("TIMER WITH ID %u CANCELED\n", timer->id); + return; + } + printf("SHUTTING DOWN IN %" PRIi64 " SECONDS\n", timer->repeat); + if (!timer->repeat) { + discord_shutdown(client); + } } int main(int argc, char *argv[]) { const char *config_file = argc > 1 ? argv[1] : "../config.json"; - - signal(SIGINT, on_sigint); ccord_global_init(); - struct discord *client = discord_config_init(config_file); - //create one shot auto deleting timer - unsigned one_shot_timer_id = - discord_timer(client, one_shot_timer_cb, "Shutting Down", 5000); - - discord_timer_ctl(client, &(struct discord_timer) { - .id = 0, /* 0 to create a new timer */ - .cb = repeating_timer_cb, - .data = &one_shot_timer_id, - .delay = 0, /* start right away */ - .interval = 100, - .repeat = 10, /* -1 for infinity, 0 for never */ - .flags = DISCORD_TIMER_DELETE_AUTO, - }); + discord_timer(client, one_shot_timer_cb, strdup("Hello World"), 1000); + discord_timer(client, one_shot_timer_cb, strdup("Hello World!!!!!!"), + 5000); + + // start a one shot timer that will never get a chance to run + discord_timer(client, one_shot_timer_cb, strdup("Hello World"), 15000); + + discord_timer_interval(client, repeating_timer_cb, NULL, 0, 1000, 10); + + // start 3 timers that will never get a chance to run + for (int i = 0; i < 3; i++) + discord_timer(client, repeating_timer_cb, NULL, 20 * 1000); discord_run(client); + // discord_cleanup will cancel all timers that are still active discord_cleanup(client); ccord_global_cleanup(); -} \ No newline at end of file +} diff --git a/include/discord-internal.h b/include/discord-internal.h index 109103a8a..4f7022a67 100644 --- a/include/discord-internal.h +++ b/include/discord-internal.h @@ -190,7 +190,7 @@ struct discord_adapter { } * global; /** idle request handles */ - QUEUE(struct discord_context) *idleq; + QUEUE(struct discord_context) * idleq; /** max amount of retries before a failed request gives up */ int retry_limit; @@ -682,26 +682,29 @@ void discord_gateway_send_presence_update(struct discord_gateway *gw); struct discord_timers { priority_queue *q; - struct discord_timer *currently_being_run; + struct { + struct discord_timer *timer; + bool skip_update_phase; + } active; }; /** * @brief prepare timers for usage - * + * * @param client the client created with discord_init() */ void discord_timers_init(struct discord *client); /** * @brief cleanup timers and call cancel any running ones - * + * * @param client the client created with discord_init() */ void discord_timers_cleanup(struct discord *client); /** * @brief run all timers that are due - * + * * @param client the client created with discord_init() * @param timers the timers to run */ @@ -709,40 +712,40 @@ void discord_timers_run(struct discord *client, struct discord_timers *timers); /** * @brief modifies or creates a timer - * + * * @param client the client created with discord_init() * @param timers the timer group to perform this operation on * @param timer the timer that should be modified * @return the id of the timer */ -unsigned _discord_timer_ctl( - struct discord *client, - struct discord_timers *timers, - struct discord_timer *timer); +unsigned _discord_timer_ctl(struct discord *client, + struct discord_timers *timers, + struct discord_timer *timer); /** * @brief modifies or creates a timer - * + * * @param client the client created with discord_init() * @param timer the timer that should be modified * @return unsigned the id of the timer */ -unsigned discord_internal_timer_ctl( - struct discord *client, - struct discord_timer *timer); +unsigned discord_internal_timer_ctl(struct discord *client, + struct discord_timer *timer); /** * @brief creates a one shot timer that automatically * deletes itself upon completion - * + * * @param client the client created with discord_init() * @param cb the callback that should be called when timer triggers * @param data user data * @param delay delay before timer should start in milliseconds - * @return unsigned + * @return unsigned */ -unsigned discord_internal_timer(struct discord *client, discord_ev_timer cb, - void *data, int64_t delay); +unsigned discord_internal_timer(struct discord *client, + discord_ev_timer cb, + void *data, + int64_t delay); /** @} DiscordInternalTimer */ /** diff --git a/include/discord.h b/include/discord.h index d45a4e1f3..1fd1dd7fa 100644 --- a/include/discord.h +++ b/include/discord.h @@ -276,9 +276,9 @@ struct logconf *discord_get_logconf(struct discord *client); /** * @brief get the io_poller used by the discord client - * + * * @param client the client created with discord_init() - * @return struct io_poller* + * @return struct io_poller* */ struct io_poller *discord_get_io_poller(struct discord *client); @@ -291,23 +291,26 @@ struct discord_timer; /**/ /** @brief callback to be used with struct discord_timer */ -typedef void (*discord_ev_timer) - (struct discord *client, struct discord_timer *ev); +typedef void (*discord_ev_timer)(struct discord *client, + struct discord_timer *ev); /** @brief flags used to change behaviour of timer */ enum discord_timer_flags { /** use milliseconds for interval and start_time */ - DISCORD_TIMER_MILLISECONDS = 0, + DISCORD_TIMER_MILLISECONDS = 0, /** use microseconds for interval and start_time */ - DISCORD_TIMER_MICROSECONDS = 1 << 0, + DISCORD_TIMER_MICROSECONDS = 1 << 0, /** whether or not timer is marked for deletion */ - DISCORD_TIMER_DELETE = 1 << 1, + DISCORD_TIMER_DELETE = 1 << 1, /** automatically delete a timer once its repeat counter runs out */ - DISCORD_TIMER_DELETE_AUTO = 1 << 2, + DISCORD_TIMER_DELETE_AUTO = 1 << 2, /** timer has been canceled. user should cleanup only */ - DISCORD_TIMER_CANCELED = 1 << 3, - /** used in the timer callback to skip update phase */ - DISCORD_TIMER_DONT_UPDATE = 1 << 4, + DISCORD_TIMER_CANCELED = 1 << 3, + + /** used in discord_timer_ctl to get the timer's data */ + DISCORD_TIMER_GET = 1 << 5, + /** timer should run using a fixed interval based on start time */ + DISCORD_TIMER_INTERVAL_FIXED = 1 << 6, }; /** @brief struct used for modifying, and getting info about a timer */ @@ -322,7 +325,7 @@ struct discord_timer { void *data; /** delay before timer should start */ int64_t delay; - /** interval that the timer should repeat at. must be > 1 */ + /** interval that the timer should repeat at. must be >= 0 */ int64_t interval; /** how many times a timer should repeat (-1 == infinity) */ int64_t repeat; @@ -330,29 +333,109 @@ struct discord_timer { /** * @brief modifies or creates a timer - * + * * @param client the client created with discord_init() * @param timer the timer that should be modified * @return the id of the timer */ -unsigned discord_timer_ctl(struct discord *client, struct discord_timer *timer); +unsigned discord_timer_ctl(struct discord *client, + struct discord_timer *timer); /** * @brief creates a one shot timer that automatically * deletes itself upon completion - * + * * @param client the client created with discord_init() * @param cb the callback that should be called when timer triggers * @param data user data * @param delay delay before timer should start in milliseconds - * @return the id of the timer + * @return the id of the timer + */ +unsigned discord_timer(struct discord *client, + discord_ev_timer cb, + void *data, + int64_t delay); + +/** + * @brief creates a repeating timer that automatically + * deletes itself upon completion + * + * @param client the client created with discord_init() + * @param cb the callback that should be called when timer triggers + * @param data user data + * @param delay delay before timer should start in milliseconds + * @param interval interval between runs. (-1 == disable repeat) + * @param repeat repetitions (-1 == infinity) + * @return the id of the timer + */ +unsigned discord_timer_interval(struct discord *client, + discord_ev_timer cb, + void *data, + int64_t delay, + int64_t interval, + int64_t repeat); + +/** + * @brief get the data associated with the timer + * + * @param client the client created with discord_init() + * @param id id of the timer + * @param timer where to copy the timer data to + * @return true on success + */ +bool discord_timer_get(struct discord *client, + unsigned id, + struct discord_timer *timer); + +/** + * @brief starts a timer + * + * @param client the client created with discord_init() + * @param id id of the timer + * @return true on success + */ +bool discord_timer_start(struct discord *client, unsigned id); + +/** + * @brief stops a timer + * + * @param client the client created with discord_init() + * @param id id of the timer + * @return true on success + */ +bool discord_timer_stop(struct discord *client, unsigned id); + +/** + * @brief cancels a timer, + * this will delete the timer if DISCORD_TIMER_DELETE_AUTO is enabled + * + * @param client the client created with discord_init() + * @param id id of the timer + * @return true on success */ -unsigned discord_timer(struct discord *client, discord_ev_timer cb, - void *data, int64_t delay); +bool discord_timer_cancel(struct discord *client, unsigned id); + +/** + * @brief deletes a timer + * + * @param client the client created with discord_init() + * @param id id of the timer + * @return true on success + */ +bool discord_timer_delete(struct discord *client, unsigned id); + +/** + * @brief cancels, and deletes a timer + * + * @param client the client created with discord_init() + * @param id id of the timer + * @return true on success + */ +bool discord_timer_cancel_and_delete(struct discord *client, unsigned id); /** @example timers.c * Demonstrates the Timer API for callback scheduling */ - + /** @} DiscordTimer */ /** @} Discord */ diff --git a/src/discord-client.c b/src/discord-client.c index 14b519426..f50f6b57c 100644 --- a/src/discord-client.c +++ b/src/discord-client.c @@ -288,49 +288,17 @@ discord_set_event_scheduler(struct discord *client, client->gw.cmds.scheduler = callback; } - -static void -discord_wake_timer_cb(struct discord *client, struct discord_timer *timer) { - if (~timer->flags & DISCORD_TIMER_CANCELED && client->wakeup_timer.cb) - client->wakeup_timer.cb(client); -} - -void -discord_set_next_wakeup(struct discord *client, int64_t delay) -{ - unsigned id = discord_internal_timer_ctl(client, - &(struct discord_timer) { - .id = client->wakeup_timer.id, - .cb = discord_wake_timer_cb, - .delay = delay, - }); - client->wakeup_timer.id = id; -} - -void -discord_set_on_wakeup(struct discord *client, discord_ev_idle callback) -{ - client->wakeup_timer.cb = callback; - if (client->wakeup_timer.id) { - discord_internal_timer_ctl(client, - &(struct discord_timer) { - .id = client->wakeup_timer.id, - .cb = discord_wake_timer_cb, - .delay = -1, - }); - } -} - void -discord_set_on_idle(struct discord *client, discord_ev_idle callback) +discord_shutdown(struct discord *client) { - client->on_idle = callback; + if (client->gw.session->status != DISCORD_SESSION_SHUTDOWN) + discord_gateway_shutdown(&client->gw); } void -discord_set_on_cycle(struct discord *client, discord_ev_idle callback) +discord_reconnect(struct discord *client, bool resume) { - client->on_cycle = callback; + discord_gateway_reconnect(&client->gw, resume); } void @@ -339,92 +307,6 @@ discord_set_on_ready(struct discord *client, discord_ev_idle callback) client->gw.cmds.cbs.on_ready = callback; } -CCORDcode -discord_run(struct discord *client) -{ - int64_t next_run, now; - CCORDcode code; - - while (1) { - if (CCORD_OK != (code = discord_gateway_start(&client->gw))) break; - - next_run = (int64_t)cog_timestamp_ms(); - while (1) { - int poll_time = 0, poll_result; - - now = (int64_t)cog_timestamp_ms(); - - if (!client->on_idle) - poll_time = now < next_run ? (int)(next_run - now) : 0; - - struct discord_timers *const timers[] = - { &client->timers.internal, &client->timers.user }; - for (unsigned i = 0; i < sizeof timers / sizeof *timers; i++) { - int64_t trigger_us, trigger_ms; - if (priority_queue_peek(timers[i]->q, &trigger_us, NULL)) { - trigger_ms = trigger_us / 1000; - if (trigger_us >= 0) { - if (trigger_ms <= now) { - poll_time = 0; - } else if (trigger_ms - now < poll_time) { - poll_time = (int)(trigger_ms - now); - } - } - } - } - - poll_result = io_poller_poll(client->io_poller, poll_time); - if (-1 == poll_result) { - /* TODO: handle poll error here */ - } - else if (0 == poll_result) { - if (ccord_has_sigint != 0) discord_shutdown(client); - if (client->on_idle) client->on_idle(client); - } - - if (client->on_cycle) client->on_cycle(client); - - if (CCORD_OK != (code = io_poller_perform(client->io_poller))) - break; - - for (unsigned i = 0; i < sizeof timers / sizeof *timers; i++) - discord_timers_run(client, timers[i]); - - if (next_run <= now) { - if (CCORD_OK != (code = discord_gateway_perform(&client->gw))) - break; - if (CCORD_OK - != (code = discord_adapter_perform(&client->adapter))) - break; - - /* enforce a min 1 sec delay between runs */ - next_run = now + 1000; - } - } - - /* stop all pending requests in case of connection shutdown */ - if (true == discord_gateway_end(&client->gw)) { - discord_adapter_stop_all(&client->adapter); - break; - } - } - - return code; -} - -void -discord_shutdown(struct discord *client) -{ - if (client->gw.session->status != DISCORD_SESSION_SHUTDOWN) - discord_gateway_shutdown(&client->gw); -} - -void -discord_reconnect(struct discord *client, bool resume) -{ - discord_gateway_reconnect(&client->gw, resume); -} - void discord_set_on_guild_role_create(struct discord *client, discord_ev_guild_role callback) diff --git a/src/discord-loop.c b/src/discord-loop.c new file mode 100644 index 000000000..74207b435 --- /dev/null +++ b/src/discord-loop.c @@ -0,0 +1,161 @@ +#include +#include + +#include "discord.h" +#include "discord-internal.h" +#include "cog-utils.h" + +static void +discord_wake_timer_cb(struct discord *client, struct discord_timer *timer) +{ + if (~timer->flags & DISCORD_TIMER_CANCELED && client->wakeup_timer.cb) + client->wakeup_timer.cb(client); +} + +void +discord_set_next_wakeup(struct discord *client, int64_t delay) +{ + unsigned id = + discord_internal_timer_ctl(client, &(struct discord_timer){ + .id = client->wakeup_timer.id, + .cb = discord_wake_timer_cb, + .delay = delay, + }); + client->wakeup_timer.id = id; +} + +void +discord_set_on_wakeup(struct discord *client, discord_ev_idle callback) +{ + client->wakeup_timer.cb = callback; + if (client->wakeup_timer.id) { + discord_internal_timer_ctl(client, &(struct discord_timer){ + .id = client->wakeup_timer.id, + .cb = discord_wake_timer_cb, + .delay = -1, + }); + } +} + +void +discord_set_on_idle(struct discord *client, discord_ev_idle callback) +{ + client->on_idle = callback; +} + +void +discord_set_on_cycle(struct discord *client, discord_ev_idle callback) +{ + client->on_cycle = callback; +} + +static inline int64_t +discord_timer_get_next_trigger(struct discord_timers *const timers[], + size_t n, + int64_t now, + int64_t max_time) +{ + if (max_time == 0) return 0; + + for (unsigned i = 0; i < n; i++) { + int64_t trigger; + if (priority_queue_peek(timers[i]->q, &trigger, NULL)) { + if (trigger < 0) continue; + + if (trigger <= now) + max_time = 0; + else if (max_time > trigger - now) + max_time = trigger - now; + } + } + return max_time; +} + +#define BREAK_ON_FAIL(code, function) \ + if (CCORD_OK != (code = function)) break + +#define CALL_IO_POLLER_POLL(poll_errno, poll_result, io_poller, delay) \ + do { \ + if (-1 == (poll_result = io_poller_poll(io_poller, (int)(delay)))) \ + poll_errno = errno; \ + } while (0) + +CCORDcode +discord_run(struct discord *client) +{ + int64_t next_run, now; + CCORDcode code; + struct discord_timers *const timers[] = { &client->timers.internal, + &client->timers.user }; + + while (1) { + BREAK_ON_FAIL(code, discord_gateway_start(&client->gw)); + + next_run = (int64_t)discord_timestamp_us(client); + while (1) { + int64_t poll_time = 0; + int poll_result, poll_errno = 0; + + now = (int64_t)discord_timestamp_us(client); + + if (!client->on_idle) { + poll_time = discord_timer_get_next_trigger( + timers, sizeof timers / sizeof *timers, now, + now < next_run ? ((next_run - now)) : 0); + } + + CALL_IO_POLLER_POLL(poll_errno, poll_result, client->io_poller, + poll_time / 1000); + + now = (int64_t)discord_timestamp_us(client); + + if (0 == poll_result) { + if (ccord_has_sigint != 0) discord_shutdown(client); + if (client->on_idle) { + client->on_idle(client); + } + else { + poll_time = discord_timer_get_next_trigger( + timers, sizeof timers / sizeof *timers, now, 999); + if (poll_time) cog_sleep_us(poll_time); + } + } + + if (client->on_cycle) client->on_cycle(client); + + for (unsigned i = 0; i < sizeof timers / sizeof *timers; i++) + discord_timers_run(client, timers[i]); + + if (poll_result >= 0 && !client->on_idle) + CALL_IO_POLLER_POLL(poll_errno, poll_result, client->io_poller, + 0); + + if (-1 == poll_result) { + /* TODO: handle poll error here */ + // use poll_errno instead of errno + (void)poll_errno; + } + + BREAK_ON_FAIL(code, io_poller_perform(client->io_poller)); + + if (next_run <= now) { + BREAK_ON_FAIL(code, discord_gateway_perform(&client->gw)); + BREAK_ON_FAIL(code, discord_adapter_perform(&client->adapter)); + + /* enforce a min 1 sec delay between runs */ + next_run = now + 1000000; + } + } + + /* stop all pending requests in case of connection shutdown */ + if (true == discord_gateway_end(&client->gw)) { + discord_adapter_stop_all(&client->adapter); + break; + } + } + + return code; +} + +#undef BREAK_ON_FAIL +#undef CALL_IO_POLLER_POLL diff --git a/src/discord-timer.c b/src/discord-timer.c index 7ee80d5dc..e3c5fd026 100644 --- a/src/discord-timer.c +++ b/src/discord-timer.c @@ -2,27 +2,29 @@ #include "discord.h" #include "discord-internal.h" +#define DISCORD_TIMER_ALLOWED_FLAGS \ + (DISCORD_TIMER_MILLISECONDS | DISCORD_TIMER_MICROSECONDS \ + | DISCORD_TIMER_DELETE | DISCORD_TIMER_DELETE_AUTO \ + | DISCORD_TIMER_INTERVAL_FIXED) + static int cmp_timers(const void *a, const void *b) { const int64_t l = *(int64_t *)a; const int64_t r = *(int64_t *)b; - if (l == r || (l < 0 && r < 0)) - return 0; + if (l == r || (l < 0 && r < 0)) return 0; if (l < 0) return 1; if (r < 0) return -1; return l > r ? 1 : -1; } void -discord_timers_init(struct discord *client) +discord_timers_init(struct discord *client) { - client->timers.internal.q = - priority_queue_create(sizeof(int64_t), sizeof(struct discord_timer), - cmp_timers, 0); - client->timers.user.q = - priority_queue_create(sizeof(int64_t), sizeof(struct discord_timer), - cmp_timers, 0); + client->timers.internal.q = priority_queue_create( + sizeof(int64_t), sizeof(struct discord_timer), cmp_timers, 0); + client->timers.user.q = priority_queue_create( + sizeof(int64_t), sizeof(struct discord_timer), cmp_timers, 0); } static void @@ -30,8 +32,8 @@ discord_timers_cancel_all(struct discord *client, priority_queue *q) { struct discord_timer timer; while ((timer.id = priority_queue_pop(q, NULL, &timer))) { - timer.flags |= DISCORD_TIMER_CANCELED; - if (timer.cb) timer.cb(client, &timer); + timer.flags |= DISCORD_TIMER_CANCELED; + if (timer.cb) timer.cb(client, &timer); } } @@ -48,75 +50,111 @@ discord_timers_cleanup(struct discord *client) } unsigned -_discord_timer_ctl( - struct discord *client, - struct discord_timers *timers, - struct discord_timer *timer) +_discord_timer_ctl(struct discord *client, + struct discord_timers *timers, + struct discord_timer *timer_ret) { - int64_t now = -1; - if (timer->flags & DISCORD_TIMER_DELETE) { - unsigned id; - if (timer->id) { - id = priority_queue_get(timers->q, timer->id, NULL, timer); - if (id) return priority_queue_del(timers->q, id) ? id : 0; + struct discord_timer timer; + memcpy(&timer, timer_ret, sizeof timer); + + int64_t key = -1; + if (timer.id) { + if (!priority_queue_get(timers->q, timer.id, &key, NULL)) return 0; + + if (timer.flags & DISCORD_TIMER_GET) { + priority_queue_get(timers->q, timer.id, NULL, timer_ret); + if (timer.flags == DISCORD_TIMER_GET) return timer.id; } - return 0; } - if (timer->delay >= 0) - now = (int64_t)discord_timestamp_us(client) + - ((timer->flags & DISCORD_TIMER_MICROSECONDS) - ? timer->delay : timer->delay * 1000); - if (!timer->id) { - return priority_queue_push(timers->q, &now, timer); - } else { - if (timers->currently_being_run - && timers->currently_being_run->id == timer->id) - timers->currently_being_run->flags |= DISCORD_TIMER_DONT_UPDATE; - if (priority_queue_update(timers->q, timer->id, &now, timer)) - return timer->id; + + int64_t now = -1; + if (timer.delay >= 0) { + now = (int64_t)discord_timestamp_us(client) + + ((timer.flags & DISCORD_TIMER_MICROSECONDS) + ? timer.delay + : timer.delay * 1000); + } + if (timer.flags & (DISCORD_TIMER_DELETE | DISCORD_TIMER_CANCELED)) now = 0; + + timer.flags &= (DISCORD_TIMER_ALLOWED_FLAGS | DISCORD_TIMER_CANCELED); + + if (!timer.id) { + return priority_queue_push(timers->q, &now, &timer); + } + else { + if (timers->active.timer && timers->active.timer->id == timer.id) + timers->active.skip_update_phase = true; + if (priority_queue_update(timers->q, timer.id, &now, &timer)) + return timer.id; return 0; } } -#define TIMER_TRY_DELETE \ - if (timer.flags & DISCORD_TIMER_DELETE) { \ - priority_queue_pop(timers->q, NULL, NULL); \ - continue; \ - } +#define TIMER_TRY_DELETE \ + do { \ + if (timer.flags & DISCORD_TIMER_DELETE) { \ + priority_queue_del(timers->q, timer.id); \ + continue; \ + } \ + } while (0) void discord_timers_run(struct discord *client, struct discord_timers *timers) { int64_t now = (int64_t)discord_timestamp_us(client); + const int64_t start_time = now; + struct discord_timer timer; - timers->currently_being_run = &timer; - for (int64_t trigger; - (timer.id = priority_queue_peek(timers->q, &trigger, &timer));) + timers->active.timer = &timer; + + for (int64_t trigger, max_iterations = 100000; + (timer.id = priority_queue_peek(timers->q, &trigger, &timer)) + && max_iterations > 0; + max_iterations--) { + // update now timestamp every so often + if ((max_iterations & 0x1F) == 0) { + now = (int64_t)discord_timestamp_us(client); + // break if we've spent too much time running timers + if (now - start_time > 3000) break; + } + + // no timers to run if (trigger > now || trigger == -1) break; - TIMER_TRY_DELETE + if (~timer.flags & DISCORD_TIMER_CANCELED) TIMER_TRY_DELETE; - if (timer.repeat > 0) + if (timer.repeat > 0 && ~timer.flags & DISCORD_TIMER_CANCELED) timer.repeat--; + + timers->active.skip_update_phase = false; if (timer.cb) timer.cb(client, &timer); - if (timer.repeat == 0 && (timer.flags & DISCORD_TIMER_DELETE_AUTO)) + if (timers->active.skip_update_phase) continue; + + if ((timer.repeat == 0 || timer.flags & DISCORD_TIMER_CANCELED) + && (timer.flags & DISCORD_TIMER_DELETE_AUTO)) + { timer.flags |= DISCORD_TIMER_DELETE; - TIMER_TRY_DELETE - + } + + TIMER_TRY_DELETE; + int64_t next = -1; - if (timer.repeat != 0) { - if (timer.interval > 0) - next = now + ((timer.flags & DISCORD_TIMER_MICROSECONDS) - ? timer.interval : timer.interval * 1000); + if (timer.repeat != 0 && timer.delay != -1 + && ~timer.flags & DISCORD_TIMER_CANCELED) + { + if (timer.interval >= 0) { + next = ((timer.flags & DISCORD_TIMER_INTERVAL_FIXED) ? trigger + : now) + + ((timer.flags & DISCORD_TIMER_MICROSECONDS) + ? timer.interval + : timer.interval * 1000); + } } - if (priority_queue_peek(timers->q, NULL, NULL) != timer.id) - continue; - if (timer.flags & DISCORD_TIMER_DONT_UPDATE) - continue; + timer.flags &= DISCORD_TIMER_ALLOWED_FLAGS; priority_queue_update(timers->q, timer.id, &next, &timer); } - timers->currently_being_run = NULL; + timers->active.timer = NULL; } unsigned @@ -125,37 +163,139 @@ discord_timer_ctl(struct discord *client, struct discord_timer *timer) return _discord_timer_ctl(client, &client->timers.user, timer); } -unsigned -discord_internal_timer_ctl(struct discord *client, - struct discord_timer *timer) +unsigned +discord_internal_timer_ctl(struct discord *client, struct discord_timer *timer) { return _discord_timer_ctl(client, &client->timers.internal, timer); } static unsigned -_discord_timer(struct discord *client, struct discord_timers *timers, - discord_ev_timer cb, void *data, int64_t delay) +_discord_timer(struct discord *client, + struct discord_timers *timers, + discord_ev_timer cb, + void *data, + int64_t delay) { struct discord_timer timer = { - .cb = cb, - .data = data, - .delay = delay, - .flags = DISCORD_TIMER_DELETE_AUTO, + .cb = cb, + .data = data, + .delay = delay, + .flags = DISCORD_TIMER_DELETE_AUTO, }; return _discord_timer_ctl(client, timers, &timer); } unsigned -discord_timer(struct discord *client, discord_ev_timer cb, - void *data, int64_t delay) +discord_timer_interval(struct discord *client, + discord_ev_timer cb, + void *data, + int64_t delay, + int64_t interval, + int64_t repeat) { - return _discord_timer(client, &client->timers.user, cb, data, delay); + struct discord_timer timer = { + .cb = cb, + .data = data, + .delay = delay, + .interval = interval, + .repeat = repeat, + .flags = DISCORD_TIMER_DELETE_AUTO, + }; + return discord_timer_ctl(client, &timer); } +unsigned +discord_timer(struct discord *client, + discord_ev_timer cb, + void *data, + int64_t delay) +{ + return _discord_timer(client, &client->timers.user, cb, data, delay); +} unsigned -discord_internal_timer(struct discord *client, discord_ev_timer cb, - void *data, int64_t delay) +discord_internal_timer(struct discord *client, + discord_ev_timer cb, + void *data, + int64_t delay) { return _discord_timer(client, &client->timers.internal, cb, data, delay); } + +bool +discord_timer_get(struct discord *client, + unsigned id, + struct discord_timer *timer) +{ + if (!id) return 0; + return priority_queue_get(client->timers.user.q, id, NULL, timer); +} + +static void +discord_timer_disable_update_if_active(struct discord_timers *timers, + unsigned id) +{ + if (!timers->active.timer) return; + if (timers->active.timer->id == id) + timers->active.skip_update_phase = true; +} + +bool +discord_timer_start(struct discord *client, unsigned id) +{ + struct discord_timer timer; + discord_timer_disable_update_if_active(&client->timers.user, id); + if (discord_timer_get(client, id, &timer)) { + if (timer.delay < 0) timer.delay = 0; + return discord_timer_ctl(client, &timer); + } + return false; +} + +bool +discord_timer_stop(struct discord *client, unsigned id) +{ + struct discord_timer timer; + discord_timer_disable_update_if_active(&client->timers.user, id); + if (discord_timer_get(client, id, &timer)) { + int64_t disabled = -1; + return priority_queue_update(client->timers.user.q, id, &disabled, + &timer); + } + return false; +} + +static bool +discord_timer_add_flags(struct discord *client, + unsigned id, + enum discord_timer_flags flags) +{ + struct discord_timer timer; + discord_timer_disable_update_if_active(&client->timers.user, id); + if (discord_timer_get(client, id, &timer)) { + timer.flags |= flags; + int64_t run_now = 0; + return priority_queue_update(client->timers.user.q, id, &run_now, + &timer); + } + return false; +} + +bool +discord_timer_cancel(struct discord *client, unsigned id) +{ + return discord_timer_add_flags(client, id, DISCORD_TIMER_CANCELED); +} + +bool +discord_timer_delete(struct discord *client, unsigned id) +{ + return discord_timer_add_flags(client, id, DISCORD_TIMER_DELETE); +} + +bool +discord_timer_cancel_and_delete(struct discord *client, unsigned id) +{ + return discord_timer_add_flags( + client, id, DISCORD_TIMER_DELETE | DISCORD_TIMER_CANCELED); +}