Files
utun2/tools/chatgui/src/nodespage.cpp
T

212 lines
10 KiB
C++

#include <QCoreApplication>
// nodespage.cpp — Nodes table + dump in settings
#include "nodespage.h"
#include "addressprofiletext.h"
#include "../db/db_manager.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QDateTime>
#include <QFont>
#include <QVector>
#include <cstring>
#include <memory>
#include "../../../lib/platform_compat.h"
#include "../../../lib/debug_config.h"
static QString hex16(uint64_t v) {
return QString("%1").arg(v, 16, 16, QChar('0')).toUpper();
}
QString NodesPage::shortenKey(const QByteArray& key) {
if (key.size() < 8) return key.toHex(' ').toUpper();
return key.left(4).toHex(' ').toUpper() + ".." + key.right(4).toHex(' ').toUpper();
}
QString NodesPage::addrStr(const void* addr, int family) {
if (family == 4) {
const uint8_t* a = (const uint8_t*)addr;
return QString("%1.%2.%3.%4").arg(a[0]).arg(a[1]).arg(a[2]).arg(a[3]);
}
if (family == 6) {
uint16_t a[8];
memcpy(a, addr, sizeof(a));
return QString("%1:%2:%3:%4:%5:%6:%7:%8")
.arg(ntohs(a[0]), 0, 16).arg(ntohs(a[1]), 0, 16)
.arg(ntohs(a[2]), 0, 16).arg(ntohs(a[3]), 0, 16)
.arg(ntohs(a[4]), 0, 16).arg(ntohs(a[5]), 0, 16)
.arg(ntohs(a[6]), 0, 16).arg(ntohs(a[7]), 0, 16);
}
return "?";
}
QString NodesPage::protoStr(int protocol) {
if (protocol & 0x04) return (protocol & 0x02) ? "TCP+REALITY" : "REALITY";
switch (protocol) {
case 1: return "UDP";
case 2: return "TCP";
case 3: return "UDP+TCP";
default: return QString::number(protocol);
}
}
NodesPage::NodesPage(DbManager* db, QWidget* parent)
: QWidget(parent)
, m_db(db)
{
auto* layout = new QVBoxLayout(this);
layout->setSpacing(8);
layout->setContentsMargins(0, 0, 0, 0);
auto* title = new QLabel(QCoreApplication::translate("ChatGui", "Known Nodes"), this);
title->setStyleSheet("font-weight: bold; font-size: 13px;");
layout->addWidget(title);
m_table = new QTableWidget(0, 5, this);
m_table->setHorizontalHeaderLabels({QCoreApplication::translate("ChatGui",
"Node ID"), QCoreApplication::translate("ChatGui",
"Name"), "x25519", "ed25519", QCoreApplication::translate("ChatGui",
"Addrs")});
m_table->horizontalHeader()->setStretchLastSection(true);
m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
m_table->setSelectionMode(QAbstractItemView::SingleSelection);
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_table->verticalHeader()->setVisible(false);
m_table->setFont(QFont("monospace", 9));
m_table->setStyleSheet(
"QTableWidget { border: 1px solid palette(mid); border-radius: 4px;"
" gridline-color: palette(midlight); background: palette(base); }"
"QHeaderView::section { background: palette(window); padding: 4px;"
" font-weight: bold; font-size: 9px; }");
layout->addWidget(m_table, 2);
auto* dumpLabel = new QLabel(QCoreApplication::translate("ChatGui", "Details"), this);
dumpLabel->setStyleSheet("font-weight: bold; font-size: 12px; margin-top: 8px;");
layout->addWidget(dumpLabel);
m_dumpText = new QPlainTextEdit(this);
m_dumpText->setReadOnly(true);
m_dumpText->setFont(QFont("monospace", 10));
m_dumpText->setStyleSheet(
"QPlainTextEdit { border: 1px solid palette(mid); border-radius: 4px;"
" padding: 8px; background: palette(base); }");
m_dumpText->setPlaceholderText(QCoreApplication::translate("ChatGui", "Click Refresh to load nodes"));
layout->addWidget(m_dumpText, 3);
auto* btnLayout = new QHBoxLayout();
btnLayout->addStretch();
m_refreshBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Refresh"), this);
m_refreshBtn->setMinimumWidth(90);
m_refreshBtn->setStyleSheet(
"QPushButton { border: 1px solid palette(mid); border-radius: 4px;"
" padding: 6px 16px; font-size: 13px; background: palette(button); }"
"QPushButton:hover { background: palette(light); }");
connect(m_refreshBtn, &QPushButton::clicked, this, &NodesPage::onRefreshClicked);
btnLayout->addWidget(m_refreshBtn);
layout->addLayout(btnLayout);
}
void NodesPage::onRefreshClicked() { refreshNodes(); }
/* Оба представления строятся из одного запроса; при отказе временные данные не попадают в виджеты. */
void NodesPage::refreshNodes() {
if (!m_db || !m_db->m_db) {
m_table->setRowCount(0);
m_dumpText->setPlainText(QCoreApplication::translate("ChatGui", "Database not connected"));
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Nodes refresh unavailable: manager=%p SQLite handle missing", static_cast<void*>(m_db));
return;
}
sqlite3* d = m_db->m_db;
struct NodeSnapshot {
quint64 id = 0;
QString name, x25519, ed25519, details;
int addresses = 0;
};
QVector<NodeSnapshot> nodes;
quint64 currentNode = 0, addressCount = 0;
const char* stage = "prepare";
QString failure;
sqlite3_stmt* st = nullptr;
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Nodes refresh begin db=%p", static_cast<void*>(d));
int rc = sqlite3_prepare_v2(d,
"SELECT n.node_id,n.name,n.x25519_pubkey,n.ed25519_pubkey,n.last_seen_at,n.created_at,"
" a.node_id,a.family,a.protocol,a.address,a.port,a.rtt,a.nat_mapping,a.nat_inbound,a.nat_source"
" FROM nodes n LEFT JOIN node_addresses a ON a.node_id=n.node_id ORDER BY n.node_id,a.family,a.port",
-1, &st, nullptr);
std::unique_ptr<sqlite3_stmt, decltype(&sqlite3_finalize)> statement(st, sqlite3_finalize);
if (rc == SQLITE_OK) {
stage = "step";
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
currentNode = quint64(sqlite3_column_int64(st, 0));
if (nodes.isEmpty() || nodes.constLast().id != currentNode) {
nodes.append(NodeSnapshot{});
auto& node = nodes.last();
node.id = currentNode; node.name = DbManager::colText(st, 1);
const auto x25519 = DbManager::colBlob(st, 2), ed25519 = DbManager::colBlob(st, 3);
node.x25519 = x25519.isEmpty() ? "-" : shortenKey(x25519);
node.ed25519 = ed25519.isEmpty() ? "-" : shortenKey(ed25519);
node.details = QCoreApplication::translate("ChatGui", "Node: %1").arg(hex16(node.id));
if (!node.name.isEmpty()) node.details += QString(" (%1)").arg(node.name);
node.details += QString(" id=0x%1\n").arg(node.id, 16, 16, QChar('0'));
node.details += QString(" x25519: %1\n ed25519: %2\n").arg(node.x25519, node.ed25519);
const qint64 lastSeen = sqlite3_column_int64(st, 4), created = sqlite3_column_int64(st, 5);
if (lastSeen > 0) node.details += QCoreApplication::translate("ChatGui", " last seen: %1\n")
.arg(QDateTime::fromSecsSinceEpoch(lastSeen).toString("yyyy-MM-dd hh:mm:ss"));
if (created > 0) node.details += QCoreApplication::translate("ChatGui", " created: %1\n")
.arg(QDateTime::fromSecsSinceEpoch(created).toString("yyyy-MM-dd hh:mm:ss"));
}
if (sqlite3_column_type(st, 6) == SQLITE_NULL) continue; // LEFT JOIN: узел без адресов.
const int family = sqlite3_column_int(st, 7), type = sqlite3_column_type(st, 9);
const void* address = sqlite3_column_blob(st, 9);
const int bytes = sqlite3_column_bytes(st, 9);
if (type != SQLITE_BLOB || !address || (family != 4 && family != 6) || bytes != (family == 4 ? 4 : 16)) {
stage = "address";
rc = sqlite3_errcode(d) == SQLITE_NOMEM ? SQLITE_NOMEM : SQLITE_CORRUPT;
if (rc == SQLITE_CORRUPT) failure = QString("invalid address family=%1 bytes=%2 type=%3").arg(family).arg(bytes).arg(type);
break;
}
auto& node = nodes.last();
if (!node.addresses) node.details += QCoreApplication::translate("ChatGui", " addresses:\n");
const int protocol = sqlite3_column_int(st, 8), port = sqlite3_column_int(st, 10), rtt = sqlite3_column_int(st, 11);
const auto profile = addressProfileText(sqlite3_column_int(st, 12), sqlite3_column_int(st, 13), sqlite3_column_int(st, 14));
node.details += QString(" %1:%2 %3 %4 rtt=%5ms\n").arg(addrStr(address, family)).arg(port)
.arg(protoStr(protocol), profile, rtt > 0 ? QString::number(rtt / 10) : "-");
++node.addresses; ++addressCount;
}
}
if (rc != SQLITE_DONE && failure.isEmpty()) failure = QString::fromUtf8(sqlite3_errmsg(d));
const int finalized = sqlite3_finalize(statement.release());
if (rc == SQLITE_DONE && finalized != SQLITE_OK) {
stage = "finalize"; failure = QString::fromUtf8(sqlite3_errstr(finalized));
}
if (rc != SQLITE_DONE || finalized != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Nodes refresh failed stage=%s rc=%d finalize=%d node=%016llx: %s",
stage, rc, finalized, static_cast<unsigned long long>(currentNode), qPrintable(failure));
m_table->setRowCount(0);
m_dumpText->setPlainText(QCoreApplication::translate("ChatGui", "Query error"));
return;
}
QString dump;
for (const auto& node : nodes) {
if (!dump.isEmpty()) dump += '\n';
dump += node.details;
if (!node.addresses) dump += QCoreApplication::translate("ChatGui", " addresses: none\n");
}
if (nodes.isEmpty()) dump = QCoreApplication::translate("ChatGui", "No nodes found.");
m_table->setRowCount(0); m_table->setRowCount(nodes.size());
for (int row = 0; row < nodes.size(); ++row) {
const auto& node = nodes[row];
const QString cells[] = {hex16(node.id), node.name, node.x25519, node.ed25519, QString::number(node.addresses)};
for (int column = 0; column < 5; ++column) m_table->setItem(row, column, new QTableWidgetItem(cells[column]));
}
m_table->resizeColumnsToContents();
m_dumpText->setPlainText(dump);
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Nodes refresh complete nodes=%d addresses=%llu", int(nodes.size()),
static_cast<unsigned long long>(addressCount));
}