diff --git a/src/transport_layer/etcp_keepalive.c b/src/transport_layer/etcp_keepalive.c index 521e25fe..31b9c565 100644 --- a/src/transport_layer/etcp_keepalive.c +++ b/src/transport_layer/etcp_keepalive.c @@ -561,21 +561,29 @@ int etcp_keepalive_on_recv(struct ETCP_DGRAM* pkt) { (int)(sleep_duration / 10), (int)(sleep_duration % 10)); if (!link->is_tcp) ka_send_sleep_ack(link); /* TCP доставляет надёжно — ACK не нужен */ - } else if (link->ka_peer_sleeping) { - /* Обычный keepalive (без флагов): пир проснулся — вернуться в normal-режим. */ - link->ka_peer_sleeping = 0; - link->ka_period_ms = (uint16_t)link->keepalive_interval; - link->keepalive_timeout = (uint32_t)link->keepalive_interval * KA_TIMEOUT_MULT; - if (link->etcp) { - if (link->etcp->peer_phase_timer) { - uasync_cancel_timeout(link->etcp->instance->ua, link->etcp->peer_phase_timer); - link->etcp->peer_phase_timer = NULL; - } - ka_peer_set_phase(link->etcp, 0); + } else { + /* Обычный keepalive снимает сон соединения даже по новому линку без ka_peer_sleeping. */ + struct ETCP_CONN* conn = link->etcp; + int link_was_sleeping = link->ka_peer_sleeping; + if (link_was_sleeping || conn->peer_sleep_phase || conn->peer_phase_timer) { + DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, + "[%s] peer normal link=%u tcp=%u link_sleep=%u phase=%u phase_timer=%u: cancel standby", + conn->log_name, link->local_link_id, link->is_tcp, link->ka_peer_sleeping, + conn->peer_sleep_phase, conn->peer_phase_timer != NULL); + } + if (link_was_sleeping) { + link->ka_peer_sleeping = 0; + link->ka_period_ms = (uint16_t)link->keepalive_interval; + link->keepalive_timeout = (uint32_t)link->keepalive_interval * KA_TIMEOUT_MULT; + } + if (conn->peer_phase_timer) { + uasync_cancel_timeout(instance->ua, conn->peer_phase_timer); + conn->peer_phase_timer = NULL; } - DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[%s] peer woke up, back to normal (interval=%dms)", - link->etcp->log_name, link->keepalive_interval); - if (!link->ka_my_sleeping) + conn->peer_active_remaining = conn->peer_sleep_duration = 0; + ka_peer_set_phase(conn, 0); + /* Повторный normal RX не должен отодвигать очередную отправку keepalive. */ + if (link_was_sleeping && !link->ka_my_sleeping) restart_keepalive_timer_ms(link, link->ka_period_ms); } diff --git a/tests/test_etcp_server_lifecycle.c b/tests/test_etcp_server_lifecycle.c index e34524de..951686b2 100644 --- a/tests/test_etcp_server_lifecycle.c +++ b/tests/test_etcp_server_lifecycle.c @@ -230,6 +230,60 @@ static void check_wake_deadline(void) { teardown(); DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "PASS wake replaces long sleep deadline with normal timeout"); } +static void peer_phase_changed(struct UTUN_INSTANCE* owner, uint64_t peer, int sleeping, void* arg) { + unsigned* events = arg; + CHECK(owner == &inst && peer == 42); + events[sleeping ? 1 : 0]++; +} + +static void receive_keepalive(struct ETCP_LINK* link, uint8_t flags, uint16_t active) { + struct ETCP_DGRAM* pkt = memory_pool_alloc(inst.pkt_pool); CHECK(pkt); + memset(pkt, 0, sizeof(*pkt) + 8); pkt->link = link; + pkt->data_len = flags & KA_FLAG_SLEEP ? 8 : 4; + pkt->data[0] = ETCP_KEEPALIVE; pkt->data[1] = flags; + pkt->data[2] = 200; pkt->data[4] = active & 0xff; pkt->data[5] = active >> 8; + pkt->data[6] = 200; // 20 секунд сна + link->last_recv_local_time = get_time_tb(); + CHECK(etcp_keepalive_on_recv(pkt) == 1); +} + +/* Новый линк снимает сон всего соединения, включая ещё не наступившую SLEEP-фазу. */ +static void check_peer_wake_new_link(int tcp, int active_window) { + setup(); struct ETCP_CONN* conn = ready_conn(!tcp); struct ETCP_LINK* old = conn->links; + unsigned events[2] = {0}; utun_add_peer_sleep_cbk(&inst, peer_phase_changed, events); + receive_keepalive(old, KA_FLAG_SLEEP, active_window ? 5 : 0); + CHECK(conn->peer_phase_timer && conn->peer_sleep_phase == !active_window); + CHECK(events[1] == (unsigned)!active_window && !events[0]); + /* REINIT потока сам по себе не означает пробуждение приложения. */ + etcp_conn_reinit_id(conn, "peer restart before wake", conn->reset_id); + CHECK(conn->peer_phase_timer && conn->peer_sleep_phase == !active_window); + struct ETCP_LINK* fresh = add_link(conn, tcp); start_keepalive_timer(fresh); + CHECK(!fresh->ka_peer_sleeping && fresh->keepalive_timer); + receive_keepalive(fresh, KA_FLAG_SLEEP_ACK, 0); + CHECK(conn->peer_phase_timer && conn->peer_sleep_phase == !active_window); + void* timer = fresh->keepalive_timer; + receive_keepalive(fresh, 0, 0); + CHECK(!conn->peer_sleep_phase && !conn->peer_phase_timer && fresh->keepalive_timer == timer); + CHECK(events[0] == (unsigned)!active_window && old->ka_peer_sleeping); + uint64_t allocations = inst.ua->timer_alloc_count; + receive_keepalive(fresh, 0, 0); + CHECK(inst.ua->timer_alloc_count == allocations && events[0] == (unsigned)!active_window); + /* Частый RX не отодвигает TX; отменённый фазовый таймер не усыпляет снова. */ + unsigned packets = fresh->keepalive_sent_count; + for (unsigned i = 0; i < 6; i++) { + test_clock_run_for(inst.ua, 1000); receive_keepalive(fresh, 0, 0); + CHECK(!conn->peer_sleep_phase && !conn->peer_phase_timer); + } + CHECK(fresh->keepalive_sent_count > packets && events[0] == (unsigned)!active_window); + /* Пробуждение пира не выключает собственный standby на прежнем линке. */ + old->ka_my_sleeping = 1; receive_keepalive(old, 0, 0); + CHECK(old->ka_my_sleeping && !old->ka_peer_sleeping && !old->keepalive_timer); + CHECK(!conn->peer_phase_timer && events[0] == (unsigned)!active_window); + utun_remove_peer_sleep_cbk(&inst, peer_phase_changed, events); + teardown(); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "PASS peer wake via new link tcp=%d active_window=%d: phase cancelled, TX preserved", tcp, active_window); +} + /* Мягкое закрытие работает без NCD entry; KEEP_ALIVE сохраняет транспорт дольше прежних 5 секунд. */ static void check_graceful(void) { setup(); struct ETCP_CONN* conn = ready_conn(0); @@ -251,6 +305,7 @@ int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); test_clock_init(10000); debug_set_category_level(DEBUG_CATEGORY_CONNECTION, DEBUG_LEVEL_DEBUG); debug_set_category_level(DEBUG_CATEGORY_KEEPALIVE, DEBUG_LEVEL_DEBUG); + debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_INFO); size_t baseline = u_get_allocated_count(); check_pending(); CHECK(u_get_allocated_count() == baseline); check_reconnect(); CHECK(u_get_allocated_count() == baseline); @@ -262,6 +317,9 @@ int main(void) { check_sockless_tcp_keepalive(); CHECK(u_get_allocated_count() == baseline); check_sleep(); CHECK(u_get_allocated_count() == baseline); check_wake_deadline(); CHECK(u_get_allocated_count() == baseline); + for (int tcp = 0; tcp <= 1; tcp++) for (int active = 0; active <= 1; active++) { + check_peer_wake_new_link(tcp, active); CHECK(u_get_allocated_count() == baseline); + } check_remote_down(); CHECK(u_get_allocated_count() == baseline); check_graceful(); CHECK(u_get_allocated_count() == baseline); test_clock_finish(); return 0; diff --git a/tests/test_standby_transport.c b/tests/test_standby_transport.c index 22626b0a..f4fc0b8d 100644 --- a/tests/test_standby_transport.c +++ b/tests/test_standby_transport.c @@ -162,6 +162,41 @@ static void check_transitions(void) { DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[PASS] first background, repeated commands, UDP/TCP ready, phase-before-work, peer sleep and cleanup"); } +/* GUI прерывает даже min_sleep; быстрый возврат в foreground отменяет будущие фазы. */ +static void check_foreground_wake(void) { + setup(); standby_set_intervals_ms(2000, 120000, 90000); + assert(utun_set_client_activity(&inst, 0) == 0); + test_clock_run_for(ua, 21000); + assert(standby_get_sleep_tb() > 1190000); + standby_notify_network_activity(); + assert(standby_get_sleep_tb() > 1190000); // сеть не отменяет min_sleep + wait_after_packets = sent[0].count; assert(standby_wait(NULL, waited)); + uint64_t before = get_time_tb(); + assert(utun_set_client_activity(&inst, 1) == 0); + assert(get_time_tb() - before < 1000 && wait_calls == 1 && !standby_is_enabled()); + for (int i = 0; i < 3; i++) { + assert(!links[i].ka_my_sleeping && !links[i].ka_sleep_announce_timer && links[i].keepalive_timer); + assert(sent[i].flags == 0); + } + /* При потере первого normal-пакета следующий уходит по обычному таймеру UDP/TCP. */ + unsigned udp_packets = sent[0].count, tcp_packets = sent[2].count; + test_clock_run_for(ua, 21000); + assert(sent[0].count > udp_packets && sent[2].count > tcp_packets); + assert(sent[0].flags == 0 && sent[2].flags == 0 && !standby_is_enabled()); + for (unsigned i = 0; i < 3; i++) { + assert(utun_set_client_activity(&inst, 0) == 0 && standby_is_enabled()); + test_clock_run_for(ua, 100); + assert(utun_set_client_activity(&inst, 1) == 0 && !standby_is_enabled()); + } + test_clock_run_for(ua, 25000); + assert(!standby_is_enabled() && inst.client_activity == CLIENT_ACTIVITY_ACTIVE); + for (int i = 0; i < 3; i++) assert(!links[i].ka_my_sleeping && !links[i].ka_sleep_announce_timer); + assert(utun_set_client_activity(&inst, 0) == 0); + test_clock_run_for(ua, 21000); assert(standby_get_sleep_tb() > 1190000); + teardown(); + DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[PASS] foreground bypasses min_sleep, repeated normal TX, rapid hide/show and final background sleep"); +} + /* Ошибка ENTER сохраняет normal; потеря фазового таймера возвращает normal, без зависания сна. */ static void check_timer_failures(void) { setup(); assert(utun_set_client_activity(&inst, 1) == 0); @@ -182,7 +217,7 @@ static void check_timer_failures(void) { int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); test_clock_init(1000); - check_transitions(); check_transitions(); check_timer_failures(); + check_transitions(); check_transitions(); check_foreground_wake(); check_timer_failures(); DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "PASS: standby transport regression tests"); test_clock_finish(); return 0;