212 lines
10 KiB
C++
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));
|
|
}
|