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.
This commit is contained in:
evgeny committed 2026-10-19 12:33:35 +02:00
1 parent bdb6bf80a4
commit ffe563bb9f
13 files changed
+253 -26

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
/* Desktop запускает UTUN вместе с чатом; ошибка TUN откатывает запуск до сигнала started. */
#include "../transport/utun_node.h"
#include "../transport/gui_bridge.h"
#include <QtTest>
#include <QFile>
#include <QTemporaryDir>
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<QByteArray>("settings");
QTest::addColumn<bool>("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<Probe*>(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"
+20 -2
View File
@@ -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