From ffe563bb9f6a741b9b88bca8daeadaf66557420d Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 19 Oct 2026 12:32:47 +0200 Subject: [PATCH] Enable VibeChat UTUN service and package the Wintun driver Add a settings button to open the current config in an external editor. Wait for core, UTUN and chat startup before reporting readiness; verify startup failures and restarts. Include Wintun DLL and license notices in Windows builds and installers. --- doc/service_lifecycle.md | 24 ++++- src/tun_windows.c | 2 +- tools/chatgui/CMakeLists.txt | 15 +++ tools/chatgui/build.sh | 7 +- tools/chatgui/package_win.sh | 8 +- tools/chatgui/src/settingsdialog.cpp | 32 ++++++ tools/chatgui/src/settingsdialog.h | 2 + tools/chatgui/tests/test_utun_node.cpp | 99 +++++++++++++++++++ tools/chatgui/tests/test_windows_installer.py | 22 ++++- tools/chatgui/translations/chatgui_en.ts | 4 + tools/chatgui/translations/chatgui_ru.ts | 4 + tools/chatgui/transport/utun_node.cpp | 53 +++++++--- tools/chatgui/transport/utun_node.h | 7 +- 13 files changed, 253 insertions(+), 26 deletions(-) create mode 100644 tools/chatgui/tests/test_utun_node.cpp diff --git a/doc/service_lifecycle.md b/doc/service_lifecycle.md index e308884d..8ecce733 100644 --- a/doc/service_lifecycle.md +++ b/doc/service_lifecycle.md @@ -22,11 +22,33 @@ utun_instance_destroy(inst); ``` `utun_instance_init()` — сокращение для запуска ядра, UTUN и чата, если чат включён -в конфигурации. Desktop GUI и Android запускают ядро и чат явно, без UTUN. +в конфигурации. Desktop GUI запускает ядро, UTUN и чат явно; TUN создаётся при +`[global] tun_enabled=yes`. Без TUN и NAT запуск чата не требует прав администратора. +Desktop подтверждает start только после готовности всех сервисов и uasync poll; +ошибка TUN откатывает запуск и возвращается в GUI. Android запускает ядро и чат без UTUN. Повторный start работающего сервиса и повторный stop допустимы. Ошибка старта сервиса откатывает созданные им ресурсы. `utun_instance_stop()` останавливает цикл приложения; освобождение ядра выполняет `utun_instance_destroy()`. +Для TUN в desktop `vibechat.cfg` задаются те же параметры, что и у CLI: + +```ini +[global] +tun_enabled=yes +tun_ifname=vibechat +tun_ip=10.42.0.1 + +[routing] +my_subnet=10.42.0.1/32 +route_subnet=10.42.0.0/24 +``` + +Адреса и маршруты выбираются под свою сеть; изменения применяются после перезапуска. +На Windows нужна `wintun.dll` рядом с `vibechat.exe` и запуск с правами администратора. +CMake копирует DLL и лицензии из `lib/`; Windows-упаковка явно включает DLL, проверяет +её архитектуру x64 и зависимости, поскольку `LoadLibrary` не отражается в импортах EXE. +На Linux необходимы доступ к `/dev/net/tun` и права на настройку интерфейса и маршрутов. + Успех запуска требует успешной инициализации всех обязательных ресурсов: заданных listen-сокетов, мониторинга, TUN и всех `route_subnet` при включённом TUN. Частичный набор сокетов или маршрутов — ошибка запуска. `utun_started` устанавливается только diff --git a/src/tun_windows.c b/src/tun_windows.c index 803af048..f624e7aa 100644 --- a/src/tun_windows.c +++ b/src/tun_windows.c @@ -47,7 +47,7 @@ static int wintun_load_dll(void) if (!wintun_dll) wintun_dll = LoadLibraryA("wintun.dll"); if (!wintun_dll) { - DEBUG_ERROR(DEBUG_CATEGORY_TUN, "wintun.dll not found"); + DEBUG_ERROR(DEBUG_CATEGORY_TUN, "Cannot load wintun.dll; place it beside the executable: error=%lu", GetLastError()); return -1; } diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 5ab20164..71d8a3a0 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -350,6 +350,16 @@ if(WIN32) set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/vibechat.rc" PROPERTIES OBJECT_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/resources/icons/app_icon.ico") message(STATUS "Windows executable icon: resources/icons/app_icon.ico") + # Wintun загружается через LoadLibrary и не попадает в обход импортов EXE. + add_custom_command(TARGET vibechat POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../../lib/wintun.dll" + "$/wintun.dll" + COMMAND ${CMAKE_COMMAND} -E make_directory "$/licenses/wintun" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_CURRENT_SOURCE_DIR}/../../lib/wintun-LICENSE.txt" + "${CMAKE_CURRENT_SOURCE_DIR}/../../lib/wintun-api-LICENSE.txt" + "$/licenses/wintun" + COMMENT "Deploying Wintun driver DLL and licenses" VERBATIM) endif() target_include_directories(vibechat PRIVATE ${CMAKE_SOURCE_DIR}/../../lib ${CMAKE_SOURCE_DIR}/../../src ${CMAKE_SOURCE_DIR}/db ${CMAKE_SOURCE_DIR}/../../lib/libopus/include ${FFMPEG_INCLUDE_DIRS}) @@ -594,6 +604,11 @@ target_include_directories(test_radio_mute_bridge PRIVATE ${UTUN_INCLUDE_DIRS}) target_link_libraries(test_radio_mute_bridge PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun_voice utun pthread) add_test(NAME test_radio_mute_bridge COMMAND test_radio_mute_bridge) set_tests_properties(test_radio_mute_bridge PROPERTIES TIMEOUT 15 ENVIRONMENT "QT_QPA_PLATFORM=offscreen") +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) +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) if(UNIX AND NOT APPLE) target_compile_definitions(test_radio_member_mute PRIVATE UTUN_TEST_WRAP_VAD) diff --git a/tools/chatgui/build.sh b/tools/chatgui/build.sh index df943c8b..fcd17d75 100755 --- a/tools/chatgui/build.sh +++ b/tools/chatgui/build.sh @@ -72,6 +72,11 @@ test ${PIPESTATUS[0]} -eq 0 || { log "Build FAILED"; exit 1; } log "" log "[3/4] Deploying runtime DLLs..." +# Повторная сборка восстанавливает DLL, даже если EXE не перелинковывался. +cp "$SRC_DIR/../../lib/wintun.dll" "$BUILD_DIR/wintun.dll" || { + log " ERROR: cannot deploy bundled wintun.dll to $BUILD_DIR"; exit 1; +} +log " OK: wintun.dll (bundled x64 TUN driver)" RLOTTIE_DLL="$SRC_DIR/rlottie_build/librlottie.dll" [ -f "$RLOTTIE_DLL" ] || { log " ERROR: librlottie.dll not found: $RLOTTIE_DLL"; exit 1; } cp "$RLOTTIE_DLL" "$BUILD_DIR/librlottie.dll" || { @@ -96,7 +101,7 @@ fi MAIN_DLLS=( "Qt${QT_MAJOR}Core.dll" "Qt${QT_MAJOR}Gui.dll" "Qt${QT_MAJOR}Network.dll" "Qt${QT_MAJOR}Widgets.dll" "platforms/qwindows.dll" "imageformats/qjpeg.dll" "imageformats/qwebp.dll" - "libstdc++-6.dll" "libgcc_s_*.dll" "libwinpthread-1.dll" "librlottie.dll" "zlib1.dll" "libcrypto-*.dll" + "libstdc++-6.dll" "libgcc_s_*.dll" "libwinpthread-1.dll" "librlottie.dll" "wintun.dll" "zlib1.dll" "libcrypto-*.dll" ) if [ -d "$FFMPEG_BIN" ]; then MAIN_DLLS+=("avcodec-*.dll" "avformat-*.dll" "avutil-*.dll" "swscale-*.dll" "swresample-*.dll") diff --git a/tools/chatgui/package_win.sh b/tools/chatgui/package_win.sh index 9a92bb3b..228ccd8c 100644 --- a/tools/chatgui/package_win.sh +++ b/tools/chatgui/package_win.sh @@ -50,7 +50,7 @@ if [[ -f "$BUILD_DIR/chatgui_capture_enabled.txt" && "$(tr -d '\r\n' < "$BUILD_D CAPTURE_RUNTIME=(Qt6Multimedia.dll Qt6MultimediaWidgets.dll multimedia/ffmpegmediaplugin.dll) fi for file in "${QT_PACKAGE}Core.dll" "${QT_PACKAGE}Gui.dll" "${QT_PACKAGE}Network.dll" "${QT_PACKAGE}Widgets.dll" \ - librlottie.dll platforms/qwindows.dll imageformats/qjpeg.dll imageformats/qwebp.dll "${CAPTURE_RUNTIME[@]}"; do + librlottie.dll wintun.dll platforms/qwindows.dll imageformats/qjpeg.dll imageformats/qwebp.dll "${CAPTURE_RUNTIME[@]}"; do [[ -f "$BUILD_DIR/$file" ]] || { echo "ERROR: missing runtime file: $BUILD_DIR/$file" >&2; exit 1; } done @@ -64,13 +64,15 @@ done mkdir -p "$PACKAGE_DIR/licenses/rlottie" cp "$SRC_DIR/rlottie_src/COPYING" "$PACKAGE_DIR/licenses/rlottie/" cp -R "$SRC_DIR/rlottie_src/licenses" "$PACKAGE_DIR/licenses/rlottie/" +mkdir -p "$PACKAGE_DIR/licenses/wintun" +cp "$SRC_DIR/../../lib/wintun-LICENSE.txt" "$SRC_DIR/../../lib/wintun-api-LICENSE.txt" "$PACKAGE_DIR/licenses/wintun/" # Проверяем архитектуру/полноту уже собранного комплекта, включая динамические Qt-плагины. SYSTEM_DIR=$(cygpath -u "${WINDIR:-C:\\Windows}/System32") source "$SRC_DIR/compiler_runtime.sh" declare -A LICENSE_PACKAGES=() declare -A INCLUDED=() -FILES=(vibechat.exe platforms/qwindows.dll imageformats/qjpeg.dll imageformats/qwebp.dll) +FILES=(vibechat.exe wintun.dll platforms/qwindows.dll imageformats/qjpeg.dll imageformats/qwebp.dll) FILES+=("${CAPTURE_RUNTIME[@]}") HAS_FFMPEG=0 for ((i = 0; i < ${#FILES[@]}; i++)); do @@ -101,7 +103,7 @@ for ((i = 0; i < ${#FILES[@]}; i++)); do echo "INFO: verified installer runtime: ${file#"$PACKAGE_DIR/"}" # Лицензии внешних библиотек берём из пакетов той же UCRT64-сборки. - [[ "$name" != vibechat.exe && "$name" != librlottie.dll ]] || continue + [[ "$name" != vibechat.exe && "$name" != librlottie.dll && "$name" != wintun.dll ]] || continue if [[ -f "$SRC_DIR/ffmpeg/ffmpeg_build/install/bin/$name" ]] && ! chatgui_is_compiler_runtime_dll "$name"; then HAS_FFMPEG=1 continue diff --git a/tools/chatgui/src/settingsdialog.cpp b/tools/chatgui/src/settingsdialog.cpp index e0a3e613..4e07a3f7 100644 --- a/tools/chatgui/src/settingsdialog.cpp +++ b/tools/chatgui/src/settingsdialog.cpp @@ -37,6 +37,10 @@ extern "C" { #include #include #include +#include +#include +#include +#include SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNode* node, AudioRecorder* recorder, QWidget* parent) @@ -246,6 +250,10 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod // --- Bottom buttons --- auto* btnLayout = new QHBoxLayout(); btnLayout->setSpacing(8); + auto* editConfigBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Edit config"), rightPanel); + editConfigBtn->setObjectName(QStringLiteral("editConfig")); + connect(editConfigBtn, &QPushButton::clicked, this, &SettingsDialog::onEditConfig); + btnLayout->addWidget(editConfigBtn); btnLayout->addStretch(); auto* cancelBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Cancel"), rightPanel); @@ -494,6 +502,30 @@ void SettingsDialog::onSave() { accept(); } +void SettingsDialog::onEditConfig() { + const QString path = QFileInfo(m_configPath).absoluteFilePath(); +#ifdef Q_OS_WIN + QProcess editor; + editor.setProgram(QStringLiteral("notepad.exe")); + editor.setArguments({path}); + const bool opened = editor.startDetached(); + if (!opened) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Settings: cannot start Notepad path=%s error=%s", qPrintable(path), qPrintable(editor.errorString())); + } +#else + const bool opened = QDesktopServices::openUrl(QUrl::fromLocalFile(path)); + if (!opened) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Settings: desktop service could not open config path=%s", qPrintable(path)); + } +#endif + if (!opened) { + QMessageBox::warning(this, QCoreApplication::translate("ChatGui", "Edit config"), + QCoreApplication::translate("ChatGui", "Could not open the configuration file in a text editor:\n%1").arg(path)); + return; + } + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Settings: config editor launched path=%s", qPrintable(path)); +} + void SettingsDialog::onOpenConnMonitor() { if (m_connMonitorWindow) { m_connMonitorWindow->raise(); diff --git a/tools/chatgui/src/settingsdialog.h b/tools/chatgui/src/settingsdialog.h index abed3197..f6e5eca6 100644 --- a/tools/chatgui/src/settingsdialog.h +++ b/tools/chatgui/src/settingsdialog.h @@ -33,6 +33,8 @@ public: private slots: void onCategoryChanged(int row); void onSave(); + // Открывает текущий конфиг во внешнем текстовом редакторе; ошибки показывает пользователю. + void onEditConfig(); void onOpenConnMonitor(); private: diff --git a/tools/chatgui/tests/test_utun_node.cpp b/tools/chatgui/tests/test_utun_node.cpp new file mode 100644 index 00000000..daec5d7e --- /dev/null +++ b/tools/chatgui/tests/test_utun_node.cpp @@ -0,0 +1,99 @@ +/* Desktop запускает UTUN вместе с чатом; ошибка TUN откатывает запуск до сигнала started. */ +#include "../transport/utun_node.h" +#include "../transport/gui_bridge.h" +#include +#include +#include + +extern "C" { +#include "utun_instance.h" +#include "tun_if.h" +#include "routing_layer/topo_group.h" +#include "u_async.h" +#include "debug_config.h" +} + +static const QByteArray identity = + "my_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n" + "my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n"; + +class UtunNodeTest : public QObject { + Q_OBJECT +private slots: + void initTestCase() { gui_bridge_init(); } + void cleanup() { debug_disable_file_output(); } + + void services_data() { + QTest::addColumn("settings"); + QTest::addColumn("withTun"); + QTest::newRow("chat-default") << QByteArray() << false; + QTest::newRow("tun-disabled") << QByteArray("tun_enabled=no\ntun_ifname=unused\ntun_ip=198.18.0.1\n") << false; +#ifndef Q_OS_WIN + QTest::newRow("tun-queues") << QByteArray("tun_enabled=yes\ntun_test_mode=1\ntun_ifname=vibetest\ntun_ip=198.18.0.1\n") << true; +#endif + if (qEnvironmentVariableIsSet("UTUN_TEST_REAL_TUN")) + QTest::newRow("tun-real") << QByteArray("tun_enabled=yes\ntun_ifname=vibetest\ntun_ip=198.18.0.1\n") << true; + } + + void services() { + QFETCH(QByteArray, settings); QFETCH(bool, withTun); + 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 + settings + "[server:local]\naddr=127.0.0.1:0\ntype=private\n" + "[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=info,tun=debug,general=info"); + QSignalSpy started(&node, &UtunNode::started); + for (int attempt = 0; attempt < 2; ++attempt) { + const bool running = node.start(path); + if (!running) { + directory.setAutoRemove(false); + qWarning() << "Startup diagnostics:" << directory.filePath("node.log"); + } + QVERIFY(running); QCOMPARE(started.count(), attempt + 1); + QVERIFY(node.isRunning() && gui_bridge_is_uasync_ready()); + bool ready = false, valid = false; + struct Probe { QObject* receiver; bool* ready; bool* valid; bool tun; } probe{this, &ready, &valid, withTun}; + gui_bridge_post_uasync_fn([](void* arg) { + auto* p = static_cast(arg); + auto* inst = gui_bridge_get_inst(); + const bool ok = inst && inst->core_started && inst->utun_started && inst->chat_started && + uasync_is_running(inst->ua) && topo_groups_get_default(inst->topo_groups) && + (inst->tun != nullptr) == p->tun; + DEBUG_INFO(DEBUG_CATEGORY_TUN, "desktop startup test: services=%d expected_tun=%d", ok, p->tun); + QMetaObject::invokeMethod(p->receiver, [p, ok] { *p->valid = ok; *p->ready = true; }, Qt::QueuedConnection); + }, &probe); + QTRY_VERIFY_WITH_TIMEOUT(ready, 3000); QVERIFY(valid); + node.stop(); + QVERIFY(!node.isRunning() && !node.instance() && !gui_bridge_get_inst() && !gui_bridge_is_uasync_ready()); + } + } + + void tunFailureRejectsStartup() { + QTemporaryDir directory; QVERIFY(directory.isValid()); + const QString path = directory.filePath("node.cfg"); + QFile config(path); QVERIFY(config.open(QIODevice::WriteOnly)); + // Нет tun_ip: отказ до обращения к ОС воспроизводится и на Windows без установленного драйвера. + const QByteArray text = "[global]\n" + identity + + "tun_enabled=yes\n[server:local]\naddr=127.0.0.1:0\ntype=private\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")); + QSignalSpy started(&node, &UtunNode::started), errors(&node, &UtunNode::error); + QVERIFY(!node.start(path)); QCOMPARE(started.count(), 0); + QVERIFY(!node.isRunning() && !node.instance() && !gui_bridge_get_inst() && !gui_bridge_is_uasync_ready()); + QTRY_COMPARE(errors.count(), 1); + const QString error = errors.first().first().toString(); + QVERIFY2(error.contains("utun_service_start"), qPrintable(error)); + QVERIFY(config.open(QIODevice::WriteOnly | QIODevice::Truncate)); + QByteArray disabled = text; disabled.replace("tun_enabled=yes", "tun_enabled=no"); + QCOMPARE(config.write(disabled), disabled.size()); config.close(); + QVERIFY(node.start(path)); QCOMPARE(started.count(), 1); + node.stop(); QVERIFY(!node.isRunning() && !gui_bridge_is_uasync_ready()); + } +}; +QTEST_MAIN(UtunNodeTest) +#include "test_utun_node.moc" diff --git a/tools/chatgui/tests/test_windows_installer.py b/tools/chatgui/tests/test_windows_installer.py index a5b99e36..418f02d9 100644 --- a/tools/chatgui/tests/test_windows_installer.py +++ b/tools/chatgui/tests/test_windows_installer.py @@ -52,6 +52,9 @@ try: shutil.copyfile(source / name, target) for name in ('LICENSE', 'LICENSING', 'THIRD_PARTY_NOTICES'): write(case / name, 'fixture license\n') + for name in ('wintun-LICENSE.txt', 'wintun-api-LICENSE.txt'): + write(case / 'lib' / name, 'Wintun notice\n') + write(case / 'lib/wintun.dll', 'DLL Name: KERNEL32.dll\n') write(gui / 'rlottie_src/COPYING', 'rlottie notice\n') write(gui / 'rlottie_src/licenses/COPYING.MIT', 'MIT notice\n') write(prefix / 'share/licenses/fixture/COPYING', 'runtime notice\n') @@ -91,7 +94,7 @@ files = sorted(str(p.relative_to(package)) for p in package.rglob('*') if p.is_f 'Qt6Widgets.dll': ['Qt6Core.dll', 'Qt6Gui.dll'], 'Qt6Core.dll': ['KERNEL32.dll', 'api-ms-win-core-test.dll'], 'Qt6Gui.dll': ['helper.dll'], 'Qt6Network.dll': ['Qt6Core.dll'], - 'helper.dll': ['Qt6Core.dll'], 'librlottie.dll': ['KERNEL32.dll'], + 'helper.dll': ['Qt6Core.dll'], 'librlottie.dll': ['KERNEL32.dll'], 'wintun.dll': ['KERNEL32.dll'], 'platforms/qwindows.dll': ['platform-helper.dll'], 'imageformats/qjpeg.dll': ['jpeg-helper.dll'], 'imageformats/qwebp.dll': ['webp-helper.dll'], 'platform-helper.dll': ['KERNEL32.dll'], 'jpeg-helper.dll': ['KERNEL32.dll'], 'webp-helper.dll': ['KERNEL32.dll'], @@ -103,7 +106,8 @@ files = sorted(str(p.relative_to(package)) for p in package.rglob('*') if p.is_f runtime = prefix / 'share/qt6/plugins' / name else: runtime = prefix / 'bin' / name - write(runtime, content) + if name != 'wintun.dll': + write(runtime, content) for name in ('vibechat.cfg', 'chatgui.cfg', 'chat_data/chats.db', 'chat_data/media/private.bin', 'chatgui.log', 'vibechat.lock', 'test_gui.exe', 'CMakeCache.txt', 'Qt5Core.dll', 'obsolete.dll'): write(build / name, 'PRIVATE OR STALE FILE\n') @@ -112,6 +116,7 @@ files = sorted(str(p.relative_to(package)) for p in package.rglob('*') if p.is_f assert not (build / 'installer').exists(), 'Preflight created build artifacts' result = package() expected = {'vibechat.exe', *libraries, 'licenses/LICENSE', 'licenses/LICENSING', 'licenses/THIRD_PARTY_NOTICES', + 'licenses/wintun/wintun-LICENSE.txt', 'licenses/wintun/wintun-api-LICENSE.txt', 'licenses/rlottie/COPYING', 'licenses/rlottie/licenses/COPYING.MIT', 'licenses/runtime/fixture/COPYING'} assert set(result['files']) == expected, result['files'] print('PASS: complete dependency graph, Qt plugin helpers and license notices; private files and stale DLLs excluded') @@ -120,6 +125,17 @@ files = sorted(str(p.relative_to(package)) for p in package.rglob('*') if p.is_f assert result2['files'] == result['files'] print('PASS: repeated packaging and paths containing spaces') + driver = build / 'wintun.dll' + saved_driver = driver.read_text() + driver.unlink() + failed = run(ok=False) + assert 'missing runtime file:' in failed.stderr and 'wintun.dll' in failed.stderr + write(driver, 'wrong-architecture\n') + failed = run(ok=False) + assert 'wintun.dll is not a Windows x64 binary' in failed.stderr + write(driver, saved_driver) + print('PASS: dynamically loaded Wintun and its licenses are included; missing/wrong architecture DLL is rejected') + capture = {'Qt6Multimedia.dll': ['Qt6Core.dll'], 'Qt6MultimediaWidgets.dll': ['Qt6Multimedia.dll'], 'multimedia/ffmpegmediaplugin.dll': ['capture-helper.dll', 'MFPlat.dll'], 'capture-helper.dll': ['KERNEL32.dll']} write(root / 'Windows/System32/MFPlat.dll', 'system multimedia file\n') @@ -207,11 +223,13 @@ printf '%s\\n' "$*" >> "$CMAKE_CALLS" original_exe += f'DLL Name: {name}\n' write(build / 'vibechat.exe', original_exe) calls = root / 'cmake-calls.log' + (build / 'wintun.dll').unlink() built = subprocess.run(['bash', str(gui / 'build.sh'), '--installer', '-j2'], cwd=gui, env={**environment, 'CMAKE_CALLS': str(calls)}, text=True, capture_output=True) assert built.returncode == 0, built.stdout + built.stderr assert '-DCMAKE_BUILD_TYPE=Release' in calls.read_text() assert 'installer created:' in built.stdout and 'Build SUCCESS' in built.stdout + assert (build / 'wintun.dll').read_bytes() == (case / 'lib/wintun.dll').read_bytes() failed = subprocess.run(['bash', str(gui / 'build.sh'), '--installer'], cwd=gui, env={**environment, 'CMAKE_CALLS': str(calls), 'COMPILER_FAIL': '1'}, text=True, capture_output=True) assert failed.returncode != 0 and 'Build SUCCESS' not in failed.stdout diff --git a/tools/chatgui/translations/chatgui_en.ts b/tools/chatgui/translations/chatgui_en.ts index 2b67e926..c4a5d710 100644 --- a/tools/chatgui/translations/chatgui_en.ts +++ b/tools/chatgui/translations/chatgui_en.ts @@ -3,6 +3,10 @@ ChatGui + Edit configEdit config + Could not open the configuration file in a text editor: +%1Could not open the configuration file in a text editor: +%1 Maximum QPMaximum QP Adapt QP to the target bitrateAdapt QP to the target bitrate Current QP ceiling: %1Current QP ceiling: %1 diff --git a/tools/chatgui/translations/chatgui_ru.ts b/tools/chatgui/translations/chatgui_ru.ts index 582d3de5..a2a4371d 100644 --- a/tools/chatgui/translations/chatgui_ru.ts +++ b/tools/chatgui/translations/chatgui_ru.ts @@ -3,6 +3,10 @@ ChatGui + Edit configРедактировать конфиг + Could not open the configuration file in a text editor: +%1Не удалось открыть конфиг в текстовом редакторе: +%1 Maximum QPВерхний предел QP Adapt QP to the target bitrateПодстраивать QP под заданный битрейт Current QP ceiling: %1Текущий предел QP: %1 diff --git a/tools/chatgui/transport/utun_node.cpp b/tools/chatgui/transport/utun_node.cpp index fa50bee0..184197ff 100644 --- a/tools/chatgui/transport/utun_node.cpp +++ b/tools/chatgui/transport/utun_node.cpp @@ -2,6 +2,7 @@ #include "utun_node.h" #include "stream_bridge.h" #include +#include #include #include "../../lib/socket_compat.h" @@ -47,15 +48,12 @@ bool UtunNode::start(const QString& configPath) { m_configPath = configPath; m_stop = false; m_running = true; - m_thread = std::thread(&UtunNode::runLoop, this); - - for (int i = 0; i < 50 && m_running && !m_instance; ++i) - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - if (!m_instance) { - m_stop = true; + std::promise startup; + auto ready = startup.get_future(); + m_thread = std::thread(&UtunNode::runLoop, this, std::move(startup)); + if (!ready.get()) { if (m_thread.joinable()) m_thread.join(); - m_running = false; + finalize(); return false; } emit started(); @@ -125,7 +123,7 @@ bool UtunNode::ntpEnabled() const { return m_instance ? m_instance->ntp.enabled : false; } -void UtunNode::runLoop() { +void UtunNode::runLoop(std::promise startup) { debug_config_init(); debug_enable_function_name(0); @@ -180,11 +178,13 @@ void UtunNode::runLoop() { g_debug_config.thread_marker = 2; } - utun_instance_set_tun_init_enabled(0); + utun_instance_set_tun_init_enabled(1); struct UASYNC* ua = uasync_create(); if (!ua) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop startup: uasync_create failed"); QMetaObject::invokeMethod(this, [this] { emit error("uasync_create failed"); }); + startup.set_value(false); return; } m_ua = ua; @@ -194,9 +194,11 @@ void UtunNode::runLoop() { * иначе auto_socket не сможет сохранить/переиспользовать порты (БД ещё не открыта). */ 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)); QMetaObject::invokeMethod(this, [this] { emit error("parse_config failed"); }); uasync_destroy(ua, 0); m_ua = nullptr; + startup.set_value(false); return; } cfg->global.db_sync_enabled = 1; @@ -207,25 +209,35 @@ void UtunNode::runLoop() { m_instance = utun_instance_create_from_config(ua, cfg); if (!m_instance) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop startup: instance creation failed"); free_config(cfg); QMetaObject::invokeMethod(this, [this] { emit error("utun_instance_create_from_config failed"); }); uasync_destroy(ua, 0); m_ua = nullptr; + startup.set_value(false); return; } m_instance->test_user_ptr = this; - /* set ua for gui_bridge before init, so GUI can post */ - gui_bridge_set_uasync(ua); - gui_bridge_set_inst(m_instance); - if (utun_core_start(m_instance) != 0) { - QMetaObject::invokeMethod(this, [this] { emit error("utun_instance_init failed"); }); + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop startup: utun_core_start failed"); + QMetaObject::invokeMethod(this, [this] { emit error("utun_core_start failed"); }); utun_instance_destroy(m_instance); m_instance = nullptr; uasync_destroy(ua, 0); m_ua = nullptr; + startup.set_value(false); + return; + } + + if (utun_service_start(m_instance) != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_TUN, "desktop startup: UTUN service failed tun_enabled=%d interface=%s", + cfg->global.tun_enabled, cfg->global.tun_ifname); + QMetaObject::invokeMethod(this, [this] { emit error("utun_service_start failed; check TUN configuration, driver and permissions"); }); + utun_instance_destroy(m_instance); m_instance = nullptr; + uasync_destroy(ua, 0); m_ua = nullptr; + startup.set_value(false); return; } @@ -242,9 +254,12 @@ void UtunNode::runLoop() { }); if (chat_service_start(m_instance) != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "desktop startup: chat_service_start failed"); QMetaObject::invokeMethod(this, [this] { emit error("chat_service_start failed"); }); utun_instance_destroy(m_instance); m_instance = nullptr; - uasync_destroy(ua, 0); m_ua = nullptr; return; + uasync_destroy(ua, 0); m_ua = nullptr; + startup.set_value(false); + return; } call_audio_init(m_instance); call_video_port_attach(m_instance); @@ -257,7 +272,13 @@ void UtunNode::runLoop() { DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "chat_core + chat_sync initialized"); uasync_mark_running(ua); + gui_bridge_set_uasync(ua); + gui_bridge_set_inst(m_instance); 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, + cfg->global.tun_enabled, cfg->global.tun_ifname); + startup.set_value(true); while (!m_stop) { uasync_poll(ua, 100); diff --git a/tools/chatgui/transport/utun_node.h b/tools/chatgui/transport/utun_node.h index 8ad72fec..820f1655 100644 --- a/tools/chatgui/transport/utun_node.h +++ b/tools/chatgui/transport/utun_node.h @@ -1,4 +1,5 @@ -/* Desktop-владелец встроенного ядра: читает INI, создаёт uasync и запускает core/chat в std::thread. +/* Desktop-владелец встроенного ядра: читает INI, создаёт uasync и запускает core/UTUN/chat в std::thread. + * TUN создаётся по конфигу; start подтверждает запуск только после готовности сервисов и poll. * requestStop сигнализирует завершение, stop ждёт поток; worker уничтожает instance, * finalize после завершения worker освобождает uasync и сбрасывает ссылку GUI-моста. * instance возвращает заимствованный указатель: операции ядра отправлять в uasync через gui_bridge, @@ -11,6 +12,7 @@ #include #include #include +#include #include struct UTUN_INSTANCE; @@ -21,6 +23,7 @@ public: explicit UtunNode(QObject* parent = nullptr); ~UtunNode(); + // Запускает сервисы по INI configPath; ждёт готовности, возвращает false после отката ошибки. bool start(const QString& configPath); void requestStop(); void stop(); @@ -46,7 +49,7 @@ signals: void error(const QString& msg); private: - void runLoop(); + void runLoop(std::promise startup); UTUN_INSTANCE* m_instance = nullptr; struct UASYNC* m_ua = nullptr;