mirror of
https://github.com/wownero/wowlet.git
synced 2026-10-17 07:11:29 +02:00
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:
@@ -54,7 +54,12 @@ jobs:
|
|||||||
cd build
|
cd build
|
||||||
# ARCH=x86-64: portable generic baseline. NEVER 'native' for a release — the CI runner's
|
# ARCH=x86-64: portable generic baseline. NEVER 'native' for a release — the CI runner's
|
||||||
# CPU (often newer than users') would bake in unsupported instructions → SIGILL on launch.
|
# CPU (often newer than users') would bake in unsupported instructions → SIGILL on launch.
|
||||||
cmake -DWITH_SCANNER=OFF -DARCH=x86-64 -DWOWNEROD_DIR=/tmp/wownerod-embed -DTOR_DIR=/tmp/tor-embed -DTOR_VERSION=15.0 ..
|
# wowlet: TOR_VERSION must EXACTLY match `tor --version` of the embedded binary, else
|
||||||
|
# TorManager::unpackBins() sees embedded > the unpacked copy and rewrites Tor every launch.
|
||||||
|
# The tor-expert-bundle's RELEASE number (15.0) is NOT its tor version (0.4.8.x) — derive it.
|
||||||
|
TORVER=$(/tmp/tor-embed/tor --version | grep -oE '[0-9]+(\.[0-9]+){3}' | head -1)
|
||||||
|
echo "embedded tor version: $TORVER"
|
||||||
|
cmake -DWITH_SCANNER=OFF -DARCH=x86-64 -DWOWNEROD_DIR=/tmp/wownerod-embed -DTOR_DIR=/tmp/tor-embed -DTOR_VERSION="$TORVER" ..
|
||||||
cmake --build . -j $(nproc)
|
cmake --build . -j $(nproc)
|
||||||
- name: package AppImage
|
- name: package AppImage
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
cmake_minimum_required(VERSION 3.18)
|
cmake_minimum_required(VERSION 3.18)
|
||||||
|
|
||||||
project(feather
|
project(feather
|
||||||
VERSION "5.0.0"
|
VERSION "5.0.1"
|
||||||
DESCRIPTION "A free Monero desktop wallet"
|
DESCRIPTION "A free Monero desktop wallet"
|
||||||
LANGUAGES CXX C ASM
|
LANGUAGES CXX C ASM
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -198,6 +198,14 @@ ifneq ($(host),$(build))
|
|||||||
$(1)_cmake += -DCMAKE_SYSTEM_NAME=$($(host_os)_cmake_system)
|
$(1)_cmake += -DCMAKE_SYSTEM_NAME=$($(host_os)_cmake_system)
|
||||||
$(1)_cmake += -DCMAKE_C_COMPILER_TARGET=$(host)
|
$(1)_cmake += -DCMAKE_C_COMPILER_TARGET=$(host)
|
||||||
$(1)_cmake += -DCMAKE_CXX_COMPILER_TARGET=$(host)
|
$(1)_cmake += -DCMAKE_CXX_COMPILER_TARGET=$(host)
|
||||||
|
# wowlet: confine find_package to the depends sysroot so cross cmake builds don't grab host
|
||||||
|
# /usr -dev packages (libzip's find_package(ZLIB) was picking up host zlib1g-dev). Mirrors
|
||||||
|
# toolchain.cmake.in; no-op on clean CI, needed on dev boxes with -dev libs. Upstreamable.
|
||||||
|
$(1)_cmake += -DCMAKE_FIND_ROOT_PATH=$(host_prefix)
|
||||||
|
$(1)_cmake += -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER
|
||||||
|
$(1)_cmake += -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY
|
||||||
|
$(1)_cmake += -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY
|
||||||
|
$(1)_cmake += -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ONLY
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
endef
|
endef
|
||||||
|
|||||||
+9
-1
@@ -36,6 +36,7 @@
|
|||||||
#include "utils/AppData.h"
|
#include "utils/AppData.h"
|
||||||
#include "utils/AsyncTask.h"
|
#include "utils/AsyncTask.h"
|
||||||
#include "utils/ColorScheme.h"
|
#include "utils/ColorScheme.h"
|
||||||
|
#include "utils/DaemonManager.h" // wowlet: for daemonStateChanged reconnect (node-toggle race)
|
||||||
#include "utils/Icons.h"
|
#include "utils/Icons.h"
|
||||||
#include "utils/TorManager.h"
|
#include "utils/TorManager.h"
|
||||||
#include "utils/WebsocketNotifier.h"
|
#include "utils/WebsocketNotifier.h"
|
||||||
@@ -512,6 +513,13 @@ void MainWindow::initWalletContext() {
|
|||||||
this->onConnectionStatusChanged(status);
|
this->onConnectionStatusChanged(status);
|
||||||
m_nodes->autoConnect();
|
m_nodes->autoConnect();
|
||||||
});
|
});
|
||||||
|
// wowlet: the embedded node's RPC port comes up asynchronously after daemonManager()->start(), so a
|
||||||
|
// freshly-toggled local node would otherwise sit Disconnected until the next status tick. Reconnect the
|
||||||
|
// instant its RPC is up. (autoConnect's runLocalNode branch routes to 127.0.0.1; start() is idempotent.)
|
||||||
|
connect(daemonManager(), &DaemonManager::daemonStateChanged, this, [this](bool up){
|
||||||
|
if (up && conf()->get(Config::runLocalNode).toBool())
|
||||||
|
m_nodes->autoConnect(true);
|
||||||
|
});
|
||||||
connect(m_wallet, &Wallet::currentSubaddressAccountChanged, this, &MainWindow::updateTitle);
|
connect(m_wallet, &Wallet::currentSubaddressAccountChanged, this, &MainWindow::updateTitle);
|
||||||
connect(m_wallet, &Wallet::walletPassphraseNeeded, this, &MainWindow::onWalletPassphraseNeeded);
|
connect(m_wallet, &Wallet::walletPassphraseNeeded, this, &MainWindow::onWalletPassphraseNeeded);
|
||||||
|
|
||||||
@@ -703,7 +711,7 @@ void MainWindow::onWebsocketStatusChanged(bool enabled) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onProxySettingsChangedConnect() {
|
void MainWindow::onProxySettingsChangedConnect() {
|
||||||
m_nodes->connectToNode();
|
m_nodes->reconnect(); // wowlet: clear blocklist + force-reconnect after a live Tor/proxy switch (test 2)
|
||||||
this->onProxySettingsChanged();
|
this->onProxySettingsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ void Settings::setupNetworkTab() {
|
|||||||
daemonManager()->start();
|
daemonManager()->start();
|
||||||
else
|
else
|
||||||
daemonManager()->stop(false); // non-blocking: don't freeze Settings while wownerod flushes
|
daemonManager()->stop(false); // non-blocking: don't freeze Settings while wownerod flushes
|
||||||
m_nodes->autoConnect(true);
|
m_nodes->reconnect(); // wowlet: clear blocklist + force-reconnect (handles a previously-failed remote)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
m_nodes = new Nodes(this, nullptr);
|
m_nodes = new Nodes(this, nullptr);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
// SPDX-FileCopyrightText: The Monero Project
|
// SPDX-FileCopyrightText: The Monero Project
|
||||||
|
|
||||||
|
#include <QQuickWindow>
|
||||||
#include <QSslSocket>
|
#include <QSslSocket>
|
||||||
|
|
||||||
#include "Application.h"
|
#include "Application.h"
|
||||||
@@ -88,6 +89,14 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) {
|
|||||||
QApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::Round);
|
QApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::Round);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// wowlet: force the QtQuick scene graph to the software renderer. The app's only QtQuick surface
|
||||||
|
// (the Mining tab's QQuickWidget) otherwise drives the default RHI/GL path, which faults (0xc0000005)
|
||||||
|
// on broken-GL environments — notably VirtualBox's VMSVGA — crashing the whole app and orphaning the
|
||||||
|
// embedded node. The mining widget is small + static, so software rendering costs nothing meaningful
|
||||||
|
// and guarantees no GL-init crash on any user's hardware. (Confirmed in a VMSVGA Win11 VM: the software
|
||||||
|
// backend launches cleanly where the default GL backend crashed.)
|
||||||
|
QQuickWindow::setGraphicsApi(QSGRendererInterface::Software);
|
||||||
|
|
||||||
Application app(argc, argv);
|
Application app(argc, argv);
|
||||||
|
|
||||||
QApplication::setApplicationName("wowlet");
|
QApplication::setApplicationName("wowlet");
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
// SPDX-FileCopyrightText: The Monero Project
|
||||||
|
|
||||||
|
#include "utils/ChildProcessGuard.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
#if defined(Q_OS_WIN)
|
||||||
|
#include <windows.h>
|
||||||
|
#elif defined(Q_OS_UNIX)
|
||||||
|
#include <csignal>
|
||||||
|
#if defined(Q_OS_LINUX)
|
||||||
|
#include <sys/prctl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace ChildProcessGuard
|
||||||
|
{
|
||||||
|
|
||||||
|
#if defined(Q_OS_WIN)
|
||||||
|
|
||||||
|
// One process-wide job for all wowlet children. Created lazily on first use; the HANDLE is
|
||||||
|
// intentionally never closed — it must stay open for wowlet's entire lifetime so the job persists.
|
||||||
|
// On process death (clean OR crash) the kernel closes it, fires KILL_ON_JOB_CLOSE, and reaps the kids.
|
||||||
|
static HANDLE g_job = nullptr;
|
||||||
|
|
||||||
|
static HANDLE jobHandle()
|
||||||
|
{
|
||||||
|
if (g_job)
|
||||||
|
return g_job;
|
||||||
|
|
||||||
|
HANDLE job = CreateJobObjectW(nullptr, nullptr);
|
||||||
|
if (!job) {
|
||||||
|
qWarning() << "ChildProcessGuard: CreateJobObject failed:" << GetLastError();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
JOBOBJECT_EXTENDED_LIMIT_INFORMATION info{};
|
||||||
|
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||||
|
if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, &info, sizeof(info))) {
|
||||||
|
qWarning() << "ChildProcessGuard: SetInformationJobObject failed:" << GetLastError();
|
||||||
|
CloseHandle(job);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_job = job;
|
||||||
|
return g_job;
|
||||||
|
}
|
||||||
|
|
||||||
|
void installPreStart(QProcess *) { /* no-op on Windows: assignment happens post-start */ }
|
||||||
|
|
||||||
|
void adoptAfterStart(QProcess *process)
|
||||||
|
{
|
||||||
|
HANDLE job = jobHandle();
|
||||||
|
if (!job)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// QProcess::start() is asynchronous: right after it returns the child often has no PID yet. Wait
|
||||||
|
// (bounded) for it to actually start, else the assign is silently skipped and the child survives a
|
||||||
|
// parent crash (orphaned) — this is exactly what stranded wownerod in testing. Returns the instant
|
||||||
|
// the process is created, so the UI-thread cost is tiny.
|
||||||
|
if (process->state() == QProcess::Starting)
|
||||||
|
process->waitForStarted(5000);
|
||||||
|
|
||||||
|
const qint64 pid = process->processId(); // 0 if it still isn't running
|
||||||
|
if (pid == 0) {
|
||||||
|
qWarning() << "ChildProcessGuard: child has no PID; not assigned to job";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssignProcessToJobObject needs PROCESS_SET_QUOTA | PROCESS_TERMINATE. QProcess does not expose
|
||||||
|
// the child HANDLE, so re-open it by PID.
|
||||||
|
HANDLE proc = OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, FALSE,
|
||||||
|
static_cast<DWORD>(pid));
|
||||||
|
if (!proc) {
|
||||||
|
qWarning() << "ChildProcessGuard: OpenProcess failed for pid" << pid << ":" << GetLastError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AssignProcessToJobObject(job, proc))
|
||||||
|
qWarning() << "ChildProcessGuard: AssignProcessToJobObject failed:" << GetLastError();
|
||||||
|
else
|
||||||
|
qInfo() << "ChildProcessGuard: bound child pid" << pid << "to kill-on-close job";
|
||||||
|
|
||||||
|
CloseHandle(proc); // closing this per-child handle does NOT remove the process from the job
|
||||||
|
}
|
||||||
|
|
||||||
|
#elif defined(Q_OS_UNIX)
|
||||||
|
|
||||||
|
void installPreStart(QProcess *process)
|
||||||
|
{
|
||||||
|
#if defined(Q_OS_LINUX)
|
||||||
|
// Runs in the forked child between fork() and exec(). Keep it async-signal-safe.
|
||||||
|
process->setChildProcessModifier([] {
|
||||||
|
::prctl(PR_SET_PDEATHSIG, SIGKILL);
|
||||||
|
// Fork-race guard: if the parent already died before prctl ran, self-terminate.
|
||||||
|
if (::getppid() == 1)
|
||||||
|
::raise(SIGKILL);
|
||||||
|
});
|
||||||
|
#else
|
||||||
|
Q_UNUSED(process) // macOS/BSD: no PR_SET_PDEATHSIG; rely on aboutToQuit + destructor stop()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void adoptAfterStart(QProcess *) { /* no-op on Unix: handled pre-start */ }
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
void installPreStart(QProcess *) {}
|
||||||
|
void adoptAfterStart(QProcess *) {}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace ChildProcessGuard
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
// SPDX-FileCopyrightText: The Monero Project
|
||||||
|
|
||||||
|
#ifndef WOWLET_CHILDPROCESSGUARD_H
|
||||||
|
#define WOWLET_CHILDPROCESSGUARD_H
|
||||||
|
|
||||||
|
#include <QProcess>
|
||||||
|
|
||||||
|
// wowlet: OS-level "die with parent" for spawned child processes (embedded wownerod, bundled tor).
|
||||||
|
//
|
||||||
|
// QProcess does NOT kill its child when the parent exits or crashes; it only detaches. Feather's
|
||||||
|
// TorManager/DaemonManager relied solely on an explicit stop() in the normal-quit path, so an
|
||||||
|
// unhandled crash (e.g. a 0xc0000005 access violation) orphaned the children. This binds the
|
||||||
|
// child's lifetime to wowlet's at the kernel level, which survives a hard crash where no C++
|
||||||
|
// destructor and no Qt teardown runs.
|
||||||
|
//
|
||||||
|
// Windows: a single process-wide Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. Assign each
|
||||||
|
// child after start(); when wowlet's process dies the job's last handle closes and the
|
||||||
|
// kernel terminates every process still in the job.
|
||||||
|
// Unix: prctl(PR_SET_PDEATHSIG, SIGKILL) installed in the child (via setChildProcessModifier),
|
||||||
|
// so the kernel SIGKILLs the child when the parent dies.
|
||||||
|
namespace ChildProcessGuard
|
||||||
|
{
|
||||||
|
// Unix: arrange for `process` to receive SIGKILL when wowlet dies. Call BEFORE process->start().
|
||||||
|
// No-op on Windows (the Job Object handles it post-start).
|
||||||
|
void installPreStart(QProcess *process);
|
||||||
|
|
||||||
|
// Windows: assign the (now-running) child to wowlet's kill-on-close job. Call right AFTER start()
|
||||||
|
// returns and the child has a PID. No-op on Unix.
|
||||||
|
void adoptAfterStart(QProcess *process);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif //WOWLET_CHILDPROCESSGUARD_H
|
||||||
+121
-4
@@ -6,10 +6,21 @@
|
|||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
|
#include <QEventLoop>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QNetworkRequest>
|
||||||
#include <QRegularExpression>
|
#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/config.h"
|
||||||
|
#include "utils/networktype.h"
|
||||||
#include "utils/TorManager.h"
|
#include "utils/TorManager.h"
|
||||||
#include "utils/Utils.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::readyReadStandardOutput, this, &DaemonManager::handleProcessOutput);
|
||||||
connect(m_process, &QProcess::errorOccurred, this, &DaemonManager::handleProcessError);
|
connect(m_process, &QProcess::errorOccurred, this, &DaemonManager::handleProcessError);
|
||||||
connect(m_process, &QProcess::stateChanged, this, &DaemonManager::onStateChanged);
|
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);
|
QPointer<DaemonManager> DaemonManager::m_instance(nullptr);
|
||||||
@@ -51,11 +67,22 @@ void DaemonManager::start() {
|
|||||||
return;
|
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)) {
|
if (Utils::portOpen(this->rpcHost, this->rpcPort)) {
|
||||||
// Something is already listening on our RPC port; assume it is a usable local node.
|
if (this->verifyAdoptableNode()) {
|
||||||
m_alreadyRunning = true;
|
qDebug() << "Adopting existing wownerod on" << this->rpcAddress();
|
||||||
emit daemonStateChanged(true);
|
m_alreadyRunning = true;
|
||||||
return;
|
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;
|
m_restarts += 1;
|
||||||
@@ -106,6 +133,7 @@ void DaemonManager::start() {
|
|||||||
qDebug() << QString("Starting wownerod: %1 %2").arg(this->daemonPath, arguments.join(" "));
|
qDebug() << QString("Starting wownerod: %1 %2").arg(this->daemonPath, arguments.join(" "));
|
||||||
|
|
||||||
m_process->start(this->daemonPath, arguments);
|
m_process->start(this->daemonPath, arguments);
|
||||||
|
ChildProcessGuard::adoptAfterStart(m_process); // wowlet: bind to kill-on-crash job (Windows)
|
||||||
m_started = true;
|
m_started = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,6 +266,95 @@ bool DaemonManager::shouldStartDaemon() {
|
|||||||
return true;
|
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) {
|
void DaemonManager::setErrorMessage(const QString &msg) {
|
||||||
this->errorMsg = msg;
|
this->errorMsg = msg;
|
||||||
if (!msg.isEmpty())
|
if (!msg.isEmpty())
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ private slots:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
bool shouldStartDaemon();
|
bool shouldStartDaemon();
|
||||||
|
// wowlet: verify an already-open RPC port really is a healthy wownero-mainnet wownerod before
|
||||||
|
// adopting it, instead of trusting a bare TCP port-open probe. Prevents the wallet from silently
|
||||||
|
// syncing against a stale/stuck orphan (e.g. left by a prior crash) or a process squatting
|
||||||
|
// 127.0.0.1:34568. Synchronous, bounded (~1.5s), UI-thread-safe.
|
||||||
|
bool verifyAdoptableNode();
|
||||||
void setErrorMessage(const QString &msg);
|
void setErrorMessage(const QString &msg);
|
||||||
|
|
||||||
QProcess *m_process;
|
QProcess *m_process;
|
||||||
|
|||||||
+62
-19
@@ -6,6 +6,7 @@
|
|||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
|
|
||||||
|
#include "utils/ChildProcessGuard.h" // wowlet: OS-level die-with-parent for bundled tor
|
||||||
#include "utils/config.h"
|
#include "utils/config.h"
|
||||||
#include "utils/Utils.h"
|
#include "utils/Utils.h"
|
||||||
#include "utils/os/tails.h"
|
#include "utils/os/tails.h"
|
||||||
@@ -37,15 +38,26 @@ TorManager::TorManager(QObject *parent)
|
|||||||
connect(m_process, &QProcess::readyReadStandardOutput, this, &TorManager::handleProcessOutput);
|
connect(m_process, &QProcess::readyReadStandardOutput, this, &TorManager::handleProcessOutput);
|
||||||
connect(m_process, &QProcess::errorOccurred, this, &TorManager::handleProcessError);
|
connect(m_process, &QProcess::errorOccurred, this, &TorManager::handleProcessError);
|
||||||
connect(m_process, &QProcess::stateChanged, this, &TorManager::stateChanged);
|
connect(m_process, &QProcess::stateChanged, this, &TorManager::stateChanged);
|
||||||
|
|
||||||
|
ChildProcessGuard::installPreStart(m_process); // wowlet: SIGKILL tor if wowlet dies (Unix)
|
||||||
|
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]{ this->stop(); }); // wowlet
|
||||||
}
|
}
|
||||||
|
|
||||||
QPointer<TorManager> TorManager::m_instance(nullptr);
|
QPointer<TorManager> TorManager::m_instance(nullptr);
|
||||||
|
|
||||||
void TorManager::init() {
|
void TorManager::init() {
|
||||||
|
// wowlet: a live proxy/useLocalTor toggle re-runs init(). Reset failure/retry state so switching
|
||||||
|
// system->bundled Tor after a prior failure (Linux unpack, or a FailedToStart that latched
|
||||||
|
// m_stopRetries in handleProcessError) can actually start again instead of staying silently dead.
|
||||||
|
m_restarts = 0;
|
||||||
|
m_stopRetries = false;
|
||||||
|
m_unpacked = false; // re-evaluate the unpack on the next start()
|
||||||
|
|
||||||
m_localTor = !shouldStartTorDaemon();
|
m_localTor = !shouldStartTorDaemon();
|
||||||
|
|
||||||
auto state = m_process->state();
|
auto state = m_process->state();
|
||||||
if (m_localTor && (state == QProcess::ProcessState::Running || state == QProcess::ProcessState::Starting)) {
|
if (m_localTor && (state == QProcess::ProcessState::Running || state == QProcess::ProcessState::Starting)) {
|
||||||
|
m_stopRetries = true; // wowlet: intentional kill (switching to system Tor); don't auto-restart it
|
||||||
m_process->kill();
|
m_process->kill();
|
||||||
m_started = false;
|
m_started = false;
|
||||||
}
|
}
|
||||||
@@ -54,6 +66,8 @@ void TorManager::init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TorManager::stop() {
|
void TorManager::stop() {
|
||||||
|
m_stopRetries = true; // wowlet: intentional stop — suppress the stateChanged() auto-restart
|
||||||
|
m_checkConnectionTimer->stop(); // wowlet: stop probing the connection after an intentional stop
|
||||||
m_process->kill();
|
m_process->kill();
|
||||||
m_started = false;
|
m_started = false;
|
||||||
}
|
}
|
||||||
@@ -82,6 +96,17 @@ void TorManager::start() {
|
|||||||
this->torPath = alternativeTorFile;
|
this->torPath = alternativeTorFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wowlet: never hand QProcess a non-existent path (the Linux "empty folder" symptom). If the bundled
|
||||||
|
// binary isn't on disk, degrade to local/external-Tor mode + surface an error instead of burning the
|
||||||
|
// 4-retry budget on FailedToStart. With clearnet sync, node sync is unaffected; only broadcast-over-Tor
|
||||||
|
// is lost, which DaemonManager tolerates via clearnet relay.
|
||||||
|
if (!QFileInfo(this->torPath).isFile()) {
|
||||||
|
this->setErrorMessage("Bundled Tor binary not found; using external/local Tor if available: " + this->torPath);
|
||||||
|
m_localTor = true;
|
||||||
|
this->checkConnection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
qDebug() << QString("Start process: %1").arg(this->torPath);
|
qDebug() << QString("Start process: %1").arg(this->torPath);
|
||||||
|
|
||||||
m_restarts += 1;
|
m_restarts += 1;
|
||||||
@@ -102,6 +127,7 @@ void TorManager::start() {
|
|||||||
qDebug() << QString("%1 %2").arg(this->torPath, arguments.join(" "));
|
qDebug() << QString("%1 %2").arg(this->torPath, arguments.join(" "));
|
||||||
|
|
||||||
m_process->start(this->torPath, arguments);
|
m_process->start(this->torPath, arguments);
|
||||||
|
ChildProcessGuard::adoptAfterStart(m_process); // wowlet: bind to kill-on-crash job (Windows)
|
||||||
m_started = true;
|
m_started = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,37 +227,54 @@ bool TorManager::unpackBins() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
SemanticVersion embeddedVersion = SemanticVersion::fromString(QString(TOR_VERSION));
|
// wowlet: QFile::copy never creates parent dirs; without this the copy loop can silently no-op on
|
||||||
SemanticVersion filesystemVersion = this->getVersion(torPath);
|
// a fresh profile, leaving an empty tor/ dir and "tor failed to start" (Lucas/Fedora). main.cpp
|
||||||
qDebug() << QString("Tor versions: embedded %1, filesystem %2").arg(embeddedVersion.toString(), filesystemVersion.toString());
|
// also mkpaths this, but make unpackBins() self-sufficient. Mirrors DaemonManager::unpackBins().
|
||||||
if (SemanticVersion::isValid(filesystemVersion) && (embeddedVersion > filesystemVersion)) {
|
QDir().mkpath(this->torDir);
|
||||||
qInfo() << "Embedded version is newer, overwriting.";
|
|
||||||
QFile::setPermissions(torPath, QFile::ReadOther | QFile::WriteOther);
|
|
||||||
if (!QFile::remove(torPath)) {
|
|
||||||
qWarning() << "Unable to remove old Tor binary";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (embeddedVersion > filesystemVersion) {
|
// wowlet: unpack when the binary is MISSING, not only when a version compare says "newer". Overwrite
|
||||||
|
// only when the embedded build is a VALID newer version (getVersion() of a non-existent file returns
|
||||||
|
// an invalid SemanticVersion, so it must not gate the first unpack).
|
||||||
|
bool present = QFileInfo(this->torPath).isFile();
|
||||||
|
SemanticVersion embeddedVersion = SemanticVersion::fromString(QString(TOR_VERSION));
|
||||||
|
SemanticVersion filesystemVersion = present ? this->getVersion(this->torPath) : SemanticVersion();
|
||||||
|
qDebug() << QString("Tor versions: embedded %1, filesystem %2").arg(embeddedVersion.toString(), filesystemVersion.toString());
|
||||||
|
bool needsOverwrite = present && SemanticVersion::isValid(filesystemVersion)
|
||||||
|
&& (embeddedVersion > filesystemVersion);
|
||||||
|
|
||||||
|
if (!present || needsOverwrite) {
|
||||||
|
if (needsOverwrite) {
|
||||||
|
qInfo() << "Embedded Tor is newer, overwriting.";
|
||||||
|
QFile::setPermissions(this->torPath, QFile::ReadOther | QFile::WriteOther);
|
||||||
|
QFile::remove(this->torPath);
|
||||||
|
}
|
||||||
QDirIterator it(":/assets/tor", QDirIterator::Subdirectories);
|
QDirIterator it(":/assets/tor", QDirIterator::Subdirectories);
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
QString assetFile = it.next();
|
QString assetFile = it.next();
|
||||||
QFileInfo assetFileInfo = QFileInfo(assetFile);
|
QString filePath = QDir(this->torDir).filePath(QFileInfo(assetFile).fileName());
|
||||||
QFile f(assetFile);
|
QFile f(assetFile);
|
||||||
QString filePath = QDir(this->torDir).filePath(assetFileInfo.fileName());
|
|
||||||
f.copy(filePath);
|
f.copy(filePath);
|
||||||
f.close();
|
f.close();
|
||||||
}
|
}
|
||||||
qInfo() << "Wrote Tor binaries to: " << this->torDir;
|
qInfo() << "Wrote Tor binaries to:" << this->torDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(Q_OS_UNIX)
|
#if defined(Q_OS_UNIX)
|
||||||
QFile tor(this->torPath);
|
// wowlet: only chmod if the binary actually landed.
|
||||||
tor.setPermissions(QFile::ExeUser | QFile::ExeGroup | QFile::ExeOther
|
if (QFileInfo(this->torPath).isFile()) {
|
||||||
| QFile::ReadOwner | QFile::ReadGroup | QFile::ReadOther);
|
QFile tor(this->torPath);
|
||||||
|
tor.setPermissions(QFile::ExeUser | QFile::ExeGroup | QFile::ExeOther
|
||||||
|
| QFile::ReadOwner | QFile::ReadGroup | QFile::ReadOther);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// wowlet: do NOT report success if nothing landed — return false so shouldStartTorDaemon() falls
|
||||||
|
// back to --use-local-tor cleanly (see :297-303) instead of start()ing a missing binary.
|
||||||
|
if (!QFileInfo(this->torPath).isFile()) {
|
||||||
|
qWarning() << "Tor was not unpacked (no embedded Tor, or copy failed):" << this->torPath;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
m_unpacked = true;
|
m_unpacked = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -310,7 +353,7 @@ bool TorManager::shouldStartTorDaemon() {
|
|||||||
SemanticVersion TorManager::getVersion(const QString &fileName) {
|
SemanticVersion TorManager::getVersion(const QString &fileName) {
|
||||||
QProcess process;
|
QProcess process;
|
||||||
process.setProcessChannelMode(QProcess::MergedChannels);
|
process.setProcessChannelMode(QProcess::MergedChannels);
|
||||||
process.start(this->torPath, QStringList() << "--version");
|
process.start(fileName, QStringList() << "--version"); // wowlet: use the passed path, not this->torPath
|
||||||
process.waitForFinished(-1);
|
process.waitForFinished(-1);
|
||||||
QString output = process.readAllStandardOutput();
|
QString output = process.readAllStandardOutput();
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,10 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
|
|||||||
|
|
||||||
// Tor
|
// Tor
|
||||||
{Config::proxy, {QS("proxy"), Config::Proxy::Tor}},
|
{Config::proxy, {QS("proxy"), Config::Proxy::Tor}},
|
||||||
{Config::torPrivacyLevel, {QS("torPrivacyLevel"), 1}},
|
// wowlet: allTorExceptNode(0) — NEVER sync blocks over Tor. Block sync is ALWAYS clearnet (speed,
|
||||||
|
// the standard monero model). Tor is ONLY for tx broadcast (via DaemonManager's --tx-proxy), which
|
||||||
|
// is independent of this. This is what makes remote nodes work with OR without Tor.
|
||||||
|
{Config::torPrivacyLevel, {QS("torPrivacyLevel"), 0}},
|
||||||
{Config::torOnlyAllowOnion, {QS("torOnlyAllowOnion"), false}},
|
{Config::torOnlyAllowOnion, {QS("torOnlyAllowOnion"), false}},
|
||||||
{Config::socks5Host, {QS("socks5Host"), "127.0.0.1"}},
|
{Config::socks5Host, {QS("socks5Host"), "127.0.0.1"}},
|
||||||
{Config::socks5Port, {QS("socks5Port"), "9050"}},
|
{Config::socks5Port, {QS("socks5Port"), "9050"}},
|
||||||
|
|||||||
@@ -330,6 +330,15 @@ void Nodes::autoConnect(bool forceReconnect) {
|
|||||||
this->updateModels();
|
this->updateModels();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wowlet: clear the recent-failure blocklist then force a reconnect. autoConnect(true) alone is not
|
||||||
|
// enough — pickEligibleNode() still filters the failed node out of m_recentFailures, so a node that
|
||||||
|
// dropped during a transient Tor/proxy outage would stay blocklisted ("permanent Disconnected"). The
|
||||||
|
// node/Tor live-toggle handlers call this to recover.
|
||||||
|
void Nodes::reconnect() {
|
||||||
|
m_recentFailures.clear();
|
||||||
|
this->autoConnect(true);
|
||||||
|
}
|
||||||
|
|
||||||
FeatherNode Nodes::pickEligibleNode() {
|
FeatherNode Nodes::pickEligibleNode() {
|
||||||
// Pick a node at random to connect to
|
// Pick a node at random to connect to
|
||||||
auto rtn = FeatherNode();
|
auto rtn = FeatherNode();
|
||||||
|
|||||||
@@ -173,6 +173,9 @@ public slots:
|
|||||||
void onNodeSourceChanged(NodeSource nodeSource);
|
void onNodeSourceChanged(NodeSource nodeSource);
|
||||||
void setCustomNodes(const QList<FeatherNode>& nodes);
|
void setCustomNodes(const QList<FeatherNode>& nodes);
|
||||||
void autoConnect(bool forceReconnect = false);
|
void autoConnect(bool forceReconnect = false);
|
||||||
|
// wowlet: clear the recent-failure blocklist and force-reconnect. Used by the node/Tor live-toggle
|
||||||
|
// handlers so a node blocklisted during a transient outage (e.g. a Tor toggle) is actually retried.
|
||||||
|
void reconnect();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onWalletRefreshed();
|
void onWalletRefreshed();
|
||||||
|
|||||||
Reference in New Issue
Block a user