chat: invite-ссылки с выбором достижимого узла + tcp_io фикс half-close FIN

- Общий модуль invite_build: сборка utun://-ссылок, авто-выбор лучшего
  достижимого узла, список кандидатов (вынесено из android jni_bridge)
- chatgui: диалог invite с выбором узла (комбобокс), события INVITE_CANDIDATES /
  INVITE_LINK_READY через gui_bridge
- headless/chatcli: invite <ch_id> [node_id], команда invite_nodes
- tcp_io: error_cb отличает грациозный EPOLLHUP (SO_ERROR==0) от реальной ошибки —
  при приостановленном чтении FIN не роняет недослейные данные (фикс flaky half-close)
- run_test.sh: half-close echo на выделенном порту 19190 + отдельный DNAT (был недостижим)
This commit is contained in:
evgeny committed 2026-09-07 13:59:15 +03:00
1 parent a17a85a383
commit 1345f44e10
23 files changed
+872 -539

No files matched your search

@@ -46,6 +46,8 @@ static gui_member_updated_fn g_member_updated_cb = nullptr;
static gui_member_removed_fn g_member_removed_cb = nullptr;
static gui_bgp_info_fn g_bgp_info_cb = nullptr;
static gui_admin_key_received_fn g_admin_key_received_cb = nullptr;
static gui_invite_link_ready_fn g_invite_link_ready_cb = nullptr;
static gui_invite_candidates_fn g_invite_candidates_cb = nullptr;
static struct UASYNC* g_ua = nullptr;
/* ── GuiBridgeReceiver implementation ── */
@@ -210,6 +212,29 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) {
}
}
break;
case GUI_EVT_INVITE_LINK_READY:
if (dlen >= 3) {
uint8_t chLen = d[0];
uint16_t linkLen = 0; memcpy(&linkLen, d + 1 + chLen, 2);
if (dlen >= 1 + chLen + 2 + linkLen) {
if (linkLen == 0) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: INVITE_LINK_READY ch=%.*s — empty link (build failed)",
chLen, (const char*)d + 1);
if (g_invite_link_ready_cb) g_invite_link_ready_cb((const char*)d + 1, chLen, "");
} else {
if (g_invite_link_ready_cb) g_invite_link_ready_cb((const char*)d + 1, chLen, (const char*)(d + 1 + chLen + 2));
}
}
}
break;
case GUI_EVT_INVITE_CANDIDATES:
if (dlen >= 2) {
uint8_t chLen = d[0];
if (dlen >= 1 + chLen + 10) {
if (g_invite_candidates_cb) g_invite_candidates_cb((const char*)d + 1, chLen, d + 1 + chLen, dlen - 1 - chLen);
}
}
break;
default:
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: unknown event type %d", eventType);
break;
@@ -335,6 +360,14 @@ void gui_bridge_set_admin_key_received_cb(gui_admin_key_received_fn cb) {
g_admin_key_received_cb = cb;
}
void gui_bridge_set_invite_link_ready_cb(gui_invite_link_ready_fn cb) {
g_invite_link_ready_cb = cb;
}
void gui_bridge_set_invite_candidates_cb(gui_invite_candidates_fn cb) {
g_invite_candidates_cb = cb;
}
} /* extern "C" */
/* ── uTun nodeinfo callback (called from uasync thread) ── */