Browse Source

Synchronize standby transitions and immediate keepalive phase announcements

master
evgeny 18 hours ago
parent
commit
9e3a417059
  1. 9
      doc/service_lifecycle.md
  2. 20
      src/transport_layer/etcp_connections.c
  3. 52
      src/transport_layer/etcp_keepalive.c
  4. 9
      src/transport_layer/etcp_keepalive.h
  5. 21
      src/utun_instance.c
  6. 6
      src/utun_instance.h
  7. 17
      tests/Makefile.am
  8. 180
      tests/test_standby_transport.c
  9. 6
      tools/chatgui-android/jni_bridge/android_jni_bridge.c
  10. 111
      tools/chatgui-android/libutun_lite/standby.c
  11. 11
      tools/chatgui-android/libutun_lite/standby.h
  12. 26
      tools/chatgui-android/libutun_lite/tests/test_standby.c

9
doc/service_lifecycle.md

@ -72,3 +72,12 @@ wake lock и сервис. Перед `NativeLib.stop/restart/destroy` выпо
ACTIVE и выключают Android standby. Состояние не истекает по таймеру. После ACTIVE и выключают Android standby. Состояние не истекает по таймеру. После
завершения звонка в фоне возвращается standby; экран может гаснуть во время завершения звонка в фоне возвращается standby; экран может гаснуть во время
разговора независимо от частичного wake lock сервиса. разговора независимо от частичного wake lock сервиса.
`utun_set_client_activity()` в сборке с `UTUN_HAVE_STANDBY` применяет фактический
режим duty-cycle до рассылки событий активности; JNI не переключает его отдельно.
Повторная команда не перезапускает фазу и анонсы, но первый фоновый запуск включает
standby даже при начальном `client_activity=STANDBY`. Keepalive подписан на режим
и фазы самого standby; новые UDP/TCP-линки сразу анонсируют текущую фазу после
handshake. Пробуждение по таймеру и внешней сети сначала сообщает ACTIVE-окно
пирам, затем запускает ожидающие задачи. При выходе сначала восстанавливаются
линки; deinit отменяет ожидающие задачи и очищает подписки старого ядра.

20
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->peer_reset_id) etcp_session_start(etcp); // сервер создаёт ETCP_CONN после STCP handshake
if (etcp->initialized == 0) etcp_conn_ready(etcp); 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; } 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 */ etcp_keepalive_link_ready(link);
if (!etcp_keepalive_announce_mode(link)) {
etcp_link_send_keepalive(link);
start_keepalive_timer(link);
}
loadbalancer_link_ready(link); loadbalancer_link_ready(link);
etcp_fire_link_status_cbk(link, old_state, link->link_status); 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)", 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] 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); 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); loadbalancer_link_ready(link);
// Restart NAT check after link is up (e.g. after address change or reinit) // 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); 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); 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); 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 */ etcp_keepalive_link_ready(link);
if (!etcp_keepalive_announce_mode(link)) {
etcp_link_send_keepalive(link);
start_keepalive_timer(link);
}
loadbalancer_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; return 0;
} }
if (link->link_state == LINK_STATE_TRY_RECONNECT) {// 0 - just init, 1 - handshake, 2 - try reconnect, 3 - connected if (link->link_state == LINK_STATE_TRY_RECONNECT) {// 0 - just init, 1 - handshake, 2 - try reconnect, 3 - connected
/* в standby анонс спячки вместо обычного flagless-keepalive */ etcp_keepalive_link_ready(link);
if (!etcp_keepalive_announce_mode(link)) {
start_keepalive_timer(link);
etcp_link_send_keepalive(link);
}
int old_state = link->link_state; link->link_state = LINK_STATE_CONNECTED; int old_state = link->link_state; link->link_state = LINK_STATE_CONNECTED;
etcp_fire_link_status_cbk(link, old_state, link->link_status); etcp_fire_link_status_cbk(link, old_state, link->link_status);
} }

52
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); 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) { if (inst->connections) {
for (struct ll_entry* e = inst->connections->head; e; e = e->next) { for (struct ll_entry* e = inst->connections->head; e; e = e->next) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data;
if (!ce || !ce->conn) continue; if (!ce || !ce->conn) continue;
for (struct ETCP_LINK* l = ce->conn->links; l; l = l->next) 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) { 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; struct tcp_conn_entry* te = (struct tcp_conn_entry*)e->data;
if (!te || !te->etcp_conn) continue; if (!te || !te->etcp_conn) continue;
for (struct ETCP_LINK* l = te->etcp_conn->links; l; l = l->next) 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 #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). // Вход в ACTIVE-окно — анонс-серия с ретраями до ACK (active_remaining>0).
static void ka_active_announce(struct ETCP_LINK* link) { static void ka_active_announce(struct ETCP_LINK* link) {
if (!link->ka_my_sleeping) return; 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; return 0;
} }
#ifndef UTUN_HAVE_STANDBY
static void etcp_keepalive_on_activity(struct UTUN_INSTANCE* inst, int active, void* arg) { static void etcp_keepalive_on_activity(struct UTUN_INSTANCE* inst, int active, void* arg) {
(void)arg; (void)arg;
if (!inst || inst->client_type != CLIENT_TYPE_MOBILE) return; 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)", 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); active ? "ACTIVE(wake)" : "STANDBY(sleep)", (unsigned long long)inst->node_id);
} }
#endif
// На только что (пере)инициализированном линке: если я в standby — анонсировать спячку // Сразу сообщает текущий режим/фазу нового или восстановленного линка и запускает нужный механизм keepalive.
// (возвращает 1, обычный flagless в этом случае слать НЕ надо); иначе 0. void etcp_keepalive_link_ready(struct ETCP_LINK* link) {
int etcp_keepalive_announce_mode(struct ETCP_LINK* link) { if (!link || !link->etcp || !link->etcp->instance) {
if (!link || !link->etcp || !link->etcp->instance) return 0; DEBUG_ERROR(DEBUG_CATEGORY_KEEPALIVE, "link_ready: missing link/connection/instance link=%p", (void*)link);
struct UTUN_INSTANCE* inst = link->etcp->instance; return;
if (inst->client_type != CLIENT_TYPE_MOBILE) return 0; }
if (!inst->standby_enabled) return 0; /* не в standby — обычный режим */ 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); ka_enter_sleep(link);
return 1; } 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) { void etcp_keepalive_register(struct UTUN_INSTANCE* inst) {
if (!inst) return; if (!inst) return;
utun_add_activity_cbk(inst, etcp_keepalive_on_activity, NULL);
#ifdef UTUN_HAVE_STANDBY #ifdef UTUN_HAVE_STANDBY
standby_set_switch_callback(ka_standby_switch, inst);
standby_set_phase_callback(ka_phase_cb, inst); standby_set_phase_callback(ka_phase_cb, inst);
#else
utun_add_activity_cbk(inst, etcp_keepalive_on_activity, NULL);
#endif #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);
} }

9
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, int etcp_keepalive_on_recv(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt,
struct ETCP_LINK* link, size_t pkt_len); struct ETCP_LINK* link, size_t pkt_len);
// Подписка на смену active mode (foreground/background) и фазы спячки duty-cycle. // Подписка на фактический режим/фазу standby; без duty-cycle — на активность клиента.
// Вызывается из init_connections. // Вызывается из init_connections.
void etcp_keepalive_register(struct UTUN_INSTANCE* inst); void etcp_keepalive_register(struct UTUN_INSTANCE* inst);
// На только что (пере)инициализированном линке: если я в standby — анонсировать // После INIT/INIT_RESPONSE, TCP handshake и reconnect сразу анонсирует текущий режим/фазу.
// спячку (возвращает 1, обычный flagless в этом случае слать НЕ надо); иначе 0. // В standby запускает sleep-анонс с ретраями UDP; в normal — обычный keepalive.
// Вызывается из etcp_link_enter_ready_tcp / handle_init_response_client / TRY_RECONNECT. void etcp_keepalive_link_ready(struct ETCP_LINK* link);
int etcp_keepalive_announce_mode(struct ETCP_LINK* link);
#ifdef __cplusplus #ifdef __cplusplus
} }

21
src/utun_instance.c

@ -31,6 +31,9 @@
#include "transport_layer/node_conn_direct.h" #include "transport_layer/node_conn_direct.h"
#include "transport_layer/socket_monitor.h" #include "transport_layer/socket_monitor.h"
#include "transport_layer/auto_socket.h" #include "transport_layer/auto_socket.h"
#ifdef UTUN_HAVE_STANDBY
#include "standby.h"
#endif
#include "../lib/u_async.h" #include "../lib/u_async.h"
#include "../lib/debug_config.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/звонка не истекает по таймеру: владелец сообщает завершение сам. */ /* Явная активность 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) { if (!instance) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "client_activity: missing instance active=%d", active); 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; int state = active ? CLIENT_ACTIVITY_ACTIVE : CLIENT_ACTIVITY_STANDBY;
if (instance->client_activity == state) return; int previous = instance->client_activity;
instance->client_activity = state; 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", 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); active ? "ACTIVE" : "STANDBY", (unsigned long long)instance->node_id, instance->client_type);
utun_fire_activity_cbk(instance, state); utun_fire_activity_cbk(instance, state);
return 0;
} }

6
src/utun_instance.h

@ -243,7 +243,6 @@ struct UTUN_INSTANCE {
uint8_t client_activity; // CLIENT_ACTIVITY_STANDBY/ACTIVE uint8_t client_activity; // CLIENT_ACTIVITY_STANDBY/ACTIVE
struct utun_activity_cbk_entry* activity_cbks; // подписки на смену client_activity struct utun_activity_cbk_entry* activity_cbks; // подписки на смену client_activity
struct peer_sleep_cbk_entry* peer_sleep_cbks; // подписки на смену спячки пира (keepalive) struct peer_sleep_cbk_entry* peer_sleep_cbks; // подписки на смену спячки пира (keepalive)
uint8_t standby_enabled; // 1 = standby duty-cycle активен (chatgui-android)
// TCP proxy server (exit node) // TCP proxy server (exit node)
struct tcp_proxy_server tcp_proxy_server; struct tcp_proxy_server tcp_proxy_server;
@ -294,8 +293,9 @@ void utun_instance_stop(struct UTUN_INSTANCE *instance);
/* Глобальные переключатели создания TUN/топологии для тестов и встраивания. */ /* Глобальные переключатели создания TUN/топологии для тестов и встраивания. */
void utun_instance_set_tun_init_enabled(int enabled); void utun_instance_set_tun_init_enabled(int enabled);
void utun_instance_set_topo_group_enabled(int enabled); void utun_instance_set_topo_group_enabled(int enabled);
/* Состояние активности клиента сохраняется до следующего явного вызова; выполнять в uasync-потоке. */ /* Применяет активность и мобильный standby до уведомления подписчиков; выполнять в uasync-потоке.
void utun_set_client_activity(struct UTUN_INSTANCE* instance, int active); * Повторная команда согласует фактический режим без повторного 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_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_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); void utun_add_peer_sleep_cbk(struct UTUN_INSTANCE* instance, peer_sleep_cbk_fn fn, void* arg);

17
tests/Makefile.am

@ -112,9 +112,9 @@ check_PROGRAMS = \
bench_timeout_heap \ bench_timeout_heap \
bench_uasync_timeouts bench_uasync_timeouts
# Linux-only тест: требует root + dummy-интерфейсы + iproute2/netlink # Linux-only: test_auto_socket_dynamic требует root + dummy-интерфейсы + iproute2/netlink.
if OS_LINUX if OS_LINUX
check_PROGRAMS += test_auto_socket_dynamic check_PROGRAMS += test_auto_socket_dynamic test_standby test_standby_transport
endif endif
# Silero VAD: только при включённом ONNX Runtime (--with-silero-vad) # 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 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 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 definitions
test_etcp_bbr_SOURCES = test_etcp_bbr.c test_etcp_bbr_SOURCES = test_etcp_bbr.c
test_etcp_bbr_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_etcp_bbr_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib

180
tests/test_standby_transport.c

@ -0,0 +1,180 @@
/* Проверка режима и фаз standby на реальных keepalive-пакетах до шифрования.
* Управляемая отправка исключает сеть; отказ таймера проверяет сохранение рабочего режима. */
#include <assert.h>
#include <string.h>
#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;
}

6
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; int active = (int)(intptr_t)arg;
struct UTUN_INSTANCE* inst = instance_lite_get_instance(); struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst) return; if (!inst) return;
utun_set_client_activity(inst, active); if (utun_set_client_activity(inst, active) < 0) {
standby_set_enabled(inst, !active); 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"); bridge_log(BLEV_DEBUG, "client_activity set to %s", active ? "ACTIVE" : "STANDBY");
} }

111
tools/chatgui-android/libutun_lite/standby.c

@ -15,8 +15,8 @@
* пробуждает досрочно — следующий цикл как обычно. * пробуждает досрочно — следующий цикл как обычно.
* *
* Список ожидающих (standby_wait) — двусвязный список, элементы из memory_pool. * Список ожидающих (standby_wait) — двусвязный список, элементы из memory_pool.
* При выходе из standby (или досрочном пробуждении) все wait-таймеры * В SLEEP ожидающие будятся фазовым таймером после анонса ACTIVE пирам.
* отменяются, колбэки вызываются сразу. * При выходе из standby они будятся после восстановления режима линков.
*/ */
#include "standby.h" #include "standby.h"
#include "chat_setting.h" #include "chat_setting.h"
@ -35,6 +35,7 @@ struct standby_wait_entry {
void* timer_id; /* handle uasync_set_timeout */ void* timer_id; /* handle uasync_set_timeout */
void (*cb)(void* arg); void (*cb)(void* arg);
void* arg; void* arg;
int dispatching; /* отцеплен для пробуждения; cancel только снимает cb */
}; };
static struct UASYNC* g_ua = NULL; 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_phase_timer_cb(void* arg);
static void standby_arm_phase(void) { /* Сначала создаёт новый таймер; при ошибке сохраняет прежнюю фазу и её таймер. */
int dur_tb = (g_phase == PHASE_ACTIVE) ? standby_active_tb() : standby_sleep_tb(); static int standby_arm_phase(int phase) {
g_phase_start_tb = get_time_tb(); int dur_tb = (phase == PHASE_ACTIVE) ? standby_active_tb() : standby_sleep_tb();
g_phase_end_tb = g_phase_start_tb + (uint64_t)dur_tb; uint64_t now = get_time_tb();
g_min_sleep_end_tb = (g_phase == PHASE_SLEEP) void* timer = uasync_set_timeout(g_ua, dur_tb, NULL, standby_phase_timer_cb, "standby_phase");
? g_phase_start_tb + (uint64_t)standby_min_sleep_tb() if (!timer) {
: 0; DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: failed to arm phase=%d duration=%dms", phase, dur_tb / 10);
g_phase_timer = uasync_set_timeout(g_ua, dur_tb, NULL, standby_phase_timer_cb, "standby_phase"); return -1;
if (!g_phase_timer) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: failed to arm phase timer");
} }
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) { static void standby_phase_timer_cb(void* arg) {
(void)arg; (void)arg;
g_phase_timer = NULL; g_phase_timer = NULL;
if (!g_enabled) return; if (!g_enabled) return;
uint64_t elapsed = get_time_tb() - g_phase_start_tb; uint64_t elapsed = get_time_tb() - g_phase_start_tb;
g_phase = (g_phase == PHASE_ACTIVE) ? PHASE_SLEEP : PHASE_ACTIVE;
/* переармить фазу ДО фазового колбэка: колбэк читает g_phase_end_tb /* переармить фазу ДО фазового колбэка: колбэк читает g_phase_end_tb
(ka_active_remaining_01s), иначе он увидит старый (уже истёкший) дедлайн. */ (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) { if (g_phase == PHASE_SLEEP) {
g_cnt_sleeps++; g_cnt_sleeps++;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, 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", DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "standby: WAKE (timer), slept %d.%ds",
(int)(elapsed / STANDBY_TB_PER_SEC), (int)((elapsed % STANDBY_TB_PER_SEC) / 1000)); (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); 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) { static void standby_wake_all_waiters(void) {
struct standby_wait_entry* list = g_wait_head; struct standby_wait_entry* list = g_wait_head;
g_wait_head = g_wait_tail = NULL; g_wait_head = g_wait_tail = NULL;
while (list) { for (struct standby_wait_entry* e = list; e; e = e->next) {
struct standby_wait_entry* e = list; e->dispatching = 1;
list = e->next;
e->prev = e->next = NULL;
if (e->timer_id) { if (e->timer_id) {
uasync_cancel_timeout(g_ua, e->timer_id); uasync_cancel_timeout(g_ua, e->timer_id);
e->timer_id = NULL; e->timer_id = NULL;
} }
}
while (list) {
struct standby_wait_entry* e = list;
list = e->next;
void (*cb)(void*) = e->cb; void (*cb)(void*) = e->cb;
void* arg = e->arg; void* arg = e->arg;
wait_entry_free(e); wait_entry_free(e);
cb(arg); if (cb) cb(arg);
} }
} }
@ -213,10 +227,6 @@ void standby_init(struct UASYNC* ua) {
} }
void standby_deinit(void) { 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) { if (g_phase_timer && g_ua) {
uasync_cancel_timeout(g_ua, g_phase_timer); uasync_cancel_timeout(g_ua, g_phase_timer);
g_phase_timer = NULL; g_phase_timer = NULL;
@ -232,39 +242,46 @@ void standby_deinit(void) {
g_wait_pool = NULL; g_wait_pool = NULL;
} }
g_enabled = 0; 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_ua = NULL;
g_inst = 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)", 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); 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) { int standby_set_enabled(struct UTUN_INSTANCE* inst, int enabled) {
if (!g_ua) { if (!g_ua || (inst && inst->ua != g_ua)) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: set_enabled(%d) but not initialized", enabled); DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: set_enabled(%d) ua=%p instance_ua=%p", enabled,
return; (void*)g_ua, (void*)(inst ? inst->ua : NULL));
return -1;
} }
enabled = !!enabled;
if (inst) g_inst = inst; if (inst) g_inst = inst;
if (g_enabled == enabled) return; if (g_enabled == enabled) return 0;
if (enabled) { if (enabled) {
if (standby_arm_phase(PHASE_ACTIVE) < 0) return -1;
g_enabled = 1; g_enabled = 1;
g_phase = PHASE_ACTIVE; DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "standby: ENTER (active=%dms sleep=%dms min_sleep=%dms)",
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_active_tb() / 10, standby_sleep_tb() / 10, standby_min_sleep_tb() / 10);
standby_arm_phase();
} else { } else {
g_enabled = 0; g_enabled = 0;
if (g_phase_timer) { if (g_phase_timer) {
uasync_cancel_timeout(g_ua, g_phase_timer); uasync_cancel_timeout(g_ua, g_phase_timer);
g_phase_timer = NULL; 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); 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 (g_switch_cb) g_switch_cb(enabled, g_switch_arg);
if (!enabled) standby_wake_all_waiters();
return 0;
} }
int standby_is_enabled(void) { int standby_is_enabled(void) {
@ -334,24 +351,19 @@ void* standby_wait(void* arg, void (*cb)(void* arg)) {
} }
e->cb = cb; e->cb = cb;
e->arg = arg; e->arg = arg;
e->dispatching = 0;
/* время до начала следующего активного интервала: /* SLEEP ждёт фазового события; ACTIVE вызывает cb отложенно, без рекурсии. */
* SLEEP — ждём до конца фазы; ACTIVE — уже бодрствуем, будим сразу. */ e->timer_id = NULL;
uint64_t now = get_time_tb(); if (g_phase == PHASE_ACTIVE) {
uint64_t tb; e->timer_id = uasync_set_timeout(g_ua, 0, e, standby_wait_timer_cb, "standby_wait");
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) { if (!e->timer_id) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: uasync_set_timeout failed"); DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: uasync_set_timeout failed");
wait_entry_free(e); wait_entry_free(e);
cb(arg); cb(arg);
return NULL; return NULL;
} }
}
wait_list_push(e); wait_list_push(e);
return e; return e;
} }
@ -359,6 +371,7 @@ void* standby_wait(void* arg, void (*cb)(void* arg)) {
void standby_wait_cancel(void* handle) { void standby_wait_cancel(void* handle) {
if (!handle) return; if (!handle) return;
struct standby_wait_entry* e = (struct standby_wait_entry*)handle; struct standby_wait_entry* e = (struct standby_wait_entry*)handle;
if (e->dispatching) { e->cb = NULL; return; }
if (e->timer_id && g_ua) { if (e->timer_id && g_ua) {
uasync_cancel_timeout(g_ua, e->timer_id); uasync_cancel_timeout(g_ua, e->timer_id);
e->timer_id = NULL; e->timer_id = NULL;
@ -391,18 +404,14 @@ void standby_notify_network_activity(void) {
return; return;
} }
if (standby_arm_phase(PHASE_ACTIVE) < 0) return;
g_cnt_wakes_early++; g_cnt_wakes_early++;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, DEBUG_INFO(DEBUG_CATEGORY_GENERAL,
"standby: WAKE early (network activity), slept %d.%ds min_sleep=%dms", "standby: WAKE early (network activity), slept %d.%ds min_sleep=%dms",
(int)(slept / STANDBY_TB_PER_SEC), (int)((slept % STANDBY_TB_PER_SEC) / 1000), (int)(slept / STANDBY_TB_PER_SEC), (int)((slept % STANDBY_TB_PER_SEC) / 1000),
standby_min_sleep_tb() / 10); standby_min_sleep_tb() / 10);
if (g_phase_timer) { if (g_phase_cb) g_phase_cb(0, g_phase_cb_arg);
uasync_cancel_timeout(g_ua, g_phase_timer);
g_phase_timer = NULL;
}
g_phase = PHASE_ACTIVE;
standby_arm_phase();
standby_wake_all_waiters(); standby_wake_all_waiters();
} }

11
tools/chatgui-android/libutun_lite/standby.h

@ -22,12 +22,13 @@ struct UTUN_INSTANCE;
/* Привязать модуль к uasync. Вызывается после каждого uasync_create(). */ /* Привязать модуль к uasync. Вызывается после каждого uasync_create(). */
void standby_init(struct UASYNC* ua); void standby_init(struct UASYNC* ua);
/* Отмена таймеров и освобождение пула (без вызова колбэков). */ /* Отмена таймеров, освобождение пула и очистка подписок (без вызова колбэков). */
void standby_deinit(void); void standby_deinit(void);
/* Включить/выключить standby-режим. Логирует вход/выход (GENERAL info) /* Применяет режим после подготовки фазового таймера, затем вызывает switch-callback.
* и вызывает switch-callback. Флаг дублируется в inst->standby_enabled. */ * При выключении будит ожидающих после switch-callback. Повторная команда ничего не перезапускает.
void standby_set_enabled(struct UTUN_INSTANCE* inst, int enabled); * Вызывается из utun_set_client_activity в uasync-потоке; 0 — успех, <0 — режим не изменён. */
int standby_set_enabled(struct UTUN_INSTANCE* inst, int enabled);
int standby_is_enabled(void); int standby_is_enabled(void);
/* Колбэк, вызываемый при каждом переключении standby-режима. */ /* Колбэк, вызываемый при каждом переключении standby-режима. */
@ -53,7 +54,7 @@ int standby_get_keepalive_ms(void);
void standby_set_phase_callback(void (*cb)(int sleeping, void* arg), void* arg); void standby_set_phase_callback(void (*cb)(int sleeping, void* arg), void* arg);
/* Ждёт до начала следующего активного интервала (как uasync timeout). /* Ждёт до начала следующего активного интервала (как uasync timeout).
* При выходе из standby таймер отменяется и cb вызывается немедленно. * В SLEEP ждёт фазового события после анонса ACTIVE; при выходе cb вызывается после восстановления линков.
* Возвращает handle для standby_wait_cancel(). */ * Возвращает handle для standby_wait_cancel(). */
void* standby_wait(void* arg, void (*cb)(void* arg)); void* standby_wait(void* arg, void (*cb)(void* arg));
void standby_wait_cancel(void* handle); void standby_wait_cancel(void* handle);

26
tools/chatgui-android/libutun_lite/tests/test_standby.c

@ -45,6 +45,12 @@ static void switch_cb(int enabled, void* arg) {
/* re-entry waiter: в колбэке снова регистрирует себя (ровно один раз) */ /* re-entry waiter: в колбэке снова регистрирует себя (ровно один раз) */
static int g_reentry_calls = 0; 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) { static void reentry_cb(void* arg) {
(void)arg; (void)arg;
g_reentry_calls++; g_reentry_calls++;
@ -205,8 +211,28 @@ int main(void) {
standby_set_enabled(NULL, 0); /* EXIT → будит перерегистрированного ещё раз */ standby_set_enabled(NULL, 0); /* EXIT → будит перерегистрированного ещё раз */
CHECK(g_reentry_calls == 2, "re-entry: exit fired re-registered waiter once more"); 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(); 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); uasync_destroy(g_ua, 0);
printf("=== standby unit test: %d checks, %d failures ===\n", g_checks, g_failures); printf("=== standby unit test: %d checks, %d failures ===\n", g_checks, g_failures);

Loading…
Cancel
Save