diff --git a/tools/chatgui/transport/gui_bridge_impl.cpp b/tools/chatgui/transport/gui_bridge_impl.cpp index 43574fa3..82da220a 100644 --- a/tools/chatgui/transport/gui_bridge_impl.cpp +++ b/tools/chatgui/transport/gui_bridge_impl.cpp @@ -1,10 +1,14 @@ #include "gui_bridge.h" +#include "member_edit.h" #include "chat/chat_event.h" #include #include #include #include +#include +#include +#include #include #include #include @@ -96,6 +100,52 @@ static gui_network_changed_fn g_network_changed_cb = nullptr; static struct UASYNC* g_ua = nullptr; static std::atomic g_inst{nullptr}; +struct MemberCompletion { + QPointer context; // Читается только в GUI-потоке. + std::function callback; + QMetaObject::Connection destroyed; +}; +static QHash member_completions; // GUI-only, запись живёт до результата/уничтожения context. +static quint64 member_sequence = 0; +struct MemberEditRequest { + UTUN_INSTANCE* instance; + sqlite3* database; + quint64 request, node; + size_t nicknameBytes; + char channel[64]; + // За структурой: nickname+NUL, owner JSON+NUL; единый u_malloc-блок. +}; + +/* uasync не трогает QObject формы; корреляция и проверка lifetime происходят в GUI. */ +static void member_edit_result(quint64 request, int result) { + if (!g_receiver || !QMetaObject::invokeMethod(g_receiver, [request, result] { + auto it = member_completions.find(request); + if (it == member_completions.end()) return; + MemberCompletion completion = std::move(it.value()); member_completions.erase(it); + QObject::disconnect(completion.destroyed); + if (completion.context) completion.callback(result); + }, Qt::QueuedConnection)) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member save: result delivery failed request=%llu", + (unsigned long long)request); +} + +static void cancel_member_edit(void* arg) { + auto* request = static_cast(arg); + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member save: cancelled request=%llu ch=%s node=%016llx", + (unsigned long long)request->request, request->channel, (unsigned long long)request->node); + member_edit_result(request->request, -3); +} + +static void execute_member_edit(void* arg) { + auto* request = static_cast(arg); + const char* nickname = reinterpret_cast(request + 1); + int result = -3; + if (chat_core_get_db(request->instance) != request->database) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member save: database changed request=%llu", (unsigned long long)request->request); + } else result = chat_core_save_member(request->instance, request->channel, request->node, + nickname, nickname + request->nicknameBytes + 1); + member_edit_result(request->request, result); u_free(request); +} + /* Зарезервированный post владеет своей записью; payload принадлежит мосту только при owned=true. * Все ссылки списка защищены bridge_mutex. uasync освобождает post после callback/отмены. */ struct BridgePost { @@ -104,6 +154,7 @@ struct BridgePost { BridgePost* next; void (*function)(void*); void* arg; + void (*cancel)(void*); bool owned, linked, cancelled; }; static_assert(offsetof(BridgePost, post) == 0, "uasync must free the complete bridge post"); @@ -124,17 +175,21 @@ static void execute_post(void* arg) { if (command->linked) unlink_post(command); } if (!command->cancelled) command->function(command->arg); - else if (command->owned) u_free(command->arg); + else { + if (command->cancel) command->cancel(command->arg); + if (command->owned) u_free(command->arg); + } } /* Вызывать под bridge_mutex: принятие команды и закрытие ядра образуют одну границу владения. */ -static int post_command(void (*function)(void*), void* arg, bool owned) { +static int post_command(void (*function)(void*), void* arg, bool owned, void (*cancel)(void*) = nullptr) { 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->cancel = cancel; command->next = pending_posts; if (pending_posts) pending_posts->previous = command; pending_posts = command; @@ -151,7 +206,9 @@ static void cancel_posts() { unlink_post(command); command->cancelled = true; void* arg = command->arg; const bool owned = command->owned; + void (*cancel)(void*) = command->cancel; if (uasync_cancel_post(g_ua, &command->post) == ERR_OK) { + if (cancel) cancel(arg); if (owned) u_free(arg); cancelled++; } else { @@ -161,6 +218,40 @@ static void cancel_posts() { if (cancelled) DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "GUI bridge detached: cancelled_commands=%u", cancelled); } +/* Публикация снимка и привязка instance выполняются под тем же mutex, что detach. */ +bool gui_bridge_save_member(sqlite3* db, const QString& channel, quint64 node, const QString& nickname, + const QByteArray& ownerJson, QObject* context, std::function complete) { + if (!g_receiver || QThread::currentThread() != g_receiver->thread() || !context || context->thread() != g_receiver->thread() || + !db || !node || !complete || channel.isEmpty() || channel.size() > 63 || channel.contains(QChar(0)) || + nickname.contains(QChar(0)) || ownerJson.contains('\0')) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member save: invalid GUI request/context"); return false; + } + const QByteArray id = channel.toLatin1(), nick = nickname.toUtf8(); + bool valid = false; quint64 number = channel.toULongLong(&valid); + if (!valid || !number || number > INT64_MAX || QString::number(number) != channel || !++member_sequence) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member save: invalid channel or exhausted request ID"); return false; + } + std::lock_guard guard(bridge_mutex); + if (!g_inst || !g_ua) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member save: core unavailable"); return false; } + auto* request = static_cast(u_malloc(sizeof(MemberEditRequest) + nick.size() + ownerJson.size() + 2)); + if (!request) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member save: snapshot allocation failed"); return false; } + request->instance = g_inst; request->database = db; request->node = node; request->request = member_sequence; + request->nicknameBytes = nick.size(); memcpy(request->channel, id.constData(), id.size() + 1); + char* data = reinterpret_cast(request + 1); + memcpy(data, nick.constData(), nick.size() + 1); memcpy(data + nick.size() + 1, ownerJson.constData(), ownerJson.size() + 1); + MemberCompletion completion{context, std::move(complete), {}}; + completion.destroyed = QObject::connect(context, &QObject::destroyed, g_receiver, + [id = member_sequence] { member_completions.remove(id); }); + member_completions.insert(member_sequence, std::move(completion)); + if (post_command(execute_member_edit, request, true, cancel_member_edit) < 0) { + QObject::disconnect(member_completions.take(member_sequence).destroyed); u_free(request); + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member save: queue rejected"); return false; + } + DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "member save: queued request=%llu ch=%s node=%016llx", + (unsigned long long)member_sequence, id.constData(), (unsigned long long)node); + return true; +} + /* ── GuiBridgeReceiver implementation ── */ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { diff --git a/tools/chatgui/transport/member_edit.h b/tools/chatgui/transport/member_edit.h new file mode 100644 index 00000000..a8a11f3d --- /dev/null +++ b/tools/chatgui/transport/member_edit.h @@ -0,0 +1,18 @@ +/* Подтверждённое desktop-сохранение участника: GUI передаёт снимок, SQL/подпись выполняет uasync. + * Callback вызывается в GUI только пока жив context; уничтожение context не отменяет уже принятую запись. */ +#pragma once + +#include +#include +#include + +class QObject; +struct sqlite3; + +/* GUI-only. db — ожидаемый handle текущего ядра, без разыменования в GUI; channel — канонический ID. + * nickname сохраняется как UTF-8, пустой снимает псевдоним; ownerJson — частичный JSON или пусто для одного ника. + * false — отказ до постановки, callback не будет; true — мост владеет копиями и вернёт результат, включая отмену при stop. + * result: 0=commit, -1=ошибка, -2=конфликт, -3=отмена/другая БД. При уничтожении context callback удаляется. + * Аргументы/ошибки логируются без ника, ключей и содержимого JSON. */ +bool gui_bridge_save_member(sqlite3* db, const QString& channel, quint64 node, const QString& nickname, + const QByteArray& ownerJson, QObject* context, std::function complete);