Share the ETCP monitor across desktop entry points

This commit is contained in:
evgeny committed 2026-10-27 02:49:39 +01:00
1 parent 1b695fe247
commit 61ed80e18c
8 files changed
+131 -50

No files matched your search

+1 -1
View File
@@ -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)
+44 -14
View File
@@ -16,6 +16,8 @@ extern "C" {
#include <QScrollBar>
#include <QHash>
#include <QApplication>
#include <QCloseEvent>
#include <QSignalBlocker>
#include <cstring>
#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<uint64_t, QListWidgetItem*> 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);
+10 -6
View File
@@ -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; принадлежит окну.
+1 -12
View File
@@ -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);
-3
View File
@@ -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<quint64, QPair<QString, quint64>> m_dmDeleteRequests; /* request_id → conv_id и удаляемое поколение */
AudioRecorder *m_recorder = nullptr;
ConnMonitorWindow *m_connMonitor = nullptr;
VideoPlayerWindow *m_videoPlayer = nullptr;
QPointer<StreamPlayerWindow> m_streamPlayer;
QPointer<StreamGridWindow> m_streamGrid;
+1 -11
View File
@@ -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();
}
-3
View File
@@ -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;
@@ -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 <cstring>
#include <thread>
#include <future>
#include <QPointer>
#ifdef UTUN_TEST_BRIDGE_ALLOC_FAILURES
static int failAllocation;
@@ -48,6 +50,29 @@ struct BoundCore {
}
};
struct MonitorGuard {
QPointer<ConnMonitorWindow> window = ConnMonitorWindow::showMonitor();
MonitorGuard() { window->findChild<QTimer*>()->stop(); }
~MonitorGuard() { delete window.data(); }
};
static QByteArray connection_list(std::initializer_list<quint64> 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<QListWidget*>(); QVERIFY(list);
auto* details = window->findChild<QPlainTextEdit*>(); QVERIFY(details);
auto snapshot = connection_list({0x112233445566abcd, 0x2222});
window->onConnList(reinterpret_cast<const uint8_t*>(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<const uint8_t*>(snapshot.constData()), snapshot.size());
QCOMPARE(list->currentItem()->data(Qt::UserRole).toULongLong(), quint64(0x2222));
QCOMPARE(pending_count(core.ua), 2);
window->onConnMetrics(reinterpret_cast<const uint8_t*>("current metrics"), 15);
snapshot = connection_list({});
window->onConnList(reinterpret_cast<const uint8_t*>(snapshot.constData()), snapshot.size());
QVERIFY(details->toPlainText().isEmpty());
window->onConnMetrics(reinterpret_cast<const uint8_t*>("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<QListWidget*>(); QVERIFY(list);
auto snapshot = connection_list({7, 8});
window->onConnList(reinterpret_cast<const uint8_t*>(snapshot.constData()), snapshot.size());
window->onConnList(reinterpret_cast<const uint8_t*>(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<const uint8_t*>(snapshot.constData()), snapshot.size());
QCoreApplication::processEvents();
QCOMPARE(second.window->findChild<QListWidget*>()->count(), 1); // Деструктор старого окна не снимает новый callback.
}
#ifdef UTUN_TEST_BRIDGE_ALLOC_FAILURES
void allocationFailuresKeepOwnershipAndReleasePartialRequests() {
BoundCore core; QVERIFY(core.ua); core.attach();