v5.0.1: Windows QtQuick crash fix + node/Tor lifecycle overhaul

Windows crash chain (exposed by the embedded-node default):
- main.cpp: force the QtQuick software renderer. The Mining tab's QQuickWidget
  drove the default RHI/GL path and faulted (0xc0000005) on broken-GL hosts
  (VirtualBox VMSVGA), crashing the app and orphaning the embedded node.
  Real hardware was unaffected.
- ChildProcessGuard + DaemonManager/TorManager: kernel-level die-with-parent
  (Windows Job Object KILL_ON_JOB_CLOSE / Linux PR_SET_PDEATHSIG) so the
  embedded wownerod and bundled tor never survive a wowlet exit or crash.
- DaemonManager: verify-before-adopt via get_info (nettype/height) instead of
  trusting a bare open RPC port, so a stale/orphan node is never adopted.

Node/Tor lifecycle + remote-node connectivity:
- config: torPrivacyLevel default -> allTorExceptNode. NEVER sync blocks over
  Tor; the wallet's node connection is always clearnet. Tor is only for tx
  broadcast, so remote nodes work with or without Tor.
- nodes: add Nodes::reconnect() (clear failure blocklist + force reconnect);
  node/Tor toggle handlers use it so a transient outage no longer strands the
  wallet at "Disconnected".
- MainWindow/SettingsDialog: reconnect on node/Tor toggle and on the async
  daemonStateChanged (closes the start() race; a freshly-toggled local node
  connects immediately).
- TorManager: reset retry/unpack state in init() so a system<->bundled Tor
  re-toggle can start again; quiesce on stop(). Linux unpackBins() rework
  (mkpath + presence-based unpack + graceful return-false), getVersion() arg
  fix, and a missing-binary start guard.
- build.yml: derive TOR_VERSION from the embedded tor binary (a hardcoded
  bundle number made unpackBins rewrite Tor every launch).
- depends/funcs.mk: confine cross-cmake find_package to the depends sysroot so
  host -dev libraries don't leak into the build.
This commit is contained in:
jwinterm
2026-06-23 17:45:39 -04:00
parent a1ae9735d5
commit 0d5d745311
14 changed files with 386 additions and 28 deletions
+121 -4
View File
@@ -6,10 +6,21 @@
#include <QCoreApplication>
#include <QDir>
#include <QDirIterator>
#include <QEventLoop>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QRegularExpression>
#include <QUrl>
#include "constants.h"
#include "utils/AppData.h"
#include "utils/ChildProcessGuard.h" // wowlet: OS-level die-with-parent for the embedded node
#include "utils/config.h"
#include "utils/networktype.h"
#include "utils/TorManager.h"
#include "utils/Utils.h"
@@ -25,6 +36,11 @@ DaemonManager::DaemonManager(QObject *parent)
connect(m_process, &QProcess::readyReadStandardOutput, this, &DaemonManager::handleProcessOutput);
connect(m_process, &QProcess::errorOccurred, this, &DaemonManager::handleProcessError);
connect(m_process, &QProcess::stateChanged, this, &DaemonManager::onStateChanged);
// wowlet: SIGKILL wownerod if wowlet dies (Unix); Windows binds it to a kill-on-close job post-start.
ChildProcessGuard::installPreStart(m_process);
// wowlet: graceful stop on any clean quit, not just the last-window-closed path (flushes LMDB).
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]{ this->stop(); });
}
QPointer<DaemonManager> DaemonManager::m_instance(nullptr);
@@ -51,11 +67,22 @@ void DaemonManager::start() {
return;
}
// wowlet: don't trust a bare open port. Before adopting whatever is listening on our RPC port,
// verify via get_info that it's a healthy wownero-mainnet wownerod (right nettype, sane height,
// making progress) — not a stale/stuck orphan from a prior crash or an unrelated squatter.
if (Utils::portOpen(this->rpcHost, this->rpcPort)) {
// Something is already listening on our RPC port; assume it is a usable local node.
m_alreadyRunning = true;
emit daemonStateChanged(true);
return;
if (this->verifyAdoptableNode()) {
qDebug() << "Adopting existing wownerod on" << this->rpcAddress();
m_alreadyRunning = true;
emit daemonStateChanged(true);
return;
}
qWarning() << "Refusing to adopt unverified listener on" << this->rpcAddress()
<< "- will spawn a managed wownerod instead";
this->setErrorMessage(
QString("Found an unrecognized process on %1; starting a fresh node.").arg(this->rpcAddress()));
// fall through to spawn; if the port/data-dir is genuinely held, the spawn fails to bind
// and surfaces a clear error via handleProcessError().
}
m_restarts += 1;
@@ -106,6 +133,7 @@ void DaemonManager::start() {
qDebug() << QString("Starting wownerod: %1 %2").arg(this->daemonPath, arguments.join(" "));
m_process->start(this->daemonPath, arguments);
ChildProcessGuard::adoptAfterStart(m_process); // wowlet: bind to kill-on-crash job (Windows)
m_started = true;
}
@@ -238,6 +266,95 @@ bool DaemonManager::shouldStartDaemon() {
return true;
}
// wowlet: synchronous, bounded get_info probe used by start() to decide whether an already-open RPC
// port is a node we can safely adopt. Returns true ONLY for a healthy wownero-mainnet wownerod.
// Self-contained (own QNetworkAccessManager + local QEventLoop with a hard timeout) so it can run
// inside the synchronous start() at boot, before any wallet/DaemonRpc exists. Mirrors TorManager's
// blocking getVersion() probe idiom.
bool DaemonManager::verifyAdoptableNode() {
if (conf()->get(Config::offlineMode).toBool())
return false;
QNetworkAccessManager nam;
QNetworkRequest req(QUrl(QString("http://%1/get_info").arg(this->rpcAddress())));
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
QNetworkReply *reply = nam.post(req, QByteArray("{}"));
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
timeout.start(1500); // hard cap on UI-thread block
loop.exec();
if (!reply->isFinished()) { // timed out
reply->abort();
reply->deleteLater();
qWarning() << "get_info verify: no response from" << this->rpcAddress() << "within 1.5s";
return false;
}
bool netOk = reply->error() == QNetworkReply::NoError;
QByteArray data = reply->readAll();
reply->deleteLater();
if (!netOk || data.isEmpty() || !Utils::validateJSON(data)) {
qWarning() << "get_info verify: not a JSON-RPC daemon on" << this->rpcAddress();
return false;
}
QJsonObject obj = QJsonDocument::fromJson(data).object();
if (obj.value("status").toString() != "OK") { // 1. real wownerod RPC
qWarning() << "get_info verify: bad status" << obj.value("status").toString();
return false;
}
const QString wantNet = constants::networkType == NetworkType::MAINNET ? "mainnet" // 2. nettype
: constants::networkType == NetworkType::TESTNET ? "testnet"
: "stagenet";
if (obj.value("nettype").toString() != wantNet) {
qWarning() << "get_info verify: wrong nettype" << obj.value("nettype").toString()
<< "(want" << wantNet << ")";
return false;
}
const quint64 height = obj.value("height").toVariant().toULongLong(); // 3. height sanity
if (height == 0) {
qWarning() << "get_info verify: node reports height 0";
return false;
}
quint64 knownHeight = 0;
if (appData()->heights.contains(constants::networkType))
knownHeight = static_cast<quint64>(appData()->heights[constants::networkType]);
const quint64 peers = obj.value("outgoing_connections_count").toVariant().toULongLong()
+ obj.value("incoming_connections_count").toVariant().toULongLong();
const bool progressing = obj.value("busy_syncing").toBool() || obj.value("synchronized").toBool();
if (knownHeight > 0) {
const quint64 slack = 720; // ~1 day of wownero blocks; live-but-lagging is still adoptable
if (height + slack < knownHeight) {
if (peers == 0 && !progressing) { // 4. stuck-orphan signature: behind AND peerless AND idle
qWarning() << "get_info verify: stale/stuck node — height" << height
<< "<< known" << knownHeight << "with 0 peers, not syncing";
return false;
}
qWarning() << "get_info verify: node behind (" << height << "/" << knownHeight
<< ") but live; adopting and letting it catch up";
}
}
if (peers == 0) // soft: a freshly-spawned-but-orphaned node sits at 0 peers for minutes
qWarning() << "get_info verify: adopting node with 0 peers (likely still bootstrapping)";
qDebug() << "get_info verify: OK — nettype" << wantNet << "height" << height << "peers" << peers;
return true;
}
void DaemonManager::setErrorMessage(const QString &msg) {
this->errorMsg = msg;
if (!msg.isEmpty())