Fix desktop node startup rollback and state ownership

This commit is contained in:
evgeny committed 2026-10-27 01:38:26 +01:00
1 parent b163642b52
commit d22bae19b4
5 files changed
+297 -110

No files matched your search

+1 -1
View File
@@ -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 оставляет подготовленные
+4
View File
@@ -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)
+143
View File
@@ -3,6 +3,7 @@
#include "../transport/gui_bridge.h"
#include <QtTest>
#include <QFile>
#include <QDir>
#include <QTemporaryDir>
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<int> 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<int>("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");
+110 -86
View File
@@ -3,6 +3,7 @@
#include "stream_bridge.h"
#include <cinttypes>
#include <utility>
#include <system_error>
#include <QCoreApplication>
#include <QDir>
#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<QString> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> guard(m_snapshotMutex);
return m_snapshot.publicKey;
}
int64_t UtunNode::ntpOffsetUs() const {
return m_instance ? m_instance->ntp.offset_us : 0;
std::lock_guard<std::mutex> guard(m_snapshotMutex);
return m_snapshot.ntpOffsetUs;
}
bool UtunNode::ntpSynced() const {
return m_instance ? m_instance->ntp.synced : false;
std::lock_guard<std::mutex> guard(m_snapshotMutex);
return m_snapshot.ntpSynced;
}
bool UtunNode::ntpEnabled() const {
return m_instance ? m_instance->ntp.enabled : false;
std::lock_guard<std::mutex> guard(m_snapshotMutex);
return m_snapshot.ntpEnabled;
}
/* GUI получает только собственные строки/числа; изменяемые поля ядра читает его поток. */
void UtunNode::updateSnapshot(const UTUN_INSTANCE* inst) {
std::lock_guard<std::mutex> 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<QString> startup) {
// Повторный init не закрывает прежний FILE; путь прошлого запуска пока ещё жив.
debug_disable_file_output();
debug_config_init();
debug_enable_function_name(0);
@@ -150,33 +186,13 @@ void UtunNode::runLoop(std::promise<QString> 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<QString> startup) {
startup.set_value(QStringLiteral("uasync_create failed"));
return;
}
m_ua = ua;
{
std::lock_guard<std::mutex> 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<std::mutex> 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<QString> 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<QString> 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);
});
}
+39 -23
View File
@@ -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 <atomic>
#include <cstdint>
#include <future>
#include <mutex>
#include <thread>
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<QString> 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<bool> m_running{false};
std::atomic<bool> 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<UTUN_INSTANCE*> m_instance{nullptr}; // Публикуется после готовности; не продлевает жизнь ядра.
struct UASYNC* m_ua = nullptr; // Живёт до join; m_uaMutex защищает wakeup и откат неудачного старта.
std::mutex m_uaMutex;
std::thread m_thread; // Присоединяется GUI до повторного старта и уничтожения объекта.
std::atomic<bool> m_running{false}; // Узел владеет запущенным/останавливаемым worker.
std::atomic<bool> 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;