Build node diagnostics from one checked SQLite snapshot
This commit is contained in:
1 parent
dc1f3260e3
commit
30a205340a
2 files changed
+101
-124
No files matched your search
+88
-115
@@ -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));
|
||||
}
|
||||
@@ -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; // Дочерняя кнопка повторного чтения.
|
||||
};
|
||||
Reference in new issue
Block a user