Build node diagnostics from one checked SQLite snapshot

This commit is contained in:
evgeny committed 2026-10-27 17:27:04 +01:00
1 parent dc1f3260e3
commit 30a205340a
2 files changed
+101 -124

No files matched your search

+88 -115
View File
@@ -10,10 +10,12 @@
#include <QLabel>
#include <QDateTime>
#include <QFont>
#include <QVector>
#include <cstring>
#include <memory>
#include "../../../lib/platform_compat.h"
#include "../../../src/routing_layer/topo_node.h"
#include "../../../lib/debug_config.h"
static QString hex16(uint64_t v) {
return QString("%1").arg(v, 16, 16, QChar('0')).toUpper();
@@ -30,7 +32,8 @@ QString NodesPage::addrStr(const void* addr, int family) {
return QString("%1.%2.%3.%4").arg(a[0]).arg(a[1]).arg(a[2]).arg(a[3]);
}
if (family == 6) {
const uint16_t* a = (const uint16_t*)addr;
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)
@@ -108,131 +111,101 @@ NodesPage::NodesPage(DbManager* db, QWidget* parent)
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 (m_db=%1, m_db->m_db=%2)")
.arg((quintptr)m_db, 0, 16).arg(m_db ? (quintptr)m_db->m_db : 0, 0, 16));
qWarning("NodesPage::refreshNodes: DB not ready m_db=%p m_db->m_db=%p", (void*)m_db, m_db ? (void*)m_db->m_db : nullptr);
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;
/* ── table ── */
m_table->setRowCount(0);
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;
if (sqlite3_prepare_v2(d,
"SELECT node_id, name, x25519_pubkey, ed25519_pubkey,"
" last_seen_at, created_at FROM nodes ORDER BY node_id",
-1, &st, nullptr) != SQLITE_OK) return;
int row = 0;
while (sqlite3_step(st) == SQLITE_ROW) {
m_table->insertRow(row);
quint64 nid = (quint64)sqlite3_column_int64(st, 0);
auto setCell = [&](int col, const QString& v) {
m_table->setItem(row, col, new QTableWidgetItem(v));
};
setCell(0, hex16(nid));
setCell(1, DbManager::colText(st, 1));
QByteArray x25 = DbManager::colBlob(st, 2);
QByteArray ed = DbManager::colBlob(st, 3);
setCell(2, x25.isEmpty() ? "-" : shortenKey(x25));
setCell(3, ed.isEmpty() ? "-" : shortenKey(ed));
sqlite3_stmt* ac = nullptr;
sqlite3_prepare_v2(d, "SELECT COUNT(*) FROM node_addresses WHERE node_id=?", -1, &ac, nullptr);
if (ac) {
sqlite3_bind_int64(ac, 1, (sqlite3_int64)nid);
setCell(4, sqlite3_step(ac) == SQLITE_ROW
? QString::number(sqlite3_column_int(ac, 0))
: "0");
sqlite3_finalize(ac);
} else {
setCell(4, "0");
}
row++;
}
sqlite3_finalize(st);
m_table->resizeColumnsToContents();
/* ── text dump ── */
QString dump;
st = nullptr;
sqlite3_prepare_v2(d,
"SELECT node_id, name, x25519_pubkey, ed25519_pubkey,"
" last_seen_at, created_at FROM nodes ORDER BY node_id",
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);
if (!st) { m_dumpText->setPlainText(QCoreApplication::translate("ChatGui", "Query error")); return; }
int nodeCount = 0;
while (sqlite3_step(st) == SQLITE_ROW) {
if (nodeCount > 0) dump += "\n";
nodeCount++;
quint64 nid = (quint64)sqlite3_column_int64(st, 0);
QString name = DbManager::colText(st, 1);
QByteArray x25 = DbManager::colBlob(st, 2);
QByteArray ed = DbManager::colBlob(st, 3);
qint64 last_seen = sqlite3_column_int64(st, 4);
qint64 created = sqlite3_column_int64(st, 5);
dump += QCoreApplication::translate("ChatGui", "Node: %1").arg(hex16(nid));
if (!name.isEmpty()) dump += QString(" (%1)").arg(name);
dump += QString(" id=0x%1\n").arg(nid, 16, 16, QChar('0'));
dump += QString(" x25519: %1\n").arg(x25.isEmpty() ? "-" : shortenKey(x25));
dump += QString(" ed25519: %1\n").arg(ed.isEmpty() ? "-" : shortenKey(ed));
if (last_seen > 0)
dump += QCoreApplication::translate("ChatGui",
" last seen: %1\n").arg(QDateTime::fromSecsSinceEpoch(last_seen).toString("yyyy-MM-dd hh:mm:ss"));
if (created > 0)
dump += QCoreApplication::translate("ChatGui",
" created: %1\n").arg(QDateTime::fromSecsSinceEpoch(created).toString("yyyy-MM-dd hh:mm:ss"));
sqlite3_stmt* na = nullptr;
sqlite3_prepare_v2(d,
"SELECT family, protocol, address, port, rtt, nat_mapping, nat_inbound, nat_source"
" FROM node_addresses WHERE node_id=? ORDER BY family, port",
-1, &na, nullptr);
if (na) {
sqlite3_bind_int64(na, 1, (sqlite3_int64)nid);
int ac = 0;
while (sqlite3_step(na) == SQLITE_ROW) {
if (ac == 0) dump += QCoreApplication::translate("ChatGui", " addresses:\n");
int family = sqlite3_column_int(na, 0);
int proto = sqlite3_column_int(na, 1);
const void* addr = sqlite3_column_blob(na, 2);
int addrSz = sqlite3_column_bytes(na, 2);
int port = sqlite3_column_int(na, 3);
int rtt = sqlite3_column_int(na, 4);
int mapping = sqlite3_column_int(na, 5), inbound = sqlite3_column_int(na, 6);
int source = sqlite3_column_int(na, 7);
if (addr && addrSz > 0) {
QString label = addressProfileText(mapping, inbound, source);
dump += QString(" %1:%2 %3 %4 rtt=%5ms\n")
.arg(addrStr(addr, family))
.arg(port)
.arg(protoStr(proto))
.arg(label)
.arg(rtt > 0 ? QString::number(rtt / 10) : "-");
}
ac++;
std::unique_ptr<sqlite3_stmt, decltype(&sqlite3_finalize)> statement(st, sqlite3_finalize);
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Nodes refresh begin db=%p", static_cast<void*>(d));
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 (ac == 0) dump += QCoreApplication::translate("ChatGui", " addresses: none\n");
sqlite3_finalize(na);
} else {
dump += QCoreApplication::translate("ChatGui", " addresses: none\n");
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;
}
}
sqlite3_finalize(st);
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") + ": " + failure);
return;
}
if (nodeCount == 0) dump = QCoreApplication::translate("ChatGui", "No nodes found.");
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));
}
+13 -9
View File
@@ -1,4 +1,6 @@
// nodespage.h — Nodes table + dump in settings
/* GUI-страница известных узлов SQLite: таблица и подробности собираются из одного LEFT JOIN nodes/node_addresses.
* refreshNodes синхронно читает полный результат во временные Qt-значения; виджеты заполняются после успешного finalize.
* Ошибка чтения/данных очищает таблицу и показывает Query error, отсутствие БД — Database not connected; повторный Refresh допустим. */
#pragma once
#include <QWidget>
@@ -11,19 +13,21 @@ class DbManager;
class NodesPage : public QWidget {
Q_OBJECT
public:
/* db заимствован до уничтожения страницы, nullptr допустим. Создание не читает список узлов. */
explicit NodesPage(DbManager* db, QWidget* parent = nullptr);
/* GUI: заменить таблицу/подробности полным результатом либо явным состоянием ошибки; этапы/причины логируются в CONFIG. */
void refreshNodes();
private slots:
void onRefreshClicked();
void onRefreshClicked(); // Ручное обновление; также refreshNodes вызывает диалог при выборе категории.
private:
static QString shortenKey(const QByteArray& key);
static QString addrStr(const void* addr, int family);
static QString protoStr(int protocol);
static QString shortenKey(const QByteArray& key); // HEX uppercase: первые/последние 4 байта, при длине <8 целиком.
static QString addrStr(const void* addr, int family); // Проверенный сетевой адрес: family=4/6, обязательные 4/16 байт до возврата.
static QString protoStr(int protocol); // UDP/TCP/REALITY из битмаски; неизвестный обычный protocol — число.
DbManager* m_db;
QTableWidget* m_table;
QPlainTextEdit* m_dumpText;
QPushButton* m_refreshBtn;
DbManager* m_db; // Заимствованный читатель/handle, живёт до уничтожения страницы.
QTableWidget* m_table; // Дочерняя read-only таблица: ID, имя, X25519, Ed25519, число адресов.
QPlainTextEdit* m_dumpText; // Дочерние подробности того же результата либо сообщение об ошибке.
QPushButton* m_refreshBtn; // Дочерняя кнопка повторного чтения.
};