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:
1 parent
bdb6bf80a4
commit
ffe563bb9f
13 files changed
+253
-26
No files matched your search
@@ -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"
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user