Return correlated member save and cancellation results to live GUI contexts
This commit is contained in:
1 parent
1431507ac5
commit
096e721faa
2 files changed
+111
-2
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
Reference in new issue
Block a user