Own and cancel desktop bridge commands before core teardown

This commit is contained in:
evgeny committed 2026-10-27 02:25:05 +01:00
1 parent 75124e0efa
commit 1b695fe247
6 files changed
+389 -134

No files matched your search

+3 -3
View File
@@ -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] */
+4
View File
@@ -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)
+7 -3
View File
@@ -6,6 +6,7 @@
extern "C" {
#include "chat/chat_core.h"
#include "../../lib/mem.h"
#include "../../../lib/debug_config.h"
}
#include <QVBoxLayout>
@@ -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) {
+144
View File
@@ -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 <QtTest>
#include <cstring>
#include <thread>
#include <future>
#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<int> cpuLoads;
static bool cpuOnGuiThread = true;
static QStringList changedChannels;
static QList<QByteArray> 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<chat_msg_submit*>(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<int*>(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<int*>(u_malloc(sizeof(int))); QVERIFY(value); *value = 7;
QCOMPARE(gui_bridge_post_uasync_owned_fn([](void* p) {
QCOMPARE(*static_cast<int*>(p), 7); u_free(p);
}, value), 0);
uasync_poll(core.ua, 0); QCOMPARE(u_get_allocated_count(), baseline);
value = static_cast<int*>(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<int*>(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<void> 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<chat_msg_submit*>(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)
+77 -55
View File
@@ -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 <stdint.h>
#include <stddef.h>
#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);
+154 -73
View File
@@ -4,6 +4,7 @@
#include <QObject>
#include <QMetaObject>
#include <QByteArray>
#include <QCoreApplication>
#include <cstring>
#include <mutex>
#include <atomic>
@@ -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<uint64_t> 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<UTUN_INSTANCE*> 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<BridgePost*>(arg);
{
std::lock_guard<std::mutex> 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<BridgePost*>(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<std::mutex> 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<std::mutex> guard(join_mutex);
std::lock_guard<std::mutex> 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<std::mutex> guard(join_mutex);
if (!request) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "GUI join rejected: missing request output"); return CHAT_CONNECT_INVALID; }
std::lock_guard<std::mutex> 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<std::mutex> guard(join_mutex);
if (!request) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "GUI offer rejected: missing request output"); return CHAT_CONNECT_INVALID; }
std::lock_guard<std::mutex> 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<std::mutex> guard(bridge_mutex);
post_command(fn, arg, false);
}
int gui_bridge_post_uasync_owned_fn(void (*fn)(void*), void* arg) {
std::lock_guard<std::mutex> guard(bridge_mutex);
return post_command(fn, arg, true);
}
int gui_bridge_submit_message(struct chat_msg_submit* req) {
std::lock_guard<std::mutex> 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<posted_task*>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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<dm_delete_req*>(u_calloc(1, sizeof(dm_delete_req)));
auto* post = static_cast<posted_task*>(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<std::mutex> 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<chat_member_radio_mute_req*>(u_calloc(1, sizeof(chat_member_radio_mute_req)));
auto* post = static_cast<posted_task*>(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;
}