wowlet: embedded wownerod DaemonManager (modeled on TorManager) — spawn/monitor/graceful-stop, run-own-node default

This commit is contained in:
jwinterm
2026-06-18 08:23:02 -04:00
parent 22553065e3
commit e9ecb63a9b
5 changed files with 320 additions and 0 deletions
+7
View File
@@ -22,6 +22,7 @@
#include "utils/os/tails.h"
#include "utils/os/whonix.h"
#include "utils/TorManager.h"
#include "utils/DaemonManager.h"
#include "utils/WebsocketNotifier.h"
#include "utils/AppData.h"
@@ -47,6 +48,11 @@ WindowManager::WindowManager(QObject *parent)
this->buildTrayMenu();
m_tray->setVisible(conf()->get(Config::showTrayIcon).toBool());
// wowlet: boot the embedded wownerod node early so it syncs while the user opens their wallet.
// No-op if the user disabled the local node (Config::runLocalNode) or no daemon is bundled.
daemonManager()->init();
daemonManager()->start();
this->initSkins();
this->patchMacStylesheet();
@@ -107,6 +113,7 @@ void WindowManager::close() {
}
torManager()->stop();
daemonManager()->stop(); // wowlet: gracefully shut down the embedded wownerod (flush LMDB)
deleteLater();
+227
View File
@@ -0,0 +1,227 @@
// SPDX-License-Identifier: BSD-3-Clause
// SPDX-FileCopyrightText: The Monero Project
#include "utils/DaemonManager.h"
#include <QCoreApplication>
#include <QDir>
#include <QDirIterator>
#include <QFileInfo>
#include <QRegularExpression>
#include "utils/config.h"
#include "utils/Utils.h"
DaemonManager::DaemonManager(QObject *parent)
: QObject(parent)
, m_process(new QProcess(this))
{
this->daemonDir = Config::defaultConfigDir().filePath("node");
#if defined(Q_OS_MACOS)
// On macOS the binary ships next to the app inside the bundle.
{
QDir appBinaryDir(QCoreApplication::applicationDirPath());
appBinaryDir.cd("..");
this->daemonDir = appBinaryDir.filePath("bin");
}
#endif
this->blockchainDir = Config::defaultConfigDir().filePath("node/blockchain");
m_process->setProcessChannelMode(QProcess::MergedChannels);
connect(m_process, &QProcess::readyReadStandardOutput, this, &DaemonManager::handleProcessOutput);
connect(m_process, &QProcess::errorOccurred, this, &DaemonManager::handleProcessError);
connect(m_process, &QProcess::stateChanged, this, &DaemonManager::onStateChanged);
}
QPointer<DaemonManager> DaemonManager::m_instance(nullptr);
QString DaemonManager::rpcAddress() const {
return QString("%1:%2").arg(this->rpcHost, QString::number(this->rpcPort));
}
void DaemonManager::init() {
// Make sure the data directory exists before wownerod tries to write to it.
QDir().mkpath(this->blockchainDir);
}
void DaemonManager::start() {
if (!shouldStartDaemon())
return;
auto state = m_process->state();
if (state == QProcess::ProcessState::Running || state == QProcess::ProcessState::Starting)
return;
if (!this->unpackBins()) {
this->setErrorMessage("Unable to unpack the embedded wownerod binary.");
return;
}
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;
}
m_restarts += 1;
if (m_restarts > 4) {
this->setErrorMessage("wownerod failed to start: maximum retries exceeded");
return;
}
QStringList arguments;
arguments << "--data-dir" << this->blockchainDir;
arguments << "--rpc-bind-ip" << this->rpcHost;
arguments << "--rpc-bind-port" << QString::number(this->rpcPort);
arguments << "--p2p-bind-port" << QString::number(this->p2pPort);
arguments << "--non-interactive";
arguments << "--no-igd"; // no UPnP port mapping
arguments << "--out-peers" << "16";
arguments << "--log-file" << QDir(this->blockchainDir).filePath("wownerod.log");
// Full archival by default (wownero's chain is small); prune is an explicit opt-in.
if (conf()->get(Config::pruneBlockchain).toBool())
arguments << "--prune-blockchain";
qDebug() << QString("Starting wownerod: %1 %2").arg(this->daemonPath, arguments.join(" "));
m_process->start(this->daemonPath, arguments);
m_started = true;
}
void DaemonManager::stop() {
m_stopping = true;
if (m_process->state() == QProcess::ProcessState::NotRunning) {
m_started = false;
return;
}
// wownerod owns an LMDB database. Send SIGTERM so it flushes and exits cleanly; only hard-kill
// if it refuses to shut down, to avoid blockchain database corruption.
m_process->terminate();
if (!m_process->waitForFinished(30000)) {
qWarning() << "wownerod did not exit gracefully within 30s; killing";
m_process->kill();
m_process->waitForFinished(5000);
}
m_started = false;
}
void DaemonManager::onStateChanged(QProcess::ProcessState state) {
if (state == QProcess::ProcessState::Running) {
this->setErrorMessage("");
emit daemonStateChanged(true);
qWarning() << "wownerod started";
}
else if (state == QProcess::ProcessState::NotRunning) {
emit daemonStateChanged(false);
if (m_stopping)
return;
// Unexpected exit: try to bring it back up shortly.
QTimer::singleShot(2000, this, [this] { this->start(); });
}
}
void DaemonManager::handleProcessOutput() {
QByteArray output = m_process->readAllStandardOutput();
this->logs.append(Utils::barrayToString(output));
emit logsUpdated();
// wownerod reports sync progress as "Synced <height>/<target>". Surface it for the UI.
static const QRegularExpression syncRe(R"(Synced (\d+)\/(\d+))");
QRegularExpressionMatch m = syncRe.match(QString::fromUtf8(output));
if (m.hasMatch()) {
quint64 height = m.captured(1).toULongLong();
quint64 target = m.captured(2).toULongLong();
emit syncStatusChanged(height, target);
if (target > 0 && height >= target && !m_synced) {
m_synced = true;
emit synced();
}
}
}
void DaemonManager::handleProcessError(QProcess::ProcessError error) {
if (error == QProcess::ProcessError::Crashed)
qWarning() << "wownerod crashed or was killed";
else if (error == QProcess::ProcessError::FailedToStart)
this->setErrorMessage("wownerod binary failed to start: " + this->daemonPath);
}
bool DaemonManager::unpackBins() {
QString daemonBin = "wownerod";
#if defined(Q_OS_WIN)
daemonBin += ".exe";
#endif
this->daemonPath = QDir(this->daemonDir).filePath(daemonBin);
#if defined(Q_OS_MACOS)
// Shipped next to the app bundle; nothing to unpack.
return QFileInfo(this->daemonPath).isFile();
#endif
if (m_unpacked)
return true;
if (QFileInfo(this->daemonPath).isFile()) {
m_unpacked = true;
return true;
}
QDir().mkpath(this->daemonDir);
QDirIterator it(":/assets/wownerod", QDirIterator::Subdirectories);
while (it.hasNext()) {
QString assetFile = it.next();
QFileInfo info(assetFile);
QFile f(assetFile);
f.copy(QDir(this->daemonDir).filePath(info.fileName()));
f.close();
}
#if defined(Q_OS_UNIX)
QFile bin(this->daemonPath);
bin.setPermissions(QFile::ExeUser | QFile::ExeGroup | QFile::ExeOther
| QFile::ReadOwner | QFile::ReadGroup | QFile::ReadOther);
#endif
if (!QFileInfo(this->daemonPath).isFile()) {
qWarning() << "wownerod was not unpacked (built without embedded daemon?)";
return false;
}
m_unpacked = true;
return true;
}
bool DaemonManager::shouldStartDaemon() {
m_alreadyRunning = false;
// The headline feature: run our own node by default. Opt-out is an explicit setting.
if (!conf()->get(Config::runLocalNode).toBool())
return false;
return true;
}
void DaemonManager::setErrorMessage(const QString &msg) {
this->errorMsg = msg;
if (!msg.isEmpty())
qWarning() << "DaemonManager:" << msg;
emit statusChanged(msg);
}
DaemonManager* DaemonManager::instance()
{
if (!m_instance) {
m_instance = new DaemonManager(QCoreApplication::instance());
}
return m_instance;
}
DaemonManager::~DaemonManager() {
this->stop();
}
+80
View File
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: BSD-3-Clause
// SPDX-FileCopyrightText: The Monero Project
#ifndef WOWLET_DAEMONMANAGER_H
#define WOWLET_DAEMONMANAGER_H
#include <QObject>
#include <QPointer>
#include <QProcess>
#include <QTimer>
// Manages an embedded wownerod node: unpacks the bundled binary, spawns it, monitors it, and stops
// it gracefully on exit. Modeled on Feather's TorManager (same bundle-binary + QProcess-lifecycle
// pattern), but wownerod owns an LMDB blockchain database, so shutdown is a graceful terminate (the
// daemon must flush), never a hard kill.
//
// wowlet runs this by default (the headline feature): the wallet connects to this local node at
// 127.0.0.1, and the embedded miner mines to it. Opt-out is a deliberate setting.
class DaemonManager : public QObject {
Q_OBJECT
public:
explicit DaemonManager(QObject *parent = nullptr);
static DaemonManager* instance();
~DaemonManager() override;
void init();
void start();
void stop();
bool unpackBins();
bool isStarted() const { return m_started; }
bool isAlreadyRunning() const { return m_alreadyRunning; }
bool isSynced() const { return m_synced; }
QString rpcAddress() const; // "127.0.0.1:<rpcPort>" for the wallet to connect to
QString daemonPath;
QString daemonDir; // where the unpacked wownerod binary lives
QString blockchainDir; // --data-dir (blockchain + p2pstate)
QString rpcHost = "127.0.0.1";
quint16 rpcPort = 34568; // wownero default restricted-RPC port
quint16 p2pPort = 34567; // wownero default p2p port
QStringList logs;
QString errorMsg;
signals:
void daemonStateChanged(bool running);
void syncStatusChanged(quint64 height, quint64 targetHeight);
void synced();
void statusChanged(const QString &reason);
void logsUpdated();
private slots:
void onStateChanged(QProcess::ProcessState state);
void handleProcessOutput();
void handleProcessError(QProcess::ProcessError error);
private:
bool shouldStartDaemon();
void setErrorMessage(const QString &msg);
QProcess *m_process;
bool m_started = false;
bool m_alreadyRunning = false;
bool m_unpacked = false;
bool m_synced = false;
bool m_stopping = false;
int m_restarts = 0;
static QPointer<DaemonManager> m_instance;
};
inline DaemonManager* daemonManager()
{
return DaemonManager::instance();
}
#endif //WOWLET_DAEMONMANAGER_H
+2
View File
@@ -65,6 +65,8 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::lastSettingsPage, {QS("lastSettingsPage"), 0}},
{Config::preferredFiatCurrency,{QS("preferredFiatCurrency"), "USD"}},
{Config::skin,{QS("skin"), "Wownero"}},
{Config::runLocalNode,{QS("runLocalNode"), true}}, // wowlet: run own node by default
{Config::pruneBlockchain,{QS("pruneBlockchain"), false}}, // full archival by default
{Config::amountPrecision, {QS("amountPrecision"), 12}},
{Config::dateFormat, {QS("dateFormat"), "yyyy-MM-dd"}},
{Config::timeFormat, {QS("timeFormat"), "HH:mm"}},
+4
View File
@@ -72,6 +72,10 @@ public:
balanceShowFiat,
preferredFiatCurrency,
// Embedded node (wowlet)
runLocalNode,
pruneBlockchain,
// Network -> Proxy
proxy,
socks5Host,