diff --git a/doc/service_lifecycle.md b/doc/service_lifecycle.md index 0c77360a..d97f3f11 100644 --- a/doc/service_lifecycle.md +++ b/doc/service_lifecycle.md @@ -26,7 +26,7 @@ utun_instance_destroy(inst); `[gui] utun_enabled=yes` явно добавляет UTUN-сервис; `[global] tun_enabled=yes` управляет только созданием TUN внутри включённого UTUN. Отсутствующий GUI-флаг означает no. Без TUN и NAT запуск чата не требует прав администратора. -Desktop подтверждает start только после готовности всех сервисов и uasync poll; +Desktop подтверждает start после готовности всех сервисов и медиапривязок, перед входом в uasync poll; ошибка запрошенного TUN откатывает запуск и возвращается в GUI. Android сохраняет тот же выбор при рестарте. Повторный start работающего сервиса и повторный stop допустимы. Ошибка старта UTUN/чата откатывает созданные им ресурсы; ошибка core start оставляет подготовленные diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index b527db00..af7415d6 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -652,6 +652,10 @@ set_tests_properties(test_radio_mute_bridge PROPERTIES TIMEOUT 15 ENVIRONMENT "Q add_executable(test_utun_node tests/test_utun_node.cpp transport/utun_node.cpp transport/gui_bridge_impl.cpp transport/stream_bridge.cpp) target_include_directories(test_utun_node PRIVATE ${UTUN_INCLUDE_DIRS}) target_link_libraries(test_utun_node PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun_voice utun pthread) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_compile_definitions(test_utun_node PRIVATE UTUN_TEST_NODE_STREAM_FAILURES) + target_link_options(test_utun_node PRIVATE "-Wl,--wrap=stream_publisher_create" "-Wl,--wrap=stream_viewers_create") +endif() add_test(NAME test_utun_node COMMAND test_utun_node) set_tests_properties(test_utun_node PROPERTIES TIMEOUT 30 ENVIRONMENT "QT_QPA_PLATFORM=offscreen") set_tests_properties(test_radio_member_mute PROPERTIES TIMEOUT 20) diff --git a/tools/chatgui/tests/test_utun_node.cpp b/tools/chatgui/tests/test_utun_node.cpp index ad96927a..3bc96f0e 100644 --- a/tools/chatgui/tests/test_utun_node.cpp +++ b/tools/chatgui/tests/test_utun_node.cpp @@ -3,6 +3,7 @@ #include "../transport/gui_bridge.h" #include #include +#include #include extern "C" { @@ -12,8 +13,30 @@ extern "C" { #include "transport_layer/node_conn_direct.h" #include "u_async.h" #include "debug_config.h" +#include "call/call_video_port.h" +#include "stream/stream_publisher.h" +#include "stream/stream_viewer.h" +#include "mem.h" } +#ifdef UTUN_TEST_NODE_STREAM_FAILURES +static std::atomic streamFailure{0}; +extern "C" int __real_stream_publisher_create(struct stream_publisher**, UTUN_INSTANCE*); +extern "C" int __real_stream_viewers_create(struct stream_viewers**, UTUN_INSTANCE*); +extern "C" int __wrap_stream_publisher_create(struct stream_publisher** out, UTUN_INSTANCE* inst) { + if (streamFailure == 1) { + streamFailure = 0; DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "[INJECT] desktop publisher creation failure"); return -1; + } + return __real_stream_publisher_create(out, inst); +} +extern "C" int __wrap_stream_viewers_create(struct stream_viewers** out, UTUN_INSTANCE* inst) { + if (streamFailure == 2) { + streamFailure = 0; DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "[INJECT] desktop viewers creation failure"); return -1; + } + return __real_stream_viewers_create(out, inst); +} +#endif + static const QByteArray identity = "my_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n" "my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n"; @@ -107,6 +130,126 @@ private slots: QVERIFY(!node.isRunning() && !node.instance() && !gui_bridge_get_inst() && !gui_bridge_is_uasync_ready()); } + void restartKeepsLogPathAndDoesNotLeakFiles() { + QTemporaryDir directory; QVERIFY(directory.isValid()); + const QString path = directory.filePath("node.cfg"), logPath = directory.filePath("node.log"); + QFile config(path); QVERIFY(config.open(QIODevice::WriteOnly)); + const QByteArray text = "[global]\n" + identity + + "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n[ntp]\nenabled=no\n"; + QCOMPARE(config.write(text), text.size()); config.close(); +#ifdef Q_OS_LINUX + const int descriptors = QDir("/proc/self/fd").entryList(QDir::AllEntries | QDir::NoDotAndDotDot).size(); +#endif + { + UtunNode node; + node.setDbPath(directory.filePath("db")); node.setDebugFile(logPath); + for (int i = 0; i < 3; i++) { + QVERIFY(node.start(path)); + node.stop(); + // Путь хранится logger-ом до закрытия файла, включая время после stop. + QCOMPARE(QByteArray(g_debug_config.output_file), logPath.toUtf8()); + QCOMPARE(debug_reopen_log(), 0); + } + } + debug_disable_file_output(); +#ifdef Q_OS_LINUX + QCOMPARE(QDir("/proc/self/fd").entryList(QDir::AllEntries | QDir::NoDotAndDotDot).size(), descriptors); +#endif + } + + void mediaPortFailureRejectsStartupAndAllowsRetry() { + QTemporaryDir directory; QVERIFY(directory.isValid()); + const QString path = directory.filePath("node.cfg"); + QFile config(path); QVERIFY(config.open(QIODevice::WriteOnly)); + const QByteArray text = "[global]\n" + identity + + "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n[ntp]\nenabled=no\n"; + QCOMPARE(config.write(text), text.size()); config.close(); + struct PortOwner { + UTUN_INSTANCE instance{}; + ~PortOwner() { call_video_port_detach(&instance); } + } owner; + QCOMPARE(call_video_port_attach(&owner.instance), 0); + UtunNode node; + node.setDbPath(directory.filePath("db")); node.setDebugFile(directory.filePath("node.log")); + QSignalSpy started(&node, &UtunNode::started), errors(&node, &UtunNode::error); + const bool running = node.start(path); + if (running) node.stop(); // Старая реализация не должна оставлять поток после FAIL теста. + QVERIFY(!running); + QVERIFY(!node.isRunning() && !node.instance() && !gui_bridge_get_inst() && !gui_bridge_is_uasync_ready()); + QCOMPARE(started.count(), 0); QCOMPARE(errors.count(), 1); + QVERIFY(node.startupError().contains("call_video_port_attach")); + // Откат не снимает чужую привязку. + QVERIFY(call_video_port_attach(&owner.instance) < 0); + call_video_port_detach(&owner.instance); + QVERIFY(node.start(path)); node.stop(); + } + + void restartDiscardsPreviousStoppedAndCopiesCoreState() { + QTemporaryDir directory; QVERIFY(directory.isValid()); + const QString path = directory.filePath("node.cfg"); + QFile config(path); QVERIFY(config.open(QIODevice::WriteOnly)); + const QByteArray text = "[global]\n" + identity + + "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n[ntp]\nenabled=no\n"; + QCOMPARE(config.write(text), text.size()); config.close(); + UtunNode node; + node.setDbPath(directory.filePath("db")); node.setDebugFile(directory.filePath("node.log")); + node.setDebugCategories("sys=none,bgp=none"); + QSignalSpy stopped(&node, &UtunNode::stopped), errors(&node, &UtunNode::error); + QVERIFY(node.start(path)); + QCOMPARE(g_debug_config.category_levels[DEBUG_CATEGORY_SYS], DEBUG_LEVEL_NONE); + QCOMPARE(g_debug_config.category_levels[DEBUG_CATEGORY_BGP], DEBUG_LEVEL_NONE); + const QString id = node.nodeIdHex(); QCOMPARE(id.size(), 16); + QCOMPARE(node.pubKeyHex(), QStringLiteral("b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01")); + QVERIFY(!node.start(path)); QCOMPARE(errors.count(), 1); QVERIFY(node.isRunning()); + node.stop(); + QVERIFY(node.nodeIdHex().isEmpty() && node.pubKeyHex().isEmpty()); + QVERIFY(!node.ntpEnabled() && !node.ntpSynced() && node.ntpOffsetUs() == 0); + QVERIFY(node.start(path)); QCOMPARE(node.nodeIdHex(), id); + QCoreApplication::processEvents(); QCOMPARE(stopped.count(), 0); QVERIFY(node.isRunning()); + gui_bridge_post_uasync_fn([](void*) { + auto* inst = gui_bridge_get_inst(); + inst->ntp.offset_us = 1234567; inst->ntp.synced = 1; inst->ntp.enabled = 1; + }, nullptr); + QTRY_COMPARE_WITH_TIMEOUT(node.ntpOffsetUs(), int64_t(1234567), 3000); + QVERIFY(node.ntpSynced() && node.ntpEnabled()); + gui_bridge_post_uasync_fn([](void*) { utun_instance_stop(gui_bridge_get_inst()); }, nullptr); + QTRY_COMPARE_WITH_TIMEOUT(stopped.count(), 1, 3000); + QVERIFY(!node.instance()); QCOMPARE(node.nodeIdHex(), id); // Копия переживает ядро до stop. + node.stop(); + QVERIFY(node.nodeIdHex().isEmpty() && !node.ntpSynced()); + } + +#ifdef UTUN_TEST_NODE_STREAM_FAILURES + void streamFailureRollsBackAndAllowsRetry_data() { + QTest::addColumn("stage"); + QTest::newRow("publisher") << 1; + QTest::newRow("viewers-after-publisher") << 2; + } + + void streamFailureRollsBackAndAllowsRetry() { + QFETCH(int, stage); + QTemporaryDir directory; QVERIFY(directory.isValid()); + const QString path = directory.filePath("node.cfg"); + QFile config(path); QVERIFY(config.open(QIODevice::WriteOnly)); + const QByteArray text = "[global]\n" + identity + + "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n[ntp]\nenabled=no\n"; + QCOMPARE(config.write(text), text.size()); config.close(); + const size_t allocated = u_get_allocated_count(); + UtunNode node; + node.setDbPath(directory.filePath("db")); node.setDebugFile(directory.filePath("node.log")); + QSignalSpy started(&node, &UtunNode::started), stopped(&node, &UtunNode::stopped), errors(&node, &UtunNode::error); + streamFailure = stage; + QVERIFY(!node.start(path)); QCOMPARE(streamFailure.load(), 0); + QCOMPARE(started.count(), 0); QCOMPARE(errors.count(), 1); + QVERIFY(node.startupError().contains("stream bridge")); + QVERIFY(!node.isRunning() && !node.instance() && !gui_bridge_get_inst() && !gui_bridge_is_uasync_ready()); + QCoreApplication::processEvents(); QCOMPARE(stopped.count(), 0); + QCOMPARE(u_get_allocated_count(), allocated); + QVERIFY(node.start(path)); node.stop(); QCOMPARE(started.count(), 1); + QCOMPARE(u_get_allocated_count(), allocated); + } +#endif + void tunFailureRejectsStartup() { QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("node.cfg"); diff --git a/tools/chatgui/transport/utun_node.cpp b/tools/chatgui/transport/utun_node.cpp index 8baefa2c..d3946918 100644 --- a/tools/chatgui/transport/utun_node.cpp +++ b/tools/chatgui/transport/utun_node.cpp @@ -3,6 +3,7 @@ #include "stream_bridge.h" #include #include +#include #include #include #include "../../lib/socket_compat.h" @@ -42,16 +43,29 @@ UtunNode::UtunNode(QObject* parent) : QObject(parent) {} UtunNode::~UtunNode() { stop(); + if (!m_debugLogPath.isEmpty() && g_debug_config.output_file == m_debugLogPath.constData()) debug_disable_file_output(); } bool UtunNode::start(const QString& configPath) { - if (m_running) return false; + if (m_running) { + m_startupError = QStringLiteral("Node is already running"); + DEBUG_WARN(DEBUG_CATEGORY_SYS, "desktop startup rejected: node already running"); + emit error(m_startupError); return false; + } m_configPath = configPath; m_stop = false; m_running = true; + ++m_generation; std::promise startup; auto ready = startup.get_future(); - m_thread = std::thread(&UtunNode::runLoop, this, std::move(startup)); + try { + m_thread = std::thread(&UtunNode::runLoop, this, std::move(startup)); + } catch (const std::system_error& e) { + m_running = false; + m_startupError = QStringLiteral("Worker thread creation failed: %1").arg(QString::fromLocal8Bit(e.what())); + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop startup: %s", qPrintable(m_startupError)); + emit error(m_startupError); return false; + } m_startupError = ready.get(); if (!m_startupError.isEmpty()) { if (m_thread.joinable()) m_thread.join(); @@ -69,7 +83,7 @@ void UtunNode::setDebugFile(const QString& path) { void UtunNode::setDbPath(const QString& path) { m_dbPath = path; - QDir(path).mkpath("."); + if (!QDir(path).mkpath(".")) DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "desktop database directory creation failed: %s", qPrintable(path)); } void UtunNode::setDebugLevel(const QString& level) { @@ -82,6 +96,7 @@ void UtunNode::setDebugCategories(const QString& categories) { void UtunNode::requestStop() { m_stop = true; + std::lock_guard guard(m_uaMutex); if (m_ua) uasync_wakeup(m_ua); } @@ -96,37 +111,58 @@ void UtunNode::stop() { void UtunNode::finalize() { gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr); - if (m_ua) { uasync_destroy(m_ua, 0); m_ua = nullptr; } + { + std::lock_guard guard(m_uaMutex); + if (m_ua) { uasync_destroy(m_ua, 0); m_ua = nullptr; } + } m_instance = nullptr; + updateSnapshot(nullptr); m_running = false; DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "finalize: cleanup done"); } QString UtunNode::nodeIdHex() const { - if (!m_instance) return {}; - char buf[32]; - snprintf(buf, sizeof(buf), "%016" PRIx64, m_instance->node_id); - return QString(buf); + std::lock_guard guard(m_snapshotMutex); + return m_snapshot.nodeId; } QString UtunNode::pubKeyHex() const { - if (!m_instance || !m_instance->config) return {}; - return QString::fromLatin1(m_instance->config->global.my_public_key_hex, 64); + std::lock_guard guard(m_snapshotMutex); + return m_snapshot.publicKey; } int64_t UtunNode::ntpOffsetUs() const { - return m_instance ? m_instance->ntp.offset_us : 0; + std::lock_guard guard(m_snapshotMutex); + return m_snapshot.ntpOffsetUs; } bool UtunNode::ntpSynced() const { - return m_instance ? m_instance->ntp.synced : false; + std::lock_guard guard(m_snapshotMutex); + return m_snapshot.ntpSynced; } bool UtunNode::ntpEnabled() const { - return m_instance ? m_instance->ntp.enabled : false; + std::lock_guard guard(m_snapshotMutex); + return m_snapshot.ntpEnabled; +} + +/* GUI получает только собственные строки/числа; изменяемые поля ядра читает его поток. */ +void UtunNode::updateSnapshot(const UTUN_INSTANCE* inst) { + std::lock_guard guard(m_snapshotMutex); + if (!inst) { m_snapshot = {}; return; } + if (m_snapshot.nodeId.isEmpty()) { + char id[17]; snprintf(id, sizeof(id), "%016" PRIx64, inst->node_id); + m_snapshot.nodeId = QString::fromLatin1(id); + m_snapshot.publicKey = QString::fromLatin1(inst->config->global.my_public_key_hex, 64); + } + m_snapshot.ntpOffsetUs = inst->ntp.offset_us; + m_snapshot.ntpSynced = inst->ntp.synced; + m_snapshot.ntpEnabled = inst->ntp.enabled; } void UtunNode::runLoop(std::promise startup) { + // Повторный init не закрывает прежний FILE; путь прошлого запуска пока ещё жив. + debug_disable_file_output(); debug_config_init(); debug_enable_function_name(0); @@ -150,33 +186,13 @@ void UtunNode::runLoop(std::promise startup) { g_debug_config.category_levels[DEBUG_CATEGORY_BGP] = DEBUG_LEVEL_INFO; /* per-category overrides from config: cat=level,cat=level,... */ - if (!m_debugCategories.isEmpty()) { - QByteArray cats = m_debugCategories.toUtf8(); - char* token = strtok(cats.data(), ","); - while (token) { - while (*token == ' ') token++; - char* eq = strchr(token, '='); - if (eq) { - *eq = '\0'; - char* cat_name = token; - char* lvl_name = eq + 1; - while (*lvl_name == ' ') lvl_name++; - debug_category_t cat = get_category_by_name(cat_name); - debug_level_t lvl = debug_level_from_name(lvl_name); - if (cat != DEBUG_CATEGORY_NONE && lvl != DEBUG_LEVEL_NONE) { - g_debug_config.category_levels[cat] = lvl; - DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "debug category: %s=%s", - debug_get_category_name(cat), - lvl_name); - } - } - token = strtok(NULL, ","); - } - } + if (!m_debugCategories.isEmpty() && debug_parse_config(m_debugCategories.toUtf8().constData()) < 0) + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "desktop debug categories contain invalid settings: %s", qPrintable(m_debugCategories)); /* file output (shared with GUI via debug_config) */ if (!m_debugFile.isEmpty()) { - debug_enable_file_output(m_debugFile.toUtf8().constData(), 0); + m_debugLogPath = m_debugFile.toUtf8(); + debug_enable_file_output(m_debugLogPath.constData(), 0); debug_enable_console(1); g_debug_config.thread_marker = 2; } @@ -189,7 +205,26 @@ void UtunNode::runLoop(std::promise startup) { startup.set_value(QStringLiteral("uasync_create failed")); return; } - m_ua = ua; + { + std::lock_guard guard(m_uaMutex); + m_ua = ua; + } + + UTUN_INSTANCE* inst = nullptr; + // Один откат для любого частичного запуска, до публикации готовности в GUI. + auto failed = [&](const QString& reason) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop startup rollback: reason=%s instance=%p", qPrintable(reason), (void*)inst); + if (inst) { + gui_stream_detach(inst); call_video_port_detach(inst); + call_audio_destroy(inst); radio_audio_destroy(inst); + utun_instance_destroy(inst); + } + { + std::lock_guard guard(m_uaMutex); + uasync_destroy(ua, 0); m_ua = nullptr; + } + startup.set_value(reason); + }; /* Читаем конфиг и выставляем db_path ДО создания инстанса, чтобы topo_groups_init * (внутри utun_instance_create_from_config) открыл общую SQLite-БД раньше auto_socket_init — @@ -197,9 +232,7 @@ void UtunNode::runLoop(std::promise startup) { struct utun_config* cfg = parse_config(m_configPath.toUtf8().constData()); if (!cfg) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "desktop startup: parse_config failed path=%s", qPrintable(m_configPath)); - uasync_destroy(ua, 0); - m_ua = nullptr; - startup.set_value(QCoreApplication::translate("ChatGui", "Invalid configuration:\n%1").arg(m_configPath)); + failed(QCoreApplication::translate("ChatGui", "Invalid configuration:\n%1").arg(m_configPath)); return; } cfg->global.db_sync_enabled = 1; @@ -208,97 +241,88 @@ void UtunNode::runLoop(std::promise startup) { if (!m_dbPath.isEmpty()) snprintf(cfg->global.db_path, sizeof(cfg->global.db_path), "%s", m_dbPath.toUtf8().constData()); - m_instance = utun_instance_create_from_config(ua, cfg); - if (!m_instance) { + inst = utun_instance_create_from_config(ua, cfg); + if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop startup: instance creation failed"); free_config(cfg); - uasync_destroy(ua, 0); - m_ua = nullptr; - startup.set_value(QStringLiteral("utun_instance_create_from_config failed")); + failed(QStringLiteral("utun_instance_create_from_config failed")); return; } - m_instance->test_user_ptr = this; + inst->test_user_ptr = this; - if (utun_core_start(m_instance) != 0) { + if (utun_core_start(inst) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop startup: utun_core_start failed"); - utun_instance_destroy(m_instance); - m_instance = nullptr; - uasync_destroy(ua, 0); - m_ua = nullptr; - startup.set_value(QStringLiteral("utun_core_start failed")); + failed(QStringLiteral("utun_core_start failed")); return; } DEBUG_INFO(DEBUG_CATEGORY_SYS, "desktop service policy: utun_enabled=%d tun_enabled=%d", cfg->global.gui_utun_enabled, cfg->global.tun_enabled); - if (cfg->global.gui_utun_enabled && utun_service_start(m_instance) != 0) { + if (cfg->global.gui_utun_enabled && utun_service_start(inst) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "desktop startup: UTUN service failed tun_enabled=%d interface=%s", cfg->global.tun_enabled, cfg->global.tun_ifname); - utun_instance_destroy(m_instance); m_instance = nullptr; - uasync_destroy(ua, 0); m_ua = nullptr; - startup.set_value(QStringLiteral("utun_service_start failed; check TUN configuration, driver and permissions")); + failed(QStringLiteral("utun_service_start failed; check TUN configuration, driver and permissions")); return; } - { int sock_count = 0; for (struct ETCP_SOCKET* s = m_instance->etcp_sockets; s; s = s->next) sock_count++; + { int sock_count = 0; for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) sock_count++; DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "utun_node: after instance_init, etcp_sockets=%d conns=%d", - sock_count, queue_entry_count(m_instance->connections)); } + sock_count, queue_entry_count(inst->connections)); } - utun_add_nodeinfo_cbk(m_instance, gui_nodeinfo_cb_impl, nullptr); + utun_add_nodeinfo_cbk(inst, gui_nodeinfo_cb_impl, nullptr); /* Bridge chat events to GUI via gui_bridge */ - chat_event_set_handler(m_instance, [](struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { + chat_event_set_handler(inst, [](struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { (void)inst; gui_bridge_post(type, data, len); }); - if (chat_service_start(m_instance) != 0) { + if (chat_service_start(inst) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "desktop startup: chat_service_start failed"); - utun_instance_destroy(m_instance); m_instance = nullptr; - uasync_destroy(ua, 0); m_ua = nullptr; - startup.set_value(QStringLiteral("chat_service_start failed")); + failed(QStringLiteral("chat_service_start failed")); return; } - call_audio_init(m_instance); - call_video_port_attach(m_instance); - radio_audio_init(m_instance); - if (!gui_stream_attach(m_instance)) { - DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop stream bridge initialization failed"); - QMetaObject::invokeMethod(this, [this] { emit error("stream bridge initialization failed"); }); - } - chat_core_sync_my_addresses(m_instance); + if (call_audio_init(inst) != 0) { failed(QStringLiteral("call_audio_init failed")); return; } + if (call_video_port_attach(inst) != 0) { failed(QStringLiteral("call_video_port_attach failed")); return; } + if (radio_audio_init(inst) != 0) { failed(QStringLiteral("radio_audio_init failed")); return; } + if (!gui_stream_attach(inst)) { failed(QStringLiteral("stream bridge initialization failed")); return; } + chat_core_sync_my_addresses(inst); DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "chat_core + chat_sync initialized"); uasync_mark_running(ua); + updateSnapshot(inst); + m_instance = inst; gui_bridge_set_uasync(ua); - gui_bridge_set_inst(m_instance); + gui_bridge_set_inst(inst); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "utun_node: poll started ua=%p running=%d", (void*)ua, uasync_is_running(ua)); DEBUG_INFO(DEBUG_CATEGORY_SYS, "desktop services ready: core=%d utun=%d chat=%d tun=%d interface=%s", - m_instance->core_started, m_instance->utun_started, m_instance->chat_started, + inst->core_started, inst->utun_started, inst->chat_started, cfg->global.tun_enabled, cfg->global.tun_ifname); startup.set_value(QString()); - while (!m_stop && m_instance->running) { + const uint64_t generation = m_generation; + while (!m_stop && inst->running) { uasync_poll(ua, 100); + updateSnapshot(inst); } if (!m_stop) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop core requested stop; releasing services outside callbacks"); 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); - if (m_instance) { - DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "runLoop: destroying instance in worker thread"); - gui_stream_detach(m_instance); - call_video_port_detach(m_instance); - call_audio_destroy(m_instance); - radio_audio_destroy(m_instance); - utun_instance_destroy(m_instance); - m_instance = nullptr; - } + 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); + utun_instance_destroy(inst); DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "runLoop: emitting stopped"); - QMetaObject::invokeMethod(this, [this] { emit stopped(); }); + QMetaObject::invokeMethod(this, [this, generation] { + if (generation == m_generation) emit stopped(); + else DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "desktop stale stopped ignored: generation=%llu current=%llu", + (unsigned long long)generation, (unsigned long long)m_generation); + }); } diff --git a/tools/chatgui/transport/utun_node.h b/tools/chatgui/transport/utun_node.h index 0853f6a2..300006e1 100644 --- a/tools/chatgui/transport/utun_node.h +++ b/tools/chatgui/transport/utun_node.h @@ -1,10 +1,12 @@ -/* Desktop-владелец встроенного ядра: читает INI, создаёт uasync и запускает core/chat в std::thread. +/* Desktop-владелец единственного встроенного ядра: читает INI, создаёт uasync и запускает core/chat в std::thread. * [gui] utun_enabled=yes дополнительно запускает UTUN; tun_enabled управляет только созданием TUN. - * start подтверждает запуск только после готовности сервисов и poll. - * requestStop сигнализирует завершение, stop ждёт поток; worker уничтожает instance, - * finalize после завершения worker освобождает uasync и сбрасывает ссылку GUI-моста. - * instance возвращает заимствованный указатель: операции ядра отправлять в uasync через gui_bridge, - * согласовав время жизни с остановкой/перезапуском узла. */ + * start подтверждает готовность сервисов и медиапривязок перед входом в poll. + * start/stop/настройки принадлежат GUI-потоку; настройки менять при остановленном узле. + * requestStop допускается из другого потока, stop ждёт worker и освобождает uasync вне callbacks. + * Worker уничтожает instance; идентичность и NTP читаются через защищённую копию состояния, + * обновляемую перед подтверждением старта и после каждого poll. instance — только заимствованный + * указатель для кода ядра: использовать в uasync, согласовав время жизни с stop/restart. + * Logger общий для процесса; путь файла принадлежит объекту до закрытия лога. */ #ifndef UTUN_NODE_H #define UTUN_NODE_H @@ -14,6 +16,7 @@ #include #include #include +#include #include struct UTUN_INSTANCE; @@ -21,25 +24,27 @@ struct UTUN_INSTANCE; class UtunNode : public QObject { Q_OBJECT public: + // QObject принадлежит GUI; деструктор выполняет stop и закрывает собственный файл лога. explicit UtunNode(QObject* parent = nullptr); ~UtunNode(); - // Запускает сервисы по INI configPath; ждёт готовности, возвращает false после отката ошибки. + // Запускает сервисы по INI configPath; ждёт готовности. false — ошибка/уже запущен; см. startupError. bool start(const QString& configPath); // Причина последнего отказа start; читается в GUI-потоке после возврата start. QString startupError() const { return m_startupError; } - void requestStop(); - void stop(); + void requestStop(); // Попросить выход из poll и разбудить его; не ждёт уничтожения ядра. + void stop(); // Идемпотентно запросить остановку, join worker и очистить опубликованное состояние. + // true от начала start до stop/отката; самостоятельный выход worker сначала сообщает stopped. bool isRunning() const { return m_running; } - void finalize(); - void setDebugFile(const QString& path); - void setDbPath(const QString& path); - void setDebugLevel(const QString& level); - void setDebugCategories(const QString& categories); + void setDebugFile(const QString& path); // Файл общего logger-а, append; пустой — файл задаёт конфиг ядра. + void setDbPath(const QString& path); // Переопределить db_path INI; создать каталог, ошибку записать в лог. + void setDebugLevel(const QString& level); // Общий уровень; пустой/неизвестный — info. + void setDebugCategories(const QString& categories); // Overrides cat=level,... поверх sys=info,bgp=info. - struct UTUN_INSTANCE* instance() const { return m_instance; } + struct UTUN_INSTANCE* instance() const { return m_instance.load(); } + // Копии данных ядра, безопасны вне uasync; до старта/после stop — пустые строки, 0/false. QString nodeIdHex() const; QString pubKeyHex() const; int64_t ntpOffsetUs() const; @@ -47,21 +52,32 @@ public: bool ntpEnabled() const; signals: - void started(); - void stopped(); - void error(const QString& msg); + void started(); // Синхронно в GUI после успешного start. + void stopped(); // Через Qt-очередь после освобождения ядра; событие прежнего запуска отсекается. + void error(const QString& msg); // Ошибка start; stopped при отказе запуска не посылается. private: void runLoop(std::promise startup); + void finalize(); // Только GUI после join: сброс моста, уничтожение uasync, очистка копии состояния. + void updateSnapshot(const UTUN_INSTANCE* inst); // uasync читает ядро; NULL сбрасывает копию при finalize. - UTUN_INSTANCE* m_instance = nullptr; - struct UASYNC* m_ua = nullptr; - std::thread m_thread; - std::atomic m_running{false}; - std::atomic m_stop{false}; + struct Snapshot { + QString nodeId, publicKey; + int64_t ntpOffsetUs = 0; + bool ntpSynced = false, ntpEnabled = false; + } m_snapshot; // Копия для GUI; доступ под m_snapshotMutex, указателей ядра не содержит. + mutable std::mutex m_snapshotMutex; + std::atomic m_instance{nullptr}; // Публикуется после готовности; не продлевает жизнь ядра. + struct UASYNC* m_ua = nullptr; // Живёт до join; m_uaMutex защищает wakeup и откат неудачного старта. + std::mutex m_uaMutex; + std::thread m_thread; // Присоединяется GUI до повторного старта и уничтожения объекта. + std::atomic m_running{false}; // Узел владеет запущенным/останавливаемым worker. + std::atomic m_stop{false}; // Потокобезопасный запрос остановки. + uint64_t m_generation = 0; // Поколение запуска; GUI отбрасывает поздний stopped прежнего worker. QString m_configPath; QString m_startupError; QString m_debugFile; + QByteArray m_debugLogPath; // Стабильный UTF-8 путь, заимствованный общим logger-ом. QString m_dbPath; QString m_debugLevel; QString m_debugCategories;