mirror of
https://github.com/wownero/wowlet.git
synced 2026-10-17 15:21:30 +02:00
wowlet: embedded wownerod DaemonManager (modeled on TorManager) — spawn/monitor/graceful-stop, run-own-node default
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"}},
|
||||
|
||||
@@ -72,6 +72,10 @@ public:
|
||||
balanceShowFiat,
|
||||
preferredFiatCurrency,
|
||||
|
||||
// Embedded node (wowlet)
|
||||
runLocalNode,
|
||||
pruneBlockchain,
|
||||
|
||||
// Network -> Proxy
|
||||
proxy,
|
||||
socks5Host,
|
||||
|
||||
Reference in New Issue
Block a user