From 61ed80e18cf69b4d22d5b5c6215a60fa405df4cc Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 02:49:39 +0100 Subject: [PATCH] Share the ETCP monitor across desktop entry points --- tools/chatgui/CMakeLists.txt | 2 +- tools/chatgui/src/connmonitorwindow.cpp | 58 ++++++++++++---- tools/chatgui/src/connmonitorwindow.h | 16 +++-- tools/chatgui/src/mainwindow.cpp | 13 +--- tools/chatgui/src/mainwindow.h | 3 - tools/chatgui/src/settingsdialog.cpp | 12 +--- tools/chatgui/src/settingsdialog.h | 3 - tools/chatgui/tests/test_message_events.cpp | 74 +++++++++++++++++++++ 8 files changed, 131 insertions(+), 50 deletions(-) diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 172b0001..7e0daedd 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -492,7 +492,7 @@ else() endif() set_tests_properties(test_message_notification PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 60) -add_executable(test_message_events tests/test_message_events.cpp transport/gui_bridge_impl.cpp) +add_executable(test_message_events tests/test_message_events.cpp transport/gui_bridge_impl.cpp src/connmonitorwindow.cpp) target_link_libraries(test_message_events PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun utun_voice pthread ${CMAKE_DL_LIBS}) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_compile_definitions(test_message_events PRIVATE UTUN_TEST_BRIDGE_ALLOC_FAILURES) diff --git a/tools/chatgui/src/connmonitorwindow.cpp b/tools/chatgui/src/connmonitorwindow.cpp index 62c5efb0..53324d90 100644 --- a/tools/chatgui/src/connmonitorwindow.cpp +++ b/tools/chatgui/src/connmonitorwindow.cpp @@ -16,6 +16,8 @@ extern "C" { #include #include #include +#include +#include #include #define CONN_LIST_ENTRY_SIZE 46 @@ -30,8 +32,18 @@ static void connMetricsCb(const uint8_t* data, int len) { if (g_activeConnMonitor) g_activeConnMonitor->onConnMetrics(data, len); } -ConnMonitorWindow::ConnMonitorWindow(QWidget* parent) - : QDialog(parent) +/* Один получатель bridge callbacks для всех меню/настроек, без указателей-владельцев в этих окнах. */ +ConnMonitorWindow* ConnMonitorWindow::showMonitor() { + auto* window = g_activeConnMonitor; + if (!window) window = new ConnMonitorWindow; + if (window->isMinimized()) window->showNormal(); else window->show(); + window->raise(); window->activateWindow(); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "connection monitor shown: window=%p", (void*)window); + return window; +} + +ConnMonitorWindow::ConnMonitorWindow() + : QDialog(nullptr) { setWindowTitle(QCoreApplication::translate("ChatGui", "Connection Monitor")); resize(900, 550); @@ -89,7 +101,11 @@ ConnMonitorWindow::ConnMonitorWindow(QWidget* parent) ConnMonitorWindow::~ConnMonitorWindow() { m_timer->stop(); - if (g_activeConnMonitor == this) g_activeConnMonitor = nullptr; + if (g_activeConnMonitor == this) { + g_activeConnMonitor = nullptr; + gui_bridge_set_conn_list_cb(nullptr); gui_bridge_set_conn_metrics_cb(nullptr); + } + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "connection monitor destroyed: window=%p", (void*)this); } void ConnMonitorWindow::showEvent(QShowEvent* event) { @@ -102,6 +118,16 @@ void ConnMonitorWindow::hideEvent(QHideEvent* event) { QDialog::hideEvent(event); } +/* Снять singleton сразу при принятом close, не дожидаясь Qt::DeferredDelete. */ +void ConnMonitorWindow::closeEvent(QCloseEvent* event) { + QDialog::closeEvent(event); + if (event->isAccepted() && g_activeConnMonitor == this) { + g_activeConnMonitor = nullptr; + gui_bridge_set_conn_list_cb(nullptr); gui_bridge_set_conn_metrics_cb(nullptr); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "connection monitor closed: window=%p", (void*)this); + } +} + void ConnMonitorWindow::onTimerTick() { if (m_details->textCursor().hasSelection()) return; if (QApplication::mouseButtons()) return; @@ -114,6 +140,7 @@ void ConnMonitorWindow::onConnSelected(QListWidgetItem* current, QListWidgetItem (void)previous; if (!current) { m_selectedNodeId = 0; m_details->clear(); return; } m_selectedNodeId = (uint64_t)current->data(Qt::UserRole).toULongLong(); + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "connection monitor selection: node=%016llx", (unsigned long long)m_selectedNodeId); requestMetricsForSelected(); } @@ -128,8 +155,11 @@ void ConnMonitorWindow::requestMetricsForSelected() { } void ConnMonitorWindow::onConnList(const uint8_t* data, int len) { - if (len < 2) return; + if (!data || len < 2) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "connection monitor: invalid list header bytes=%d", len); return; } uint16_t count; memcpy(&count, data, 2); + if (count > 250 || len != 2 + count * CONN_LIST_ENTRY_SIZE) { + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "connection monitor: invalid list count=%u bytes=%d", count, len); return; + } const uint8_t* p = data + 2; QHash existing; @@ -138,9 +168,9 @@ void ConnMonitorWindow::onConnList(const uint8_t* data, int len) { existing[(uint64_t)item->data(Qt::UserRole).toULongLong()] = item; } - m_connList->blockSignals(true); + QSignalBlocker blocker(m_connList); - for (uint16_t i = 0; i < count && (p + CONN_LIST_ENTRY_SIZE) <= (data + len); i++) { + for (uint16_t i = 0; i < count; i++) { uint64_t nodeId; memcpy(&nodeId, p, 8); p += 8; uint8_t flags = *p++; uint8_t linkCount = *p++; @@ -162,7 +192,7 @@ void ConnMonitorWindow::onConnList(const uint8_t* data, int len) { else if (proxyClient) proxyRole = " C"; else if (proxyServer) proxyRole = " S"; - QString nodeHex = QString("%1").arg(nodeId, 16, 16, QChar('0')).right(16).left(4); + QString nodeHex = QString("%1").arg(nodeId, 16, 16, QChar('0')).right(4); QString rttStr = rtt > 0 ? QString::number(rtt / 10.0f, 'f', 1) : "-"; QString text = QString("[%1] %2 %3%4 %5L rtt=%6ms") .arg(nodeHex) @@ -188,16 +218,16 @@ void ConnMonitorWindow::onConnList(const uint8_t* data, int len) { for (auto it = existing.begin(); it != existing.end(); ++it) delete *it; - m_connList->blockSignals(false); - - if (!m_selectedNodeId && m_connList->count() > 0) { - m_connList->setCurrentRow(0); - onConnSelected(m_connList->item(0), nullptr); - } + if (!m_connList->currentItem() && m_connList->count() > 0) m_connList->setCurrentRow(0); + auto* current = m_connList->currentItem(); + const uint64_t selected = current ? current->data(Qt::UserRole).toULongLong() : 0; + blocker.unblock(); + if (selected != m_selectedNodeId) onConnSelected(current, nullptr); } void ConnMonitorWindow::onConnMetrics(const uint8_t* data, int len) { - if (len <= 0) return; + if (!data || len <= 0) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "connection monitor: invalid metrics bytes=%d", len); return; } + if (!m_selectedNodeId) return; int scrollVal = m_details->verticalScrollBar()->value(); m_details->setPlainText(QString::fromUtf8((const char*)data, len)); m_details->verticalScrollBar()->setValue(scrollVal); diff --git a/tools/chatgui/src/connmonitorwindow.h b/tools/chatgui/src/connmonitorwindow.h index 457c42ff..7ac8455e 100644 --- a/tools/chatgui/src/connmonitorwindow.h +++ b/tools/chatgui/src/connmonitorwindow.h @@ -1,6 +1,7 @@ /* Немодальное окно диагностики ETCP: список соединений и текстовые метрики выбранного узла. - * Создавать в куче, открывать через show(): закрытие удаляет окно (WA_DeleteOnClose). - * Одновременно используется один экземпляр: он получает глобальные callbacks gui_bridge. + * Открывать через showMonitor(): возвращает существующее окно или создаёт новое без parent. + * Указатель заимствован до close, которое удаляет окно (WA_DeleteOnClose). Фабрика гарантирует + * один экземпляр-получатель глобальных callbacks gui_bridge для всех точек входа GUI. * Все методы работают в GUI-потоке; ядро собирает снимки асинхронно в uasync. * Таймер 100 мс запускается при создании/show и останавливается при hide. Выделенный текст * или нажатая кнопка мыши приостанавливают новые запросы, но не применение готовых ответов. */ @@ -15,25 +16,28 @@ class ConnMonitorWindow : public QDialog { Q_OBJECT public: - // Создать окно и зарегистрировать callbacks; parent задаёт обычное Qt-владение. - explicit ConnMonitorWindow(QWidget* parent = nullptr); + // Показать/поднять единственный монитор; повторный вызов не меняет его callbacks и выбор. + static ConnMonitorWindow* showMonitor(); ~ConnMonitorWindow() override; - // Применить список [count:uint16][entry:46B]* в native endian. data заимствован на время вызова. + // Применить полный список [count:uint16][entry:46B]* (count<=250), native endian. + // Неверная длина не меняет список/выбор. data заимствован на время вызова. void onConnList(const uint8_t* data, int len); // Показать len байт UTF-8 без требования завершающего NUL, сохранив позицию прокрутки. - // data заимствован на время вызова; текст копируется в документ Qt. + // data заимствован на время вызова; без выбранного узла ответ игнорируется. void onConnMetrics(const uint8_t* data, int len); protected: void showEvent(QShowEvent* event) override; void hideEvent(QHideEvent* event) override; + void closeEvent(QCloseEvent* event) override; private slots: void onTimerTick(); void onConnSelected(QListWidgetItem* current, QListWidgetItem* previous); private: + ConnMonitorWindow(); // Создание принадлежит фабрике; окно само удаляется при закрытии. void requestMetricsForSelected(); QListWidget* m_connList; // Список узлов, Qt::UserRole содержит полный node_id; принадлежит окну. diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index 88c07d63..db0d0111 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -771,7 +771,7 @@ void MainWindow::setupConnects() { connect(m_channelList, &ChannelList::joinRequested, this, &MainWindow::showJoinDialog); connect(m_channelList, &ChannelList::createGroupRequested, this, &MainWindow::onCreateGroupRequested); connect(m_channelList, &ChannelList::settingsRequested, this, &MainWindow::showSettings); - connect(m_channelList, &ChannelList::connMonitorRequested, this, &MainWindow::showConnMonitor); + connect(m_channelList, &ChannelList::connMonitorRequested, this, [] { ConnMonitorWindow::showMonitor(); }); connect(m_channelList, &ChannelList::dmDeleteRequested, this, &MainWindow::onDmDeleteRequested); connect(m_messageList, &MessageList::channelSettingsRequested, this, &MainWindow::showChannelSettings); connect(m_accountList, &AccountList::memberRenamed, @@ -1333,17 +1333,6 @@ void MainWindow::showSettings() { reloadChannels(); } -void MainWindow::showConnMonitor() { - if (m_connMonitor) { - m_connMonitor->raise(); - m_connMonitor->activateWindow(); - return; - } - m_connMonitor = new ConnMonitorWindow(nullptr); - connect(m_connMonitor, &QDialog::destroyed, this, [this] { m_connMonitor = nullptr; }); - m_connMonitor->show(); -} - void MainWindow::openVideo(const QString& filePath) { if (!m_videoPlayer) { m_videoPlayer = new VideoPlayerWindow(nullptr); diff --git a/tools/chatgui/src/mainwindow.h b/tools/chatgui/src/mainwindow.h index 61de3d19..29f364f4 100644 --- a/tools/chatgui/src/mainwindow.h +++ b/tools/chatgui/src/mainwindow.h @@ -19,7 +19,6 @@ class DbManager; class QSplitter; class UtunNode; class AudioRecorder; -class ConnMonitorWindow; class VideoPlayerWindow; class StreamPlayerWindow; class StreamGridWindow; @@ -59,7 +58,6 @@ private slots: void showJoinDialog(); void onCreateGroupRequested(); void showSettings(); - void showConnMonitor(); void openVideo(const QString& filePath); void openImage(const QString& filePath); void showChannelSettings(const QString& channelId); @@ -134,7 +132,6 @@ private: quint64 m_dmDeleteSequence = 0; QHash> m_dmDeleteRequests; /* request_id → conv_id и удаляемое поколение */ AudioRecorder *m_recorder = nullptr; - ConnMonitorWindow *m_connMonitor = nullptr; VideoPlayerWindow *m_videoPlayer = nullptr; QPointer m_streamPlayer; QPointer m_streamGrid; diff --git a/tools/chatgui/src/settingsdialog.cpp b/tools/chatgui/src/settingsdialog.cpp index 07fc1cda..6fe9d001 100644 --- a/tools/chatgui/src/settingsdialog.cpp +++ b/tools/chatgui/src/settingsdialog.cpp @@ -204,7 +204,7 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod connLayout->addWidget(hintLabel); auto* openBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Open Connection Monitor"), connPage); openBtn->setStyleSheet("QPushButton { padding: 10px 20px; font-size: 13px; }"); - connect(openBtn, &QPushButton::clicked, this, &SettingsDialog::onOpenConnMonitor); + connect(openBtn, &QPushButton::clicked, this, [] { ConnMonitorWindow::showMonitor(); }); connLayout->addWidget(openBtn); m_pages->addWidget(connPage); } @@ -557,13 +557,3 @@ void SettingsDialog::onEditConfig() { } DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Settings: config editor launched path=%s", qPrintable(path)); } - -void SettingsDialog::onOpenConnMonitor() { - if (m_connMonitorWindow) { - m_connMonitorWindow->raise(); - m_connMonitorWindow->activateWindow(); - return; - } - m_connMonitorWindow = new ConnMonitorWindow(nullptr); - m_connMonitorWindow->show(); -} diff --git a/tools/chatgui/src/settingsdialog.h b/tools/chatgui/src/settingsdialog.h index 8f7e4065..cc923a3c 100644 --- a/tools/chatgui/src/settingsdialog.h +++ b/tools/chatgui/src/settingsdialog.h @@ -15,7 +15,6 @@ class NetworkSettingsPage; class DatabaseSettingsPage; class StatusPage; class NodesPage; -class ConnMonitorWindow; class SoundSettingsPage; class AudioDeviceSettingsPage; class StorageSettingsPage; @@ -35,7 +34,6 @@ private slots: void onSave(); // Открывает текущий конфиг во внешнем текстовом редакторе; ошибки показывает пользователю. void onEditConfig(); - void onOpenConnMonitor(); private: void loadProfileFromConfig(const QString& path); @@ -57,7 +55,6 @@ private: DatabaseSettingsPage* m_databasePage; StatusPage* m_statusPage; NodesPage* m_nodesPage; - ConnMonitorWindow* m_connMonitorWindow = nullptr; SoundSettingsPage* m_soundPage; AudioDeviceSettingsPage* m_audioDevicePage; StorageSettingsPage* m_storagePage; diff --git a/tools/chatgui/tests/test_message_events.cpp b/tools/chatgui/tests/test_message_events.cpp index e1371e87..e839868d 100644 --- a/tools/chatgui/tests/test_message_events.cpp +++ b/tools/chatgui/tests/test_message_events.cpp @@ -1,5 +1,6 @@ /* Формат канального события: live/history сохраняются при передаче в GUI, битые события отбрасываются. */ #include "../transport/gui_bridge.h" +#include "../src/connmonitorwindow.h" #include "../../../lib/debug_config.h" #include "../../../src/chat/chat_event.h" #include "../../../src/chat/chat_core.h" @@ -10,6 +11,7 @@ #include #include #include +#include #ifdef UTUN_TEST_BRIDGE_ALLOC_FAILURES static int failAllocation; @@ -48,6 +50,29 @@ struct BoundCore { } }; +struct MonitorGuard { + QPointer window = ConnMonitorWindow::showMonitor(); + MonitorGuard() { window->findChild()->stop(); } + ~MonitorGuard() { delete window.data(); } +}; + +static QByteArray connection_list(std::initializer_list nodes) { + QByteArray bytes(2 + 46 * nodes.size(), '\0'); + uint16_t count = nodes.size(); memcpy(bytes.data(), &count, 2); + int index = 0; + for (quint64 node : nodes) { + char* row = bytes.data() + 2 + 46 * index++; + memcpy(row, &node, 8); row[8] = 3; row[9] = 1; memcpy(row + 14, "peer", 4); + } + return bytes; +} + +static int pending_count(UASYNC* ua) { + int count = 0; + for (auto* task = ua->posted_tasks_head; task; task = task->next) count++; + return count; +} + static void cpu_received(int percent) { cpuLoads.append(percent); cpuOnGuiThread &= QThread::currentThread() == qApp->thread(); @@ -261,6 +286,55 @@ private slots: DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "[PASS] bridge detach race: accepted=%u rejected=%u, producer joined, no leaks", accepted, rejected); } + void monitorSelectionSendsOneRequestAndRecoversAfterRemoval() { + BoundCore core; QVERIFY(core.ua); core.attach(); + MonitorGuard monitor; + auto* window = monitor.window.data(); + auto* list = window->findChild(); QVERIFY(list); + auto* details = window->findChild(); QVERIFY(details); + auto snapshot = connection_list({0x112233445566abcd, 0x2222}); + window->onConnList(reinterpret_cast(snapshot.constData()), snapshot.size()); + QCOMPARE(list->count(), 2); QCOMPARE(pending_count(core.ua), 1); + QVERIFY(list->currentItem()->text().contains("[abcd]")); + snapshot = connection_list({0x2222}); + window->onConnList(reinterpret_cast(snapshot.constData()), snapshot.size()); + QCOMPARE(list->currentItem()->data(Qt::UserRole).toULongLong(), quint64(0x2222)); + QCOMPARE(pending_count(core.ua), 2); + window->onConnMetrics(reinterpret_cast("current metrics"), 15); + snapshot = connection_list({}); + window->onConnList(reinterpret_cast(snapshot.constData()), snapshot.size()); + QVERIFY(details->toPlainText().isEmpty()); + window->onConnMetrics(reinterpret_cast("late metrics"), 12); + QVERIFY(details->toPlainText().isEmpty()); + } + + void monitorRejectsIncompleteListsWithoutDestroyingExistingRows() { + BoundCore core; QVERIFY(core.ua); core.attach(); + MonitorGuard monitor; + auto* window = monitor.window.data(); + auto* list = window->findChild(); QVERIFY(list); + auto snapshot = connection_list({7, 8}); + window->onConnList(reinterpret_cast(snapshot.constData()), snapshot.size()); + window->onConnList(reinterpret_cast(snapshot.constData()), 2); + QCOMPARE(list->count(), 2); + QCOMPARE(list->currentItem()->data(Qt::UserRole).toULongLong(), quint64(7)); + } + + void monitorFactoryReusesWindowAndReopensBeforeDeferredDelete() { + BoundCore core; QVERIFY(core.ua); core.attach(); + MonitorGuard first; + QCOMPARE(ConnMonitorWindow::showMonitor(), first.window.data()); + first.window->close(); + MonitorGuard second; + QVERIFY(second.window); + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + QVERIFY(!first.window); QCOMPARE(ConnMonitorWindow::showMonitor(), second.window.data()); + const auto snapshot = connection_list({7}); + gui_bridge_post(GUI_EVT_CONN_LIST, reinterpret_cast(snapshot.constData()), snapshot.size()); + QCoreApplication::processEvents(); + QCOMPARE(second.window->findChild()->count(), 1); // Деструктор старого окна не снимает новый callback. + } + #ifdef UTUN_TEST_BRIDGE_ALLOC_FAILURES void allocationFailuresKeepOwnershipAndReleasePartialRequests() { BoundCore core; QVERIFY(core.ua); core.attach();