From 9e3a41705956114ee6e7cc64664bb1fc26db5f15 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 30 Sep 2026 20:42:40 +0300 Subject: [PATCH] Synchronize standby transitions and immediate keepalive phase announcements --- doc/service_lifecycle.md | 9 + src/transport_layer/etcp_connections.c | 20 +- src/transport_layer/etcp_keepalive.c | 54 ++++-- src/transport_layer/etcp_keepalive.h | 9 +- src/utun_instance.c | 21 +- src/utun_instance.h | 6 +- tests/Makefile.am | 17 +- tests/test_standby_transport.c | 180 ++++++++++++++++++ .../jni_bridge/android_jni_bridge.c | 6 +- tools/chatgui-android/libutun_lite/standby.c | 121 ++++++------ tools/chatgui-android/libutun_lite/standby.h | 11 +- .../libutun_lite/tests/test_standby.c | 26 +++ 12 files changed, 373 insertions(+), 107 deletions(-) create mode 100644 tests/test_standby_transport.c diff --git a/doc/service_lifecycle.md b/doc/service_lifecycle.md index 50638375..2a995885 100644 --- a/doc/service_lifecycle.md +++ b/doc/service_lifecycle.md @@ -72,3 +72,12 @@ wake lock и сервис. Перед `NativeLib.stop/restart/destroy` выпо ACTIVE и выключают Android standby. Состояние не истекает по таймеру. После завершения звонка в фоне возвращается standby; экран может гаснуть во время разговора независимо от частичного wake lock сервиса. + +`utun_set_client_activity()` в сборке с `UTUN_HAVE_STANDBY` применяет фактический +режим duty-cycle до рассылки событий активности; JNI не переключает его отдельно. +Повторная команда не перезапускает фазу и анонсы, но первый фоновый запуск включает +standby даже при начальном `client_activity=STANDBY`. Keepalive подписан на режим +и фазы самого standby; новые UDP/TCP-линки сразу анонсируют текущую фазу после +handshake. Пробуждение по таймеру и внешней сети сначала сообщает ACTIVE-окно +пирам, затем запускает ожидающие задачи. При выходе сначала восстанавливаются +линки; deinit отменяет ожидающие задачи и очищает подписки старого ядра. diff --git a/src/transport_layer/etcp_connections.c b/src/transport_layer/etcp_connections.c index 70db1336..881475f7 100644 --- a/src/transport_layer/etcp_connections.c +++ b/src/transport_layer/etcp_connections.c @@ -1256,11 +1256,7 @@ void etcp_link_enter_ready_tcp(struct ETCP_LINK *link) { if (!etcp->peer_reset_id) etcp_session_start(etcp); // сервер создаёт ETCP_CONN после STCP handshake if (etcp->initialized == 0) etcp_conn_ready(etcp); if (etcp->state == 2) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "enter_ready_tcp: conn DELETED after conn_ready, link=%p", (void*)link); return; } - /* в standby анонс спячки вместо обычного flagless-keepalive */ - if (!etcp_keepalive_announce_mode(link)) { - etcp_link_send_keepalive(link); - start_keepalive_timer(link); - } + etcp_keepalive_link_ready(link); loadbalancer_link_ready(link); etcp_fire_link_status_cbk(link, old_state, link->link_status); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] TCP link %d UP (mtu=%d init=%d up=%d tcp_links=%d)", @@ -1955,7 +1951,7 @@ static void send_init_response(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pk DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] Connection established (socket=%s, link=%d, addr=%s)", link->etcp->log_name, e_sock->name, link->local_link_id, sockaddr_storage_to_str(&link->remote_addr).str); } DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] Link %d UP (server, mtu=%d, addr=%s)", link->etcp->log_name, link->local_link_id, link->mtu_local, sockaddr_storage_to_str(&link->remote_addr).str); - start_keepalive_timer(link); + etcp_keepalive_link_ready(link); loadbalancer_link_ready(link); // Restart NAT check after link is up (e.g. after address change or reinit) if (link->etcp->instance->nat_det) nat_detection_link_ready(link->etcp->instance->nat_det, link); @@ -2061,11 +2057,7 @@ static int handle_init_response_client(struct ETCP_SOCKET* e_sock, struct ETCP_D loadbalancer_link_ready(link); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] Link %d UP (client, mtu=%d, addr=%s)", link->etcp->log_name, link->local_link_id, link->mtu, sockaddr_storage_to_str(&link->remote_addr).str); - /* в standby анонс спячки вместо обычного flagless-keepalive */ - if (!etcp_keepalive_announce_mode(link)) { - etcp_link_send_keepalive(link); - start_keepalive_timer(link); - } + etcp_keepalive_link_ready(link); loadbalancer_link_ready(link); @@ -2601,11 +2593,7 @@ int etcp_packet_decrypted(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, return 0; } if (link->link_state == LINK_STATE_TRY_RECONNECT) {// 0 - just init, 1 - handshake, 2 - try reconnect, 3 - connected - /* в standby анонс спячки вместо обычного flagless-keepalive */ - if (!etcp_keepalive_announce_mode(link)) { - start_keepalive_timer(link); - etcp_link_send_keepalive(link); - } + etcp_keepalive_link_ready(link); int old_state = link->link_state; link->link_state = LINK_STATE_CONNECTED; etcp_fire_link_status_cbk(link, old_state, link->link_status); } diff --git a/src/transport_layer/etcp_keepalive.c b/src/transport_layer/etcp_keepalive.c index 1288d0d1..dcd11c8b 100644 --- a/src/transport_layer/etcp_keepalive.c +++ b/src/transport_layer/etcp_keepalive.c @@ -386,13 +386,14 @@ static void ka_wake(struct ETCP_LINK* link) { typedef void (*ka_link_fn)(struct ETCP_LINK* link); -static void ka_for_each_link(struct UTUN_INSTANCE* inst, ka_link_fn fn) { +static int ka_for_each_link(struct UTUN_INSTANCE* inst, ka_link_fn fn) { + int count = 0; if (inst->connections) { for (struct ll_entry* e = inst->connections->head; e; e = e->next) { struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; if (!ce || !ce->conn) continue; for (struct ETCP_LINK* l = ce->conn->links; l; l = l->next) - if (l->initialized) fn(l); + if (l->initialized) { fn(l); count++; } } } if (inst->tcp_connections) { @@ -400,12 +401,22 @@ static void ka_for_each_link(struct UTUN_INSTANCE* inst, ka_link_fn fn) { struct tcp_conn_entry* te = (struct tcp_conn_entry*)e->data; if (!te || !te->etcp_conn) continue; for (struct ETCP_LINK* l = te->etcp_conn->links; l; l = l->next) - if (l->initialized) fn(l); + if (l->initialized) { fn(l); count++; } } } + return count; } #ifdef UTUN_HAVE_STANDBY +// Фактический режим standby меняется после подготовки его фазы и таймера. +static void ka_standby_switch(int enabled, void* arg) { + struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg; + if (!inst || inst->client_type != CLIENT_TYPE_MOBILE) return; + int links = ka_for_each_link(inst, enabled ? ka_enter_sleep : ka_wake); + DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "standby mode=%s applied to %d links node=%016llx", + enabled ? "sleep" : "normal", links, (unsigned long long)inst->node_id); +} + // Вход в ACTIVE-окно — анонс-серия с ретраями до ACK (active_remaining>0). static void ka_active_announce(struct ETCP_LINK* link) { if (!link->ka_my_sleeping) return; @@ -540,6 +551,7 @@ int etcp_keepalive_on_recv(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, return 0; } +#ifndef UTUN_HAVE_STANDBY static void etcp_keepalive_on_activity(struct UTUN_INSTANCE* inst, int active, void* arg) { (void)arg; if (!inst || inst->client_type != CLIENT_TYPE_MOBILE) return; @@ -547,23 +559,37 @@ static void etcp_keepalive_on_activity(struct UTUN_INSTANCE* inst, int active, v DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "keepalive activity change: %s -> applied to links (node=%016llx)", active ? "ACTIVE(wake)" : "STANDBY(sleep)", (unsigned long long)inst->node_id); } +#endif -// На только что (пере)инициализированном линке: если я в standby — анонсировать спячку -// (возвращает 1, обычный flagless в этом случае слать НЕ надо); иначе 0. -int etcp_keepalive_announce_mode(struct ETCP_LINK* link) { - if (!link || !link->etcp || !link->etcp->instance) return 0; - struct UTUN_INSTANCE* inst = link->etcp->instance; - if (inst->client_type != CLIENT_TYPE_MOBILE) return 0; - if (!inst->standby_enabled) return 0; /* не в standby — обычный режим */ - ka_enter_sleep(link); - return 1; +// Сразу сообщает текущий режим/фазу нового или восстановленного линка и запускает нужный механизм keepalive. +void etcp_keepalive_link_ready(struct ETCP_LINK* link) { + if (!link || !link->etcp || !link->etcp->instance) { + DEBUG_ERROR(DEBUG_CATEGORY_KEEPALIVE, "link_ready: missing link/connection/instance link=%p", (void*)link); + return; + } + int sleeping = 0; +#ifdef UTUN_HAVE_STANDBY + sleeping = link->etcp->instance->client_type == CLIENT_TYPE_MOBILE && standby_is_enabled(); +#endif + DEBUG_DEBUG(DEBUG_CATEGORY_KEEPALIVE, "[%s] link ready id=%u tcp=%u server=%u mode=%s", + link->etcp->log_name, link->local_link_id, link->is_tcp, link->is_server, sleeping ? "standby" : "normal"); + if (sleeping) { + ka_enter_sleep(link); + } else if (link->ka_my_sleeping) { + ka_wake(link); + } else { + etcp_link_send_keepalive(link); + if (!link->ka_peer_sleeping) start_keepalive_timer(link); + } } void etcp_keepalive_register(struct UTUN_INSTANCE* inst) { if (!inst) return; - utun_add_activity_cbk(inst, etcp_keepalive_on_activity, NULL); #ifdef UTUN_HAVE_STANDBY + standby_set_switch_callback(ka_standby_switch, inst); standby_set_phase_callback(ka_phase_cb, inst); +#else + utun_add_activity_cbk(inst, etcp_keepalive_on_activity, NULL); #endif - DEBUG_DEBUG(DEBUG_CATEGORY_KEEPALIVE, "keepalive activity + phase callbacks registered"); + DEBUG_DEBUG(DEBUG_CATEGORY_KEEPALIVE, "keepalive mode/phase callbacks registered node=%016llx", (unsigned long long)inst->node_id); } diff --git a/src/transport_layer/etcp_keepalive.h b/src/transport_layer/etcp_keepalive.h index bd77d2b6..f6741848 100644 --- a/src/transport_layer/etcp_keepalive.h +++ b/src/transport_layer/etcp_keepalive.h @@ -64,14 +64,13 @@ void start_keepalive_timer(struct ETCP_LINK* link); int etcp_keepalive_on_recv(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, struct ETCP_LINK* link, size_t pkt_len); -// Подписка на смену active mode (foreground/background) и фазы спячки duty-cycle. +// Подписка на фактический режим/фазу standby; без duty-cycle — на активность клиента. // Вызывается из init_connections. void etcp_keepalive_register(struct UTUN_INSTANCE* inst); -// На только что (пере)инициализированном линке: если я в standby — анонсировать -// спячку (возвращает 1, обычный flagless в этом случае слать НЕ надо); иначе 0. -// Вызывается из etcp_link_enter_ready_tcp / handle_init_response_client / TRY_RECONNECT. -int etcp_keepalive_announce_mode(struct ETCP_LINK* link); +// После INIT/INIT_RESPONSE, TCP handshake и reconnect сразу анонсирует текущий режим/фазу. +// В standby запускает sleep-анонс с ретраями UDP; в normal — обычный keepalive. +void etcp_keepalive_link_ready(struct ETCP_LINK* link); #ifdef __cplusplus } diff --git a/src/utun_instance.c b/src/utun_instance.c index 83306231..2bf79733 100644 --- a/src/utun_instance.c +++ b/src/utun_instance.c @@ -31,6 +31,9 @@ #include "transport_layer/node_conn_direct.h" #include "transport_layer/socket_monitor.h" #include "transport_layer/auto_socket.h" +#ifdef UTUN_HAVE_STANDBY +#include "standby.h" +#endif #include "../lib/u_async.h" #include "../lib/debug_config.h" @@ -1129,16 +1132,26 @@ void utun_fire_peer_sleep_cbk(struct UTUN_INSTANCE* instance, uint64_t peer_node } /* Явная активность UI/звонка не истекает по таймеру: владелец сообщает завершение сам. */ -void utun_set_client_activity(struct UTUN_INSTANCE* instance, int active) { +int utun_set_client_activity(struct UTUN_INSTANCE* instance, int active) { if (!instance) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "client_activity: missing instance active=%d", active); - return; + return -1; } - if (instance->client_type == CLIENT_TYPE_SERVER) return; + if (instance->client_type == CLIENT_TYPE_SERVER) return 0; int state = active ? CLIENT_ACTIVITY_ACTIVE : CLIENT_ACTIVITY_STANDBY; - if (instance->client_activity == state) return; + int previous = instance->client_activity; instance->client_activity = state; +#ifdef UTUN_HAVE_STANDBY + if (instance->client_type == CLIENT_TYPE_MOBILE && standby_set_enabled(instance, !active) < 0) { + instance->client_activity = previous; + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "client_activity: standby transition failed node=%016llx requested=%d previous=%d", + (unsigned long long)instance->node_id, state, previous); + return -1; + } +#endif + if (previous == state) return 0; DEBUG_INFO(DEBUG_CATEGORY_BGP, "client_activity set %s until explicit change node=%016llx type=%u", active ? "ACTIVE" : "STANDBY", (unsigned long long)instance->node_id, instance->client_type); utun_fire_activity_cbk(instance, state); + return 0; } diff --git a/src/utun_instance.h b/src/utun_instance.h index b058668a..70bef9d5 100644 --- a/src/utun_instance.h +++ b/src/utun_instance.h @@ -243,7 +243,6 @@ struct UTUN_INSTANCE { uint8_t client_activity; // CLIENT_ACTIVITY_STANDBY/ACTIVE struct utun_activity_cbk_entry* activity_cbks; // подписки на смену client_activity struct peer_sleep_cbk_entry* peer_sleep_cbks; // подписки на смену спячки пира (keepalive) - uint8_t standby_enabled; // 1 = standby duty-cycle активен (chatgui-android) // TCP proxy server (exit node) struct tcp_proxy_server tcp_proxy_server; @@ -294,8 +293,9 @@ void utun_instance_stop(struct UTUN_INSTANCE *instance); /* Глобальные переключатели создания TUN/топологии для тестов и встраивания. */ void utun_instance_set_tun_init_enabled(int enabled); void utun_instance_set_topo_group_enabled(int enabled); -/* Состояние активности клиента сохраняется до следующего явного вызова; выполнять в uasync-потоке. */ -void utun_set_client_activity(struct UTUN_INSTANCE* instance, int active); +/* Применяет активность и мобильный standby до уведомления подписчиков; выполнять в uasync-потоке. + * Повторная команда согласует фактический режим без повторного BGP-события. 0 — успех, <0 — ошибка. */ +int utun_set_client_activity(struct UTUN_INSTANCE* instance, int active); void utun_add_activity_cbk(struct UTUN_INSTANCE* instance, utun_activity_cbk_fn fn, void* arg); void utun_remove_activity_cbk(struct UTUN_INSTANCE* instance, utun_activity_cbk_fn fn, void* arg); void utun_add_peer_sleep_cbk(struct UTUN_INSTANCE* instance, peer_sleep_cbk_fn fn, void* arg); diff --git a/tests/Makefile.am b/tests/Makefile.am index bde3a6f5..700c7429 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -112,9 +112,9 @@ check_PROGRAMS = \ bench_timeout_heap \ bench_uasync_timeouts -# Linux-only тест: требует root + dummy-интерфейсы + iproute2/netlink +# Linux-only: test_auto_socket_dynamic требует root + dummy-интерфейсы + iproute2/netlink. if OS_LINUX -check_PROGRAMS += test_auto_socket_dynamic +check_PROGRAMS += test_auto_socket_dynamic test_standby test_standby_transport endif # Silero VAD: только при включённом ONNX Runtime (--with-silero-vad) @@ -146,6 +146,19 @@ CRYPTO_LIBS = -lcrypto # Common libraries (lib/dns.c собирается с -DDNS_RANDOM=RAND_bytes → нужен -lcrypto) COMMON_LIBS = $(top_builddir)/lib/libuasync.a $(CRYPTO_LIBS) -lpthread $(WIN_LIBS) @FFMPEG_LIBS@ $(top_builddir)/lib/libopus/libopus_internal.a -lm +# Вариант общего ядра с Android duty-cycle, без JNI и устройства. +if OS_LINUX +standby_test_sources = ../tools/chatgui-android/libutun_lite/standby.c ../src/utun_instance.c ../src/transport_layer/etcp_keepalive.c +standby_test_cppflags = $(AM_CPPFLAGS) -I$(top_srcdir)/src/chat -I$(top_srcdir)/tools/chatgui-android/libutun_lite -DUTUN_HAVE_STANDBY +test_standby_SOURCES = ../tools/chatgui-android/libutun_lite/tests/test_standby.c $(standby_test_sources) +test_standby_CPPFLAGS = $(standby_test_cppflags) +test_standby_LDADD = $(LIBUTUN) $(COMMON_LIBS) +test_standby_transport_SOURCES = test_standby_transport.c $(standby_test_sources) +test_standby_transport_CPPFLAGS = $(standby_test_cppflags) +test_standby_transport_LDADD = $(LIBUTUN) $(COMMON_LIBS) +test_standby_transport_LDFLAGS = -Wl,--wrap=etcp_encrypt_send -Wl,--wrap=uasync_set_timeout +endif + # Test definitions test_etcp_bbr_SOURCES = test_etcp_bbr.c test_etcp_bbr_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib diff --git a/tests/test_standby_transport.c b/tests/test_standby_transport.c new file mode 100644 index 00000000..728c2904 --- /dev/null +++ b/tests/test_standby_transport.c @@ -0,0 +1,180 @@ +/* Проверка режима и фаз standby на реальных keepalive-пакетах до шифрования. + * Управляемая отправка исключает сеть; отказ таймера проверяет сохранение рабочего режима. */ +#include +#include +#include "utun_instance.h" +#include "etcp.h" +#include "etcp_keepalive.h" +#include "standby.h" +#include "debug_config.h" + +static struct UASYNC* ua; +static struct UTUN_INSTANCE inst; +static struct ETCP_CONN udp, tcp; +static struct ETCP_LINK links[3]; +static struct ll_queue udp_queue, tcp_queue; +static struct ll_entry *udp_entry, *tcp_entry; +static struct { unsigned count; uint8_t flags; uint16_t active, sleep; } sent[3]; +static int fail_phase_timer, activity_events, wait_calls; +static unsigned wait_after_packets; + +/* Перехватывает сформированный keepalive, сохраняя его реальные флаги и тайминг. */ +int __wrap_etcp_encrypt_send(struct ETCP_DGRAM* pkt) { + assert(pkt && pkt->data[0] == ETCP_KEEPALIVE && pkt->data_len >= 4); + int i; + for (i = 0; i < 3 && pkt->link != &links[i]; i++); + assert(i < 3); + sent[i].count++; + sent[i].flags = pkt->data[1]; + sent[i].active = pkt->data_len >= 6 ? pkt->data[4] | ((uint16_t)pkt->data[5] << 8) : 0; + sent[i].sleep = pkt->data_len >= 8 ? pkt->data[6] | ((uint16_t)pkt->data[7] << 8) : 0; + return pkt->data_len; +} + +void* __real_uasync_set_timeout(struct UASYNC* ua, int tb, void* arg, timeout_callback_t cb, const char* name); + +/* Отказывает только фазовому таймеру, остальные таймеры выполняются настоящим uasync. */ +void* __wrap_uasync_set_timeout(struct UASYNC* loop, int tb, void* arg, timeout_callback_t cb, const char* name) { + if (fail_phase_timer && name && !strcmp(name, "standby_phase")) { + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "[TEST] injected standby phase timer failure"); + return NULL; + } + return __real_uasync_set_timeout(loop, tb, arg, cb, name); +} + +/* Подписчики активности видят уже согласованные режимы, без промежуточного состояния. */ +static void activity_changed(struct UTUN_INSTANCE* owner, int active, void* arg) { + (void)arg; + assert(owner == &inst && inst.client_activity == active); + assert(standby_is_enabled() == !active); + for (int i = 0; i < 3; i++) assert(links[i].ka_my_sleeping == !active); + activity_events++; +} + +/* Работа возобновляется после отправки новой фазы пирам или выхода из standby. */ +static void waited(void* arg) { + (void)arg; + assert(sent[0].count > wait_after_packets); + if (standby_is_enabled()) { + assert(standby_get_active_remaining_tb() > 0 && sent[0].flags == KA_FLAG_SLEEP && sent[0].active > 0); + } else { + assert(inst.client_activity == CLIENT_ACTIVITY_ACTIVE && !links[0].ka_my_sleeping && sent[0].flags == 0); + } + wait_calls++; +} + +/* Мобильный узел с исходящим UDP, входящим UDP и TCP линками. */ +static void setup(void) { + ua = uasync_create(); assert(ua); + memset(&inst, 0, sizeof(inst)); memset(&udp, 0, sizeof(udp)); memset(&tcp, 0, sizeof(tcp)); + memset(links, 0, sizeof(links)); memset(sent, 0, sizeof(sent)); + activity_events = wait_calls = fail_phase_timer = 0; + inst.ua = ua; inst.client_type = CLIENT_TYPE_MOBILE; inst.client_activity = CLIENT_ACTIVITY_STANDBY; + udp.instance = tcp.instance = &inst; + strcpy(udp.log_name, "standby-test-udp"); strcpy(tcp.log_name, "standby-test-tcp"); + for (int i = 0; i < 3; i++) { + links[i].etcp = i == 2 ? &tcp : &udp; + links[i].local_link_id = i + 1; links[i].initialized = links[i].link_status = 1; + links[i].keepalive_interval = links[i].ka_period_ms = 2000; + links[i].keepalive_timeout = 20000; links[i].last_recv_local_time = get_time_tb(); + } + links[0].next = &links[1]; links[1].is_server = 1; links[2].is_tcp = 1; + udp.links = &links[0]; tcp.links = &links[2]; + udp_entry = queue_entry_new(sizeof(struct conn_queue_entry)); assert(udp_entry); + tcp_entry = queue_entry_new(sizeof(struct tcp_conn_entry)); assert(tcp_entry); + ((struct conn_queue_entry*)udp_entry->data)->conn = &udp; + ((struct tcp_conn_entry*)tcp_entry->data)->etcp_conn = &tcp; + memset(&udp_queue, 0, sizeof(udp_queue)); memset(&tcp_queue, 0, sizeof(tcp_queue)); + udp_queue.head = udp_entry; tcp_queue.head = tcp_entry; + inst.connections = &udp_queue; inst.tcp_connections = &tcp_queue; + standby_set_intervals_ms(200, 300, 0); + standby_init(ua); etcp_keepalive_register(&inst); + utun_add_activity_cbk(&inst, activity_changed, NULL); + for (int i = 0; i < 3; i++) etcp_keepalive_link_ready(&links[i]); +} + +/* Отменяет таймеры и подписки перед повторной инициализацией ядра. */ +static void teardown(void) { + standby_deinit(); + for (int i = 0; i < 3; i++) { + if (links[i].keepalive_timer) uasync_cancel_timeout(ua, links[i].keepalive_timer); + if (links[i].ka_sleep_announce_timer) uasync_cancel_timeout(ua, links[i].ka_sleep_announce_timer); + } + while (inst.activity_cbks) utun_remove_activity_cbk(&inst, inst.activity_cbks->fn, inst.activity_cbks->arg); + queue_entry_free(udp_entry); queue_entry_free(tcp_entry); + uasync_poll(ua, 0); + assert(ua->timer_alloc_count == ua->timer_free_count); + uasync_destroy(ua, 0); +} + +/* Первый фон при начальном STANDBY, повторы, новые линки, обе причины wake и выход в ACTIVE. */ +static void check_transitions(void) { + setup(); + assert(!standby_is_enabled()); + for (int i = 0; i < 3; i++) assert(sent[i].count == 1 && sent[i].flags == 0 && links[i].keepalive_timer); + assert(utun_set_client_activity(&inst, 0) == 0 && standby_is_enabled() && activity_events == 0); + for (int i = 0; i < 3; i++) { + assert(links[i].ka_my_sleeping && !links[i].keepalive_timer); + assert(sent[i].count == 2 && sent[i].flags == KA_FLAG_SLEEP && sent[i].active > 0 && sent[i].sleep == 3); + } + uint64_t timers = ua->timer_alloc_count; + assert(utun_set_client_activity(&inst, 0) == 0 && ua->timer_alloc_count == timers); + assert(sent[0].count == 2 && sent[1].count == 2 && sent[2].count == 2); + uasync_poll(ua, -1); assert(standby_get_sleep_tb() > 0); + for (int i = 0; i < 3; i++) { + etcp_keepalive_link_ready(&links[i]); + assert(sent[i].flags == KA_FLAG_SLEEP && sent[i].active == 0 && sent[i].sleep > 0 && !links[i].keepalive_timer); + } + assert(links[0].ka_sleep_announce_timer && links[1].ka_sleep_announce_timer && !links[2].ka_sleep_announce_timer); + timers = ua->timer_alloc_count; wait_after_packets = sent[0].count; + assert(standby_wait(NULL, waited)); assert(ua->timer_alloc_count == timers); + fail_phase_timer = 1; + standby_notify_network_activity(); + assert(standby_get_sleep_tb() > 0 && wait_calls == 0 && sent[0].count == wait_after_packets); + fail_phase_timer = 0; + standby_notify_network_activity(); assert(wait_calls == 1); + uasync_poll(ua, -1); assert(standby_get_sleep_tb() > 0); + wait_after_packets = sent[0].count; + void* cancelled = standby_wait(NULL, waited); assert(cancelled); + standby_wait_cancel(cancelled); + assert(standby_wait(NULL, waited)); + uasync_poll(ua, -1); assert(wait_calls == 2 && standby_get_active_remaining_tb() > 0); + uasync_poll(ua, -1); assert(standby_get_sleep_tb() > 0); + wait_after_packets = sent[0].count; assert(standby_wait(NULL, waited)); + links[0].ka_peer_sleeping = 1; + assert(utun_set_client_activity(&inst, 1) == 0 && activity_events == 1 && wait_calls == 3); + for (int i = 0; i < 3; i++) { + assert(!links[i].ka_my_sleeping && sent[i].flags == 0 && !links[i].ka_sleep_announce_timer); + } + assert(!links[0].keepalive_timer && links[1].keepalive_timer && links[2].keepalive_timer); + timers = ua->timer_alloc_count; unsigned packets = sent[0].count; + assert(utun_set_client_activity(&inst, 1) == 0 && ua->timer_alloc_count == timers); + assert(activity_events == 1 && sent[0].count == packets); + assert(utun_set_client_activity(&inst, 0) == 0 && activity_events == 2); + teardown(); + DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[PASS] first background, repeated commands, UDP/TCP ready, phase-before-work, peer sleep and cleanup"); +} + +/* Ошибка ENTER сохраняет normal; потеря фазового таймера возвращает normal, без зависания сна. */ +static void check_timer_failures(void) { + setup(); assert(utun_set_client_activity(&inst, 1) == 0); + fail_phase_timer = 1; + assert(utun_set_client_activity(&inst, 0) < 0); + assert(inst.client_activity == CLIENT_ACTIVITY_ACTIVE && !standby_is_enabled() && activity_events == 1); + for (int i = 0; i < 3; i++) assert(!links[i].ka_my_sleeping && links[i].keepalive_timer); + fail_phase_timer = 0; + assert(utun_set_client_activity(&inst, 0) == 0); + fail_phase_timer = 1; + uasync_poll(ua, -1); + assert(!standby_is_enabled() && inst.client_activity == CLIENT_ACTIVITY_ACTIVE && activity_events == 3); + for (int i = 0; i < 3; i++) assert(!links[i].ka_my_sleeping && links[i].keepalive_timer); + fail_phase_timer = 0; teardown(); + DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[PASS] timer failure rollback and safe normal fallback"); +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); + check_transitions(); check_transitions(); check_timer_failures(); + DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "PASS: standby transport regression tests"); + return 0; +} diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index eddebe82..cc2f935c 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.c @@ -1182,8 +1182,10 @@ static void bridge_set_active_trampoline(void* arg) { int active = (int)(intptr_t)arg; struct UTUN_INSTANCE* inst = instance_lite_get_instance(); if (!inst) return; - utun_set_client_activity(inst, active); - standby_set_enabled(inst, !active); + if (utun_set_client_activity(inst, active) < 0) { + bridge_log(BLEV_ERROR, "client_activity transition failed requested=%s", active ? "ACTIVE" : "STANDBY"); + return; + } bridge_log(BLEV_DEBUG, "client_activity set to %s", active ? "ACTIVE" : "STANDBY"); } diff --git a/tools/chatgui-android/libutun_lite/standby.c b/tools/chatgui-android/libutun_lite/standby.c index 546dc02d..fe2c4f5d 100644 --- a/tools/chatgui-android/libutun_lite/standby.c +++ b/tools/chatgui-android/libutun_lite/standby.c @@ -15,8 +15,8 @@ * пробуждает досрочно — следующий цикл как обычно. * * Список ожидающих (standby_wait) — двусвязный список, элементы из memory_pool. - * При выходе из standby (или досрочном пробуждении) все wait-таймеры - * отменяются, колбэки вызываются сразу. + * В SLEEP ожидающие будятся фазовым таймером после анонса ACTIVE пирам. + * При выходе из standby они будятся после восстановления режима линков. */ #include "standby.h" #include "chat_setting.h" @@ -35,6 +35,7 @@ struct standby_wait_entry { void* timer_id; /* handle uasync_set_timeout */ void (*cb)(void* arg); void* arg; + int dispatching; /* отцеплен для пробуждения; cancel только снимает cb */ }; static struct UASYNC* g_ua = NULL; @@ -103,28 +104,38 @@ static int standby_min_sleep_tb(void) { 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 int standby_arm_phase(int phase) { + int dur_tb = (phase == PHASE_ACTIVE) ? standby_active_tb() : standby_sleep_tb(); + uint64_t now = get_time_tb(); + void* timer = uasync_set_timeout(g_ua, dur_tb, NULL, standby_phase_timer_cb, "standby_phase"); + if (!timer) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: failed to arm phase=%d duration=%dms", phase, dur_tb / 10); + return -1; } + if (g_phase_timer) uasync_cancel_timeout(g_ua, g_phase_timer); + g_phase_timer = timer; + g_phase = phase; + g_phase_start_tb = now; + g_phase_end_tb = now + (uint64_t)dur_tb; + g_min_sleep_end_tb = phase == PHASE_SLEEP ? now + (uint64_t)standby_min_sleep_tb() : 0; + return 0; } +static void standby_wake_all_waiters(void); + 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 (standby_arm_phase(g_phase == PHASE_ACTIVE ? PHASE_SLEEP : PHASE_ACTIVE) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: phase timer lost; returning to normal mode"); + if (g_inst) utun_set_client_activity(g_inst, 1); else standby_set_enabled(NULL, 0); + return; + } if (g_phase == PHASE_SLEEP) { g_cnt_sleeps++; DEBUG_INFO(DEBUG_CATEGORY_GENERAL, @@ -137,6 +148,7 @@ static void standby_phase_timer_cb(void* arg) { 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); + standby_wake_all_waiters(); } } @@ -173,18 +185,20 @@ static void wait_list_push(struct standby_wait_entry* e) { 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; + for (struct standby_wait_entry* e = list; e; e = e->next) { + e->dispatching = 1; if (e->timer_id) { uasync_cancel_timeout(g_ua, e->timer_id); e->timer_id = NULL; } + } + while (list) { + struct standby_wait_entry* e = list; + list = e->next; void (*cb)(void*) = e->cb; void* arg = e->arg; wait_entry_free(e); - cb(arg); + if (cb) cb(arg); } } @@ -213,10 +227,6 @@ void standby_init(struct UASYNC* ua) { } 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; @@ -232,39 +242,46 @@ void standby_deinit(void) { g_wait_pool = NULL; } g_enabled = 0; + g_phase = PHASE_ACTIVE; + g_phase_start_tb = g_phase_end_tb = g_min_sleep_end_tb = 0; g_ua = NULL; g_inst = NULL; + g_switch_cb = NULL; + g_switch_arg = NULL; + g_phase_cb = NULL; + g_phase_cb_arg = 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; +int standby_set_enabled(struct UTUN_INSTANCE* inst, int enabled) { + if (!g_ua || (inst && inst->ua != g_ua)) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: set_enabled(%d) ua=%p instance_ua=%p", enabled, + (void*)g_ua, (void*)(inst ? inst->ua : NULL)); + return -1; } + enabled = !!enabled; if (inst) g_inst = inst; - if (g_enabled == enabled) return; + if (g_enabled == enabled) return 0; if (enabled) { + if (standby_arm_phase(PHASE_ACTIVE) < 0) return -1; g_enabled = 1; - g_phase = PHASE_ACTIVE; - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "standby: ENTER (app background, active=%dms sleep=%dms min_sleep=%dms)", + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "standby: ENTER (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)", + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "standby: EXIT (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); + if (!enabled) standby_wake_all_waiters(); + return 0; } int standby_is_enabled(void) { @@ -334,23 +351,18 @@ void* standby_wait(void* arg, void (*cb)(void* arg)) { } e->cb = cb; e->arg = arg; + e->dispatching = 0; - /* время до начала следующего активного интервала: - * 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; + /* SLEEP ждёт фазового события; ACTIVE вызывает cb отложенно, без рекурсии. */ + e->timer_id = NULL; + if (g_phase == PHASE_ACTIVE) { + e->timer_id = uasync_set_timeout(g_ua, 0, 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; @@ -359,6 +371,7 @@ void* standby_wait(void* arg, void (*cb)(void* arg)) { void standby_wait_cancel(void* handle) { if (!handle) return; struct standby_wait_entry* e = (struct standby_wait_entry*)handle; + if (e->dispatching) { e->cb = NULL; return; } if (e->timer_id && g_ua) { uasync_cancel_timeout(g_ua, e->timer_id); e->timer_id = NULL; @@ -391,18 +404,14 @@ void standby_notify_network_activity(void) { return; } + if (standby_arm_phase(PHASE_ACTIVE) < 0) 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(); + if (g_phase_cb) g_phase_cb(0, g_phase_cb_arg); standby_wake_all_waiters(); } diff --git a/tools/chatgui-android/libutun_lite/standby.h b/tools/chatgui-android/libutun_lite/standby.h index fd5ab241..8631e40a 100644 --- a/tools/chatgui-android/libutun_lite/standby.h +++ b/tools/chatgui-android/libutun_lite/standby.h @@ -22,12 +22,13 @@ struct UTUN_INSTANCE; /* Привязать модуль к uasync. Вызывается после каждого uasync_create(). */ void standby_init(struct UASYNC* ua); -/* Отмена таймеров и освобождение пула (без вызова колбэков). */ +/* Отмена таймеров, освобождение пула и очистка подписок (без вызова колбэков). */ void standby_deinit(void); -/* Включить/выключить standby-режим. Логирует вход/выход (GENERAL info) - * и вызывает switch-callback. Флаг дублируется в inst->standby_enabled. */ -void standby_set_enabled(struct UTUN_INSTANCE* inst, int enabled); +/* Применяет режим после подготовки фазового таймера, затем вызывает switch-callback. + * При выключении будит ожидающих после switch-callback. Повторная команда ничего не перезапускает. + * Вызывается из utun_set_client_activity в uasync-потоке; 0 — успех, <0 — режим не изменён. */ +int standby_set_enabled(struct UTUN_INSTANCE* inst, int enabled); int standby_is_enabled(void); /* Колбэк, вызываемый при каждом переключении standby-режима. */ @@ -53,7 +54,7 @@ int standby_get_keepalive_ms(void); void standby_set_phase_callback(void (*cb)(int sleeping, void* arg), void* arg); /* Ждёт до начала следующего активного интервала (как uasync timeout). - * При выходе из standby таймер отменяется и cb вызывается немедленно. + * В SLEEP ждёт фазового события после анонса ACTIVE; при выходе cb вызывается после восстановления линков. * Возвращает handle для standby_wait_cancel(). */ void* standby_wait(void* arg, void (*cb)(void* arg)); void standby_wait_cancel(void* handle); diff --git a/tools/chatgui-android/libutun_lite/tests/test_standby.c b/tools/chatgui-android/libutun_lite/tests/test_standby.c index b9f9d5e1..0e7e32ab 100644 --- a/tools/chatgui-android/libutun_lite/tests/test_standby.c +++ b/tools/chatgui-android/libutun_lite/tests/test_standby.c @@ -45,6 +45,12 @@ static void switch_cb(int enabled, void* arg) { /* re-entry waiter: в колбэке снова регистрирует себя (ровно один раз) */ static int g_reentry_calls = 0; +static void* g_cancel_other = NULL; +static void cancel_other_cb(void* arg) { + (void)arg; + standby_wait_cancel(g_cancel_other); + g_cancel_other = NULL; +} static void reentry_cb(void* arg) { (void)arg; g_reentry_calls++; @@ -205,8 +211,28 @@ int main(void) { standby_set_enabled(NULL, 0); /* EXIT → будит перерегистрированного ещё раз */ CHECK(g_reentry_calls == 2, "re-entry: exit fired re-registered waiter once more"); + /* ── 17. колбэк отменяет другого waiter из уже отцепленного списка ── */ + reset_standby(); + standby_set_enabled(NULL, 1); + run_to_next_phase(); + CHECK(standby_wait(NULL, cancel_other_cb) != NULL, "cross-cancel: first waiter registered"); + g_cancel_other = standby_wait(NULL, wait_cb); + CHECK(g_cancel_other != NULL, "cross-cancel: second waiter registered"); + standby_set_enabled(NULL, 0); + CHECK(g_wait_calls == 0 && g_cancel_other == NULL, "cross-cancel: cancelled callback not dispatched"); + + /* ── 18. даже неиспользованный standby очищает подписки при deinit ── */ + reset_standby(); + standby_deinit(); + standby_init(g_ua); + standby_set_enabled(NULL, 1); + CHECK(g_switch_calls == 0, "deinit: old switch callback cleared"); + standby_set_enabled(NULL, 0); + /* ── итог ── */ standby_deinit(); + uasync_poll(g_ua, 0); + CHECK(g_ua->timer_alloc_count == g_ua->timer_free_count, "cleanup: all timers freed before loop destruction"); uasync_destroy(g_ua, 0); printf("=== standby unit test: %d checks, %d failures ===\n", g_checks, g_failures);