From 3368b271f531283fbff670c3350c4a37008a41dd Mon Sep 17 00:00:00 2001 From: Kapil Gupta Date: Mon, 16 Mar 2026 11:41:06 +0530 Subject: [PATCH] fix(esp_wifi): Redesigned eloop code and extend UTs --- components/wpa_supplicant/port/eloop.c | 1060 ++++++++++++----- .../test_apps/main/test_eloop.c | 528 +++++++- 2 files changed, 1243 insertions(+), 345 deletions(-) diff --git a/components/wpa_supplicant/port/eloop.c b/components/wpa_supplicant/port/eloop.c index a5674d38e6a..1e81dc2ae0f 100644 --- a/components/wpa_supplicant/port/eloop.c +++ b/components/wpa_supplicant/port/eloop.c @@ -6,7 +6,7 @@ * See README for more details. */ /* - * SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -19,38 +19,344 @@ #include "esp_wifi_driver.h" #include "rom/ets_sys.h" #include +#include bool current_task_is_wifi_task(void); +enum eloop_state { + ELOOP_STATE_STOPPED = 0, + ELOOP_STATE_RUNNING, + ELOOP_STATE_DESTROYING, +}; + struct eloop_timeout { struct dl_list list; struct os_reltime time; void *eloop_data; void *user_data; eloop_timeout_handler handler; + eloop_timeout_handler destroy_handler; + eloop_blocking_timeout_handler blocking_handler; + void *sync_semph; + atomic_uint ref_count; + int ret; + bool blocking; #ifdef ELOOP_DEBUG char func_name[100]; int line; #endif - void *sync_semph; - int ret; - eloop_blocking_timeout_handler blocking_handler; }; struct eloop_data { struct dl_list timeout; ETSTimer eloop_timer; - atomic_bool eloop_started; - atomic_bool timeout_running; - void *eloop_semph; + atomic_int state; + atomic_uint dispatch_count; }; #define ELOOP_LOCK() os_mutex_lock(eloop_data_lock) #define ELOOP_UNLOCK() os_mutex_unlock(eloop_data_lock) static void *eloop_data_lock = NULL; - static struct eloop_data eloop; +static atomic_bool eloop_lifecycle_busy = false; + +static void eloop_lifecycle_lock(void) +{ + bool expected = false; + + while (!atomic_compare_exchange_weak(&eloop_lifecycle_busy, &expected, true)) { + expected = false; + vTaskDelay(1); + } +} + +static void eloop_lifecycle_unlock(void) +{ + atomic_store(&eloop_lifecycle_busy, false); +} + +#ifdef ELOOP_DEBUG +static void eloop_debug_log_timeout(const char *action, + const struct eloop_timeout *timeout, + const struct os_reltime *remaining) +{ + if (timeout->blocking) { + if (remaining) { + wpa_printf(MSG_DEBUG, + "ELOOP: %s blocking timer fn:%p scheduled by %s:%d " + "eloop_data=%p user_data=%p in %llu.%06d", + action, timeout->blocking_handler, timeout->func_name, + timeout->line, timeout->eloop_data, timeout->user_data, + (unsigned long long) remaining->sec, + (int) remaining->usec); + } else { + wpa_printf(MSG_DEBUG, + "ELOOP: %s blocking timer fn:%p scheduled by %s:%d " + "eloop_data=%p user_data=%p", + action, timeout->blocking_handler, timeout->func_name, + timeout->line, timeout->eloop_data, timeout->user_data); + } + } else { + if (remaining) { + wpa_printf(MSG_DEBUG, + "ELOOP: %s timer fn:%p scheduled by %s:%d " + "eloop_data=%p user_data=%p in %llu.%06d", + action, timeout->handler, timeout->func_name, + timeout->line, timeout->eloop_data, timeout->user_data, + (unsigned long long) remaining->sec, + (int) remaining->usec); + } else { + wpa_printf(MSG_DEBUG, + "ELOOP: %s timer fn:%p scheduled by %s:%d " + "eloop_data=%p user_data=%p", + action, timeout->handler, timeout->func_name, + timeout->line, timeout->eloop_data, timeout->user_data); + } + } +} +#endif + +static bool eloop_is_running(void) +{ + return atomic_load(&eloop.state) == ELOOP_STATE_RUNNING; +} + +static int eloop_get_reltime_now(struct os_reltime *time) +{ + return os_get_reltime(time); +} + +static void *eloop_alloc(size_t len) +{ + return os_zalloc(len); +} + +static void *eloop_sync_sem_create(void) +{ + return os_semphr_create(1, 0); +} + +static void eloop_timeout_get(struct eloop_timeout *timeout) +{ + atomic_fetch_add(&timeout->ref_count, 1); +} + +static void eloop_timeout_put(struct eloop_timeout *timeout) +{ + if (atomic_fetch_sub(&timeout->ref_count, 1) != 1) { + return; + } + + if (timeout->sync_semph) { + os_semphr_delete(timeout->sync_semph); + } + os_free(timeout); +} + +static void eloop_remaining(const struct os_reltime *deadline, + const struct os_reltime *now, + struct os_reltime *remaining) +{ + if (os_reltime_before(now, deadline)) { + os_reltime_sub(deadline, now, remaining); + } else { + remaining->sec = 0; + remaining->usec = 0; + } +} + +static uint32_t eloop_reltime_to_ms_ceil(const struct os_reltime *remaining) +{ + uint64_t ms = remaining->sec * 1000; + + ms += ((uint32_t)remaining->usec + 999U) / 1000U; + if (ms > UINT32_MAX) { + return UINT32_MAX; + } + + return (uint32_t)ms; +} + +static int eloop_get_abs_timeout(unsigned int secs, unsigned int usecs, + struct os_reltime *time) +{ + os_time_t now_sec; + + if (eloop_get_reltime_now(time) < 0) { + return -1; + } + + now_sec = time->sec; + time->sec += secs; + if (time->sec < now_sec) { + return -1; + } + + time->usec += usecs; + while (time->usec >= 1000000) { + time->sec++; + time->usec -= 1000000; + } + + if (time->sec < now_sec) { + return -1; + } + + return 0; +} + +static int eloop_insert_timeout_locked(struct eloop_timeout *timeout) +{ + struct eloop_timeout *tmp; + int count = 0; + + dl_list_for_each(tmp, &eloop.timeout, struct eloop_timeout, list) { + if (os_reltime_before(&timeout->time, &tmp->time)) { + dl_list_add(tmp->list.prev, &timeout->list); + return count; + } + count++; + } + + dl_list_add_tail(&eloop.timeout, &timeout->list); + return count; +} + +static void eloop_arm_next_locked(void) +{ + struct os_reltime now, remaining; + struct eloop_timeout *timeout; + uint32_t ms; + + os_timer_disarm(&eloop.eloop_timer); + if (!eloop_is_running()) { +#ifdef ELOOP_DEBUG + wpa_printf(MSG_DEBUG, "ELOOP: timer arm skipped because eloop is not running"); +#endif + return; + } + + if (dl_list_empty(&eloop.timeout)) { +#ifdef ELOOP_DEBUG + wpa_printf(MSG_DEBUG, "ELOOP: timer arm skipped because timeout list is empty"); +#endif + return; + } + + timeout = dl_list_first(&eloop.timeout, struct eloop_timeout, list); + if (eloop_get_reltime_now(&now) < 0) { + os_timer_arm(&eloop.eloop_timer, 100, 0); + return; + } + + eloop_remaining(&timeout->time, &now, &remaining); + ms = eloop_reltime_to_ms_ceil(&remaining); +#ifdef ELOOP_DEBUG + eloop_debug_log_timeout("arming next", timeout, &remaining); +#endif + os_timer_arm(&eloop.eloop_timer, ms, 0); +} + +static bool eloop_timeout_matches(struct eloop_timeout *timeout, + eloop_timeout_handler handler, + void *eloop_data, + void *user_data) +{ + if (timeout->blocking || timeout->handler != handler) { + return false; + } + + if (timeout->eloop_data != eloop_data && eloop_data != ELOOP_ALL_CTX) { + return false; + } + + if (timeout->user_data != user_data && user_data != ELOOP_ALL_CTX) { + return false; + } + + return true; +} + +static void eloop_finish_blocking_timeout(struct eloop_timeout *timeout, int ret) +{ + timeout->ret = ret; +#ifdef ELOOP_DEBUG + wpa_printf(MSG_DEBUG, "ELOOP: waking blocking timer scheduled by %s:%d with ret=%d", + timeout->func_name, timeout->line, ret); +#endif + os_semphr_give(timeout->sync_semph); + eloop_timeout_put(timeout); +} + +static void eloop_run_async_timeout(struct eloop_timeout *timeout, + eloop_timeout_handler handler) +{ + if (handler) { + handler(timeout->eloop_data, timeout->user_data); + } + eloop_timeout_put(timeout); +} + +static void eloop_release_timeout_list(struct dl_list *timeouts) +{ + struct eloop_timeout *timeout, *prev; + + dl_list_for_each_safe(timeout, prev, timeouts, struct eloop_timeout, list) { + dl_list_del(&timeout->list); +#ifdef ELOOP_DEBUG + eloop_debug_log_timeout("releasing cancelled", timeout, NULL); +#endif + eloop_timeout_put(timeout); + } +} + +static void eloop_fail_blocking_timeout_list(struct dl_list *timeouts) +{ + struct eloop_timeout *timeout, *prev; + + dl_list_for_each_safe(timeout, prev, timeouts, struct eloop_timeout, list) { + dl_list_del(&timeout->list); +#ifdef ELOOP_DEBUG + eloop_debug_log_timeout("failing pending destroy-time", timeout, NULL); +#endif + eloop_finish_blocking_timeout(timeout, -1); + } +} + +static void eloop_run_destroy_timeout_list(struct dl_list *timeouts) +{ + struct eloop_timeout *timeout, *prev; + + dl_list_for_each_safe(timeout, prev, timeouts, struct eloop_timeout, list) { + dl_list_del(&timeout->list); +#ifdef ELOOP_DEBUG + wpa_printf(MSG_INFO, "ELOOP: destroying timer %p scheduled by %s:%d", + timeout->handler, timeout->func_name, timeout->line); +#endif + eloop_run_async_timeout(timeout, timeout->destroy_handler); + } +} + +#ifdef ELOOP_DEBUG +static struct eloop_timeout *eloop_timeout_alloc(const char *func, int line) +#else +static struct eloop_timeout *eloop_timeout_alloc(void) +#endif +{ + struct eloop_timeout *timeout = eloop_alloc(sizeof(*timeout)); + + if (!timeout) { + return NULL; + } + + atomic_init(&timeout->ref_count, 1); +#ifdef ELOOP_DEBUG + os_strlcpy(timeout->func_name, func, sizeof(timeout->func_name)); + timeout->line = line; +#endif + return timeout; +} static int eloop_run_wrapper(void *data) { @@ -69,22 +375,62 @@ static void eloop_run_timer(void *args) esp_wifi_ipc_internal(&cfg, false); } -int eloop_init(void) +static void eloop_reset_state_locked(void) { os_memset(&eloop, 0, sizeof(eloop)); dl_list_init(&eloop.timeout); os_timer_disarm(&eloop.eloop_timer); os_timer_setfn(&eloop.eloop_timer, (ETSTimerFunc *)eloop_run_timer, NULL); + atomic_store(&eloop.dispatch_count, 0); + atomic_store(&eloop.state, ELOOP_STATE_RUNNING); +} - eloop_data_lock = os_recursive_mutex_create(); +int eloop_init(void) +{ +#ifdef ELOOP_DEBUG + bool initialized = false; +#endif - if (!eloop_data_lock) { - wpa_printf(MSG_ERROR, "failed to create eloop data loop"); - return -1; + eloop_lifecycle_lock(); + if (eloop_data_lock) { + int current_state = atomic_load(&eloop.state); + + if (current_state == ELOOP_STATE_RUNNING) { + eloop_lifecycle_unlock(); + return 0; + } + if (current_state == ELOOP_STATE_DESTROYING) { + eloop_lifecycle_unlock(); + return -1; + } + + ELOOP_LOCK(); + eloop_reset_state_locked(); + ELOOP_UNLOCK(); +#ifdef ELOOP_DEBUG + initialized = true; +#endif + } else { + eloop_data_lock = os_recursive_mutex_create(); + if (!eloop_data_lock) { + wpa_printf(MSG_ERROR, "failed to create eloop data lock"); + eloop_lifecycle_unlock(); + return -1; + } + + ELOOP_LOCK(); + eloop_reset_state_locked(); + ELOOP_UNLOCK(); +#ifdef ELOOP_DEBUG + initialized = true; +#endif } - atomic_store(&eloop.eloop_started, true); - atomic_store(&eloop.timeout_running, false); - + eloop_lifecycle_unlock(); +#ifdef ELOOP_DEBUG + if (initialized) { + wpa_printf(MSG_DEBUG, "ELOOP: initialized"); + } +#endif return 0; } @@ -98,89 +444,67 @@ int eloop_register_timeout(unsigned int secs, unsigned int usecs, void *eloop_data, void *user_data) #endif { - struct eloop_timeout *timeout, *tmp; - os_time_t now_sec; + struct eloop_timeout *timeout; #ifdef ELOOP_DEBUG int count = 0; #endif - if (!atomic_load(&eloop.eloop_started)) { + if (!eloop_is_running()) { return -1; } - timeout = os_zalloc(sizeof(*timeout)); - if (timeout == NULL) { + +#ifdef ELOOP_DEBUG + timeout = eloop_timeout_alloc(func, line); +#else + timeout = eloop_timeout_alloc(); +#endif + if (!timeout) { return -1; } - if (os_get_reltime(&timeout->time) < 0) { - os_free(timeout); + + if (eloop_get_abs_timeout(secs, usecs, &timeout->time) < 0) { + wpa_printf(MSG_DEBUG, + "ELOOP: Too long timeout (secs=%u usecs=%u) to ever happen - ignore it", + secs, usecs); + eloop_timeout_put(timeout); return -1; } - now_sec = timeout->time.sec; - timeout->time.sec += secs; - if (timeout->time.sec < now_sec) { - goto overflow; - } - timeout->time.usec += usecs; - while (timeout->time.usec >= 1000000) { - timeout->time.sec++; - timeout->time.usec -= 1000000; - } - if (timeout->time.sec < now_sec) { - goto overflow; - } + timeout->eloop_data = eloop_data; timeout->user_data = user_data; timeout->handler = handler; -#ifdef ELOOP_DEBUG - os_strlcpy(timeout->func_name, func, 100); - timeout->line = line; -#endif + /* Destroy callbacks must be explicitly opt-in to avoid side effects during teardown. */ + timeout->destroy_handler = NULL; - /* Maintain timeouts in order of increasing time */ ELOOP_LOCK(); - dl_list_for_each(tmp, &eloop.timeout, struct eloop_timeout, list) { - if (os_reltime_before(&timeout->time, &tmp->time)) { - dl_list_add(tmp->list.prev, &timeout->list); - goto run; - } -#ifdef ELOOP_DEBUG - count++; -#endif + if (!eloop_is_running()) { + ELOOP_UNLOCK(); + eloop_timeout_put(timeout); + return -1; } - dl_list_add_tail(&eloop.timeout, &timeout->list); - -run: #ifdef ELOOP_DEBUG - wpa_printf(MSG_DEBUG, "ELOOP: Added one timer from %s:%d to call %p, current order=%d", - timeout->func_name, line, timeout->handler, count); + count = eloop_insert_timeout_locked(timeout); + wpa_printf(MSG_DEBUG, "ELOOP: Added timer from %s:%d to call %p, current order=%d", + timeout->func_name, timeout->line, timeout->handler, count); +#else + eloop_insert_timeout_locked(timeout); #endif - os_timer_disarm(&eloop.eloop_timer); - os_timer_arm(&eloop.eloop_timer, 0, 0); + eloop_arm_next_locked(); ELOOP_UNLOCK(); return 0; - -overflow: - /* - * Integer overflow - assume long enough timeout to be assumed - * to be infinite, i.e., the timeout would never happen. - */ - wpa_printf(MSG_DEBUG, - "ELOOP: Too long timeout (secs=%u usecs=%u) to ever happen - ignore it", - secs, usecs); - os_free(timeout); - return -1; } #ifdef ELOOP_DEBUG -int eloop_register_timeout_blocking_debug(eloop_blocking_timeout_handler handler, void *eloop_data, - void *user_data, const char *func, int line) +int eloop_register_timeout_blocking_debug(eloop_blocking_timeout_handler handler, + void *eloop_data, void *user_data, + const char *func, int line) #else int eloop_register_timeout_blocking(eloop_blocking_timeout_handler handler, void *eloop_data, void *user_data) #endif { - struct eloop_timeout *timeout, *tmp; + struct eloop_timeout *timeout; #ifdef ELOOP_DEBUG int count = 0; #endif @@ -191,107 +515,69 @@ int eloop_register_timeout_blocking(eloop_blocking_timeout_handler handler, return -1; } - if (!atomic_load(&eloop.eloop_started)) { + if (!eloop_is_running()) { return -1; } - timeout = os_zalloc(sizeof(*timeout)); - if (timeout == NULL) { + +#ifdef ELOOP_DEBUG + timeout = eloop_timeout_alloc(func, line); +#else + timeout = eloop_timeout_alloc(); +#endif + if (!timeout) { return -1; } - if (os_get_reltime(&timeout->time) < 0) { - os_free(timeout); + + if (eloop_get_abs_timeout(0, 0, &timeout->time) < 0) { + eloop_timeout_put(timeout); return -1; } + timeout->eloop_data = eloop_data; timeout->user_data = user_data; timeout->blocking_handler = handler; -#ifdef ELOOP_DEBUG - os_strlcpy(timeout->func_name, func, 100); - timeout->line = line; -#endif - - ELOOP_LOCK(); - if (!eloop.eloop_semph) { - eloop.eloop_semph = os_semphr_create(1, 0); - } - ELOOP_UNLOCK(); - - if (!eloop.eloop_semph) { + timeout->blocking = true; + timeout->sync_semph = eloop_sync_sem_create(); + if (!timeout->sync_semph) { wpa_printf(MSG_INFO, "ELOOP: sync semphr not available"); - os_free(timeout); + eloop_timeout_put(timeout); return -1; } - timeout->sync_semph = eloop.eloop_semph; - /* Maintain timeouts in order of increasing time */ - ELOOP_LOCK(); - dl_list_for_each(tmp, &eloop.timeout, struct eloop_timeout, list) { - if (os_reltime_before(&timeout->time, &tmp->time)) { - dl_list_add(tmp->list.prev, &timeout->list); - goto run; - } -#ifdef ELOOP_DEBUG - count++; -#endif - } - dl_list_add_tail(&eloop.timeout, &timeout->list); -run: + /* Keep one ref for the waiting caller and one ref for eloop ownership. */ + eloop_timeout_get(timeout); + + ELOOP_LOCK(); + if (!eloop_is_running()) { + ELOOP_UNLOCK(); + /* Drop both refs: caller-held ref + eloop-owned ref acquired above. */ + eloop_timeout_put(timeout); + eloop_timeout_put(timeout); + return -1; + } #ifdef ELOOP_DEBUG - wpa_printf(MSG_DEBUG, "ELOOP: Added one blocking timer from %s:%d to call %p, current order=%d", - timeout->func_name, line, timeout->handler, count); + count = eloop_insert_timeout_locked(timeout); + wpa_printf(MSG_DEBUG, "ELOOP: Added blocking timer from %s:%d to call %p, current order=%d", + timeout->func_name, timeout->line, timeout->blocking_handler, count); +#else + eloop_insert_timeout_locked(timeout); #endif - os_timer_disarm(&eloop.eloop_timer); - os_timer_arm(&eloop.eloop_timer, 0, 0); + eloop_arm_next_locked(); ELOOP_UNLOCK(); - wpa_printf(MSG_DEBUG, "ELOOP: waiting for sync semphr"); - os_semphr_take(eloop.eloop_semph, OS_BLOCK); - +#ifdef ELOOP_DEBUG + eloop_debug_log_timeout("waiting on", timeout, NULL); +#endif + os_semphr_take(timeout->sync_semph, OS_BLOCK); ret = timeout->ret; - os_free(timeout); +#ifdef ELOOP_DEBUG + wpa_printf(MSG_DEBUG, "ELOOP: blocking timer scheduled by %s:%d completed with ret=%d", + timeout->func_name, timeout->line, ret); +#endif + eloop_timeout_put(timeout); return ret; } -static bool timeout_exists(struct eloop_timeout *old) -{ - struct eloop_timeout *timeout, *prev; - dl_list_for_each_safe(timeout, prev, &eloop.timeout, - struct eloop_timeout, list) { - if (old == timeout) { - return true; - } - } - - return false; -} - -static void eloop_remove_blocking_timeout(struct eloop_timeout *timeout) -{ - bool timeout_present = false; - ELOOP_LOCK(); - /* Make sure timeout still exists(Another context may have deleted this) */ - timeout_present = timeout_exists(timeout); - if (timeout_present) { - dl_list_del(&timeout->list); - } - ELOOP_UNLOCK(); -} - -static void eloop_remove_timeout(struct eloop_timeout *timeout) -{ - bool timeout_present = false; - ELOOP_LOCK(); - /* Make sure timeout still exists(Another context may have deleted this) */ - timeout_present = timeout_exists(timeout); - if (timeout_present) { - dl_list_del(&timeout->list); - } - ELOOP_UNLOCK(); - if (timeout_present) { - os_free(timeout); - } -} - #ifdef ELOOP_DEBUG int eloop_cancel_timeout_debug(eloop_timeout_handler handler, void *eloop_data, void *user_data, const char *func, int line) @@ -300,246 +586,392 @@ int eloop_cancel_timeout(eloop_timeout_handler handler, void *eloop_data, void *user_data) #endif { + struct dl_list removed; struct eloop_timeout *timeout, *prev; - int removed = 0; + int removed_count = 0; + if (!eloop_data_lock) { + return 0; + } + + dl_list_init(&removed); + + ELOOP_LOCK(); dl_list_for_each_safe(timeout, prev, &eloop.timeout, struct eloop_timeout, list) { - if (timeout->handler == handler && - (timeout->eloop_data == eloop_data || - eloop_data == ELOOP_ALL_CTX) && - (timeout->user_data == user_data || - user_data == ELOOP_ALL_CTX)) { - eloop_remove_timeout(timeout); - removed++; + if (!eloop_timeout_matches(timeout, handler, eloop_data, user_data)) { + continue; } + + dl_list_del(&timeout->list); + dl_list_add_tail(&removed, &timeout->list); + removed_count++; } + if (removed_count) { + eloop_arm_next_locked(); + } + ELOOP_UNLOCK(); + #ifdef ELOOP_DEBUG - wpa_printf(MSG_DEBUG, "ELOOP: %s:%d called to remove timer handler=%p, removed count=%d", - func, line, handler, removed); + wpa_printf(MSG_DEBUG, "ELOOP: %s:%d removed timer handler=%p count=%d", + func, line, handler, removed_count); #endif - return removed; + eloop_release_timeout_list(&removed); + return removed_count; } int eloop_cancel_timeout_one(eloop_timeout_handler handler, void *eloop_data, void *user_data, struct os_reltime *remaining) { - struct eloop_timeout *timeout, *prev; - int removed = 0; + struct eloop_timeout *timeout, *prev, *removed = NULL; struct os_reltime now; - os_get_reltime(&now); - remaining->sec = remaining->usec = 0; + if (!eloop_data_lock) { + remaining->sec = remaining->usec = 0; + return 0; + } + remaining->sec = remaining->usec = 0; + if (eloop_get_reltime_now(&now) < 0) { + return 0; + } + + ELOOP_LOCK(); dl_list_for_each_safe(timeout, prev, &eloop.timeout, struct eloop_timeout, list) { - if (timeout->handler == handler && - (timeout->eloop_data == eloop_data) && - (timeout->user_data == user_data)) { - removed = 1; - if (os_reltime_before(&now, &timeout->time)) { - os_reltime_sub(&timeout->time, &now, remaining); - } - eloop_remove_timeout(timeout); - break; + if (!eloop_timeout_matches(timeout, handler, eloop_data, user_data)) { + continue; } + + eloop_remaining(&timeout->time, &now, remaining); + dl_list_del(&timeout->list); + removed = timeout; + eloop_arm_next_locked(); + break; } - return removed; + ELOOP_UNLOCK(); + + if (removed) { +#ifdef ELOOP_DEBUG + eloop_debug_log_timeout("removed single", removed, remaining); +#endif + eloop_timeout_put(removed); + return 1; + } + + return 0; } int eloop_is_timeout_registered(eloop_timeout_handler handler, void *eloop_data, void *user_data) { - struct eloop_timeout *tmp; + struct eloop_timeout *timeout; + int registered = 0; - dl_list_for_each(tmp, &eloop.timeout, struct eloop_timeout, list) { - if (tmp->handler == handler && - tmp->eloop_data == eloop_data && - tmp->user_data == user_data) { - return 1; - } + if (!eloop_data_lock) { + return 0; } - return 0; + ELOOP_LOCK(); + dl_list_for_each(timeout, &eloop.timeout, struct eloop_timeout, list) { + if (!eloop_timeout_matches(timeout, handler, eloop_data, user_data)) { + continue; + } + + registered = 1; + break; + } + ELOOP_UNLOCK(); + + return registered; } int eloop_deplete_timeout(unsigned int req_secs, unsigned int req_usecs, eloop_timeout_handler handler, void *eloop_data, void *user_data) { - struct os_reltime now, requested, remaining; - struct eloop_timeout *tmp; + struct os_reltime now, requested, remaining, new_time; + struct eloop_timeout *timeout; + int ret = -1; - dl_list_for_each(tmp, &eloop.timeout, struct eloop_timeout, list) { - if (tmp->handler == handler && - tmp->eloop_data == eloop_data && - tmp->user_data == user_data) { - requested.sec = req_secs; - requested.usec = req_usecs; - os_get_reltime(&now); - os_reltime_sub(&tmp->time, &now, &remaining); - if (os_reltime_before(&requested, &remaining)) { - eloop_cancel_timeout(handler, eloop_data, - user_data); - eloop_register_timeout(requested.sec, - requested.usec, - handler, eloop_data, - user_data); - return 1; - } - return 0; - } + if (!eloop_data_lock) { + return -1; } - return -1; + requested.sec = req_secs; + requested.usec = req_usecs; + + ELOOP_LOCK(); + if (eloop_get_reltime_now(&now) < 0) { + ELOOP_UNLOCK(); + return -1; + } + dl_list_for_each(timeout, &eloop.timeout, struct eloop_timeout, list) { + if (!eloop_timeout_matches(timeout, handler, eloop_data, user_data)) { + continue; + } + + eloop_remaining(&timeout->time, &now, &remaining); + ret = 0; + if (os_reltime_before(&requested, &remaining)) { + if (eloop_get_abs_timeout(requested.sec, requested.usec, &new_time) < 0) { + ret = -1; + break; + } + + dl_list_del(&timeout->list); + timeout->time = new_time; + eloop_insert_timeout_locked(timeout); + eloop_arm_next_locked(); + ret = 1; +#ifdef ELOOP_DEBUG + eloop_debug_log_timeout("depleted", timeout, &requested); +#endif + } +#ifdef ELOOP_DEBUG + if (ret == 0) { + eloop_debug_log_timeout("deplete left unchanged", timeout, &remaining); + } +#endif + break; + } + ELOOP_UNLOCK(); + +#ifdef ELOOP_DEBUG + if (ret == -1) { + wpa_printf(MSG_DEBUG, "ELOOP: deplete could not find timer fn:%p eloop_data=%p user_data=%p", + handler, eloop_data, user_data); + } +#endif + return ret; } int eloop_replenish_timeout(unsigned int req_secs, unsigned int req_usecs, eloop_timeout_handler handler, void *eloop_data, void *user_data) { - struct os_reltime now, requested, remaining; - struct eloop_timeout *tmp; + struct os_reltime now, requested, remaining, new_time; + struct eloop_timeout *timeout; + int ret = -1; - dl_list_for_each(tmp, &eloop.timeout, struct eloop_timeout, list) { - if (tmp->handler == handler && - tmp->eloop_data == eloop_data && - tmp->user_data == user_data) { - requested.sec = req_secs; - requested.usec = req_usecs; - os_get_reltime(&now); - os_reltime_sub(&tmp->time, &now, &remaining); - if (os_reltime_before(&remaining, &requested)) { - eloop_cancel_timeout(handler, eloop_data, - user_data); - eloop_register_timeout(requested.sec, - requested.usec, - handler, eloop_data, - user_data); - return 1; - } - return 0; - } + if (!eloop_data_lock) { + return -1; } - return -1; + requested.sec = req_secs; + requested.usec = req_usecs; + + ELOOP_LOCK(); + if (eloop_get_reltime_now(&now) < 0) { + ELOOP_UNLOCK(); + return -1; + } + dl_list_for_each(timeout, &eloop.timeout, struct eloop_timeout, list) { + if (!eloop_timeout_matches(timeout, handler, eloop_data, user_data)) { + continue; + } + + eloop_remaining(&timeout->time, &now, &remaining); + ret = 0; + if (os_reltime_before(&remaining, &requested)) { + if (eloop_get_abs_timeout(requested.sec, requested.usec, &new_time) < 0) { + ret = -1; + break; + } + + dl_list_del(&timeout->list); + timeout->time = new_time; + eloop_insert_timeout_locked(timeout); + eloop_arm_next_locked(); + ret = 1; +#ifdef ELOOP_DEBUG + eloop_debug_log_timeout("replenished", timeout, &requested); +#endif + } +#ifdef ELOOP_DEBUG + if (ret == 0) { + eloop_debug_log_timeout("replenish left unchanged", timeout, &remaining); + } +#endif + break; + } + ELOOP_UNLOCK(); + +#ifdef ELOOP_DEBUG + if (ret == -1) { + wpa_printf(MSG_DEBUG, "ELOOP: replenish could not find timer fn:%p eloop_data=%p user_data=%p", + handler, eloop_data, user_data); + } +#endif + return ret; } void eloop_run(void) { - struct os_reltime tv, now; - - if (!atomic_load(&eloop.eloop_started)) { + if (!eloop_is_running()) { return; } - atomic_store(&eloop.timeout_running, true); - while (!dl_list_empty(&eloop.timeout)) { + while (eloop_is_running()) { struct eloop_timeout *timeout; + struct os_reltime now; ELOOP_LOCK(); - timeout = dl_list_first(&eloop.timeout, struct eloop_timeout, - list); - ELOOP_UNLOCK(); - if (timeout) { - os_get_reltime(&now); - if (os_reltime_before(&now, &timeout->time)) { - /* we don't need to process it rn, do it later */ - uint32_t ms; - os_reltime_sub(&timeout->time, &now, &tv); - ms = tv.sec * 1000 + tv.usec / 1000; - ELOOP_LOCK(); - os_timer_disarm(&eloop.eloop_timer); - os_timer_arm(&eloop.eloop_timer, ms, 0); - ELOOP_UNLOCK(); - goto out; - } else { - void *eloop_data = timeout->eloop_data; - void *user_data = timeout->user_data; - void *sync_semaphr = timeout->sync_semph; - eloop_timeout_handler handler = - timeout->handler; -#ifdef ELOOP_DEBUG - char fn_name[100] = {0}; - int line = timeout->line; - os_strlcpy(fn_name, timeout->func_name, 100); -#endif - /* will be freed in caller context in blocking call */ - if (!sync_semaphr) { - eloop_remove_timeout(timeout); -#ifdef ELOOP_DEBUG - wpa_printf(MSG_DEBUG, "ELOOP: Running timer fn:%p scheduled by %s:%d ", - handler, fn_name, line); -#endif - handler(eloop_data, user_data); - } else { - eloop_remove_blocking_timeout(timeout); - eloop_blocking_timeout_handler handler2 = - timeout->blocking_handler; -#ifdef ELOOP_DEBUG - wpa_printf(MSG_DEBUG, "ELOOP: Running blocking timer fn:%p scheduled by %s:%d ", - handler2, fn_name, line); -#endif - timeout->ret = handler2(eloop_data, user_data); -#ifdef ELOOP_DEBUG - wpa_printf(MSG_DEBUG, "ELOOP: releasing sync semaphor"); -#endif - os_semphr_give(sync_semaphr); - } - } + if (!eloop_is_running()) { + ELOOP_UNLOCK(); + break; } + + if (dl_list_empty(&eloop.timeout)) { + os_timer_disarm(&eloop.eloop_timer); + ELOOP_UNLOCK(); + break; + } + + timeout = dl_list_first(&eloop.timeout, struct eloop_timeout, list); + if (eloop_get_reltime_now(&now) < 0) { + eloop_arm_next_locked(); + ELOOP_UNLOCK(); + break; + } + if (os_reltime_before(&now, &timeout->time)) { +#ifdef ELOOP_DEBUG + struct os_reltime remaining; + + eloop_remaining(&timeout->time, &now, &remaining); + eloop_debug_log_timeout("not ready, deferring", timeout, &remaining); +#endif + eloop_arm_next_locked(); + ELOOP_UNLOCK(); + break; + } + + dl_list_del(&timeout->list); + atomic_fetch_add(&eloop.dispatch_count, 1); + ELOOP_UNLOCK(); + + if (timeout->blocking) { +#ifdef ELOOP_DEBUG + wpa_printf(MSG_DEBUG, "ELOOP: Running blocking timer fn:%p scheduled by %s:%d", + timeout->blocking_handler, timeout->func_name, timeout->line); +#endif + eloop_finish_blocking_timeout(timeout, + timeout->blocking_handler(timeout->eloop_data, + timeout->user_data)); + } else { +#ifdef ELOOP_DEBUG + wpa_printf(MSG_DEBUG, "ELOOP: Running timer fn:%p scheduled by %s:%d", + timeout->handler, timeout->func_name, timeout->line); +#endif + eloop_run_async_timeout(timeout, timeout->handler); + } + + atomic_fetch_sub(&eloop.dispatch_count, 1); } -out: - atomic_store(&eloop.timeout_running, false); - return; } void eloop_destroy(void) { + struct dl_list pending_timeouts; + struct dl_list pending_waiters; struct eloop_timeout *timeout, *prev; + uint32_t wait_loops = 0; + const uint32_t wait_step_ms = 100; + const uint32_t wait_warn_loops = 5000 / wait_step_ms; + int old_state; +#ifdef ELOOP_DEBUG + int async_count = 0; + int blocking_count = 0; +#endif - if (!atomic_load(&eloop.eloop_started)) { + eloop_lifecycle_lock(); + if (!eloop_data_lock) { + eloop_lifecycle_unlock(); return; } - atomic_store(&eloop.eloop_started, false); + old_state = atomic_exchange(&eloop.state, ELOOP_STATE_DESTROYING); + if (old_state == ELOOP_STATE_STOPPED) { + atomic_store(&eloop.state, ELOOP_STATE_STOPPED); + eloop_lifecycle_unlock(); + return; + } + if (old_state == ELOOP_STATE_DESTROYING) { + eloop_lifecycle_unlock(); + return; + } - while (atomic_load(&eloop.timeout_running)) { - vTaskDelay(100 / portTICK_PERIOD_MS); // Yield CPU - } - dl_list_for_each_safe(timeout, prev, &eloop.timeout, - struct eloop_timeout, list) { #ifdef ELOOP_DEBUG - struct os_reltime now; - os_get_reltime(&now); - int sec, usec; - sec = timeout->time.sec - now.sec; - usec = timeout->time.usec - now.usec; - if (timeout->time.usec < now.usec) { - sec--; - usec += 1000000; - } - wpa_printf(MSG_INFO, "ELOOP: remaining timeout: %d.%06d " - "eloop_data=%p user_data=%p handler=%p", - sec, usec, timeout->eloop_data, timeout->user_data, - timeout->handler); + wpa_printf(MSG_DEBUG, "ELOOP: destroy requested"); #endif - if (timeout->handler) { - timeout->handler(timeout->eloop_data, timeout->user_data); + dl_list_init(&pending_timeouts); + dl_list_init(&pending_waiters); + + ELOOP_LOCK(); + os_timer_disarm(&eloop.eloop_timer); + dl_list_for_each_safe(timeout, prev, &eloop.timeout, struct eloop_timeout, list) { +#ifdef ELOOP_DEBUG + struct os_reltime now, remaining; + + if (eloop_get_reltime_now(&now) < 0) { + remaining.sec = 0; + remaining.usec = 0; + } else { + eloop_remaining(&timeout->time, &now, &remaining); + } + wpa_printf(MSG_INFO, "ELOOP: remaining timeout: %llu.%06d eloop_data=%p user_data=%p handler=%p", + remaining.sec, remaining.usec, timeout->eloop_data, + timeout->user_data, timeout->handler); +#endif + dl_list_del(&timeout->list); + if (timeout->blocking) { +#ifdef ELOOP_DEBUG + blocking_count++; + eloop_debug_log_timeout("moving pending destroy-time", timeout, NULL); +#endif + dl_list_add_tail(&pending_waiters, &timeout->list); + } else { +#ifdef ELOOP_DEBUG + async_count++; + eloop_debug_log_timeout("moving pending destroy-time", timeout, NULL); +#endif + dl_list_add_tail(&pending_timeouts, &timeout->list); } - eloop_remove_timeout(timeout); } - if (eloop_data_lock) { - os_mutex_delete(eloop_data_lock); - eloop_data_lock = NULL; - } - if (eloop.eloop_semph) { - os_semphr_delete(eloop.eloop_semph); - eloop.eloop_semph = NULL; + ELOOP_UNLOCK(); + +#ifdef ELOOP_DEBUG + wpa_printf(MSG_DEBUG, "ELOOP: waiting for %u in-flight dispatches before destroy", + atomic_load(&eloop.dispatch_count)); +#endif + while (atomic_load(&eloop.dispatch_count) != 0) { + if (++wait_loops >= wait_warn_loops) { + wpa_printf(MSG_ERROR, + "ELOOP: destroy still waiting for %u in-flight dispatches after %u ms", + atomic_load(&eloop.dispatch_count), wait_warn_loops * wait_step_ms); + wait_loops = 0; + } + vTaskDelay(wait_step_ms / portTICK_PERIOD_MS); } + +#ifdef ELOOP_DEBUG + wpa_printf(MSG_DEBUG, "ELOOP: destroy draining %d async timers and %d blocking waiters", + async_count, blocking_count); +#endif + eloop_fail_blocking_timeout_list(&pending_waiters); + eloop_run_destroy_timeout_list(&pending_timeouts); + os_timer_disarm(&eloop.eloop_timer); os_timer_done(&eloop.eloop_timer); + + ELOOP_LOCK(); os_memset(&eloop, 0, sizeof(eloop)); + dl_list_init(&eloop.timeout); + ELOOP_UNLOCK(); + eloop_lifecycle_unlock(); +#ifdef ELOOP_DEBUG + wpa_printf(MSG_DEBUG, "ELOOP: destroyed"); +#endif } diff --git a/components/wpa_supplicant/test_apps/main/test_eloop.c b/components/wpa_supplicant/test_apps/main/test_eloop.c index dbbb38475a7..bd68e804f46 100644 --- a/components/wpa_supplicant/test_apps/main/test_eloop.c +++ b/components/wpa_supplicant/test_apps/main/test_eloop.c @@ -1,11 +1,13 @@ /* - * SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Unlicense OR CC0-1.0 */ #include #include #include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" #include "esp_system.h" #include "esp_log.h" #include "esp_system.h" @@ -19,29 +21,110 @@ #include "memory_checks.h" #include "test_wpa_supplicant_common.h" -static uint32_t timeouts_usec[6] = { 10000, 1000, 10000, 5000, 15000, 1000 }; -static uint32_t timeouts_sec[6] = { 10, 1, 10, 5, 15, 1 }; +bool current_task_is_wifi_task(void); +static void eloop_test_reset_hooks(void) +{ +} + +static const uint32_t timeouts_usec[6] = { 300000, 100000, 300000, 200000, 400000, 100000 }; +static const uint32_t timeouts_sec[6] = { 0, 0, 0, 0, 0, 0 }; static int executed_order[6]; static int t; static struct os_reltime ts; +struct timeout_test_ctx { + void *sem; + int count; + int last_id; + bool ran_in_wifi_task; + struct os_reltime armed_at; + struct os_reltime fired_at; +}; + +struct chain_test_ctx { + void *sem; + struct timeout_test_ctx *followup_ctx; + struct timeout_test_ctx *cancel_ctx; + int followup_id; + int cancel_id; + int count; + bool ran_in_wifi_task; +}; + +struct blocking_waiter_ctx { + int idx; + eloop_blocking_timeout_handler handler; + void *done_sem; +}; + +static void *blocking_gate; +static void *blocking_done_sem; +static int blocking_results[2]; +static bool blocking_in_wifi_task[2]; +static int blocking_ids[2] = { 11, 22 }; +static int blocking_task_index[2] = { 0, 1 }; + +static uint32_t reltime_diff_ms(const struct os_reltime *start, + const struct os_reltime *end) +{ + struct os_reltime diff; + + os_reltime_sub(end, start, &diff); + return diff.sec * 1000U + diff.usec / 1000U; +} + +static void wait_sem_or_fail(void *sem, uint32_t timeout_ms) +{ + TEST_ASSERT_EQUAL(pdTRUE, + os_semphr_take(sem, os_task_ms_to_tick(timeout_ms))); +} + +static void expect_sem_timeout(void *sem, uint32_t timeout_ms) +{ + TEST_ASSERT_EQUAL(pdFALSE, + os_semphr_take(sem, os_task_ms_to_tick(timeout_ms))); +} + +static void eloop_test_wifi_start(void) +{ + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + + cfg.nvs_enable = false; + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); +} + +static void eloop_test_wifi_stop(void) +{ + TEST_ESP_OK(esp_wifi_stop()); + TEST_ESP_OK(esp_wifi_deinit()); +} + +static void reset_timeout_ctx(struct timeout_test_ctx *ctx, void *sem) +{ + memset(ctx, 0, sizeof(*ctx)); + ctx->sem = sem; + ctx->last_id = -1; + os_get_reltime(&ctx->armed_at); +} + /* there is only single instance of esp_timer so no need of protection */ -static void callback(void *a, void *b) +static void ordered_timeout_callback(void *a, void *b) { int *i = a; struct os_time age, now; + int32_t ms_diff; os_get_reltime(&now); os_time_sub(&now, &ts, &age); - int32_t ms_diff = (age.sec - timeouts_sec[*i]) * 1000 + - (age.usec - timeouts_usec[*i]) / 1000; + ms_diff = (age.sec - timeouts_sec[*i]) * 1000 + + (age.usec - timeouts_usec[*i]) / 1000; if (t > 5) { TEST_ASSERT(0); } - /* let's give 50 ms offset for this small block */ + /* Allow some jitter from scheduling into pptask context. */ if (ms_diff > 50) { executed_order[t] = -1; } else { @@ -51,49 +134,432 @@ static void callback(void *a, void *b) ESP_LOGI("Eloop Test", "timer[%d] ran after %" PRId32 " msec of scheduled time", *i, ms_diff); +} +static void generic_timeout_callback(void *eloop_data, void *user_data) +{ + struct timeout_test_ctx *ctx = eloop_data; + int *id = user_data; + + ctx->count++; + ctx->last_id = id ? *id : -1; + ctx->ran_in_wifi_task = current_task_is_wifi_task(); + os_get_reltime(&ctx->fired_at); + + if (ctx->sem) { + os_semphr_give(ctx->sem); + } +} + +static void chain_timeout_callback(void *eloop_data, void *user_data) +{ + struct chain_test_ctx *ctx = eloop_data; + (void) user_data; + + ctx->count++; + ctx->ran_in_wifi_task = current_task_is_wifi_task(); + eloop_cancel_timeout(generic_timeout_callback, ctx->cancel_ctx, + &ctx->cancel_id); + os_get_reltime(&ctx->followup_ctx->armed_at); + TEST_ASSERT_EQUAL(0, eloop_register_timeout(0, 0, generic_timeout_callback, + ctx->followup_ctx, + &ctx->followup_id)); + + if (ctx->sem) { + os_semphr_give(ctx->sem); + } +} + +static int blocking_callback(void *a, void *b) +{ + int idx = *(int *) a; + int *id = b; + + blocking_in_wifi_task[idx] = current_task_is_wifi_task(); + os_semphr_take(blocking_gate, OS_BLOCK); + return *id; +} + +static int immediate_blocking_callback(void *a, void *b) +{ + int idx = *(int *) a; + int *id = b; + + blocking_in_wifi_task[idx] = current_task_is_wifi_task(); + return *id; +} + +static void blocking_waiter_task(void *arg) +{ + struct blocking_waiter_ctx *ctx = arg; + int idx = ctx->idx; + + blocking_results[idx] = eloop_register_timeout_blocking(ctx->handler, + &blocking_task_index[idx], + &blocking_ids[idx]); + os_semphr_give(ctx->done_sem ? ctx->done_sem : blocking_done_sem); + vTaskDelete(NULL); +} + +static void assert_eloop_inactive_api_behavior(void) +{ + struct timeout_test_ctx ctx; + struct os_reltime remaining = { .sec = 1, .usec = 1 }; + int id = 91; + + memset(&ctx, 0, sizeof(ctx)); + TEST_ASSERT_EQUAL(-1, eloop_register_timeout(0, 1000, + generic_timeout_callback, + &ctx, &id)); + TEST_ASSERT_EQUAL(-1, eloop_register_timeout_blocking(immediate_blocking_callback, + &blocking_task_index[0], + &blocking_ids[0])); + TEST_ASSERT_EQUAL(0, eloop_cancel_timeout(generic_timeout_callback, + &ctx, &id)); + TEST_ASSERT_EQUAL(0, eloop_cancel_timeout_one(generic_timeout_callback, + &ctx, &id, &remaining)); + TEST_ASSERT_EQUAL(0, remaining.sec); + TEST_ASSERT_EQUAL(0, remaining.usec); + TEST_ASSERT_EQUAL(0, eloop_is_timeout_registered(generic_timeout_callback, + &ctx, &id)); + TEST_ASSERT_EQUAL(-1, eloop_deplete_timeout(0, 1000, + generic_timeout_callback, + &ctx, &id)); + TEST_ASSERT_EQUAL(-1, eloop_replenish_timeout(0, 1000, + generic_timeout_callback, + &ctx, &id)); } extern const wifi_osi_funcs_t *wifi_funcs; -/* Check if eloop runs its timers correctly & in correct order */ + TEST_CASE("Test eloop timers run", "[eloop]") { + int execution_order[6] = { 1, 5, 3, 0, 2, 4 }; + int index[6] = { 0, 1, 2, 3, 4, 5 }; + int fired_before_stop; + + eloop_test_reset_hooks(); set_leak_threshold(1000); - wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); - cfg.nvs_enable = false; - ESP_ERROR_CHECK(esp_wifi_init(&cfg)); - TEST_ESP_OK(esp_wifi_stop()); - TEST_ESP_OK(esp_wifi_deinit()); + eloop_test_wifi_start(); + eloop_test_wifi_stop(); /* Reset memory stats since some is leaked during the first initialization */ test_utils_record_free_mem(); - int execution_order[6] = {1, 5, 3, 0, 2, 4}; - int index[6] = {0, 1, 2, 3, 4, 5}; t = 0; - - /* We need pptask to run eloop, wifi init will do that */ - ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + eloop_test_wifi_start(); os_get_reltime(&ts); for (int i = 0; i < 6; i++) { - eloop_register_timeout(timeouts_sec[i], timeouts_usec[i], - callback, &index[i], NULL); + TEST_ASSERT_EQUAL(0, eloop_register_timeout(timeouts_sec[i], timeouts_usec[i], + ordered_timeout_callback, + &index[i], NULL)); } - /* wait for all timers to run */ - os_sleep(20, 0); - /* check the execution order, this will also check whether they were fired at correct time */ + os_sleep(1, 0); TEST_ASSERT(memcmp(execution_order, executed_order, 6 * sizeof(int)) == 0); t = 0; - /* Add timers to check deinit happens gracefully */ for (int i = 0; i < 6; i++) { - eloop_register_timeout(timeouts_sec[i], timeouts_usec[i], - callback, &index[i], NULL); + TEST_ASSERT_EQUAL(0, eloop_register_timeout(timeouts_sec[i], timeouts_usec[i], + ordered_timeout_callback, + &index[i], NULL)); } - /* Stop wifi before all the timers have run */ - os_sleep(2, 0); - TEST_ESP_OK(esp_wifi_stop()); - TEST_ESP_OK(esp_wifi_deinit()); - os_sleep(3, 0); + /* Pending async timers are dropped during eloop destroy; only callbacks + * already dispatched before stop should run. */ + vTaskDelay(250 / portTICK_PERIOD_MS); + fired_before_stop = t; + eloop_test_wifi_stop(); + vTaskDelay(300 / portTICK_PERIOD_MS); + TEST_ASSERT_EQUAL(fired_before_stop, t); + TEST_ASSERT_TRUE(t >= 1); + TEST_ASSERT_TRUE(t < 6); +} + +TEST_CASE("Test eloop rejects APIs before init and after destroy", "[eloop]") +{ + eloop_test_reset_hooks(); + assert_eloop_inactive_api_behavior(); + + eloop_test_wifi_start(); + eloop_test_wifi_stop(); + + assert_eloop_inactive_api_behavior(); +} + +TEST_CASE("Test eloop can reinitialize repeatedly", "[eloop]") +{ + for (int i = 0; i < 3; i++) { + struct timeout_test_ctx ctx; + void *sem = os_semphr_create(1, 0); + int user_id = 100 + i; + + TEST_ASSERT_NOT_NULL(sem); + eloop_test_reset_hooks(); + reset_timeout_ctx(&ctx, sem); + eloop_test_wifi_start(); + + TEST_ASSERT_EQUAL(0, eloop_register_timeout(0, 50000, + generic_timeout_callback, + &ctx, &user_id)); + wait_sem_or_fail(sem, 1000); + TEST_ASSERT_EQUAL(1, ctx.count); + TEST_ASSERT_TRUE(ctx.ran_in_wifi_task); + + eloop_test_wifi_stop(); + os_semphr_delete(sem); + } +} + +TEST_CASE("Test eloop cancel and query APIs", "[eloop]") +{ + struct timeout_test_ctx ctx_a, ctx_b, ctx_c; + void *sem_a = os_semphr_create(1, 0); + void *sem_b = os_semphr_create(1, 0); + void *sem_c = os_semphr_create(1, 0); + int user_a = 1; + int user_b = 2; + + TEST_ASSERT_NOT_NULL(sem_a); + TEST_ASSERT_NOT_NULL(sem_b); + TEST_ASSERT_NOT_NULL(sem_c); + + eloop_test_reset_hooks(); + reset_timeout_ctx(&ctx_a, sem_a); + reset_timeout_ctx(&ctx_b, sem_b); + reset_timeout_ctx(&ctx_c, sem_c); + + eloop_test_wifi_start(); + TEST_ASSERT_EQUAL(0, eloop_register_timeout(0, 400000, generic_timeout_callback, + &ctx_a, &user_a)); + TEST_ASSERT_EQUAL(0, eloop_register_timeout(0, 400000, generic_timeout_callback, + &ctx_b, &user_b)); + TEST_ASSERT_EQUAL(0, eloop_register_timeout(0, 400000, generic_timeout_callback, + &ctx_c, &user_a)); + + TEST_ASSERT_EQUAL(1, eloop_is_timeout_registered(generic_timeout_callback, + &ctx_a, &user_a)); + TEST_ASSERT_EQUAL(1, eloop_is_timeout_registered(generic_timeout_callback, + &ctx_b, &user_b)); + TEST_ASSERT_EQUAL(1, eloop_is_timeout_registered(generic_timeout_callback, + &ctx_c, &user_a)); + + TEST_ASSERT_EQUAL(2, eloop_cancel_timeout(generic_timeout_callback, + ELOOP_ALL_CTX, &user_a)); + TEST_ASSERT_EQUAL(0, eloop_is_timeout_registered(generic_timeout_callback, + &ctx_a, &user_a)); + TEST_ASSERT_EQUAL(0, eloop_is_timeout_registered(generic_timeout_callback, + &ctx_c, &user_a)); + TEST_ASSERT_EQUAL(1, eloop_is_timeout_registered(generic_timeout_callback, + &ctx_b, &user_b)); + + wait_sem_or_fail(sem_b, 1000); + expect_sem_timeout(sem_a, 50); + expect_sem_timeout(sem_c, 50); + + TEST_ASSERT_EQUAL(0, eloop_is_timeout_registered(generic_timeout_callback, + &ctx_b, &user_b)); + TEST_ASSERT_EQUAL(0, ctx_a.count); + TEST_ASSERT_EQUAL(1, ctx_b.count); + TEST_ASSERT_EQUAL(0, ctx_c.count); + TEST_ASSERT_TRUE(ctx_b.ran_in_wifi_task); + + eloop_test_wifi_stop(); + os_semphr_delete(sem_a); + os_semphr_delete(sem_b); + os_semphr_delete(sem_c); +} + +TEST_CASE("Test eloop cancel_timeout_one returns remaining", "[eloop]") +{ + struct timeout_test_ctx ctx; + struct os_reltime remaining; + uint32_t remaining_ms; + void *sem = os_semphr_create(1, 0); + int user_id = 7; + + TEST_ASSERT_NOT_NULL(sem); + eloop_test_reset_hooks(); + reset_timeout_ctx(&ctx, sem); + + eloop_test_wifi_start(); + TEST_ASSERT_EQUAL(0, eloop_register_timeout(0, 600000, generic_timeout_callback, + &ctx, &user_id)); + TEST_ASSERT_EQUAL(1, eloop_is_timeout_registered(generic_timeout_callback, + &ctx, &user_id)); + + vTaskDelay(150 / portTICK_PERIOD_MS); + TEST_ASSERT_EQUAL(1, eloop_cancel_timeout_one(generic_timeout_callback, + &ctx, &user_id, &remaining)); + remaining_ms = remaining.sec * 1000U + remaining.usec / 1000U; + TEST_ASSERT_TRUE(remaining_ms >= 250 && remaining_ms <= 550); + TEST_ASSERT_EQUAL(0, eloop_is_timeout_registered(generic_timeout_callback, + &ctx, &user_id)); + TEST_ASSERT_EQUAL(0, eloop_cancel_timeout_one(generic_timeout_callback, + &ctx, &user_id, &remaining)); + + expect_sem_timeout(sem, 700); + TEST_ASSERT_EQUAL(0, ctx.count); + + eloop_test_wifi_stop(); + os_semphr_delete(sem); +} + +TEST_CASE("Test eloop deplete and replenish APIs", "[eloop]") +{ + struct timeout_test_ctx deplete_ctx, replenish_ctx, nochange_ctx; + void *deplete_sem = os_semphr_create(1, 0); + void *replenish_sem = os_semphr_create(1, 0); + int deplete_id = 31; + int replenish_id = 32; + int nochange_id = 33; + uint32_t elapsed_ms; + + TEST_ASSERT_NOT_NULL(deplete_sem); + TEST_ASSERT_NOT_NULL(replenish_sem); + + eloop_test_reset_hooks(); + eloop_test_wifi_start(); + + reset_timeout_ctx(&deplete_ctx, deplete_sem); + TEST_ASSERT_EQUAL(0, eloop_register_timeout(0, 800000, generic_timeout_callback, + &deplete_ctx, &deplete_id)); + TEST_ASSERT_EQUAL(1, eloop_deplete_timeout(0, 150000, generic_timeout_callback, + &deplete_ctx, &deplete_id)); + wait_sem_or_fail(deplete_sem, 600); + elapsed_ms = reltime_diff_ms(&deplete_ctx.armed_at, &deplete_ctx.fired_at); + TEST_ASSERT_TRUE(elapsed_ms < 600); + TEST_ASSERT_TRUE(deplete_ctx.ran_in_wifi_task); + + reset_timeout_ctx(&replenish_ctx, replenish_sem); + TEST_ASSERT_EQUAL(0, eloop_register_timeout(0, 150000, generic_timeout_callback, + &replenish_ctx, &replenish_id)); + TEST_ASSERT_EQUAL(1, eloop_replenish_timeout(0, 600000, generic_timeout_callback, + &replenish_ctx, &replenish_id)); + expect_sem_timeout(replenish_sem, 250); + wait_sem_or_fail(replenish_sem, 1000); + elapsed_ms = reltime_diff_ms(&replenish_ctx.armed_at, &replenish_ctx.fired_at); + TEST_ASSERT_TRUE(elapsed_ms >= 450 && elapsed_ms <= 1100); + TEST_ASSERT_TRUE(replenish_ctx.ran_in_wifi_task); + + reset_timeout_ctx(&nochange_ctx, NULL); + TEST_ASSERT_EQUAL(0, eloop_register_timeout(0, 200000, generic_timeout_callback, + &nochange_ctx, &nochange_id)); + TEST_ASSERT_EQUAL(0, eloop_deplete_timeout(0, 500000, generic_timeout_callback, + &nochange_ctx, &nochange_id)); + TEST_ASSERT_EQUAL(1, eloop_cancel_timeout(generic_timeout_callback, + &nochange_ctx, &nochange_id)); + + reset_timeout_ctx(&nochange_ctx, NULL); + TEST_ASSERT_EQUAL(0, eloop_register_timeout(0, 600000, generic_timeout_callback, + &nochange_ctx, &nochange_id)); + TEST_ASSERT_EQUAL(0, eloop_replenish_timeout(0, 100000, generic_timeout_callback, + &nochange_ctx, &nochange_id)); + TEST_ASSERT_EQUAL(1, eloop_cancel_timeout(generic_timeout_callback, + &nochange_ctx, &nochange_id)); + + TEST_ASSERT_EQUAL(-1, eloop_deplete_timeout(0, 100000, generic_timeout_callback, + &nochange_ctx, &nochange_id)); + TEST_ASSERT_EQUAL(-1, eloop_replenish_timeout(0, 100000, generic_timeout_callback, + &nochange_ctx, &nochange_id)); + + eloop_test_wifi_stop(); + os_semphr_delete(deplete_sem); + os_semphr_delete(replenish_sem); +} + +TEST_CASE("Test eloop callbacks can schedule and cancel timers", "[eloop]") +{ + struct timeout_test_ctx followup_ctx, cancel_ctx; + struct chain_test_ctx chain_ctx; + void *chain_sem = os_semphr_create(1, 0); + void *followup_sem = os_semphr_create(1, 0); + void *cancel_sem = os_semphr_create(1, 0); + + TEST_ASSERT_NOT_NULL(chain_sem); + TEST_ASSERT_NOT_NULL(followup_sem); + TEST_ASSERT_NOT_NULL(cancel_sem); + + eloop_test_reset_hooks(); + reset_timeout_ctx(&followup_ctx, followup_sem); + reset_timeout_ctx(&cancel_ctx, cancel_sem); + memset(&chain_ctx, 0, sizeof(chain_ctx)); + chain_ctx.sem = chain_sem; + chain_ctx.followup_ctx = &followup_ctx; + chain_ctx.cancel_ctx = &cancel_ctx; + chain_ctx.followup_id = 41; + chain_ctx.cancel_id = 42; + + eloop_test_wifi_start(); + TEST_ASSERT_EQUAL(0, eloop_register_timeout(0, 400000, generic_timeout_callback, + &cancel_ctx, &chain_ctx.cancel_id)); + TEST_ASSERT_EQUAL(0, eloop_register_timeout(0, 100000, chain_timeout_callback, + &chain_ctx, NULL)); + + wait_sem_or_fail(chain_sem, 500); + wait_sem_or_fail(followup_sem, 500); + expect_sem_timeout(cancel_sem, 500); + + TEST_ASSERT_EQUAL(1, chain_ctx.count); + TEST_ASSERT_TRUE(chain_ctx.ran_in_wifi_task); + TEST_ASSERT_EQUAL(1, followup_ctx.count); + TEST_ASSERT_TRUE(followup_ctx.ran_in_wifi_task); + TEST_ASSERT_EQUAL(0, cancel_ctx.count); + TEST_ASSERT_EQUAL(0, eloop_is_timeout_registered(generic_timeout_callback, + &cancel_ctx, &chain_ctx.cancel_id)); + + eloop_test_wifi_stop(); + os_semphr_delete(chain_sem); + os_semphr_delete(followup_sem); + os_semphr_delete(cancel_sem); +} + +TEST_CASE("Test eloop blocking waiters stay paired", "[eloop]") +{ + struct blocking_waiter_ctx waiter_ctx_a = { + .idx = 0, + .handler = blocking_callback, + }; + struct blocking_waiter_ctx waiter_ctx_b = { + .idx = 1, + .handler = blocking_callback, + }; + + blocking_results[0] = 0; + blocking_results[1] = 0; + blocking_in_wifi_task[0] = false; + blocking_in_wifi_task[1] = false; + + eloop_test_reset_hooks(); + eloop_test_wifi_start(); + + blocking_gate = os_semphr_create(2, 0); + blocking_done_sem = os_semphr_create(2, 0); + TEST_ASSERT_NOT_NULL(blocking_gate); + TEST_ASSERT_NOT_NULL(blocking_done_sem); + waiter_ctx_a.done_sem = blocking_done_sem; + waiter_ctx_b.done_sem = blocking_done_sem; + + TEST_ASSERT_EQUAL(pdPASS, xTaskCreate(blocking_waiter_task, "eloop_blk_a", + 4096, &waiter_ctx_a, 3, NULL)); + vTaskDelay(50 / portTICK_PERIOD_MS); + TEST_ASSERT_EQUAL(pdPASS, xTaskCreate(blocking_waiter_task, "eloop_blk_b", + 4096, &waiter_ctx_b, 4, NULL)); + vTaskDelay(50 / portTICK_PERIOD_MS); + + os_semphr_give(blocking_gate); + os_semphr_give(blocking_gate); + + wait_sem_or_fail(blocking_done_sem, 2000); + wait_sem_or_fail(blocking_done_sem, 2000); + TEST_ASSERT_EQUAL(11, blocking_results[0]); + TEST_ASSERT_EQUAL(22, blocking_results[1]); + TEST_ASSERT_TRUE(blocking_in_wifi_task[0]); + TEST_ASSERT_TRUE(blocking_in_wifi_task[1]); + + os_semphr_delete(blocking_done_sem); + os_semphr_delete(blocking_gate); + blocking_done_sem = NULL; + blocking_gate = NULL; + + eloop_test_wifi_stop(); }