diff --git a/tools/chatgui/src/nodespage.cpp b/tools/chatgui/src/nodespage.cpp index 0e7b5b0ce..90d027854 100644 --- a/tools/chatgui/src/nodespage.cpp +++ b/tools/chatgui/src/nodespage.cpp @@ -10,10 +10,12 @@ #include #include #include +#include #include +#include #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(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 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 statement(st, sqlite3_finalize); + DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Nodes refresh begin db=%p", static_cast(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(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(addressCount)); } diff --git a/tools/chatgui/src/nodespage.h b/tools/chatgui/src/nodespage.h index 99603d954..060bbc5d0 100644 --- a/tools/chatgui/src/nodespage.h +++ b/tools/chatgui/src/nodespage.h @@ -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 @@ -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; // Дочерняя кнопка повторного чтения. };