Fix desktop node startup rollback and state ownership
This commit is contained in:
1 parent
b163642b52
commit
d22bae19b4
5 files changed
+297
-110
No files matched your search
@@ -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 оставляет подготовленные
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user