Return correlated member save and cancellation results to live GUI contexts

This commit is contained in:
evgeny committed 2026-10-28 00:58:59 +01:00
1 parent 1431507ac5
commit 096e721faa
2 files changed
+111 -2

No files matched your search

+93 -2
View File
@@ -1,10 +1,14 @@
#include "gui_bridge.h"
#include "member_edit.h"
#include "chat/chat_event.h"
#include <QObject>
#include <QMetaObject>
#include <QByteArray>
#include <QCoreApplication>
#include <QPointer>
#include <QHash>
#include <QThread>
#include <cstring>
#include <mutex>
#include <atomic>
@@ -96,6 +100,52 @@ static gui_network_changed_fn g_network_changed_cb = nullptr;
static struct UASYNC* g_ua = nullptr;
static std::atomic<UTUN_INSTANCE*> g_inst{nullptr};
struct MemberCompletion {
QPointer<QObject> context; // Читается только в GUI-потоке.
std::function<void(int)> callback;
QMetaObject::Connection destroyed;
};
static QHash<quint64, MemberCompletion> 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<MemberEditRequest*>(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<MemberEditRequest*>(arg);
const char* nickname = reinterpret_cast<const char*>(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<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->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<void(int)> 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<std::mutex> guard(bridge_mutex);
if (!g_inst || !g_ua) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member save: core unavailable"); return false; }
auto* request = static_cast<MemberEditRequest*>(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<char*>(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) {
+18
View File
@@ -0,0 +1,18 @@
/* Подтверждённое desktop-сохранение участника: GUI передаёт снимок, SQL/подпись выполняет uasync.
* Callback вызывается в GUI только пока жив context; уничтожение context не отменяет уже принятую запись. */
#pragma once
#include <QByteArray>
#include <QString>
#include <functional>
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<void(int)> complete);