Share the ETCP monitor across desktop entry points
This commit is contained in:
1 parent
1b695fe247
commit
61ed80e18c
8 files changed
+131
-50
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; принадлежит окну.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user