From adefef675ce50b68bba70fa1617a567adeff00b1 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 23:47:57 +0100 Subject: [PATCH] Detach GUI SQLite readers before worker teardown --- tools/chatgui/src/mainwindow.cpp | 11 +++++++++- tools/chatgui/transport/utun_node.cpp | 31 +++++++++++++++++++++++++++ tools/chatgui/transport/utun_node.h | 12 +++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index d595e544..c5032637 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -331,6 +331,11 @@ void MainWindow::setupNode() { connect(m_node, &UtunNode::started, this, [this]() { qDebug("UtunNode started"); }); + connect(m_node, &UtunNode::stopping, this, [this] { + m_autoPlayTimer->stop(); m_autoPlayQueue.clear(); m_autoPlayDownloading.clear(); + if (m_db) m_db->setDb(nullptr); + DEBUG_INFO(DEBUG_CATEGORY_SYS, "desktop GUI database detached before core teardown"); + }); connect(m_node, &UtunNode::stopped, this, [this]() { qDebug("UtunNode stopped"); qApp->quit(); @@ -514,7 +519,11 @@ void MainWindow::setupBridgeCallbacks() { gui_bridge_set_channel_peers_online_cb(onChannelPeersOnlineCallback); gui_bridge_set_db_ready_cb([]() { if (!s_mainWindow || !s_mainWindow->m_db) return; - s_mainWindow->m_db->setDb(chat_core_get_db(gui_bridge_get_inst())); + auto* node = s_mainWindow->m_node; + auto* database = node ? node->databaseHandle() : nullptr; + if (!database) { DEBUG_WARN(DEBUG_CATEGORY_SYS, "desktop DB_READY ignored while core unavailable/stopping"); return; } + s_mainWindow->m_db->setDb(database); + s_mainWindow->m_db->setMyNodeId(node->nodeIdHex().toULongLong(nullptr, 16)); if (AvatarManager::instance()) AvatarManager::instance()->initialize(); RadioAudioEngine::instance()->loadSettings(s_mainWindow->m_cfgPath); s_mainWindow->m_channelList->loadChannels(); diff --git a/tools/chatgui/transport/utun_node.cpp b/tools/chatgui/transport/utun_node.cpp index d3946918..3f2be471 100644 --- a/tools/chatgui/transport/utun_node.cpp +++ b/tools/chatgui/transport/utun_node.cpp @@ -54,6 +54,8 @@ bool UtunNode::start(const QString& configPath) { } m_configPath = configPath; m_stop = false; + m_stopping = false; + { std::lock_guard guard(m_guiStopMutex); m_guiDetached = false; } m_running = true; ++m_generation; std::promise startup; @@ -102,12 +104,28 @@ void UtunNode::requestStop() { void UtunNode::stop() { if (!m_running) return; + m_stopping = true; + detachGuiReaders(); requestStop(); if (m_thread.joinable()) m_thread.join(); finalize(); m_running = false; } +/* GUI уже закончил текущие чтения; worker может закрыть общий handle после всех direct-слотов. */ +void UtunNode::detachGuiReaders() { + { std::lock_guard guard(m_guiStopMutex); if (m_guiDetached) return; } + emit stopping(); + { std::lock_guard guard(m_guiStopMutex); m_guiDetached = true; } + m_guiStopCondition.notify_all(); + DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "desktop GUI readers detached: generation=%llu", (unsigned long long)m_generation); +} + +struct sqlite3* UtunNode::databaseHandle() const { + std::lock_guard guard(m_snapshotMutex); + return m_stopping ? nullptr : m_snapshot.database; +} + void UtunNode::finalize() { gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr); @@ -150,6 +168,7 @@ bool UtunNode::ntpEnabled() const { void UtunNode::updateSnapshot(const UTUN_INSTANCE* inst) { std::lock_guard guard(m_snapshotMutex); if (!inst) { m_snapshot = {}; return; } + m_snapshot.database = inst->topo_sqlite_db; if (m_snapshot.nodeId.isEmpty()) { char id[17]; snprintf(id, sizeof(id), "%016" PRIx64, inst->node_id); m_snapshot.nodeId = QString::fromLatin1(id); @@ -308,12 +327,24 @@ void UtunNode::runLoop(std::promise startup) { } if (!m_stop) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop core requested stop; releasing services outside callbacks"); + m_stopping = true; gui_bridge_set_inst(nullptr); m_instance = nullptr; uasync_mark_stopped(ua); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "utun_node: poll stopped ua=%p running=%d requested=%d", (void*)ua, uasync_is_running(ua), (int)m_stop); + { + std::unique_lock guard(m_guiStopMutex); + if (!m_guiDetached) { + DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "desktop teardown waiting for GUI readers: generation=%llu", (unsigned long long)generation); + if (!QMetaObject::invokeMethod(this, [this, generation] { + if (generation == m_generation) detachGuiReaders(); + }, Qt::QueuedConnection)) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop cannot queue GUI reader detach"); + m_guiStopCondition.wait(guard, [this] { return m_guiDetached; }); + } + } + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "runLoop: destroying instance in worker thread"); gui_stream_detach(inst); call_video_port_detach(inst); call_audio_destroy(inst); radio_audio_destroy(inst); diff --git a/tools/chatgui/transport/utun_node.h b/tools/chatgui/transport/utun_node.h index 300006e1..2fe5ff95 100644 --- a/tools/chatgui/transport/utun_node.h +++ b/tools/chatgui/transport/utun_node.h @@ -16,10 +16,12 @@ #include #include #include +#include #include #include struct UTUN_INSTANCE; +struct sqlite3; class UtunNode : public QObject { Q_OBJECT @@ -43,6 +45,9 @@ public: void setDebugCategories(const QString& categories); // Overrides cat=level,... поверх sys=info,bgp=info. struct UTUN_INSTANCE* instance() const { return m_instance.load(); } + /* GUI: заимствованный handle, живой до stopping; после сигнала новые чтения запрещены. + * Слот stopping снимает handle без вызова stop/join. Worker ждёт завершения слотов перед destroy. */ + struct sqlite3* databaseHandle() const; // Копии данных ядра, безопасны вне uasync; до старта/после stop — пустые строки, 0/false. QString nodeIdHex() const; @@ -53,18 +58,21 @@ public: signals: void started(); // Синхронно в GUI после успешного start. + void stopping(); // GUI: освободить заимствованные данные; SQLite/ядро ещё живы до возврата слотов. void stopped(); // Через Qt-очередь после освобождения ядра; событие прежнего запуска отсекается. void error(const QString& msg); // Ошибка start; stopped при отказе запуска не посылается. private: void runLoop(std::promise startup); void finalize(); // Только GUI после join: сброс моста, уничтожение uasync, очистка копии состояния. + void detachGuiReaders(); // GUI: stopping и подтверждение worker; идемпотентно в пределах запуска. void updateSnapshot(const UTUN_INSTANCE* inst); // uasync читает ядро; NULL сбрасывает копию при finalize. struct Snapshot { QString nodeId, publicKey; int64_t ntpOffsetUs = 0; bool ntpSynced = false, ntpEnabled = false; + struct sqlite3* database = nullptr; // Заимствован до подтверждения stopping в GUI. } m_snapshot; // Копия для GUI; доступ под m_snapshotMutex, указателей ядра не содержит. mutable std::mutex m_snapshotMutex; std::atomic m_instance{nullptr}; // Публикуется после готовности; не продлевает жизнь ядра. @@ -73,6 +81,10 @@ private: std::thread m_thread; // Присоединяется GUI до повторного старта и уничтожения объекта. std::atomic m_running{false}; // Узел владеет запущенным/останавливаемым worker. std::atomic m_stop{false}; // Потокобезопасный запрос остановки. + std::atomic m_stopping{false}; // Закрывает публикацию handle перед ожиданием GUI. + std::mutex m_guiStopMutex; // Подтверждение отсоединения GUI до уничтожения SQLite. + std::condition_variable m_guiStopCondition; + bool m_guiDetached = true; // Под m_guiStopMutex; сбрасывается перед start. uint64_t m_generation = 0; // Поколение запуска; GUI отбрасывает поздний stopped прежнего worker. QString m_configPath; QString m_startupError;