You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
427 lines
15 KiB
427 lines
15 KiB
/* |
|
* standby.c — режим standby с duty-cycle (активный/неактивный интервалы) |
|
* |
|
* Синглтон (одна instance на процесс — как instance_lite.c). |
|
* Таймеры только через uasync. Интервалы читаются лениво из chat_setting, |
|
* поэтому их изменение через GUI применяется на границе следующей фазы. |
|
* |
|
* Duty-cycle: |
|
* standby включён → фаза ACTIVE на standby_active_sec, затем SLEEP на |
|
* standby_sleep_sec, и так по кругу, пока standby не выключат. |
|
* |
|
* Досрочное пробуждение: |
|
* в фазе SLEEP первые standby_min_sleep_sec секунд спим безусловно; |
|
* после них standby_notify_network_activity() (внешняя сетевая активность) |
|
* пробуждает досрочно — следующий цикл как обычно. |
|
* |
|
* Список ожидающих (standby_wait) — двусвязный список, элементы из memory_pool. |
|
* При выходе из standby (или досрочном пробуждении) все wait-таймеры |
|
* отменяются, колбэки вызываются сразу. |
|
*/ |
|
#include "standby.h" |
|
#include "chat_setting.h" |
|
#include "utun_instance.h" |
|
#include "debug_config.h" |
|
#include "memory_pool.h" |
|
|
|
#define STANDBY_TB_PER_SEC 10000 /* timebase = 0.1ms → 1 сек = 10000 TB */ |
|
|
|
#define PHASE_ACTIVE 0 |
|
#define PHASE_SLEEP 1 |
|
|
|
struct standby_wait_entry { |
|
struct standby_wait_entry* prev; |
|
struct standby_wait_entry* next; |
|
void* timer_id; /* handle uasync_set_timeout */ |
|
void (*cb)(void* arg); |
|
void* arg; |
|
}; |
|
|
|
static struct UASYNC* g_ua = NULL; |
|
static struct UTUN_INSTANCE* g_inst = NULL; /* для чтения chat_setting (per-instance) */ |
|
static int g_enabled = 0; |
|
static int g_phase = PHASE_ACTIVE; |
|
static uint64_t g_phase_start_tb = 0; /* начало текущей фазы */ |
|
static uint64_t g_phase_end_tb = 0; /* дедлайн текущей фазы */ |
|
static uint64_t g_min_sleep_end_tb = 0; /* до этой точки спим безусловно */ |
|
static void* g_phase_timer = NULL; |
|
static void (*g_switch_cb)(int enabled, void* arg) = NULL; |
|
static void* g_switch_arg = NULL; |
|
static void (*g_phase_cb)(int sleeping, void* arg) = NULL; |
|
static void* g_phase_cb_arg = NULL; |
|
|
|
static struct memory_pool* g_wait_pool = NULL; |
|
static struct standby_wait_entry* g_wait_head = NULL; |
|
static struct standby_wait_entry* g_wait_tail = NULL; |
|
|
|
/* счётчики для диагностики */ |
|
static uint32_t g_cnt_wakes_timer = 0; |
|
static uint32_t g_cnt_wakes_early = 0; |
|
static uint32_t g_cnt_sleeps = 0; |
|
static uint32_t g_cnt_net_events = 0; |
|
static uint32_t g_cnt_net_ignored = 0; |
|
|
|
static int standby_active_sec(void) { |
|
return chat_setting_get_int(g_inst, "standby_active_sec", 2); |
|
} |
|
|
|
static int standby_sleep_sec(void) { |
|
return chat_setting_get_int(g_inst, "standby_sleep_sec", 60); |
|
} |
|
|
|
static int standby_min_sleep_sec(void) { |
|
int m = chat_setting_get_int(g_inst, "standby_min_sleep_sec", 15); |
|
if (m < 0) m = 0; |
|
int s = standby_sleep_sec(); |
|
if (m > s) m = s; |
|
return m; |
|
} |
|
|
|
/* override интервалов (мс) для тестов: 0 = использовать chat_setting */ |
|
static int g_override = 0; |
|
static int g_active_ms = 0; |
|
static int g_sleep_ms = 0; |
|
static int g_min_sleep_ms = 0; |
|
|
|
static int standby_active_tb(void) { |
|
return g_override ? g_active_ms * 10 : standby_active_sec() * STANDBY_TB_PER_SEC; |
|
} |
|
|
|
static int standby_sleep_tb(void) { |
|
return g_override ? g_sleep_ms * 10 : standby_sleep_sec() * STANDBY_TB_PER_SEC; |
|
} |
|
|
|
static int standby_min_sleep_tb(void) { |
|
int m = g_override ? g_min_sleep_ms * 10 : standby_min_sleep_sec() * STANDBY_TB_PER_SEC; |
|
if (m < 0) m = 0; |
|
int s = standby_sleep_tb(); |
|
if (m > s) m = s; |
|
return m; |
|
} |
|
|
|
/* ── фазовый таймер ── */ |
|
|
|
static void standby_phase_timer_cb(void* arg); |
|
|
|
static void standby_arm_phase(void) { |
|
int dur_tb = (g_phase == PHASE_ACTIVE) ? standby_active_tb() : standby_sleep_tb(); |
|
g_phase_start_tb = get_time_tb(); |
|
g_phase_end_tb = g_phase_start_tb + (uint64_t)dur_tb; |
|
g_min_sleep_end_tb = (g_phase == PHASE_SLEEP) |
|
? g_phase_start_tb + (uint64_t)standby_min_sleep_tb() |
|
: 0; |
|
g_phase_timer = uasync_set_timeout(g_ua, dur_tb, NULL, standby_phase_timer_cb, "standby_phase"); |
|
if (!g_phase_timer) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: failed to arm phase timer"); |
|
} |
|
} |
|
|
|
static void standby_phase_timer_cb(void* arg) { |
|
(void)arg; |
|
g_phase_timer = NULL; |
|
if (!g_enabled) return; |
|
uint64_t elapsed = get_time_tb() - g_phase_start_tb; |
|
g_phase = (g_phase == PHASE_ACTIVE) ? PHASE_SLEEP : PHASE_ACTIVE; |
|
/* переармить фазу ДО фазового колбэка: колбэк читает g_phase_end_tb |
|
(ka_active_remaining_01s), иначе он увидит старый (уже истёкший) дедлайн. */ |
|
standby_arm_phase(); |
|
if (g_phase == PHASE_SLEEP) { |
|
g_cnt_sleeps++; |
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, |
|
"standby: SLEEP (active done in %d.%ds, sleep=%dms min_sleep=%dms)", |
|
(int)(elapsed / STANDBY_TB_PER_SEC), (int)((elapsed % STANDBY_TB_PER_SEC) / 1000), |
|
standby_sleep_tb() / 10, standby_min_sleep_tb() / 10); |
|
if (g_phase_cb) g_phase_cb(1, g_phase_cb_arg); |
|
} else { |
|
g_cnt_wakes_timer++; |
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "standby: WAKE (timer), slept %d.%ds", |
|
(int)(elapsed / STANDBY_TB_PER_SEC), (int)((elapsed % STANDBY_TB_PER_SEC) / 1000)); |
|
if (g_phase_cb) g_phase_cb(0, g_phase_cb_arg); |
|
} |
|
} |
|
|
|
/* ── список ожидающих ── */ |
|
|
|
static struct standby_wait_entry* wait_entry_alloc(void) { |
|
if (!g_wait_pool) { |
|
g_wait_pool = memory_pool_init(sizeof(struct standby_wait_entry), "standby_pool"); |
|
if (!g_wait_pool) return NULL; |
|
} |
|
return (struct standby_wait_entry*)memory_pool_alloc(g_wait_pool); |
|
} |
|
|
|
static void wait_entry_free(struct standby_wait_entry* e) { |
|
memory_pool_free(g_wait_pool, e); |
|
} |
|
|
|
static void wait_list_remove(struct standby_wait_entry* e) { |
|
if (e->prev) e->prev->next = e->next; else g_wait_head = e->next; |
|
if (e->next) e->next->prev = e->prev; else g_wait_tail = e->prev; |
|
e->prev = e->next = NULL; |
|
} |
|
|
|
static void wait_list_push(struct standby_wait_entry* e) { |
|
e->prev = g_wait_tail; |
|
e->next = NULL; |
|
if (g_wait_tail) g_wait_tail->next = e; else g_wait_head = e; |
|
g_wait_tail = e; |
|
} |
|
|
|
/* Отменяет все wait-таймеры и вызывает их колбэки немедленно. |
|
* Сначала отцепляем весь список — новые регистрации в колбэках |
|
* (standby_wait) попадут в свежий список и не будут задеты этим проходом. */ |
|
static void standby_wake_all_waiters(void) { |
|
struct standby_wait_entry* list = g_wait_head; |
|
g_wait_head = g_wait_tail = NULL; |
|
while (list) { |
|
struct standby_wait_entry* e = list; |
|
list = e->next; |
|
e->prev = e->next = NULL; |
|
if (e->timer_id) { |
|
uasync_cancel_timeout(g_ua, e->timer_id); |
|
e->timer_id = NULL; |
|
} |
|
void (*cb)(void*) = e->cb; |
|
void* arg = e->arg; |
|
wait_entry_free(e); |
|
cb(arg); |
|
} |
|
} |
|
|
|
static void standby_wait_timer_cb(void* arg) { |
|
struct standby_wait_entry* e = (struct standby_wait_entry*)arg; |
|
e->timer_id = NULL; |
|
wait_list_remove(e); |
|
void (*cb)(void*) = e->cb; |
|
void* user_arg = e->arg; |
|
wait_entry_free(e); |
|
cb(user_arg); |
|
} |
|
|
|
/* ── публичный API ── */ |
|
|
|
void standby_init(struct UASYNC* ua) { |
|
g_ua = ua; |
|
g_inst = NULL; |
|
g_enabled = 0; |
|
g_phase = PHASE_ACTIVE; |
|
g_phase_timer = NULL; |
|
g_cnt_wakes_timer = g_cnt_wakes_early = 0; |
|
g_cnt_sleeps = g_cnt_net_events = g_cnt_net_ignored = 0; |
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "standby: init (active=%dms sleep=%dms min_sleep=%dms)", |
|
standby_active_tb() / 10, standby_sleep_tb() / 10, standby_min_sleep_tb() / 10); |
|
} |
|
|
|
void standby_deinit(void) { |
|
if (!g_enabled && !g_phase_timer && !g_wait_head && !g_wait_pool) { |
|
g_ua = NULL; |
|
return; |
|
} |
|
if (g_phase_timer && g_ua) { |
|
uasync_cancel_timeout(g_ua, g_phase_timer); |
|
g_phase_timer = NULL; |
|
} |
|
while (g_wait_head) { |
|
struct standby_wait_entry* e = g_wait_head; |
|
if (e->timer_id && g_ua) uasync_cancel_timeout(g_ua, e->timer_id); |
|
wait_list_remove(e); |
|
wait_entry_free(e); |
|
} |
|
if (g_wait_pool) { |
|
memory_pool_destroy(g_wait_pool); |
|
g_wait_pool = NULL; |
|
} |
|
g_enabled = 0; |
|
g_ua = NULL; |
|
g_inst = NULL; |
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "standby: deinit (wakes_timer=%u wakes_early=%u sleeps=%u net=%u ignored=%u)", |
|
g_cnt_wakes_timer, g_cnt_wakes_early, g_cnt_sleeps, g_cnt_net_events, g_cnt_net_ignored); |
|
} |
|
|
|
void standby_set_enabled(struct UTUN_INSTANCE* inst, int enabled) { |
|
if (!g_ua) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: set_enabled(%d) but not initialized", enabled); |
|
return; |
|
} |
|
if (inst) g_inst = inst; |
|
if (g_enabled == enabled) return; |
|
|
|
if (enabled) { |
|
g_enabled = 1; |
|
g_phase = PHASE_ACTIVE; |
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "standby: ENTER (app background, active=%dms sleep=%dms min_sleep=%dms)", |
|
standby_active_tb() / 10, standby_sleep_tb() / 10, standby_min_sleep_tb() / 10); |
|
standby_arm_phase(); |
|
} else { |
|
g_enabled = 0; |
|
if (g_phase_timer) { |
|
uasync_cancel_timeout(g_ua, g_phase_timer); |
|
g_phase_timer = NULL; |
|
} |
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "standby: EXIT (app foreground, wakes_timer=%u wakes_early=%u sleeps=%u net=%u ignored=%u)", |
|
g_cnt_wakes_timer, g_cnt_wakes_early, g_cnt_sleeps, g_cnt_net_events, g_cnt_net_ignored); |
|
standby_wake_all_waiters(); |
|
} |
|
|
|
if (inst) inst->standby_enabled = (uint8_t)enabled; |
|
if (g_switch_cb) g_switch_cb(enabled, g_switch_arg); |
|
} |
|
|
|
int standby_is_enabled(void) { |
|
return g_enabled; |
|
} |
|
|
|
void standby_set_switch_callback(void (*cb)(int enabled, void* arg), void* arg) { |
|
g_switch_cb = cb; |
|
g_switch_arg = arg; |
|
} |
|
|
|
int standby_get_sleep_tb(void) { |
|
if (!g_enabled) return 0; |
|
if (g_phase == PHASE_ACTIVE) return 0; |
|
uint64_t now = get_time_tb(); |
|
if (now >= g_phase_end_tb) return 0; |
|
return (int)(g_phase_end_tb - now); |
|
} |
|
|
|
int standby_get_active_remaining_tb(void) { |
|
if (!g_enabled) return 0; |
|
if (g_phase == PHASE_SLEEP) return 0; |
|
uint64_t now = get_time_tb(); |
|
if (now >= g_phase_end_tb) return 0; |
|
return (int)(g_phase_end_tb - now); |
|
} |
|
|
|
int standby_get_sleep_duration_tb(void) { |
|
return standby_sleep_tb(); |
|
} |
|
|
|
int standby_get_wake_deadline_tb(void) { |
|
if (!g_enabled) return 0; |
|
if (g_phase == PHASE_SLEEP) return standby_get_sleep_tb(); |
|
/* ACTIVE: остаток активного + полная длительность предстоящего сна */ |
|
return standby_get_active_remaining_tb() + standby_sleep_tb(); |
|
} |
|
|
|
int standby_get_keepalive_ms(void) { |
|
int ms = chat_setting_get_int(g_inst, "standby_keepalive_ms", 30000); |
|
if (ms < 100) ms = 100; |
|
return ms; |
|
} |
|
|
|
void standby_set_phase_callback(void (*cb)(int sleeping, void* arg), void* arg) { |
|
g_phase_cb = cb; |
|
g_phase_cb_arg = arg; |
|
} |
|
|
|
void* standby_wait(void* arg, void (*cb)(void* arg)) { |
|
if (!cb) return NULL; |
|
if (!g_ua) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: wait() but not initialized"); |
|
return NULL; |
|
} |
|
if (!g_enabled) { |
|
/* standby выключен → будим сразу (отложенно, чтобы не было рекурсии) */ |
|
uasync_call_soon(g_ua, arg, (timeout_callback_t)cb); |
|
return NULL; |
|
} |
|
|
|
struct standby_wait_entry* e = wait_entry_alloc(); |
|
if (!e) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: wait_entry_alloc failed"); |
|
cb(arg); |
|
return NULL; |
|
} |
|
e->cb = cb; |
|
e->arg = arg; |
|
|
|
/* время до начала следующего активного интервала: |
|
* SLEEP — ждём до конца фазы; ACTIVE — уже бодрствуем, будим сразу. */ |
|
uint64_t now = get_time_tb(); |
|
uint64_t tb; |
|
if (g_phase == PHASE_SLEEP) { |
|
tb = (now < g_phase_end_tb) ? (g_phase_end_tb - now) : 0; |
|
} else { |
|
tb = 0; |
|
} |
|
|
|
e->timer_id = uasync_set_timeout(g_ua, (int)tb, e, standby_wait_timer_cb, "standby_wait"); |
|
if (!e->timer_id) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: uasync_set_timeout failed"); |
|
wait_entry_free(e); |
|
cb(arg); |
|
return NULL; |
|
} |
|
wait_list_push(e); |
|
return e; |
|
} |
|
|
|
void standby_wait_cancel(void* handle) { |
|
if (!handle) return; |
|
struct standby_wait_entry* e = (struct standby_wait_entry*)handle; |
|
if (e->timer_id && g_ua) { |
|
uasync_cancel_timeout(g_ua, e->timer_id); |
|
e->timer_id = NULL; |
|
} |
|
wait_list_remove(e); |
|
wait_entry_free(e); |
|
} |
|
|
|
/* ── внешняя сетевая активность ── */ |
|
|
|
void standby_notify_network_activity(void) { |
|
g_cnt_net_events++; |
|
if (!g_enabled) { |
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "standby: network activity ignored (standby off)"); |
|
return; |
|
} |
|
if (g_phase != PHASE_SLEEP) { |
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "standby: network activity ignored (phase=ACTIVE)"); |
|
return; |
|
} |
|
|
|
uint64_t now = get_time_tb(); |
|
uint64_t slept = now - g_phase_start_tb; |
|
if (now < g_min_sleep_end_tb) { |
|
g_cnt_net_ignored++; |
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, |
|
"standby: network activity caught, min_sleep not elapsed (%d.%ds left)", |
|
(int)((g_min_sleep_end_tb - now) / STANDBY_TB_PER_SEC), |
|
(int)(((g_min_sleep_end_tb - now) % STANDBY_TB_PER_SEC) / 1000)); |
|
return; |
|
} |
|
|
|
g_cnt_wakes_early++; |
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, |
|
"standby: WAKE early (network activity), slept %d.%ds min_sleep=%dms", |
|
(int)(slept / STANDBY_TB_PER_SEC), (int)((slept % STANDBY_TB_PER_SEC) / 1000), |
|
standby_min_sleep_tb() / 10); |
|
|
|
if (g_phase_timer) { |
|
uasync_cancel_timeout(g_ua, g_phase_timer); |
|
g_phase_timer = NULL; |
|
} |
|
g_phase = PHASE_ACTIVE; |
|
standby_arm_phase(); |
|
standby_wake_all_waiters(); |
|
} |
|
|
|
void standby_log_rx(const char* kind, const char* who) { |
|
if (!g_enabled || g_phase != PHASE_SLEEP) return; |
|
uint64_t slept = get_time_tb() - g_phase_start_tb; |
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, |
|
"standby: rx in SLEEP: %s from %s, slept %d.%ds", |
|
kind ? kind : "?", who ? who : "?", |
|
(int)(slept / STANDBY_TB_PER_SEC), (int)((slept % STANDBY_TB_PER_SEC) / 1000)); |
|
} |
|
|
|
void standby_set_intervals_ms(int active_ms, int sleep_ms, int min_sleep_ms) { |
|
if (active_ms <= 0 || sleep_ms <= 0) { |
|
g_override = 0; |
|
return; |
|
} |
|
g_override = 1; |
|
g_active_ms = active_ms; |
|
g_sleep_ms = sleep_ms; |
|
g_min_sleep_ms = min_sleep_ms < 0 ? 0 : min_sleep_ms; |
|
}
|
|
|