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
+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();
}