From 1b695fe247cdf4871e6de20a44e56fa15df3dce4 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 02:25:05 +0100 Subject: [PATCH] Own and cancel desktop bridge commands before core teardown --- src/chat/chat_event.h | 6 +- tools/chatgui/CMakeLists.txt | 4 + tools/chatgui/src/connmonitorwindow.cpp | 10 +- tools/chatgui/tests/test_message_events.cpp | 144 +++++++++++++ tools/chatgui/transport/gui_bridge.h | 132 +++++++----- tools/chatgui/transport/gui_bridge_impl.cpp | 227 +++++++++++++------- 6 files changed, 389 insertions(+), 134 deletions(-) diff --git a/src/chat/chat_event.h b/src/chat/chat_event.h index 33b8106e..ecd5be5e 100644 --- a/src/chat/chat_event.h +++ b/src/chat/chat_event.h @@ -54,10 +54,10 @@ extern "C" { #define CHAT_EVT_INVITE_CANDIDATES 30 /* [ch_id_len:1][ch_id:var][auto_node_id:8][count:2][node_id:8]* — достижимые узлы для invite */ #define CHAT_EVT_CHANNEL_DELETED 31 /* [ch_id_len:1][ch_id:var] — канал удалён локально */ #define CHAT_EVT_DM_MSG_RECEIVED 32 /* [conv_id_len:1][conv_id:var][author_node_id:8] */ -#define CHAT_EVT_DM_CONV_UPDATED 33 /* [conv_id_len:1][conv_id:var][generation:8LE] — беседа создана/изменена */ +#define CHAT_EVT_DM_CONV_UPDATED 33 /* [conv_id_len:1][conv_id:var][generation:8 native] — беседа создана/изменена */ #define CHAT_EVT_CALL_INCOMING 34 /* [ch_id_len:1][ch_id:var][caller_node_id:8][call_id:8] */ -#define CHAT_EVT_CALL_RINGING 35 /* [call_id:8] */ -#define CHAT_EVT_CALL_ACCEPTED 36 /* [call_id:8] */ +#define CHAT_EVT_CALL_RINGING 35 /* [call_id:8][reason:1=0], native */ +#define CHAT_EVT_CALL_ACCEPTED 36 /* [call_id:8][reason:1=0], native */ #define CHAT_EVT_CALL_ENDED 37 /* [call_id:8][reason:1] */ #define CHAT_EVT_CALL_DECLINED 38 /* [call_id:8][reason:1] */ #define CHAT_EVT_CALL_STATS 39 /* [call_id:8][rtt:2][buffer:2][tempo:2][dropped:2][underruns:2][min:2][max:2][reserve:2] */ diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index e45d82a9..172b0001 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -494,6 +494,10 @@ set_tests_properties(test_message_notification PROPERTIES ENVIRONMENT "QT_QPA_PL add_executable(test_message_events tests/test_message_events.cpp transport/gui_bridge_impl.cpp) target_link_libraries(test_message_events PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun utun_voice pthread ${CMAKE_DL_LIBS}) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_compile_definitions(test_message_events PRIVATE UTUN_TEST_BRIDGE_ALLOC_FAILURES) + target_link_options(test_message_events PRIVATE "-Wl,--wrap=u_calloc_impl") +endif() add_test(NAME test_message_events COMMAND test_message_events) set_tests_properties(test_message_events PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 15) diff --git a/tools/chatgui/src/connmonitorwindow.cpp b/tools/chatgui/src/connmonitorwindow.cpp index 5fd87a31..62c5efb0 100644 --- a/tools/chatgui/src/connmonitorwindow.cpp +++ b/tools/chatgui/src/connmonitorwindow.cpp @@ -6,6 +6,7 @@ extern "C" { #include "chat/chat_core.h" #include "../../lib/mem.h" +#include "../../../lib/debug_config.h" } #include @@ -104,6 +105,7 @@ void ConnMonitorWindow::hideEvent(QHideEvent* event) { void ConnMonitorWindow::onTimerTick() { if (m_details->textCursor().hasSelection()) return; if (QApplication::mouseButtons()) return; + if (!gui_bridge_is_uasync_ready() || !gui_bridge_get_inst()) return; gui_bridge_post_uasync_fn(chat_core_collect_conn_list_trampoline, gui_bridge_get_inst()); if (m_selectedNodeId) requestMetricsForSelected(); } @@ -116,11 +118,13 @@ void ConnMonitorWindow::onConnSelected(QListWidgetItem* current, QListWidgetItem } void ConnMonitorWindow::requestMetricsForSelected() { + auto* inst = gui_bridge_get_inst(); + if (!inst || !m_selectedNodeId || !gui_bridge_is_uasync_ready()) return; struct chat_node_arg* arg = (struct chat_node_arg*)u_malloc(sizeof(struct chat_node_arg)); - if (!arg) return; - arg->inst = gui_bridge_get_inst(); + if (!arg) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "connection monitor: metrics request allocation failed"); return; } + arg->inst = inst; arg->node_id = m_selectedNodeId; - gui_bridge_post_uasync_fn(chat_core_collect_conn_metrics_trampoline, arg); + if (gui_bridge_post_uasync_owned_fn(chat_core_collect_conn_metrics_trampoline, arg) < 0) u_free(arg); } void ConnMonitorWindow::onConnList(const uint8_t* data, int len) { diff --git a/tools/chatgui/tests/test_message_events.cpp b/tools/chatgui/tests/test_message_events.cpp index 051c3611..e1371e87 100644 --- a/tools/chatgui/tests/test_message_events.cpp +++ b/tools/chatgui/tests/test_message_events.cpp @@ -2,9 +2,25 @@ #include "../transport/gui_bridge.h" #include "../../../lib/debug_config.h" #include "../../../src/chat/chat_event.h" +#include "../../../src/chat/chat_core.h" +#include "../../../src/utun_instance.h" +#include "../../../lib/u_async.h" +#include "../../../lib/mem.h" #include #include #include +#include + +#ifdef UTUN_TEST_BRIDGE_ALLOC_FAILURES +static int failAllocation; +extern "C" void* __real_u_calloc_impl(uint32_t, uint32_t, const char*); +extern "C" void* __wrap_u_calloc_impl(uint32_t count, uint32_t size, const char* where) { + if (failAllocation && strstr(where, "gui_bridge_impl.cpp") && --failAllocation == 0) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "[INJECT] GUI bridge allocation failure at %s", where); return nullptr; + } + return __real_u_calloc_impl(count, size, where); +} +#endif struct ReceivedMessage { QString channel; @@ -17,6 +33,20 @@ static QList cpuLoads; static bool cpuOnGuiThread = true; static QStringList changedChannels; static QList edits; +static int readyEvents; + +struct BoundCore { + UASYNC* ua = uasync_create(); + UTUN_INSTANCE instance{}; + ~BoundCore() { + gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr); + if (ua) { uasync_mark_stopped(ua); uasync_destroy(ua, 0); } + } + void attach() { + instance.ua = ua; uasync_mark_running(ua); + gui_bridge_set_uasync(ua); gui_bridge_set_inst(&instance); + } +}; static void cpu_received(int percent) { cpuLoads.append(percent); @@ -134,6 +164,120 @@ private slots: QCOMPARE(changedChannels.size(), 1); gui_bridge_set_channel_updated_cb(nullptr); } + + void shutdownCancelsOwnedCommandsAndKeepsBorrowedArguments() { + const size_t before = u_get_allocated_count(); + { + BoundCore core; QVERIFY(core.ua); core.attach(); + const size_t baseline = u_get_allocated_count(); + auto* req = static_cast(u_calloc(1, sizeof(chat_msg_submit))); QVERIFY(req); + QCOMPARE(gui_bridge_submit_message(req), 0); + QCOMPARE(gui_bridge_delete_dm("123", 1, 1), 0); + QCOMPARE(gui_bridge_toggle_member_radio_mute("123", 7), 0); + QCOMPARE(gui_bridge_request_cpu_load(), 1); + int calls = 0; + gui_bridge_post_uasync_fn([](void* p) { ++*static_cast(p); }, &calls); + gui_bridge_set_inst(nullptr); + QVERIFY(!gui_bridge_is_uasync_ready()); + uasync_poll(core.ua, 0); + QCOMPARE(calls, 0); + QCOMPARE(u_get_allocated_count(), baseline); + } + QCOMPARE(u_get_allocated_count(), before); + } + + void invalidPostArgumentsDoNotTurnIntoEmptyEvents() { + readyEvents = 0; + gui_bridge_set_db_ready_cb([] { readyEvents++; }); + gui_bridge_post(GUI_EVT_DB_READY, nullptr, -1); + gui_bridge_post(GUI_EVT_DB_READY, nullptr, 10); + QCoreApplication::processEvents(); + gui_bridge_set_db_ready_cb(nullptr); + QCOMPARE(readyEvents, 0); + } + + void repeatedInitKeepsQueuedEventsAndJoinRequiresOutput() { + post_message(message_event(42, false)); + gui_bridge_init(); + QTRY_COMPARE(received.size(), 1); QCOMPARE(received[0].author, quint64(42)); + QVERIFY(gui_bridge_join_link("", 0, nullptr) < 0); + QVERIFY(gui_bridge_offer_link(1, "", 0, nullptr) < 0); + uint64_t request = 42; + QVERIFY(gui_bridge_join_link("", 0, &request) < 0); QCOMPARE(request, uint64_t(42)); + QVERIFY(gui_bridge_offer_link(1, "", 0, &request) < 0); QCOMPARE(request, uint64_t(42)); + } + + void ownedPostsDeliverOrCancelAndRejectedArgumentsStayWithCaller() { + const size_t before = u_get_allocated_count(); + { + BoundCore core; QVERIFY(core.ua); core.attach(); + const size_t baseline = u_get_allocated_count(); + auto* value = static_cast(u_malloc(sizeof(int))); QVERIFY(value); *value = 7; + QCOMPARE(gui_bridge_post_uasync_owned_fn([](void* p) { + QCOMPARE(*static_cast(p), 7); u_free(p); + }, value), 0); + uasync_poll(core.ua, 0); QCOMPARE(u_get_allocated_count(), baseline); + value = static_cast(u_malloc(sizeof(int))); QVERIFY(value); + QCOMPARE(gui_bridge_post_uasync_owned_fn([](void*) { QFAIL("cancelled command executed"); }, value), 0); + gui_bridge_set_inst(nullptr); + QCOMPARE(u_get_allocated_count(), baseline); + value = static_cast(u_malloc(sizeof(int))); QVERIFY(value); + QVERIFY(gui_bridge_post_uasync_owned_fn([](void*) {}, value) < 0); + QCOMPARE(u_get_allocated_count(), baseline + 1); u_free(value); + core.attach(); + QCOMPARE(gui_bridge_request_cpu_load(), 1); // Отмена прежнего запуска не повреждает очередь следующего. + } + QCOMPARE(u_get_allocated_count(), before); + } + + void concurrentProducerCanOutliveCoreDetachAndUasyncDestroy() { + const size_t before = u_get_allocated_count(); + BoundCore core; QVERIFY(core.ua); core.attach(); + const auto general = g_debug_config.category_levels[DEBUG_CATEGORY_GENERAL]; + const auto sys = g_debug_config.category_levels[DEBUG_CATEGORY_SYS]; + debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_DISABLED); + debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_WARN); + std::promise first; + auto queued = first.get_future(); + unsigned accepted = 0, rejected = 0; + bool allocated = true; + std::thread producer([&] { + for (unsigned i = 0; i < 128; i++) { + void* arg = u_malloc(1); + if (!arg) { allocated = false; if (!i) first.set_value(); break; } + if (gui_bridge_post_uasync_owned_fn([](void* p) { u_free(p); }, arg) == 0) accepted++; + else { rejected++; u_free(arg); } + if (!i) first.set_value(); + } + }); + queued.wait(); + gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr); + uasync_mark_stopped(core.ua); uasync_destroy(core.ua, 0); core.ua = nullptr; + producer.join(); + debug_set_category_level(DEBUG_CATEGORY_GENERAL, general); + debug_set_category_level(DEBUG_CATEGORY_SYS, sys); + QVERIFY(allocated); QCOMPARE(accepted + rejected, 128u); + QCOMPARE(u_get_allocated_count(), before); + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "[PASS] bridge detach race: accepted=%u rejected=%u, producer joined, no leaks", accepted, rejected); + } + +#ifdef UTUN_TEST_BRIDGE_ALLOC_FAILURES + void allocationFailuresKeepOwnershipAndReleasePartialRequests() { + BoundCore core; QVERIFY(core.ua); core.attach(); + const size_t baseline = u_get_allocated_count(); + auto* req = static_cast(u_calloc(1, sizeof(chat_msg_submit))); QVERIFY(req); + failAllocation = 1; QVERIFY(gui_bridge_submit_message(req) < 0); QCOMPARE(failAllocation, 0); + QCOMPARE(u_get_allocated_count(), baseline + 1); u_free(req); + for (int point : {1, 2}) { + failAllocation = point; QVERIFY(gui_bridge_delete_dm("123", 1, 1) < 0); QCOMPARE(failAllocation, 0); + QCOMPARE(u_get_allocated_count(), baseline); + failAllocation = point; QVERIFY(gui_bridge_toggle_member_radio_mute("123", 7) < 0); QCOMPARE(failAllocation, 0); + QCOMPARE(u_get_allocated_count(), baseline); + } + failAllocation = 1; QCOMPARE(gui_bridge_request_cpu_load(), 0); QCOMPARE(failAllocation, 0); + QCOMPARE(u_get_allocated_count(), baseline); + } +#endif }; QTEST_MAIN(MessageEventTest) diff --git a/tools/chatgui/transport/gui_bridge.h b/tools/chatgui/transport/gui_bridge.h index bdd4034c..c09256f1 100644 --- a/tools/chatgui/transport/gui_bridge.h +++ b/tools/chatgui/transport/gui_bridge.h @@ -1,7 +1,11 @@ -/* Мост desktop GUI ↔ ядро: post копирует бинарное событие в Qt-очередь, callbacks работают в GUI. - * post_uasync_fn передаёт функцию и arg без копирования в сетевой поток; arg освобождает обработчик. - * Регистрация callbacks и UI-состояние принадлежат GUI. Полученный inst заимствован; - * чтение указателя не гарантирует время жизни объекта при stop/finalize. */ +/* Мост одного desktop GUI ↔ ядра: post копирует бинарное событие в Qt-очередь, callbacks работают в GUI. + * Инициализация/регистрация callbacks принадлежат GUI; NULL снимает callback. Данные callbacks + * заимствованы на время вызова, строки не обязательно NUL-terminated, если передана длина. + * post_uasync_fn передаёт заимствованный arg без копирования; owned_fn принимает отдельный u_malloc-блок. + * Перед destroy ядра его поток вызывает set_inst(NULL) вне poll/callbacks: закрывает приём команд, + * отменяет ожидающие post и освобождает переданные мосту аргументы. Затем можно снять и уничтожить uasync. + * Полученный inst заимствован: само чтение указателя не гарантирует время жизни при stop/finalize. + * Сырые callbacks (data,len) не заменяют проверку формата в получателе соответствующего codec. */ #ifndef GUI_BRIDGE_H #define GUI_BRIDGE_H @@ -12,6 +16,7 @@ extern "C" { #include #include #include "../../../src/chat/chat_member.h" +#include "../../../src/chat/chat_event.h" struct UASYNC; struct TOPO_GROUP; @@ -20,73 +25,84 @@ struct UTUN_INSTANCE; /* ── Типы уведомлений uasync→GUI (fire-and-forget) ── */ -#define GUI_EVT_MSG_RECEIVED 1 /* data: [ch_id_len:1][ch_id:var][author_node_id:8][initial_sync:1] */ -#define GUI_EVT_CONNECT_RESULT 2 /* data: [node_id:8][result:4][channel_id:8] */ -#define GUI_EVT_NEW_PEER 3 /* data: [node_id:8] */ -#define GUI_EVT_CHANNEL_UPDATED 4 /* data: [ch_id_len:1][ch_id:var] */ -#define GUI_EVT_MEMBERS_CHANGED 5 /* data: [ch_id_len:1][ch_id:var] */ -#define GUI_EVT_MY_NODE_ID 6 /* data: [node_id:8] */ -#define GUI_EVT_AUTO_CONNECT_STATUS 7 /* data: [status:1][total_tried:2][node_count:2][success:2] */ -#define GUI_EVT_CHANNEL_PEERS_ONLINE 8 /* data: [ch_id_len:1][ch_id:var][online_count:2] */ -#define GUI_EVT_DB_READY 9 /* data: none — DB is open, tables created */ -#define GUI_EVT_STATUS_REFRESH 10 /* data: status text (null-terminated string) */ -#define GUI_EVT_NODE_CHANGED 11 /* data: [node_id:8] */ -#define GUI_EVT_ATTACHMENT_DOWNLOADED 14 /* data: [ch_id_len:1][ch_id:var][msg_id:8] */ -#define GUI_EVT_DOWNLOAD_PROGRESS 15 /* data: [ch_id_len:1][ch_id:var][msg_id:8][blocks_done:4][num_blocks:4] */ -#define GUI_EVT_NODEINFO_UPDATE 16 /* Сигнал изменения маршрутов для PM presence; без live-полей карточки. */ -#define GUI_EVT_CONN_LIST 20 /* data: [count:2][entry:46B]* — binary conn list */ -#define GUI_EVT_CONN_METRICS 21 /* data: text dump of selected connection metrics */ -#define GUI_EVT_MEMBER_DETAIL 22 /* Адресованный ответ диагностики; codec в chat_member.h. */ -#define GUI_EVT_MEMBER_LIST 23 /* data: [ch_id_len:1][ch_id:var][count:2][member:CHAT_MEMBER_DISPLAY_SIZE]* */ -#define GUI_EVT_MEMBER_UPDATED 24 /* data: [ch_id_len:1][ch_id:var][count:1][member:CHAT_MEMBER_DISPLAY_SIZE] */ -#define GUI_EVT_MEMBER_REMOVED 25 /* data: [ch_id_len:1][ch_id:var][node_id:8] — мембер удалён из канала */ -#define GUI_EVT_ADMIN_KEY_RECEIVED 28 /* data: [ch_id_len:1][ch_id:var][from_node_id:8] — получен канальный ключ */ -#define GUI_EVT_INVITE_LINK_READY 29 /* data: [ch_id_len:1][ch_id:var][request_id:8][link_len:2][link:var] — пустой link = ошибка */ -#define GUI_EVT_INVITE_CANDIDATES 30 /* data: [ch_id_len:1][ch_id:var][auto_node_id:8][count:2][node_id:8]* — достижимые узлы для invite */ -#define GUI_EVT_DM_MSG_RECEIVED 32 /* data: [conv_id_len:1][conv_id:var][author_node_id:8] */ -#define GUI_EVT_DM_CONV_UPDATED 33 /* data: [conv_id_len:1][conv_id:var][generation:8] — беседа создана/изменена */ -#define GUI_EVT_CALL_INCOMING 34 /* data: [ch_id_len:1][ch_id:var][caller_node_id:8][call_id:8] */ -#define GUI_EVT_CALL_RINGING 35 /* data: [call_id:8] */ -#define GUI_EVT_CALL_ACCEPTED 36 /* data: [call_id:8] */ -#define GUI_EVT_CALL_ENDED 37 /* data: [call_id:8][reason:1] */ -#define GUI_EVT_CALL_DECLINED 38 /* data: [call_id:8][reason:1] */ -#define GUI_EVT_CALL_STATS 39 /* data: [call_id:8][rtt:2][buffer:2][tempo:2][dropped:2][underruns:2][min:2][max:2][reserve:2] */ -#define GUI_EVT_CALL_ERROR 40 /* data: [call_id:8][err:1][text:var] */ -#define GUI_EVT_DM_MESSAGES 41 /* data: [conv_id_len:1][conv_id:var][generation:8][JSON:utf8] */ -#define GUI_EVT_CALL_PATH 42 /* data: [call_id:8][text:var] — BGP-маршрут до пира звонка */ -#define GUI_EVT_RADIO_TALK 43 /* data: [group_id:8][src_node_id:8][stream_id:2][on:1] — кто-то начал/закончил говорить */ -#define GUI_EVT_CALL_CONNECTION 44 /* data: [call_id:8][CONN_TYPE:1][next_hop_node_id:8] */ +/* Коды/payload определяет chat_event.h; GUI-алиасы не дублируют числовую таблицу ядра. */ +#define GUI_EVT_MSG_RECEIVED CHAT_EVT_MSG_RECEIVED +#define GUI_EVT_CONNECT_RESULT CHAT_EVT_CONNECT_RESULT +#define GUI_EVT_NEW_PEER CHAT_EVT_NEW_PEER +#define GUI_EVT_CHANNEL_UPDATED CHAT_EVT_CHANNEL_UPDATED +#define GUI_EVT_MEMBERS_CHANGED CHAT_EVT_MEMBERS_CHANGED +#define GUI_EVT_MY_NODE_ID CHAT_EVT_MY_NODE_ID +#define GUI_EVT_AUTO_CONNECT_STATUS CHAT_EVT_AUTO_CONNECT_STATUS +#define GUI_EVT_CHANNEL_PEERS_ONLINE CHAT_EVT_CHANNEL_PEERS_ONLINE +#define GUI_EVT_DB_READY CHAT_EVT_DB_READY +#define GUI_EVT_STATUS_REFRESH CHAT_EVT_STATUS_REFRESH +#define GUI_EVT_KEYS_GENERATED CHAT_EVT_KEYS_GENERATED +#define GUI_EVT_ATTACHMENT_DOWNLOADED CHAT_EVT_ATTACHMENT_DOWNLOADED +#define GUI_EVT_DOWNLOAD_PROGRESS CHAT_EVT_DOWNLOAD_PROGRESS +#define GUI_EVT_NODEINFO_UPDATE CHAT_EVT_NODEINFO_UPDATED +#define GUI_EVT_CONN_LIST CHAT_EVT_CONN_LIST +#define GUI_EVT_CONN_METRICS CHAT_EVT_CONN_METRICS +#define GUI_EVT_MEMBER_DETAIL CHAT_EVT_MEMBER_DETAIL +#define GUI_EVT_MEMBER_LIST CHAT_EVT_MEMBER_LIST +#define GUI_EVT_MEMBER_UPDATED CHAT_EVT_MEMBER_UPDATED +#define GUI_EVT_MEMBER_REMOVED CHAT_EVT_MEMBER_REMOVED +#define GUI_EVT_ADMIN_KEY_RECEIVED CHAT_EVT_ADMIN_KEY_RECEIVED +#define GUI_EVT_INVITE_LINK_READY CHAT_EVT_INVITE_LINK_READY +#define GUI_EVT_INVITE_CANDIDATES CHAT_EVT_INVITE_CANDIDATES +#define GUI_EVT_DM_MSG_RECEIVED CHAT_EVT_DM_MSG_RECEIVED +#define GUI_EVT_DM_CONV_UPDATED CHAT_EVT_DM_CONV_UPDATED +#define GUI_EVT_CALL_INCOMING CHAT_EVT_CALL_INCOMING +#define GUI_EVT_CALL_RINGING CHAT_EVT_CALL_RINGING +#define GUI_EVT_CALL_ACCEPTED CHAT_EVT_CALL_ACCEPTED +#define GUI_EVT_CALL_ENDED CHAT_EVT_CALL_ENDED +#define GUI_EVT_CALL_DECLINED CHAT_EVT_CALL_DECLINED +#define GUI_EVT_CALL_STATS CHAT_EVT_CALL_STATS +#define GUI_EVT_CALL_ERROR CHAT_EVT_CALL_ERROR +#define GUI_EVT_DM_MESSAGES CHAT_EVT_DM_MESSAGES +#define GUI_EVT_CALL_PATH CHAT_EVT_CALL_PATH +#define GUI_EVT_RADIO_TALK CHAT_EVT_RADIO_TALK +#define GUI_EVT_CALL_CONNECTION CHAT_EVT_CALL_CONNECTION /* ── API ── */ -/* Инициализация моста (вызывается из GUI при старте) */ +/* Идемпотентная инициализация из GUI после создания QCoreApplication, до запуска worker. Она владеет приёмником; + * все producers должны завершиться до её уничтожения. Повторный init сохраняет очередь и callbacks. */ void gui_bridge_init(void); -/* Установить uasync (вызывается из UtunNode после создания ua) */ +/* Установить uasync перед публикацией instance; снять после set_inst(NULL) и завершения worker. + * Замена uasync при опубликованном instance отклоняется с ERROR. uasync мосту не принадлежит. */ void gui_bridge_set_uasync(struct UASYNC* ua); -/* Установить UTUN_INSTANCE (вызывается из UtunNode после создания instance). - Читается GUI-потоком через gui_bridge_get_inst() для сборки trampoline-аргументов. */ +/* Опубликовать готовый instance из uasync после set_uasync. NULL снимается в uasync до destroy, + * вне callbacks: синхронно отменяет ожидающие команды и освобождает аргументы owned-запросов. + * Начатый callback завершается до остановки poll; отмена ещё не начатой команды не посылает результат в GUI. */ void gui_bridge_set_inst(struct UTUN_INSTANCE* inst); /* Текущий заимствованный UTUN_INSTANCE (NULL пока не создан/после сброса моста). */ struct UTUN_INSTANCE* gui_bridge_get_inst(void); -/* uasync → GUI: уведомление (fire-and-forget, через Qt::QueuedConnection) */ +/* uasync → GUI: синхронно скопировать data_len байт и поставить событие в Qt::QueuedConnection. + * len>=0, при len>0 data обязателен; (NULL,0) — пустое событие. Ошибки аргументов/доставки логируются. */ void gui_bridge_post(int event_type, const uint8_t* data, int data_len); -/* GUI → uasync: передать fn(arg). Проверить готовность заранее: без ua запрос отбрасывается, - * функция не вызывается и arg не освобождается мостом. */ +/* GUI → uasync: передать fn(arg) без владения arg. Нужны опубликованные ua/instance. + * При отказе/остановке fn не вызывается, arg остаётся владельцу; ошибка постановки логируется. + * Владелец обеспечивает жизнь arg до callback/отмены. Для malloc-команд использовать owned_fn. */ void gui_bridge_post_uasync_fn(void (*fn)(void*), void* arg); -/* Зарезервировать доставку composer-запроса. 0 — владение req перешло ядру; -1 — req остаётся вызывающему. */ +/* Передать отдельный u_malloc/u_calloc-блок arg: 0 — мост принял владение, callback обязан освободить arg; + * отмена до callback освобождает его мостом. -1 — arg остаётся вызывающему. Вложенные ресурсы не освобождаются. */ +int gui_bridge_post_uasync_owned_fn(void (*fn)(void*), void* arg); +/* Composer-запрос: 0 — владение req передано, callback/отмена освободят блок; -1 — req остаётся вызывающему. */ struct chat_msg_submit; int gui_bridge_submit_message(struct chat_msg_submit* req); -/* Проверить готовность uasync (g_ua != NULL) */ +/* Проверить наличие опубликованных uasync и instance. Только подсказка готовности, не резервация их lifetime. */ int gui_bridge_is_uasync_ready(void); /* Callback для результатов подключения (вызывается из GUI-потока) */ typedef void (*gui_connect_result_fn)(uint64_t node_id, uint64_t channel_id, int result, uint64_t request); +/* Скопировать invite-ссылку в JOIN port. request обязателен: при 0 записан ID будущего CONNECT_RESULT; + * <0 — CHAT_CONNECT_* из chat_sync.h, команда не принята и *request не меняется. channel — собственный канал offer. */ int gui_bridge_offer_link(uint64_t channel, const char* link, size_t len, uint64_t* request); int gui_bridge_join_link(const char* link, size_t len, uint64_t* request); void gui_bridge_set_connect_result_cb(gui_connect_result_fn cb); @@ -104,7 +120,7 @@ typedef void (*gui_my_node_id_fn)(uint64_t node_id); void gui_bridge_set_my_node_id_cb(gui_my_node_id_fn cb); /* Callback для статуса авто-подключения (вызывается из GUI-потока) - * status: 0=in_progress, 1=done(3+), 2=stopped */ + * status: 0=in_progress, 1=done, 2=stopped */ typedef void (*gui_auto_connect_status_fn)(uint8_t status, uint16_t total_tried, uint16_t node_count, uint16_t connected); void gui_bridge_set_auto_connect_status_cb(gui_auto_connect_status_fn cb); @@ -170,7 +186,7 @@ void gui_bridge_set_member_removed_cb(gui_member_removed_fn cb); typedef void (*gui_admin_key_received_fn)(const uint8_t* data, int len); void gui_bridge_set_admin_key_received_cb(gui_admin_key_received_fn cb); -/* Callback: готова invite-ссылка (link = пустая строка при ошибке, детали в логе) */ +/* Готовая invite-ссылка: link — заимствованный NUL-terminated UTF-8; пустая строка при ошибке, детали в логе. */ typedef void (*gui_invite_link_ready_fn)(const char* ch_id, int ch_id_len, uint64_t request_id, const char* link); void gui_bridge_set_invite_link_ready_cb(gui_invite_link_ready_fn cb); @@ -203,8 +219,10 @@ void gui_bridge_set_call_declined_cb(gui_call_ended_fn cb); typedef void (*gui_call_error_fn)(uint64_t call_id, uint8_t err, const char* text, int len); void gui_bridge_set_call_error_cb(gui_call_error_fn cb); -/* CALL_STATS: все длительности в мс; extrema относятся к последним 10 секундным интервалам. */ -typedef void (*gui_call_stats_fn)(uint64_t call_id, int rtt, int buffer, int tempo, +/* CALL_STATS: rtt/buffer/minimum/maximum/reserve — мс; tempo — скорость ×100, dropped/underruns — счётчики. + * Все восемь значений передаются uint16 BE после call_id в native endian; callback получает int. + * Extrema относятся к последним десяти секундным интервалам. */ +typedef void (*gui_call_stats_fn)(uint64_t call_id, int rtt, int buffer, int tempo_x100, int dropped, int underruns, int minimum, int maximum, int reserve); void gui_bridge_set_call_stats_cb(gui_call_stats_fn cb); @@ -225,12 +243,14 @@ void gui_bridge_set_radio_talk_cb(gui_radio_talk_fn cb); typedef void (*gui_dm_msg_received_fn)(const char* conv_id, int conv_id_len, uint64_t author); void gui_bridge_set_dm_msg_received_cb(gui_dm_msg_received_fn cb); -/* Callback: DM-беседа создана/изменена (вызывается из GUI-потока). */ -/* Снимок BGP-доступности PM и уведомление об изменениях сети (GUI-поток). */ +/* Уведомление об изменениях сети, GUI-поток: CHANNEL_UPDATED/MEMBERS_CHANGED, SERVICE_STARTED/STOPPED, + * NODEINFO_UPDATED/LINK_STATUS_CHANGED, MEMBER_UPDATED/REMOVED, CHANNEL_DELETED и DM_CONV_UPDATED. + * nodeinfo callback может отправить только сигнал без payload; живые поля карточки получает MEMBER_DETAIL. */ typedef void (*gui_network_changed_fn)(int event_type); void gui_bridge_set_network_changed_cb(gui_network_changed_fn cb); void gui_bridge_set_dm_call_route_cb(void (*cb)(const uint8_t* data, int len)); +/* DM-беседа создана/изменена: conv_id — заимствованная строка длины conv_id_len, generation — версия удаления. */ typedef void (*gui_dm_conv_updated_fn)(const char* conv_id, int conv_id_len, uint64_t generation); void gui_bridge_set_dm_conv_updated_cb(gui_dm_conv_updated_fn cb); @@ -260,6 +280,8 @@ void gui_bridge_set_radio_mute_result_cb(gui_dm_messages_fn cb); /* CHAT_EVT_RADIO_CALL: id/group/peer:8LE,state/reason/incoming:1, GUI-поток. */ void gui_bridge_set_radio_call_cb(gui_dm_messages_fn cb); void gui_bridge_set_stream_sources_cb(gui_dm_messages_fn cb); +/* Переключить локальное приглушение участника. 0 — поставлено в uasync; -1 — ядро/аргументы/память недоступны. + * channel_id — непустой ID до 63 байт, node_id ненулевой; результат приходит через RADIO_MUTE_RESULT. */ int gui_bridge_toggle_member_radio_mute(const char* channel_id, uint64_t node_id); /* CHAT_EVT_ATTACHMENT_PREPARATION: target + job UUID + state. */ void gui_bridge_set_attachment_preparation_cb(gui_dm_messages_fn cb); diff --git a/tools/chatgui/transport/gui_bridge_impl.cpp b/tools/chatgui/transport/gui_bridge_impl.cpp index 70ee1f0c..fabc2f58 100644 --- a/tools/chatgui/transport/gui_bridge_impl.cpp +++ b/tools/chatgui/transport/gui_bridge_impl.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,7 @@ extern "C" { #include "../../../src/member_files/protocol.h" #include "../../../src/chat/chat_member.h" #include "../../../src/chat/chat_core.h" +#include "../../../src/chat/chat_sync.h" #include "../../../src/chat/chat_join_port.h" #include "../../../src/dm/dm_core.h" } @@ -26,14 +28,12 @@ extern "C" { class GuiBridgeReceiver : public QObject { Q_OBJECT public: - void* ua = nullptr; - GuiBridgeReceiver(QObject* parent = nullptr) : QObject(parent) {} Q_INVOKABLE void processPost(int eventType, QByteArray data); }; -static std::mutex join_mutex; +static std::mutex bridge_mutex; static struct chat_join_port* join_port = nullptr; static std::atomic join_sequence{1}; @@ -94,7 +94,72 @@ static gui_dm_messages_fn g_radio_call_cb = nullptr; static gui_dm_messages_fn g_stream_sources_cb = nullptr; static gui_network_changed_fn g_network_changed_cb = nullptr; static struct UASYNC* g_ua = nullptr; -static struct UTUN_INSTANCE* g_inst = nullptr; +static std::atomic g_inst{nullptr}; + +/* Зарезервированный post владеет своей записью; payload принадлежит мосту только при owned=true. + * Все ссылки списка защищены bridge_mutex. uasync освобождает post после callback/отмены. */ +struct BridgePost { + struct posted_task post; + BridgePost* previous; + BridgePost* next; + void (*function)(void*); + void* arg; + bool owned, linked, cancelled; +}; +static_assert(offsetof(BridgePost, post) == 0, "uasync must free the complete bridge post"); +static BridgePost* pending_posts = nullptr; + +/* Удаление за O(1), без ожидания исполнения функции пользователя под mutex моста. */ +static void unlink_post(BridgePost* command) { + if (command->previous) command->previous->next = command->next; + else pending_posts = command->next; + if (command->next) command->next->previous = command->previous; + command->linked = false; +} + +static void execute_post(void* arg) { + auto* command = static_cast(arg); + { + std::lock_guard guard(bridge_mutex); + if (command->linked) unlink_post(command); + } + if (!command->cancelled) command->function(command->arg); + else if (command->owned) u_free(command->arg); +} + +/* Вызывать под bridge_mutex: принятие команды и закрытие ядра образуют одну границу владения. */ +static int post_command(void (*function)(void*), void* arg, bool owned) { + if (!function || !g_ua || !g_inst) { + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "GUI command rejected: core unavailable or missing callback"); return -1; + } + auto* command = static_cast(u_calloc(1, sizeof(BridgePost))); + if (!command) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "GUI command allocation failed"); return -1; } + command->function = function; command->arg = arg; command->owned = owned; command->linked = true; + command->next = pending_posts; + if (pending_posts) pending_posts->previous = command; + pending_posts = command; + command->post.callback = execute_post; command->post.arg = command; + uasync_post_reserved(g_ua, &command->post); + return 0; +} + +/* Ядро снимается в своём потоке вне poll callbacks, до destroy instance и uasync. */ +static void cancel_posts() { + unsigned cancelled = 0; + while (pending_posts) { + auto* command = pending_posts; + unlink_post(command); command->cancelled = true; + void* arg = command->arg; + const bool owned = command->owned; + if (uasync_cancel_post(g_ua, &command->post) == ERR_OK) { + if (owned) u_free(arg); + cancelled++; + } else { + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "GUI command already dispatched during detach; deferred disposal"); + } + } + if (cancelled) DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "GUI bridge detached: cancelled_commands=%u", cancelled); +} /* ── GuiBridgeReceiver implementation ── */ @@ -226,7 +291,7 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { QString chId = QString::fromUtf8((const char*)d + 1, chLen); DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: CHANNEL_UPDATED ch=%s", qPrintable(chId)); if (g_channel_updated_cb) g_channel_updated_cb((const char*)d + 1, chLen); - } + } else DEBUG_WARN(DEBUG_CATEGORY_CHAT, "gui_bridge: truncated CHANNEL_UPDATED bytes=%d channel_bytes=%u", dlen, chLen); } else { DEBUG_WARN(DEBUG_CATEGORY_CHAT, "gui_bridge: CHANNEL_UPDATED data too short %d", dlen); } @@ -235,7 +300,7 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { if (dlen >= 8) { uint64_t nid; memcpy(&nid, d, 8); if (g_my_node_id_cb) g_my_node_id_cb(nid); - } + } else DEBUG_WARN(DEBUG_CATEGORY_CHAT, "gui_bridge: truncated MY_NODE_ID bytes=%d", dlen); break; case GUI_EVT_AUTO_CONNECT_STATUS: if (dlen >= 7) { @@ -246,7 +311,7 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { const char* st_str = status == 0 ? "in_progress" : status == 1 ? "done" : "stopped"; if (status != 0) DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "gui_bridge: AUTO_CONNECT %s tried=%u/%u connected=%u", st_str, total_tried, node_count, connected); if (g_auto_connect_status_cb) g_auto_connect_status_cb(status, total_tried, node_count, connected); - } + } else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: truncated AUTO_CONNECT_STATUS bytes=%d", dlen); break; case GUI_EVT_CHANNEL_PEERS_ONLINE: if (dlen >= 3) { @@ -261,8 +326,8 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: CHANNEL_PEERS_ONLINE ch=%.*s online=%u", chLen, (const char*)d + 1, online); } if (g_channel_peers_online_cb) g_channel_peers_online_cb((const char*)d + 1, chLen, online); - } - } + } else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: truncated CHANNEL_PEERS_ONLINE bytes=%d channel_bytes=%u", dlen, chLen); + } else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: missing CHANNEL_PEERS_ONLINE header bytes=%d", dlen); break; case GUI_EVT_DB_READY: DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: DB_READY (chat DB tables created)"); @@ -271,13 +336,15 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { case GUI_EVT_STATUS_REFRESH: if (g_status_refresh_cb) g_status_refresh_cb((const char*)d, dlen); break; - case GUI_EVT_NODE_CHANGED: - if (dlen >= 8) { - uint64_t nodeId; memcpy(&nodeId, d, 8); - DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "gui_bridge: NODE_CHANGED node=0x%016llx — pubkey changed, connection closed", (unsigned long long)nodeId); - } else { - DEBUG_WARN(DEBUG_CATEGORY_CONNECTION, "gui_bridge: NODE_CHANGED data too short %d", dlen); - } + case GUI_EVT_KEYS_GENERATED: + if (dlen == 64) DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "gui_bridge: local public key generated"); + else DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "gui_bridge: invalid KEYS_GENERATED bytes=%d", dlen); + break; + case GUI_EVT_MEMBERS_CHANGED: + case CHAT_EVT_SERVICE_STARTED: + case CHAT_EVT_SERVICE_STOPPED: + case CHAT_EVT_LINK_STATUS_CHANGED: + // События сети уже переданы network_changed выше; отдельного payload callback нет. break; case GUI_EVT_ATTACHMENT_DOWNLOADED: if (dlen >= 2) { @@ -286,8 +353,8 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { int64_t msgId = 0; memcpy(&msgId, d + 1 + chLen, 8); DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "gui_bridge: ATTACHMENT_DOWNLOADED ch=%.*s msgId=%lld", chLen, (const char*)d + 1, (long long)msgId); if (g_attachment_downloaded_cb) g_attachment_downloaded_cb((const char*)d + 1, chLen, msgId); - } - } + } else DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "gui_bridge: truncated ATTACHMENT_DOWNLOADED bytes=%d channel_bytes=%u", dlen, chLen); + } else DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "gui_bridge: missing ATTACHMENT_DOWNLOADED header bytes=%d", dlen); break; case GUI_EVT_DOWNLOAD_PROGRESS: if (dlen >= 2) { @@ -298,8 +365,8 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "gui_bridge: DOWNLOAD_PROGRESS ch=%.*s msgId=%lld %d/%d", chLen, (const char*)d + 1, (long long)msgId, bd, nb); if (g_download_progress_cb) g_download_progress_cb((const char*)d + 1, chLen, msgId, bd, nb); - } - } + } else DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "gui_bridge: truncated DOWNLOAD_PROGRESS bytes=%d channel_bytes=%u", dlen, chLen); + } else DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "gui_bridge: missing DOWNLOAD_PROGRESS header bytes=%d", dlen); break; case GUI_EVT_NODEINFO_UPDATE: break; /* network_changed уже уведомлён выше. */ case 18: /* CHAT_EVT_INVITE_RECEIVED */ @@ -332,28 +399,31 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: ADMIN_KEY_RECEIVED ch=%.*s from=0x%016llx", chLen, (const char*)d + 1, (unsigned long long)from); if (g_admin_key_received_cb) g_admin_key_received_cb(d, dlen); - } - } + } else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: truncated ADMIN_KEY_RECEIVED bytes=%d channel_bytes=%u", dlen, chLen); + } else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: missing ADMIN_KEY_RECEIVED header bytes=%d", dlen); break; case GUI_EVT_INVITE_LINK_READY: if (dlen >= 11) { uint8_t chLen = d[0]; - if (dlen < 1 + chLen + 10) break; + if (dlen < 1 + chLen + 10) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: truncated INVITE_LINK_READY bytes=%d channel_bytes=%u", dlen, chLen); + break; + } uint64_t requestId; memcpy(&requestId, d + 1 + chLen, 8); uint16_t linkLen; memcpy(&linkLen, d + 1 + chLen + 8, 2); if (dlen >= 1 + chLen + 10 + linkLen) { QByteArray link((const char*)d + 1 + chLen + 10, linkLen); if (g_invite_link_ready_cb) g_invite_link_ready_cb((const char*)d + 1, chLen, requestId, link.constData()); - } - } + } else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: truncated INVITE_LINK_READY bytes=%d link_bytes=%u", dlen, linkLen); + } else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: missing INVITE_LINK_READY header bytes=%d", dlen); 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); - } - } + } else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: truncated INVITE_CANDIDATES bytes=%d channel_bytes=%u", dlen, chLen); + } else DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: missing INVITE_CANDIDATES header bytes=%d", dlen); break; case GUI_EVT_DM_MSG_RECEIVED: if (dlen >= 2) { @@ -363,8 +433,8 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: DM_MSG_RECEIVED conv=%.*s author=%llu", cl, (const char*)d + 1, (unsigned long long)author); if (g_dm_msg_received_cb) g_dm_msg_received_cb((const char*)d + 1, cl, author); - } - } + } else DEBUG_WARN(DEBUG_CATEGORY_DM, "gui_bridge: truncated DM_MSG_RECEIVED bytes=%d conversation_bytes=%u", dlen, cl); + } else DEBUG_WARN(DEBUG_CATEGORY_DM, "gui_bridge: missing DM_MSG_RECEIVED header bytes=%d", dlen); break; case GUI_EVT_DM_CONV_UPDATED: if (dlen >= 10 && d[0] && dlen == 9 + d[0]) { @@ -392,14 +462,14 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: CALL_INCOMING ch=%.*s caller=0x%016llx id=0x%016llx", chLen, (const char*)d + 1, (unsigned long long)caller, (unsigned long long)callId); if (g_call_incoming_cb) g_call_incoming_cb((const char*)d + 1, chLen, caller, callId); - } - } + } else DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: truncated CALL_INCOMING bytes=%d channel_bytes=%u", dlen, chLen); + } else DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: missing CALL_INCOMING header bytes=%d", dlen); break; case GUI_EVT_CALL_RINGING: if (dlen >= 8) { uint64_t callId; memcpy(&callId, d, 8); if (g_call_ringing_cb) g_call_ringing_cb(callId); - } + } else DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: truncated CALL_RINGING bytes=%d", dlen); break; case CHAT_EVT_CALL_PHONE_BUSY: if (dlen == 9) { @@ -411,19 +481,19 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { if (dlen >= 8) { uint64_t callId; memcpy(&callId, d, 8); if (g_call_accepted_cb) g_call_accepted_cb(callId); - } + } else DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: truncated CALL_ACCEPTED bytes=%d", dlen); break; case GUI_EVT_CALL_ENDED: if (dlen >= 9) { uint64_t callId; memcpy(&callId, d, 8); if (g_call_ended_cb) g_call_ended_cb(callId, d[8]); - } + } else DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: truncated CALL_ENDED bytes=%d", dlen); break; case GUI_EVT_CALL_DECLINED: if (dlen >= 9) { uint64_t callId; memcpy(&callId, d, 8); if (g_call_declined_cb) g_call_declined_cb(callId, d[8]); - } + } else DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: truncated CALL_DECLINED bytes=%d", dlen); break; case GUI_EVT_CALL_ERROR: if (dlen >= 9) { @@ -432,7 +502,7 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: CALL_ERROR id=0x%016llx err=%u text=%.*s", (unsigned long long)callId, (unsigned)err, dlen - 9, (const char*)d + 9); if (g_call_error_cb) g_call_error_cb(callId, err, (const char*)d + 9, dlen - 9); - } + } else DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: truncated CALL_ERROR bytes=%d", dlen); break; case GUI_EVT_CALL_PATH: if (dlen >= 9) { @@ -440,7 +510,7 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: CALL_PATH id=0x%016llx text=%.*s", (unsigned long long)callId, dlen - 8, (const char*)d + 8); if (g_call_path_cb) g_call_path_cb(callId, (const char*)d + 8, dlen - 8); - } + } else DEBUG_WARN(DEBUG_CATEGORY_CALL, "gui_bridge: truncated CALL_PATH bytes=%d", dlen); break; case GUI_EVT_CALL_CONNECTION: if (dlen == 17 && d[8] <= 3) { @@ -469,7 +539,7 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { memcpy(&src, d + 8, 8); uint8_t on = d[18]; if (g_radio_talk_cb) g_radio_talk_cb(groupId, src, on); - } + } else DEBUG_WARN(DEBUG_CATEGORY_RADIO, "gui_bridge: truncated RADIO_TALK bytes=%d", dlen); break; default: DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "gui_bridge: unknown event type %d", eventType); @@ -482,24 +552,33 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { extern "C" { void gui_bridge_init(void) { - g_receiver = new GuiBridgeReceiver(); - g_receiver->ua = g_ua; + if (!QCoreApplication::instance()) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "GUI bridge requires QCoreApplication"); return; } + if (g_receiver) { DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "GUI bridge already initialized"); return; } + g_receiver = new GuiBridgeReceiver(QCoreApplication::instance()); + QObject::connect(g_receiver, &QObject::destroyed, [] { g_receiver = nullptr; }); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "GUI bridge initialized in GUI thread"); } void gui_bridge_set_uasync(struct UASYNC* ua) { + std::lock_guard guard(bridge_mutex); + if (g_inst && ua != g_ua) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "GUI bridge uasync replacement rejected: detach instance first"); return; + } g_ua = ua; - if (g_receiver) g_receiver->ua = ua; } void gui_bridge_set_inst(struct UTUN_INSTANCE* inst) { struct chat_join_port* replacement = inst ? chat_sync_join_port(inst) : nullptr; - std::lock_guard guard(join_mutex); + std::lock_guard guard(bridge_mutex); + if (g_inst != inst) cancel_posts(); chat_join_port_release(join_port); join_port = replacement; g_inst = inst; + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "GUI bridge instance %s: inst=%p ua=%p", inst ? "attached" : "detached", (void*)inst, (void*)g_ua); } int gui_bridge_join_link(const char* link, size_t len, uint64_t* request) { - std::lock_guard guard(join_mutex); + if (!request) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "GUI join rejected: missing request output"); return CHAT_CONNECT_INVALID; } + std::lock_guard guard(bridge_mutex); uint64_t id = join_sequence.fetch_add(1); int result = chat_join_port_submit(join_port, id, link, len); if (!result) *request = id; @@ -507,7 +586,8 @@ int gui_bridge_join_link(const char* link, size_t len, uint64_t* request) { } int gui_bridge_offer_link(uint64_t channel, const char* link, size_t len, uint64_t* request) { - std::lock_guard guard(join_mutex); + if (!request) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "GUI offer rejected: missing request output"); return CHAT_CONNECT_INVALID; } + std::lock_guard guard(bridge_mutex); uint64_t id = join_sequence.fetch_add(1); int result = chat_join_port_offer(join_port, id, channel, link, len); if (!result) *request = id; @@ -519,38 +599,44 @@ struct UTUN_INSTANCE* gui_bridge_get_inst(void) { } void gui_bridge_post(int event_type, const uint8_t* data, int data_len) { + if (data_len < 0 || (data_len && !data)) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "GUI event rejected: type=%d bytes=%d missing_data=%d", event_type, data_len, !data); + return; + } if (!g_receiver) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "gui_bridge_post: g_receiver=NULL, dropping event %d", event_type); return; } QByteArray d; if (data_len > 0) d = QByteArray((const char*)data, data_len); - QMetaObject::invokeMethod(g_receiver, "processPost", + if (!QMetaObject::invokeMethod(g_receiver, "processPost", Qt::QueuedConnection, Q_ARG(int, event_type), - Q_ARG(QByteArray, d)); + Q_ARG(QByteArray, d))) + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "GUI event queueing failed: type=%d bytes=%d", event_type, data_len); } void gui_bridge_post_uasync_fn(void (*fn)(void*), void* arg) { - if (!g_ua) { - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "gui_bridge_post_uasync_fn: g_ua=NULL, post dropped"); - return; - } - if (!fn) return; - uasync_post(g_ua, fn, arg); + std::lock_guard guard(bridge_mutex); + post_command(fn, arg, false); +} + +int gui_bridge_post_uasync_owned_fn(void (*fn)(void*), void* arg) { + std::lock_guard guard(bridge_mutex); + return post_command(fn, arg, true); } int gui_bridge_submit_message(struct chat_msg_submit* req) { + std::lock_guard guard(bridge_mutex); if (!req || !g_inst || !g_ua || !uasync_is_running(g_ua)) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "composer submission: core unavailable"); return -1; } - auto* post = static_cast(u_calloc(1, sizeof(posted_task))); - if (!post) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "composer submission: post allocation failed"); return -1; } - req->inst = g_inst; post->callback = chat_core_submit_trampoline; post->arg = req; uasync_post_reserved(g_ua, post); - return 0; + req->inst = g_inst; + return post_command(chat_core_submit_trampoline, req, true); } int gui_bridge_is_uasync_ready(void) { - return g_ua != NULL ? 1 : 0; + std::lock_guard guard(bridge_mutex); + return g_ua && g_inst ? 1 : 0; } void gui_bridge_set_connect_result_cb(gui_connect_result_fn cb) { @@ -590,12 +676,9 @@ void gui_bridge_set_cpu_load_cb(gui_cpu_load_fn cb) { } int gui_bridge_request_cpu_load(void) { + std::lock_guard guard(bridge_mutex); if (!g_ua || !g_inst) return 0; - auto* task = (struct posted_task*)u_calloc(1, sizeof(struct posted_task)); - if (!task) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "CPU load request allocation failed"); return 0; } - task->callback = chat_core_collect_cpu_load_trampoline; task->arg = g_inst; - uasync_post_reserved(g_ua, task); - return 1; + return post_command(chat_core_collect_cpu_load_trampoline, g_inst, false) == 0; } void gui_bridge_set_attachment_downloaded_cb(gui_attachment_downloaded_fn cb) { @@ -712,39 +795,37 @@ void gui_bridge_set_radio_call_cb(gui_dm_messages_fn cb) { g_radio_call_cb = cb; /* Резервируем post вместе с запросом: UI получает отказ, если поставить удаление в очередь невозможно. */ int gui_bridge_delete_dm(const char* conv_id, uint64_t generation, uint64_t request_id) { + std::lock_guard guard(bridge_mutex); if (!g_inst || !g_ua || !uasync_is_running(g_ua) || !conv_id || !conv_id[0] || strlen(conv_id) >= 64 || strspn(conv_id, "0123456789") != strlen(conv_id) || !generation || !request_id) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM delete: invalid request or stopped core generation=%llu request=%llu", (unsigned long long)generation, (unsigned long long)request_id); return -1; } auto* req = static_cast(u_calloc(1, sizeof(dm_delete_req))); - auto* post = static_cast(u_calloc(1, sizeof(posted_task))); - if (!req || !post) { - DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM delete: request allocation failed"); u_free(req); u_free(post); return -1; + if (!req) { + DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM delete: request allocation failed"); return -1; } req->inst = g_inst; req->generation = generation; req->request_id = request_id; strcpy(req->conv_id, conv_id); - post->callback = dm_delete_trampoline; post->arg = req; - uasync_post_reserved(g_ua, post); + if (post_command(dm_delete_trampoline, req, true) < 0) { u_free(req); return -1; } DEBUG_INFO(DEBUG_CATEGORY_DM, "DM delete queued: conv=%s generation=%llu request=%llu", conv_id, (unsigned long long)generation, (unsigned long long)request_id); return 0; } int gui_bridge_toggle_member_radio_mute(const char* channel_id, uint64_t node_id) { + std::lock_guard guard(bridge_mutex); const int running = g_inst && g_ua ? uasync_is_running(g_ua) : 0; const bool valid_channel = channel_id && channel_id[0] && strlen(channel_id) < 64; if (!g_inst || !running || !valid_channel || !node_id) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: GUI mute rejected inst=%p ua=%p running=%d valid_channel=%d node=%016llx", - (void*)g_inst, (void*)g_ua, running, valid_channel, (unsigned long long)node_id); return -1; + (void*)g_inst.load(), (void*)g_ua, running, valid_channel, (unsigned long long)node_id); return -1; } auto* req = static_cast(u_calloc(1, sizeof(chat_member_radio_mute_req))); - auto* post = static_cast(u_calloc(1, sizeof(posted_task))); - if (!req || !post) { - DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: GUI mute request allocation failed"); u_free(req); u_free(post); return -1; + if (!req) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: GUI mute request allocation failed"); return -1; } req->inst = g_inst; req->node_id = node_id; strcpy(req->ch_id, channel_id); - post->callback = chat_core_toggle_member_radio_mute_trampoline; post->arg = req; - uasync_post_reserved(g_ua, post); + if (post_command(chat_core_toggle_member_radio_mute_trampoline, req, true) < 0) { u_free(req); return -1; } DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio: GUI mute queued ch=%s node=%016llx", channel_id, (unsigned long long)node_id); return 0; }