From e6954b4152701cbb657ab9de025098328efd08c1 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 15 Oct 2026 12:24:55 +0200 Subject: [PATCH] Acknowledge call termination across router epoch changes --- src/call/call.c | 127 +++++++++++++++++++++++++++++----------------- tests/test_call.c | 97 ++++++++++++++++++++++++++++++++--- 2 files changed, 169 insertions(+), 55 deletions(-) diff --git a/src/call/call.c b/src/call/call.c index 5b06e652..4023d375 100644 --- a/src/call/call.c +++ b/src/call/call.c @@ -63,6 +63,7 @@ static const char* call_subcmd_name(uint8_t sc) { case CALL_SUBCMD_BUSY: return "BUSY"; case CALL_SUBCMD_CANCEL: return "CANCEL"; case CALL_SUBCMD_HANGUP: return "HANGUP"; + case CALL_SUBCMD_END_ACK: return "END_ACK"; case CALL_SUBCMD_MEDIA: return "MEDIA"; default: return "?"; } @@ -76,9 +77,12 @@ static void call_stats_timer_cb(void* arg); static int call_send(struct call_ctx* ctx, uint64_t group_id, uint64_t dst, const uint8_t* data, size_t len, int mode) { struct ll_entry* e = queue_entry_new(0); - if (!e) return -1; + if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call send: entry allocation failed"); return -1; } e->dgram = u_malloc(1 + len); - if (!e->dgram) { queue_entry_free(e); return -1; } + if (!e->dgram) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call send: packet allocation failed bytes=%zu", 1 + len); + queue_entry_free(e); return -1; + } e->dgram[0] = ETCP_RT_ID_CALL; if (len) memcpy(e->dgram + 1, data, len); e->len = (uint16_t)(1 + len); @@ -270,23 +274,44 @@ static void call_watch_connection(struct call_session* s) { /* ─────────────────── завершение ─────────────────── */ -/* Отложенный разрыв: единственная точка, где закрывается conn_mgr (после того, - * как финальный сигналинг — HANGUP/DECLINE — гарантированно долетел). */ +/* Финальная команда восстанавливается слоем звонка после смены эпохи роутера. */ +static void call_send_end(struct call_session* s) { + struct call_pkt_hangup packet = {s->end_subcmd, s->call_id, s->reason}; + size_t len = s->end_subcmd == CALL_SUBCMD_CANCEL ? sizeof(struct call_pkt_cancel) : sizeof(packet); + DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call end send id=%016llx command=%s elapsed_ms=%llu", + (unsigned long long)s->call_id, call_subcmd_name(s->end_subcmd), + (unsigned long long)((get_time_tb() - s->end_started_tb) / 10)); + call_send(s->ctx, s->group_id, s->peer_node_id, (const uint8_t*)&packet, len, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); +} + +/* Медиа уже остановлены. CM держим до подтверждения команды либо ограниченного таймаута. */ static void call_teardown_timer_cb(void* arg) { struct call_session* s = (struct call_session*)arg; if (!s || !s->ctx) return; struct call_ctx* ctx = s->ctx; s->teardown_timer = NULL; + if (s->end_subcmd && get_time_tb() - s->end_started_tb < CALL_END_TIMEOUT_TB) { + call_send_end(s); + s->teardown_timer = uasync_set_timeout(ctx->inst->ua, CALL_END_RETRY_TB, s, call_teardown_timer_cb, "call_end_retry"); + if (s->teardown_timer) return; + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call end retry timer allocation failed id=%016llx", (unsigned long long)s->call_id); + } else if (s->end_subcmd) { + DEBUG_WARN(DEBUG_CATEGORY_CALL, "call end confirmation timeout id=%016llx command=%s elapsed_ms=%llu", + (unsigned long long)s->call_id, call_subcmd_name(s->end_subcmd), + (unsigned long long)((get_time_tb() - s->end_started_tb) / 10)); + } if (s->cm_handle) { conn_mgr_close(s->cm_handle); s->cm_handle = NULL; } call_session_remove_and_free(ctx, s); } -static void call_end(struct call_ctx* ctx, struct call_session* s, uint8_t reason) { +static void call_end(struct call_ctx* ctx, struct call_session* s, uint8_t reason, uint8_t end_subcmd) { if (!ctx || !s || s->state == CALL_ST_ENDED) return; s->state = CALL_ST_ENDED; - call_video_transport_stop(s); + call_session_cleanup(ctx, s); s->reason = reason; + s->end_subcmd = end_subcmd; + s->end_started_tb = get_time_tb(); ctx->c_calls_ended++; if (s->ring_timer) { uasync_cancel_timeout(ctx->inst->ua, s->ring_timer); s->ring_timer = NULL; } @@ -302,12 +327,15 @@ static void call_end(struct call_ctx* ctx, struct call_session* s, uint8_t reaso call_post_connection(s); call_event_id(ctx, CHAT_EVT_CALL_ENDED, s->call_id, reason); - /* Закрытие запроса CM откладываем на 500мс для отправки финального сигналинга. */ - if (s->cm_handle) { - s->teardown_timer = uasync_set_timeout(ctx->inst->ua, CALL_TEARDOWN_DELAY_TB, s, call_teardown_timer_cb, "call_teardown"); - } else { - call_session_remove_and_free(ctx, s); + if (s->end_subcmd) call_send_end(s); + if (s->cm_handle || s->end_subcmd) { + unsigned delay = s->end_subcmd ? CALL_END_RETRY_TB : CALL_TEARDOWN_DELAY_TB; + s->teardown_timer = uasync_set_timeout(ctx->inst->ua, delay, s, call_teardown_timer_cb, "call_teardown"); + if (s->teardown_timer) return; + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call teardown timer allocation failed id=%016llx", (unsigned long long)s->call_id); } + if (s->cm_handle) { conn_mgr_close(s->cm_handle); s->cm_handle = NULL; } + call_session_remove_and_free(ctx, s); } /* ─────────────────── conn_mgr ─────────────────── */ @@ -359,12 +387,7 @@ static void call_ring_timeout_cb(void* arg) { DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: ring timeout id=%016llx -> CANCEL", CALL_ID, (unsigned long long)s->call_id); - uint8_t pkt[sizeof(struct call_pkt_cancel)]; - struct call_pkt_cancel* c = (struct call_pkt_cancel*)pkt; - c->subcmd = CALL_SUBCMD_CANCEL; - c->call_id = s->call_id; - call_send(ctx, s->group_id, s->peer_node_id, pkt, sizeof(pkt), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); - call_end(ctx, s, CALL_REASON_RING_TIMEOUT); + call_end(ctx, s, CALL_REASON_RING_TIMEOUT, CALL_SUBCMD_CANCEL); } /* Периодическая кодограмма INVITE (caller) каждые 300мс на всём протяжении вызова @@ -390,7 +413,7 @@ static void call_invite_retry_cb(void* arg) { memcpy(buf, &s->call_id, 8); buf[8] = 2; /* err: peer unreachable (no route) */ chat_event_post(ctx->inst, CHAT_EVT_CALL_ERROR, buf, sizeof(buf)); - call_end(ctx, s, CALL_REASON_CONNECT_FAIL); + call_end(ctx, s, CALL_REASON_CONNECT_FAIL, 0); return; } else { DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: INVITE codegram id=%016llx no route (strike %d/%d)", @@ -410,7 +433,7 @@ static void call_ring_dead_cb(void* arg) { DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: ring dead (2с без кодограммы) id=%016llx -> end", CALL_ID, (unsigned long long)s->call_id); - call_end(ctx, s, CALL_REASON_CANCEL); + call_end(ctx, s, CALL_REASON_CANCEL, 0); } static void call_traffic_timeout_cb(void* arg) { @@ -422,13 +445,7 @@ static void call_traffic_timeout_cb(void* arg) { DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: no traffic 20s id=%016llx -> HANGUP", CALL_ID, (unsigned long long)s->call_id); - uint8_t pkt[sizeof(struct call_pkt_hangup)]; - struct call_pkt_hangup* hu = (struct call_pkt_hangup*)pkt; - hu->subcmd = CALL_SUBCMD_HANGUP; - hu->call_id = s->call_id; - hu->reason = CALL_REASON_NO_TRAFFIC; - call_send(ctx, s->group_id, s->peer_node_id, pkt, sizeof(pkt), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); - call_end(ctx, s, CALL_REASON_NO_TRAFFIC); + call_end(ctx, s, CALL_REASON_NO_TRAFFIC, CALL_SUBCMD_HANGUP); } /* Периодическое событие CALL_STATS (раз в 1с): rtt_ms, buffer_ms, tempo_x100. */ @@ -597,7 +614,7 @@ static void call_handle_decline(struct call_ctx* ctx, uint64_t from, const uint8 if (!s || s->state == CALL_ST_ENDED) return; DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: DECLINE id=%016llx reason=%d", CALL_ID, (unsigned long long)call_id, reason); call_event_id(ctx, CHAT_EVT_CALL_DECLINED, call_id, reason); - call_end(ctx, s, CALL_REASON_DECLINE); + call_end(ctx, s, CALL_REASON_DECLINE, 0); (void)from; } @@ -607,7 +624,7 @@ static void call_handle_busy(struct call_ctx* ctx, uint64_t from, const uint8_t* struct call_session* s = call_session_find(ctx, call_id); if (!s || s->state == CALL_ST_ENDED) return; DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: BUSY id=%016llx", CALL_ID, (unsigned long long)call_id); - call_end(ctx, s, CALL_REASON_BUSY); + call_end(ctx, s, CALL_REASON_BUSY, 0); (void)from; } @@ -617,7 +634,7 @@ static void call_handle_cancel(struct call_ctx* ctx, uint64_t from, const uint8_ struct call_session* s = call_session_find(ctx, call_id); if (!s || s->state == CALL_ST_ENDED) return; DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: CANCEL id=%016llx", CALL_ID, (unsigned long long)call_id); - call_end(ctx, s, CALL_REASON_CANCEL); + call_end(ctx, s, CALL_REASON_CANCEL, 0); (void)from; } @@ -628,7 +645,7 @@ static void call_handle_hangup(struct call_ctx* ctx, uint64_t from, const uint8_ struct call_session* s = call_session_find(ctx, call_id); if (!s || s->state == CALL_ST_ENDED) return; DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: HANGUP id=%016llx reason=%d", CALL_ID, (unsigned long long)call_id, reason); - call_end(ctx, s, CALL_REASON_REMOTE_HANGUP); + call_end(ctx, s, CALL_REASON_REMOTE_HANGUP, 0); (void)from; } @@ -705,6 +722,36 @@ static void call_etcp_recv(struct ETCP_CONN* conn, struct ll_entry* entry) { } } + int terminal = subcmd == CALL_SUBCMD_HANGUP || subcmd == CALL_SUBCMD_CANCEL || subcmd == CALL_SUBCMD_DECLINE; + if (terminal || subcmd == CALL_SUBCMD_END_ACK) { + size_t expected = subcmd == CALL_SUBCMD_CANCEL || subcmd == CALL_SUBCMD_END_ACK ? 9 : 10; + if (blen != expected) { + DEBUG_WARN(DEBUG_CATEGORY_CALL, "call end invalid packet command=%s bytes=%zu expected=%zu", + call_subcmd_name(subcmd), blen, expected); + queue_dgram_free(entry); queue_entry_free(entry); return; + } + uint64_t id; memcpy(&id, body + 1, 8); + struct call_session* s = call_session_find(ctx, id); + if (terminal) { + // Подтверждаем также повторы после удаления сессии; повторного ENDED обработчики не создают. + struct call_pkt_end_ack ack = {CALL_SUBCMD_END_ACK, id}; + DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call end ack id=%016llx command=%s state=%s", + (unsigned long long)id, call_subcmd_name(subcmd), s ? call_state_name(s->state) : "absent"); + call_send(ctx, group, from, (const uint8_t*)&ack, sizeof(ack), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); + } else { + if (s && s->state == CALL_ST_ENDED && s->end_subcmd) { + DEBUG_INFO(DEBUG_CATEGORY_CALL, "call end confirmed id=%016llx command=%s elapsed_ms=%llu", + (unsigned long long)id, call_subcmd_name(s->end_subcmd), + (unsigned long long)((get_time_tb() - s->end_started_tb) / 10)); + s->end_subcmd = 0; // Ближайший teardown tick освобождает CM и сессию. + } else { + DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call end late ack ignored id=%016llx state=%s", + (unsigned long long)id, s ? call_state_name(s->state) : "absent"); + } + queue_dgram_free(entry); queue_entry_free(entry); return; + } + } + switch (subcmd) { case CALL_SUBCMD_INVITE: call_handle_invite(ctx, from, group, body, blen); break; case CALL_SUBCMD_RINGING: call_handle_ringing(ctx, from, body, blen); break; @@ -944,7 +991,7 @@ void call_start(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_nod call_open_cm(s); if (call_send_invite(ctx, s) != 0) { - call_end(ctx, s, CALL_REASON_CONNECT_FAIL); + call_end(ctx, s, CALL_REASON_CONNECT_FAIL, 0); return; } call_watch_connection(s); @@ -979,15 +1026,8 @@ void call_decline(struct UTUN_INSTANCE* inst, uint64_t call_id) { if (!s || s->role != CALL_ROLE_CALLEE || s->state == CALL_ST_ENDED) return; DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: call_decline id=%016llx", CALL_ID, (unsigned long long)call_id); - uint8_t pkt[sizeof(struct call_pkt_decline)]; - struct call_pkt_decline* d = (struct call_pkt_decline*)pkt; - d->subcmd = CALL_SUBCMD_DECLINE; - d->call_id = call_id; - d->reason = CALL_REASON_DECLINE; - call_send(ctx, s->group_id, s->peer_node_id, pkt, sizeof(pkt), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); - call_event_id(ctx, CHAT_EVT_CALL_DECLINED, call_id, CALL_REASON_DECLINE); - call_end(ctx, s, CALL_REASON_DECLINE); + call_end(ctx, s, CALL_REASON_DECLINE, CALL_SUBCMD_DECLINE); } void call_hangup(struct UTUN_INSTANCE* inst, uint64_t call_id) { @@ -997,14 +1037,7 @@ void call_hangup(struct UTUN_INSTANCE* inst, uint64_t call_id) { if (!s || s->state == CALL_ST_ENDED) return; DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: call_hangup id=%016llx", CALL_ID, (unsigned long long)call_id); - uint8_t pkt[sizeof(struct call_pkt_hangup)]; - struct call_pkt_hangup* hu = (struct call_pkt_hangup*)pkt; - hu->subcmd = CALL_SUBCMD_HANGUP; - hu->call_id = call_id; - hu->reason = CALL_REASON_LOCAL_HANGUP; - call_send(ctx, s->group_id, s->peer_node_id, pkt, sizeof(pkt), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); - - call_end(ctx, s, CALL_REASON_LOCAL_HANGUP); + call_end(ctx, s, CALL_REASON_LOCAL_HANGUP, CALL_SUBCMD_HANGUP); } /* ─────────────────── trampoline-функции (GUI → uasync) ─────────────────── */ diff --git a/tests/test_call.c b/tests/test_call.c index 32fece03..9484fcf6 100644 --- a/tests/test_call.c +++ b/tests/test_call.c @@ -19,12 +19,14 @@ #include "../dm/dm_core.h" #include "../dm/dm_crypto.h" #include "../call/call_proto.h" +#include "../call/call_priv.h" #include "../call/call_video_transport.h" #include "../call/call_video_port.h" #include "../routing_layer/topo_node_sqlite.h" #include "../routing_layer/topo_group.h" #include "../routing_layer/topo_node.h" #include "../routing_layer/conn_mgr_priv.h" +#include "../routing_layer/route_crypto.h" #include "../utun_instance.h" #include "../transport_layer/etcp.h" #include "../transport_layer/etcp_connections.h" @@ -71,13 +73,14 @@ enum call_phase { P_WAIT_ENDED1, /* обе ENDED */ P_CALL2_START, /* A: call_start (2-й звонок) */ P_WAIT_INCOMING2, /* B: INCOMING */ - P_VIDEO_NEW_CALL, P_VIDEO_NEW_PORT, + P_VIDEO_NEW_CALL, P_VIDEO_NEW_PORT, P_STALE_END, P_DECLINE, /* B: call_decline */ P_WAIT_ENDED2, /* обе ENDED + A DECLINED */ + P_TIMEOUT_START, P_TIMEOUT_INCOMING, P_TIMEOUT_ACTIVE, P_TIMEOUT_ENDED, P_DONE, }; -struct call_ctx { +struct call_test_ctx { struct UASYNC* ua; struct UTUN_INSTANCE* inst[2]; enum call_phase phase; @@ -101,6 +104,21 @@ struct call_evt { }; static struct call_evt g_ev[2]; /* [A], [B] */ +static etcp_recv_fn g_signal_recv[2]; +static int g_drop_acks[2], g_dropped_acks[2]; + +/* Имитируем потерю именно END_ACK, сохраняя доставку нового вызова и транспортных ACK. */ +static void signal_recv_loss(struct ETCP_CONN* conn, struct ll_entry* entry) { + int idx = conn->instance->test_user_ptr == &g_ev[IDX_A] ? IDX_A : IDX_B; + if (entry && entry->dgram && entry->len > ROUTER_SVC_PAYLOAD_OFF + && entry->dgram[ROUTER_SVC_PAYLOAD_OFF] == CALL_SUBCMD_END_ACK && g_drop_acks[idx]) { + if (g_drop_acks[idx] > 0) --g_drop_acks[idx]; + ++g_dropped_acks[idx]; + DEBUG_INFO(DEBUG_CATEGORY_CALL, "test drops END_ACK node=%d count=%d", idx, g_dropped_acks[idx]); + queue_dgram_free(entry); queue_entry_free(entry); return; + } + g_signal_recv[idx](conn, entry); +} /* записи принятых медиа-кадров (media_recv_cb) */ struct media_evt { @@ -322,7 +340,7 @@ static void call_evt_handler(struct UTUN_INSTANCE* inst, int type, const uint8_t if (len >= 8) memcpy(&ev->accepted_call_id, data, 8); break; case CHAT_EVT_CALL_ENDED: - ev->ended = 1; + ev->ended++; if (len >= 9) { memcpy(&ev->ended_call_id, data, 8); ev->ended_reason = data[8]; } break; case CHAT_EVT_CALL_DECLINED: @@ -436,7 +454,7 @@ static int test_pm_route(struct UTUN_INSTANCE* inst, uint64_t peer, char target[ /* ── master state-machine ── */ static void call_tick(void* arg) { - struct call_ctx* t = arg; + struct call_test_ctx* t = arg; t->timer = NULL; if (t->result) return; @@ -700,7 +718,12 @@ static void call_tick(void* arg) { } call_video_port_request_key(t->call_id); t->previous_call_id = t->call_id; + g_drop_acks[IDX_B] = 2; call_hangup(B, t->call_id); + struct call_session* ended = call_session_find(B->call, t->call_id); + if (!ended || ended->tx_q || ended->video || !ended->end_subcmd || !ended->teardown_timer) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "ended call retained media or lost final command"); t->result = 2; break; + } t->phase = P_VIDEO_PORT_CANCELLED; break; } case P_VIDEO_PORT_CANCELLED: { @@ -723,7 +746,8 @@ static void call_tick(void* arg) { } case P_WAIT_ENDED1: if (ea->ended && eb->ended && ea->ended_call_id == t->call_id) { - if (ea->ended_reason != CALL_REASON_REMOTE_HANGUP || eb->ended_reason != CALL_REASON_LOCAL_HANGUP) { + if (ea->ended != 1 || eb->ended != 1 || ea->ended_reason != CALL_REASON_REMOTE_HANGUP + || eb->ended_reason != CALL_REASON_LOCAL_HANGUP) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video teardown: HANGUP delivery failed caller_reason=%u callee_reason=%u", ea->ended_reason, eb->ended_reason); t->result = 2; break; @@ -762,20 +786,72 @@ static void call_tick(void* arg) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "late close affected the new video port"); t->result = 2; break; } if (!status.have_peer) break; + // Новый звонок работает, пока старый повторяет HANGUP после двух потерянных END_ACK. + if (call_session_find(A->call, t->previous_call_id) || call_session_find(B->call, t->previous_call_id)) break; + if (g_dropped_acks[IDX_B] != 2 || ea->ended || eb->ended) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "lost END_ACK affected the new call"); t->result = 2; break; + } + struct call_pkt_hangup stale = {CALL_SUBCMD_HANGUP, t->previous_call_id, CALL_REASON_LOCAL_HANGUP}; + struct ETCP_ROUTER_CONN* rc = etcp_router_conn_get(B, strtoull(CH_ID, NULL, 10), na, ETCP_RT_ID_CALL); + if (!rc || etcp_router_conn_send(rc, (const uint8_t*)&stale, sizeof(stale), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "cannot send stale HANGUP"); t->result = 2; break; + } + t->phase_ticks = 0; t->phase = P_STALE_END; break; + } + case P_STALE_END: { + if (++t->phase_ticks < 30) break; + struct call_video_port_status status; + if (ea->ended || eb->ended || call_video_port_status(t->call_id, &status) || status.error) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "stale HANGUP affected the new call"); t->result = 2; break; + } call_video_port_request_key(t->call_id); call_video_port_close(t->call_id); call_video_port_detach(B); // Отменяет уже поставленный post до destroy ядра. if (call_video_port_status(t->call_id, &status) != -1) { t->result = 2; break; } DEBUG_INFO(DEBUG_CATEGORY_CALL, "video teardown regression: new call survives stale close; detach cancels post"); t->video_restart_tested = 1; - call_hangup(A, t->call_id); t->phase = P_WAIT_ENDED2; break; + call_hangup(A, t->call_id); call_hangup(B, t->call_id); + t->phase = P_WAIT_ENDED2; break; } case P_DECLINE: call_decline(B, t->call_id); t->phase = P_WAIT_ENDED2; break; case P_WAIT_ENDED2: - if (ea->ended && eb->ended) t->phase = ea->declined ? P_DONE : P_CALL2_START; + if (!ea->ended || !eb->ended || call_session_find(A->call, t->call_id) || call_session_find(B->call, t->call_id)) break; + if (ea->ended != 1 || eb->ended != 1 + || ea->ended_reason != (ea->declined ? CALL_REASON_DECLINE : CALL_REASON_LOCAL_HANGUP) + || eb->ended_reason != ea->ended_reason) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "duplicate end or wrong reason after simultaneous hangup/decline"); t->result = 2; break; + } + t->phase = ea->declined ? P_TIMEOUT_START : P_CALL2_START; + break; + case P_TIMEOUT_START: + memset(ea, 0, sizeof(*ea)); memset(eb, 0, sizeof(*eb)); + call_start(A, CH_ID, nb, ++t->call_id); + t->phase = P_TIMEOUT_INCOMING; + break; + case P_TIMEOUT_INCOMING: + if (!eb->incoming) break; + call_accept(B, t->call_id); t->phase = P_TIMEOUT_ACTIVE; + break; + case P_TIMEOUT_ACTIVE: { + if (!ea->accepted || !eb->accepted) break; + g_drop_acks[IDX_A] = -1; + call_hangup(A, t->call_id); + struct call_session* s = call_session_find(A->call, t->call_id); + if (!s || !s->end_subcmd) { t->result = 2; break; } + // Сдвигаем только начало ожидания: проверяем deadline без двадцатисекундного sleep. + s->end_started_tb -= CALL_END_TIMEOUT_TB; + t->phase = P_TIMEOUT_ENDED; break; + } + case P_TIMEOUT_ENDED: + if (call_session_find(A->call, t->call_id) || call_session_find(B->call, t->call_id)) break; + if (ea->ended != 1 || eb->ended != 1 || !g_dropped_acks[IDX_A]) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "unconfirmed end did not release sessions cleanly"); t->result = 2; break; + } + DEBUG_INFO(DEBUG_CATEGORY_CALL, "call end regression: epoch reset, ACK loss, stale end, simultaneous end and deadline passed"); + t->phase = P_DONE; break; case P_DONE: t->result = 1; @@ -831,7 +907,7 @@ int main(int argc, char** argv) { struct UASYNC* ua = uasync_create(); if (!ua) { FAIL("uasync_create failed"); return 1; } - struct call_ctx t; + struct call_test_ctx t; memset(&t, 0, sizeof(t)); t.ua = ua; t.phase = P_WAIT_CONN; @@ -853,6 +929,11 @@ int main(int argc, char** argv) { t.inst[IDX_A]->test_user_ptr = &g_ev[IDX_A]; t.inst[IDX_B]->test_user_ptr = &g_ev[IDX_B]; + for (int i = 0; i < 2; ++i) { + g_signal_recv[i] = t.inst[i]->router_bindings.callbacks[ETCP_RT_ID_CALL]; + etcp_router_unbind(t.inst[i], ETCP_RT_ID_CALL); + etcp_router_bind(t.inst[i], ETCP_RT_ID_CALL, signal_recv_loss); + } chat_event_set_handler(t.inst[IDX_A], call_evt_handler); chat_event_set_handler(t.inst[IDX_B], call_evt_handler); call_set_media_recv_cb(t.inst[IDX_A], media_recv_cb, &g_media[IDX_A]);