Refactor app services and stabilize refresh/UI flows
- Add refresh scheduler and network refresh service boundaries for typed refresh results, ordered RPC collectors, applicators, and price parsing. - Add daemon lifecycle and wallet security workflow helpers while preserving App-owned command RPC, decrypt, cancellation, and UI handoff behavior. - Split balance, console, mining, amount formatting, and async task logic into focused modules with expanded Phase 4 test coverage. - Fix market price loading by triggering price refresh immediately, avoiding queue-pressure drops, tracking loading/error state, and adding translations. - Polish send, explorer, peers, settings, theme/schema, and related tab UI. - Replace checked-in generated language headers with build-generated resources. - Document the cleanup audit, UI static-state guidance, and architecture updates.
This commit is contained in:
347
src/app.cpp
347
src/app.cpp
@@ -11,7 +11,9 @@
|
||||
#include "rpc/rpc_worker.h"
|
||||
#include "rpc/connection.h"
|
||||
#include "config/settings.h"
|
||||
#include "daemon/daemon_controller.h"
|
||||
#include "daemon/embedded_daemon.h"
|
||||
#include "daemon/lifecycle_adapters.h"
|
||||
#include "daemon/xmrig_manager.h"
|
||||
#include "ui/windows/main_window.h"
|
||||
#include "ui/windows/balance_tab.h"
|
||||
@@ -82,6 +84,7 @@
|
||||
#include <filesystem>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <exception>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -96,6 +99,47 @@ using json = nlohmann::json;
|
||||
App::App() = default;
|
||||
App::~App() = default;
|
||||
|
||||
bool App::sendStopCommandSafely(rpc::RPCClient& client, const char* context)
|
||||
{
|
||||
const char* label = context ? context : "App";
|
||||
try {
|
||||
client.call("stop");
|
||||
DEBUG_LOGF("[%s] Stop command sent\n", label);
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("[%s] Stop RPC failed: %s\n", label, e.what());
|
||||
} catch (...) {
|
||||
DEBUG_LOGF("[%s] Stop RPC failed with unknown exception\n", label);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
class AppDaemonLifecycleRuntime final : public daemon::DaemonController::LifecycleRuntime {
|
||||
public:
|
||||
explicit AppDaemonLifecycleRuntime(App& app) : app_(app) {}
|
||||
|
||||
void stopDaemonWithPolicy() override { app_.stopEmbeddedDaemon(); }
|
||||
bool startDaemon() override { return app_.startEmbeddedDaemon(); }
|
||||
void resetOutputOffset() override { app_.daemon_output_offset_ = 0; }
|
||||
|
||||
void requestRpcStopAndDisconnect(const char* context, const char* reason) override
|
||||
{
|
||||
if (app_.rpc_ && app_.rpc_->isConnected()) {
|
||||
app_.sendStopCommandSafely(*app_.rpc_, context);
|
||||
app_.rpc_->disconnect();
|
||||
}
|
||||
app_.onDisconnected(reason ? reason : "Daemon lifecycle");
|
||||
}
|
||||
|
||||
int deleteBlockchainData() override
|
||||
{
|
||||
return daemon::BlockchainDataCleaner::removeBlockchainData(util::Platform::getDragonXDataDir());
|
||||
}
|
||||
|
||||
private:
|
||||
App& app_;
|
||||
};
|
||||
|
||||
bool App::init()
|
||||
{
|
||||
DEBUG_LOGF("Initializing ObsidianDragon...\n");
|
||||
@@ -219,6 +263,7 @@ bool App::init()
|
||||
// Initialize background RPC worker thread
|
||||
worker_ = std::make_unique<rpc::RPCWorker>();
|
||||
worker_->start();
|
||||
network_refresh_.markDue(services::NetworkRefreshService::Timer::Price);
|
||||
|
||||
// Forward error/warning notifications to the console tab
|
||||
// Use ConsoleTab colors so the Errors filter works correctly
|
||||
@@ -331,6 +376,7 @@ void App::update()
|
||||
if (fast_worker_) {
|
||||
fast_worker_->drainResults();
|
||||
}
|
||||
async_tasks_.reapCompleted();
|
||||
|
||||
// Auto-lock check (only when connected + encrypted + unlocked)
|
||||
if (state_.connected && state_.isUnlocked()) {
|
||||
@@ -346,21 +392,13 @@ void App::update()
|
||||
state_.rebuildAddressList();
|
||||
}
|
||||
|
||||
// Update timers
|
||||
core_timer_ += io.DeltaTime;
|
||||
address_timer_ += io.DeltaTime;
|
||||
transaction_timer_ += io.DeltaTime;
|
||||
peer_timer_ += io.DeltaTime;
|
||||
price_timer_ += io.DeltaTime;
|
||||
fast_refresh_timer_ += io.DeltaTime;
|
||||
tx_age_timer_ += io.DeltaTime;
|
||||
opid_poll_timer_ += io.DeltaTime;
|
||||
using RefreshTimer = services::NetworkRefreshService::Timer;
|
||||
network_refresh_.tick(io.DeltaTime);
|
||||
|
||||
// Fast refresh (mining stats + daemon memory) every second
|
||||
// Skip when wallet is locked — no need to poll, and queued tasks
|
||||
// would delay the PIN unlock worker task.
|
||||
if (fast_refresh_timer_ >= FAST_REFRESH_INTERVAL) {
|
||||
fast_refresh_timer_ = 0.0f;
|
||||
if (network_refresh_.consumeDue(RefreshTimer::Fast)) {
|
||||
if (state_.connected && !state_.isLocked()) {
|
||||
refreshMiningInfo();
|
||||
|
||||
@@ -435,13 +473,13 @@ void App::update()
|
||||
}
|
||||
|
||||
// Populate solo mining log lines from daemon output
|
||||
if (embedded_daemon_ && embedded_daemon_->isRunning()) {
|
||||
state_.mining.log_lines = embedded_daemon_->getRecentLines(50);
|
||||
if (daemon_controller_ && daemon_controller_->isRunning()) {
|
||||
state_.mining.log_lines = daemon_controller_->recentLines(50);
|
||||
}
|
||||
|
||||
// Check daemon output for rescan progress (offloaded to worker)
|
||||
if (embedded_daemon_ && embedded_daemon_->isRunning()) {
|
||||
std::string newOutput = embedded_daemon_->getOutputSince(daemon_output_offset_);
|
||||
if (daemon_controller_ && daemon_controller_->isRunning()) {
|
||||
std::string newOutput = daemon_controller_->outputSince(daemon_output_offset_);
|
||||
if (!newOutput.empty() && fast_worker_) {
|
||||
fast_worker_->post([this, output = std::move(newOutput)]() -> rpc::RPCWorker::MainCb {
|
||||
// Parse on worker thread — pure string work, no shared state access
|
||||
@@ -524,7 +562,7 @@ void App::update()
|
||||
};
|
||||
});
|
||||
}
|
||||
} else if (!embedded_daemon_ || !embedded_daemon_->isRunning()) {
|
||||
} else if (!daemon_controller_ || !daemon_controller_->isRunning()) {
|
||||
// Clear rescan state if daemon is not running (but preserve during restart)
|
||||
if (state_.sync.rescanning && state_.sync.rescan_progress >= 0.99f) {
|
||||
state_.sync.rescanning = false;
|
||||
@@ -534,9 +572,9 @@ void App::update()
|
||||
}
|
||||
|
||||
// Poll pending z_sendmany operations for completion
|
||||
if (opid_poll_timer_ >= OPID_POLL_INTERVAL && !pending_opids_.empty()
|
||||
if (network_refresh_.isDue(RefreshTimer::Opid) && !pending_opids_.empty()
|
||||
&& state_.connected && fast_worker_) {
|
||||
opid_poll_timer_ = 0.0f;
|
||||
network_refresh_.reset(RefreshTimer::Opid);
|
||||
auto opids = pending_opids_; // copy for worker thread
|
||||
fast_worker_->post([this, opids]() -> rpc::RPCWorker::MainCb {
|
||||
auto* rpc = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
||||
@@ -598,9 +636,7 @@ void App::update()
|
||||
transactions_dirty_ = true;
|
||||
addresses_dirty_ = true;
|
||||
last_tx_block_height_ = -1;
|
||||
core_timer_ = active_core_interval_;
|
||||
transaction_timer_ = active_tx_interval_;
|
||||
address_timer_ = active_addr_interval_;
|
||||
network_refresh_.markWalletMutationRefresh();
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -609,27 +645,22 @@ void App::update()
|
||||
// Per-category refresh with tab-aware intervals
|
||||
// Skip when wallet is locked — same reason as above.
|
||||
if (state_.connected && !state_.isLocked()) {
|
||||
if (core_timer_ >= active_core_interval_) {
|
||||
core_timer_ = 0.0f;
|
||||
if (network_refresh_.consumeDue(RefreshTimer::Core)) {
|
||||
refreshCoreData();
|
||||
}
|
||||
// Skip balance/tx/address refresh during warmup — RPC calls fail with -28
|
||||
if (!state_.warming_up) {
|
||||
if (transaction_timer_ >= active_tx_interval_) {
|
||||
transaction_timer_ = 0.0f;
|
||||
if (network_refresh_.consumeDue(RefreshTimer::Transactions)) {
|
||||
refreshTransactionData();
|
||||
}
|
||||
if (address_timer_ >= active_addr_interval_) {
|
||||
address_timer_ = 0.0f;
|
||||
if (network_refresh_.consumeDue(RefreshTimer::Addresses)) {
|
||||
refreshAddressData();
|
||||
}
|
||||
}
|
||||
if (peer_timer_ >= active_peer_interval_) {
|
||||
peer_timer_ = 0.0f;
|
||||
if (network_refresh_.consumeDue(RefreshTimer::Peers)) {
|
||||
refreshPeerInfo();
|
||||
}
|
||||
} else if (core_timer_ >= active_core_interval_) {
|
||||
core_timer_ = 0.0f;
|
||||
} else if (network_refresh_.consumeDue(RefreshTimer::Core)) {
|
||||
if (!connection_in_progress_ &&
|
||||
wizard_phase_ == WizardPhase::None &&
|
||||
!bootstrap_downloading_) {
|
||||
@@ -638,8 +669,7 @@ void App::update()
|
||||
}
|
||||
|
||||
// Price refresh every 60 seconds
|
||||
if (price_timer_ >= PRICE_INTERVAL) {
|
||||
price_timer_ = 0.0f;
|
||||
if (network_refresh_.consumeDue(RefreshTimer::Price)) {
|
||||
if (settings_->getFetchPrices()) {
|
||||
refreshPrice();
|
||||
}
|
||||
@@ -1159,7 +1189,7 @@ void App::render()
|
||||
// Use fast-lane worker for console commands to avoid head-of-line
|
||||
// blocking behind the consolidated refreshData() batch.
|
||||
// Fall back to main rpc/worker if fast-lane hasn't connected yet.
|
||||
console_tab_.render(embedded_daemon_.get(),
|
||||
console_tab_.render(daemon_controller_ ? daemon_controller_->daemon() : nullptr,
|
||||
(fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(),
|
||||
(fast_rpc_ && fast_rpc_->isConnected() && fast_worker_) ? fast_worker_.get() : worker_.get(),
|
||||
xmrig_manager_.get());
|
||||
@@ -1572,7 +1602,7 @@ void App::renderStatusBar()
|
||||
}
|
||||
|
||||
// Decrypt-import background task indicator
|
||||
if (decrypt_import_active_) {
|
||||
if (wallet_security_workflow_.importActive()) {
|
||||
ImGui::SameLine(0, sbSectionGap);
|
||||
ImGui::TextDisabled("|");
|
||||
ImGui::SameLine(0, sbSeparatorGap);
|
||||
@@ -1992,10 +2022,7 @@ void App::renderAntivirusHelpDialog()
|
||||
void App::refreshNow()
|
||||
{
|
||||
// Trigger immediate refresh on all categories
|
||||
core_timer_ = active_core_interval_;
|
||||
transaction_timer_ = active_tx_interval_;
|
||||
address_timer_ = active_addr_interval_;
|
||||
peer_timer_ = active_peer_interval_;
|
||||
network_refresh_.markImmediateRefresh();
|
||||
transactions_dirty_ = true; // Force transaction list update
|
||||
addresses_dirty_ = true; // Force address/balance update
|
||||
last_tx_block_height_ = -1; // Reset tx cache
|
||||
@@ -2155,12 +2182,12 @@ bool App::startEmbeddedDaemon()
|
||||
}
|
||||
}
|
||||
|
||||
// Create daemon manager if needed
|
||||
if (!embedded_daemon_) {
|
||||
embedded_daemon_ = std::make_unique<daemon::EmbeddedDaemon>();
|
||||
// Create daemon controller if needed
|
||||
if (!daemon_controller_) {
|
||||
daemon_controller_ = std::make_unique<daemon::DaemonController>();
|
||||
|
||||
// Set up state callback
|
||||
embedded_daemon_->setStateCallback([this](daemon::EmbeddedDaemon::State state, const std::string& msg) {
|
||||
daemon_controller_->setStateCallback([this](daemon::EmbeddedDaemon::State state, const std::string& msg) {
|
||||
switch (state) {
|
||||
case daemon::EmbeddedDaemon::State::Starting:
|
||||
daemon_status_ = TR("sb_starting_daemon");
|
||||
@@ -2181,27 +2208,17 @@ bool App::startEmbeddedDaemon()
|
||||
});
|
||||
}
|
||||
|
||||
// Sync debug logging categories from user settings
|
||||
if (settings_) {
|
||||
embedded_daemon_->setDebugCategories(settings_->getDebugCategories());
|
||||
embedded_daemon_->setMaxConnections(settings_->getMaxConnections());
|
||||
}
|
||||
|
||||
return embedded_daemon_->start();
|
||||
return daemon_controller_->start(settings_.get());
|
||||
}
|
||||
|
||||
void App::stopEmbeddedDaemon()
|
||||
{
|
||||
if (!embedded_daemon_) return;
|
||||
if (!daemon_controller_) return;
|
||||
|
||||
// Never stop an external daemon unless the user explicitly opted in
|
||||
// via the "Stop external daemon" checkbox in Settings. This is a
|
||||
// defence-in-depth guard — callers should also check, but this
|
||||
// ensures no code path accidentally shuts down a daemon we don't own.
|
||||
if (embedded_daemon_->externalDaemonDetected() &&
|
||||
!(settings_ && settings_->getStopExternalDaemon())) {
|
||||
DEBUG_LOGF("stopEmbeddedDaemon: external daemon detected — "
|
||||
"skipping (stop_external_daemon setting is off)\n");
|
||||
auto shutdownDecision = daemon_controller_->shutdownDecision(
|
||||
false, settings_ && settings_->getStopExternalDaemon());
|
||||
if (shutdownDecision.action == daemon::DaemonController::ShutdownAction::DisconnectOnly) {
|
||||
DEBUG_LOGF("stopEmbeddedDaemon: %s — skipping\n", shutdownDecision.logReason);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2231,15 +2248,12 @@ void App::stopEmbeddedDaemon()
|
||||
if (!config.rpcuser.empty() && !config.rpcpassword.empty()) {
|
||||
auto tmp_rpc = std::make_unique<rpc::RPCClient>();
|
||||
// Use a short timeout — if daemon isn't listening yet, don't block
|
||||
if (tmp_rpc->connect(config.host, config.port,
|
||||
config.rpcuser, config.rpcpassword)) {
|
||||
if (tmp_rpc->connect(config.host, config.port,
|
||||
config.rpcuser, config.rpcpassword,
|
||||
config.use_tls)) {
|
||||
DEBUG_LOGF("Temporary RPC connected, sending stop...\n");
|
||||
try {
|
||||
tmp_rpc->call("stop");
|
||||
if (sendStopCommandSafely(*tmp_rpc, "Temporary daemon stop")) {
|
||||
stop_sent = true;
|
||||
DEBUG_LOGF("Stop command sent via temporary connection\n");
|
||||
} catch (...) {
|
||||
DEBUG_LOGF("Stop RPC failed via temporary connection\n");
|
||||
}
|
||||
tmp_rpc->disconnect();
|
||||
} else {
|
||||
@@ -2263,19 +2277,21 @@ void App::stopEmbeddedDaemon()
|
||||
shutdown_status_ = "Waiting for dragonxd process to exit...";
|
||||
// 20s grace period for the RPC "stop" to complete (LevelDB flush).
|
||||
// Only after that does stop() escalate to SIGTERM, then SIGKILL.
|
||||
embedded_daemon_->stop(20000);
|
||||
daemon_controller_->stop(20000);
|
||||
}
|
||||
|
||||
bool App::isEmbeddedDaemonRunning() const
|
||||
{
|
||||
return embedded_daemon_ && embedded_daemon_->isRunning();
|
||||
return daemon_controller_ && daemon_controller_->isRunning();
|
||||
}
|
||||
|
||||
void App::rescanBlockchain()
|
||||
{
|
||||
if (!isUsingEmbeddedDaemon() || !embedded_daemon_) {
|
||||
ui::Notifications::instance().warning(
|
||||
"Rescan requires embedded daemon. Restart your daemon with -rescan manually.");
|
||||
auto decision = daemon::DaemonController::evaluateLifecycleOperation(
|
||||
daemon::DaemonController::LifecycleOperation::Rescan,
|
||||
isUsingEmbeddedDaemon(), daemon_controller_ != nullptr, isEmbeddedDaemonRunning());
|
||||
if (!decision.allowed) {
|
||||
ui::Notifications::instance().warning(decision.warning);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2285,103 +2301,65 @@ void App::rescanBlockchain()
|
||||
// Initialize rescan state for status bar display
|
||||
state_.sync.rescanning = true;
|
||||
state_.sync.rescan_progress = 0.0f;
|
||||
state_.sync.rescan_status = "Starting rescan...";
|
||||
state_.sync.rescan_status = decision.status;
|
||||
|
||||
// Set rescan flag BEFORE stopping so it's ready when we restart
|
||||
embedded_daemon_->setRescanOnNextStart(true);
|
||||
DEBUG_LOGF("[App] Rescan flag set, rescanOnNextStart=%d\n", embedded_daemon_->rescanOnNextStart() ? 1 : 0);
|
||||
daemon_controller_->prepareLifecycleOperation(decision, settings_.get());
|
||||
DEBUG_LOGF("[App] Rescan flag set, rescanOnNextStart=%d\n", daemon_controller_->rescanOnNextStart() ? 1 : 0);
|
||||
|
||||
// Stop daemon, then restart
|
||||
std::thread([this]() {
|
||||
async_tasks_.submit(decision.taskName, [this, decision](const util::AsyncTaskManager::Token& token) {
|
||||
DEBUG_LOGF("[App] Stopping daemon for rescan...\n");
|
||||
stopEmbeddedDaemon();
|
||||
if (shutting_down_) return;
|
||||
|
||||
// Wait for daemon to fully stop
|
||||
DEBUG_LOGF("[App] Waiting for daemon to fully stop...\n");
|
||||
for (int i = 0; i < 30 && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
if (shutting_down_) return;
|
||||
|
||||
// Reset output offset so we parse fresh output for rescan progress
|
||||
daemon_output_offset_ = 0;
|
||||
|
||||
DEBUG_LOGF("[App] Starting daemon with rescan flag=%d\n", embedded_daemon_->rescanOnNextStart() ? 1 : 0);
|
||||
startEmbeddedDaemon();
|
||||
}).detach();
|
||||
AppDaemonLifecycleRuntime runtime(*this);
|
||||
daemon::AsyncLifecycleTaskContext context(token, shutting_down_);
|
||||
daemon_controller_->executeLifecycleOperation(decision, runtime, context);
|
||||
});
|
||||
}
|
||||
|
||||
void App::deleteBlockchainData()
|
||||
{
|
||||
if (!isUsingEmbeddedDaemon() || !embedded_daemon_) {
|
||||
ui::Notifications::instance().warning(
|
||||
"Delete blockchain requires embedded daemon. Stop your daemon manually and delete the data directory.");
|
||||
auto decision = daemon::DaemonController::evaluateLifecycleOperation(
|
||||
daemon::DaemonController::LifecycleOperation::DeleteBlockchainData,
|
||||
isUsingEmbeddedDaemon(), daemon_controller_ != nullptr, isEmbeddedDaemonRunning());
|
||||
if (!decision.allowed) {
|
||||
ui::Notifications::instance().warning(decision.warning);
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[App] Deleting blockchain data - stopping daemon first\n");
|
||||
ui::Notifications::instance().info("Stopping daemon and deleting blockchain data...");
|
||||
|
||||
std::thread([this]() {
|
||||
daemon_controller_->prepareLifecycleOperation(decision, settings_.get());
|
||||
async_tasks_.submit(decision.taskName, [this, decision](const util::AsyncTaskManager::Token& token) {
|
||||
DEBUG_LOGF("[App] Stopping daemon for blockchain deletion...\n");
|
||||
stopEmbeddedDaemon();
|
||||
if (shutting_down_) return;
|
||||
|
||||
for (int i = 0; i < 30 && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
if (shutting_down_) return;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
std::string dataDir = util::Platform::getDragonXDataDir();
|
||||
|
||||
// Directories to remove
|
||||
const char* dirs[] = { "blocks", "chainstate", "database", "notarizations" };
|
||||
// Files to remove
|
||||
const char* files[] = { "peers.dat", "fee_estimates.dat", "banlist.dat",
|
||||
"db.log", ".lock" };
|
||||
|
||||
int removed = 0;
|
||||
std::error_code ec;
|
||||
for (auto d : dirs) {
|
||||
fs::path p = fs::path(dataDir) / d;
|
||||
if (fs::exists(p, ec)) {
|
||||
auto n = fs::remove_all(p, ec);
|
||||
if (!ec) { removed += (int)n; DEBUG_LOGF("[App] Removed %s (%d entries)\n", d, (int)n); }
|
||||
else { DEBUG_LOGF("[App] Failed to remove %s: %s\n", d, ec.message().c_str()); }
|
||||
}
|
||||
}
|
||||
for (auto f : files) {
|
||||
fs::path p = fs::path(dataDir) / f;
|
||||
if (fs::remove(p, ec)) { removed++; DEBUG_LOGF("[App] Removed %s\n", f); }
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[App] Blockchain data deleted (%d items removed), restarting daemon...\n", removed);
|
||||
|
||||
daemon_output_offset_ = 0;
|
||||
startEmbeddedDaemon();
|
||||
}).detach();
|
||||
AppDaemonLifecycleRuntime runtime(*this);
|
||||
daemon::AsyncLifecycleTaskContext context(token, shutting_down_);
|
||||
auto result = daemon_controller_->executeLifecycleOperation(decision, runtime, context);
|
||||
DEBUG_LOGF("[App] Blockchain data deleted (%d items removed), restarting daemon...\n", result.deletedItems);
|
||||
});
|
||||
}
|
||||
|
||||
bool App::stopDaemonForBootstrap()
|
||||
{
|
||||
bool wasRunning = isEmbeddedDaemonRunning();
|
||||
if (wasRunning) {
|
||||
auto decision = daemon::DaemonController::evaluateLifecycleOperation(
|
||||
daemon::DaemonController::LifecycleOperation::BootstrapStop,
|
||||
isUsingEmbeddedDaemon(), daemon_controller_ != nullptr, isEmbeddedDaemonRunning());
|
||||
if (decision.wasRunning) {
|
||||
DEBUG_LOGF("[App] Stopping embedded daemon for bootstrap download...\n");
|
||||
if (rpc_ && rpc_->isConnected()) {
|
||||
try { rpc_->call("stop"); } catch (...) {}
|
||||
rpc_->disconnect();
|
||||
}
|
||||
onDisconnected("Bootstrap");
|
||||
daemon_controller_->prepareLifecycleOperation(decision, settings_.get());
|
||||
AppDaemonLifecycleRuntime runtime(*this);
|
||||
daemon::ImmediateLifecycleTaskContext context;
|
||||
daemon_controller_->executeLifecycleOperation(decision, runtime, context);
|
||||
}
|
||||
return wasRunning;
|
||||
return decision.wasRunning;
|
||||
}
|
||||
|
||||
double App::getDaemonMemoryUsageMB() const
|
||||
{
|
||||
// If we have an embedded daemon with a tracked process handle, use it
|
||||
// directly — more reliable than a process scan since we own the handle.
|
||||
if (embedded_daemon_ && embedded_daemon_->isRunning()) {
|
||||
double mb = embedded_daemon_->getMemoryUsageMB();
|
||||
if (daemon_controller_ && daemon_controller_->isRunning()) {
|
||||
double mb = daemon_controller_->memoryUsageMB();
|
||||
daemon_mem_diag_ = "embedded";
|
||||
if (mb > 0.0) return mb;
|
||||
} else {
|
||||
@@ -2408,6 +2386,7 @@ void App::beginShutdown()
|
||||
quit_requested_ = true;
|
||||
shutdown_timer_ = 0.0f;
|
||||
shutdown_start_time_ = std::chrono::steady_clock::now();
|
||||
async_tasks_.cancelAll();
|
||||
|
||||
// Signal the RPC worker to stop accepting new tasks (non-blocking).
|
||||
// The actual thread join + rpc disconnect happen in shutdown() after
|
||||
@@ -2425,13 +2404,13 @@ void App::beginShutdown()
|
||||
xmrig_manager_->stop(3000);
|
||||
}
|
||||
|
||||
DEBUG_LOGF("beginShutdown: starting (embedded_daemon_=%s)\n",
|
||||
embedded_daemon_ ? "yes" : "no");
|
||||
DEBUG_LOGF("beginShutdown: starting (daemon_controller_=%s)\n",
|
||||
daemon_controller_ ? "yes" : "no");
|
||||
|
||||
// If no embedded daemon, just mark done — don't stop
|
||||
// an externally-managed daemon that the user started themselves.
|
||||
// Worker join + RPC disconnect happen in shutdown().
|
||||
if (!embedded_daemon_) {
|
||||
if (!daemon_controller_) {
|
||||
DEBUG_LOGF("beginShutdown: no embedded daemon, disconnecting only\n");
|
||||
shutdown_status_ = "Disconnecting...";
|
||||
if (settings_) {
|
||||
@@ -2446,21 +2425,17 @@ void App::beginShutdown()
|
||||
settings_->save();
|
||||
}
|
||||
|
||||
// If user opted to keep daemon running, just mark done.
|
||||
// Also never stop an external daemon the user started themselves,
|
||||
// unless they've explicitly enabled the "stop external daemon" setting.
|
||||
bool externalDaemon = embedded_daemon_ && embedded_daemon_->externalDaemonDetected();
|
||||
if ((settings_ && settings_->getKeepDaemonRunning()) ||
|
||||
(externalDaemon && !(settings_ && settings_->getStopExternalDaemon()))) {
|
||||
DEBUG_LOGF("beginShutdown: %s, skipping daemon stop\n",
|
||||
externalDaemon ? "external daemon (not ours to stop)"
|
||||
: "keep_daemon_running enabled");
|
||||
shutdown_status_ = "Disconnecting (daemon stays running)...";
|
||||
auto shutdownDecision = daemon_controller_->shutdownDecision(
|
||||
settings_ && settings_->getKeepDaemonRunning(),
|
||||
settings_ && settings_->getStopExternalDaemon());
|
||||
if (shutdownDecision.action == daemon::DaemonController::ShutdownAction::DisconnectOnly) {
|
||||
DEBUG_LOGF("beginShutdown: %s, skipping daemon stop\n", shutdownDecision.logReason);
|
||||
shutdown_status_ = shutdownDecision.status;
|
||||
shutdown_complete_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
shutdown_status_ = "Sending stop command to daemon...";
|
||||
shutdown_status_ = shutdownDecision.status;
|
||||
DEBUG_LOGF("beginShutdown: spawning shutdown thread for daemon stop\n");
|
||||
|
||||
// Run the daemon shutdown on a background thread so the UI
|
||||
@@ -2684,8 +2659,8 @@ void App::renderShutdownScreen()
|
||||
// -------------------------------------------------------------------
|
||||
// 6. Daemon output panel — terminal-style box
|
||||
// -------------------------------------------------------------------
|
||||
if (embedded_daemon_) {
|
||||
auto lines = embedded_daemon_->getRecentLines(8);
|
||||
if (daemon_controller_) {
|
||||
auto lines = daemon_controller_->recentLines(8);
|
||||
if (!lines.empty()) {
|
||||
float panelW = vp_size.x * shutElem("panel-width-fraction", 0.70f);
|
||||
float panelX = cx - panelW * 0.5f;
|
||||
@@ -2894,7 +2869,7 @@ void App::renderLoadingOverlay(float contentH)
|
||||
// -------------------------------------------------------------------
|
||||
// 3b. Deferred encryption status
|
||||
// -------------------------------------------------------------------
|
||||
if (deferred_encrypt_pending_) {
|
||||
if (wallet_security_.hasDeferredEncryption()) {
|
||||
curY += gap;
|
||||
ImFont* capFont = Type().caption();
|
||||
if (!capFont) capFont = ImGui::GetFont();
|
||||
@@ -2926,8 +2901,8 @@ void App::renderLoadingOverlay(float contentH)
|
||||
// -------------------------------------------------------------------
|
||||
// 3c. Daemon crash error message
|
||||
// -------------------------------------------------------------------
|
||||
if (embedded_daemon_ &&
|
||||
embedded_daemon_->getState() == daemon::EmbeddedDaemon::State::Error) {
|
||||
if (daemon_controller_ &&
|
||||
daemon_controller_->state() == daemon::EmbeddedDaemon::State::Error) {
|
||||
curY += gap;
|
||||
ImFont* capFont = Type().caption();
|
||||
if (!capFont) capFont = ImGui::GetFont();
|
||||
@@ -2943,7 +2918,7 @@ void App::renderLoadingOverlay(float contentH)
|
||||
curY += ts.y + gap * 0.5f;
|
||||
|
||||
// Error details (wrapped) — show full diagnostic info
|
||||
const std::string& errDetail = embedded_daemon_->getLastError();
|
||||
const std::string& errDetail = daemon_controller_->lastError();
|
||||
if (!errDetail.empty()) {
|
||||
float wrapW = ws.x * 0.8f;
|
||||
if (wrapW > 700.0f) wrapW = 700.0f;
|
||||
@@ -2955,7 +2930,7 @@ void App::renderLoadingOverlay(float contentH)
|
||||
}
|
||||
|
||||
// Crash count hint
|
||||
if (embedded_daemon_->getCrashCount() >= 3) {
|
||||
if (daemon_controller_->crashCount() >= 3) {
|
||||
const char* hint = "Use Settings > Restart Daemon to try again";
|
||||
ImVec2 hs2 = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, hint);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
@@ -2968,8 +2943,8 @@ void App::renderLoadingOverlay(float contentH)
|
||||
// -------------------------------------------------------------------
|
||||
// 4. Daemon output snippet (last few lines, if embedded)
|
||||
// -------------------------------------------------------------------
|
||||
if (embedded_daemon_) {
|
||||
auto lines = embedded_daemon_->getRecentLines(8);
|
||||
if (daemon_controller_) {
|
||||
auto lines = daemon_controller_->recentLines(8);
|
||||
if (!lines.empty()) {
|
||||
curY += gap;
|
||||
|
||||
@@ -3026,6 +3001,8 @@ void App::shutdown()
|
||||
// do synchronous shutdown as fallback.
|
||||
if (!shutting_down_) {
|
||||
DEBUG_LOGF("Synchronous shutdown fallback...\n");
|
||||
async_tasks_.cancelAll();
|
||||
async_tasks_.joinAll();
|
||||
if (worker_) {
|
||||
worker_->stop();
|
||||
}
|
||||
@@ -3035,7 +3012,7 @@ void App::shutdown()
|
||||
if (settings_) {
|
||||
settings_->save();
|
||||
}
|
||||
if (embedded_daemon_) {
|
||||
if (daemon_controller_) {
|
||||
stopEmbeddedDaemon();
|
||||
}
|
||||
if (rpc_) {
|
||||
@@ -3051,14 +3028,7 @@ void App::shutdown()
|
||||
if (shutdown_thread_.joinable()) {
|
||||
shutdown_thread_.join();
|
||||
}
|
||||
// Wait for wizard's external daemon stop thread
|
||||
if (wizard_stop_thread_.joinable()) {
|
||||
wizard_stop_thread_.join();
|
||||
}
|
||||
// Wait for daemon restart thread
|
||||
if (daemon_restart_thread_.joinable()) {
|
||||
daemon_restart_thread_.join();
|
||||
}
|
||||
async_tasks_.joinAll();
|
||||
// Join the RPC worker thread (was signaled in beginShutdown via requestStop)
|
||||
if (worker_) {
|
||||
worker_->stop();
|
||||
@@ -3096,43 +3066,30 @@ bool App::hasPendingRPCResults() const {
|
||||
}
|
||||
void App::restartDaemon()
|
||||
{
|
||||
if (!use_embedded_daemon_ || daemon_restarting_.load()) return;
|
||||
auto decision = daemon::DaemonController::evaluateLifecycleOperation(
|
||||
daemon::DaemonController::LifecycleOperation::ManualRestart,
|
||||
use_embedded_daemon_, daemon_controller_ != nullptr, isEmbeddedDaemonRunning(), daemon_restarting_.load());
|
||||
if (!decision.allowed) return;
|
||||
daemon_restarting_ = true;
|
||||
|
||||
// Reset crash counter on manual restart
|
||||
if (embedded_daemon_) {
|
||||
embedded_daemon_->resetCrashCount();
|
||||
}
|
||||
daemon_controller_->prepareLifecycleOperation(decision, settings_.get());
|
||||
|
||||
DEBUG_LOGF("[App] Restarting embedded daemon...\n");
|
||||
connection_status_ = TR("sb_restarting_daemon");
|
||||
|
||||
// Disconnect RPC so the loading overlay appears
|
||||
if (rpc_ && rpc_->isConnected()) {
|
||||
if (decision.disconnectRpc && rpc_ && rpc_->isConnected()) {
|
||||
rpc_->disconnect();
|
||||
}
|
||||
onDisconnected("Daemon restart");
|
||||
|
||||
// Sync debug categories from settings to daemon
|
||||
if (embedded_daemon_ && settings_) {
|
||||
embedded_daemon_->setDebugCategories(settings_->getDebugCategories());
|
||||
}
|
||||
|
||||
// Run stop + start on a background thread to avoid blocking the UI.
|
||||
// The 5-second auto-retry in render() will reconnect once the daemon
|
||||
// is back up.
|
||||
if (daemon_restart_thread_.joinable()) {
|
||||
daemon_restart_thread_.join();
|
||||
}
|
||||
daemon_restart_thread_ = std::thread([this]() {
|
||||
if (embedded_daemon_ && isEmbeddedDaemonRunning()) {
|
||||
stopEmbeddedDaemon();
|
||||
}
|
||||
if (shutting_down_) { daemon_restarting_ = false; return; }
|
||||
// Brief pause to let the port free up
|
||||
for (int i = 0; i < 5 && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
startEmbeddedDaemon();
|
||||
async_tasks_.submit(decision.taskName, [this, decision](const util::AsyncTaskManager::Token& token) {
|
||||
AppDaemonLifecycleRuntime runtime(*this);
|
||||
daemon::AsyncLifecycleTaskContext context(token, shutting_down_);
|
||||
daemon_controller_->executeLifecycleOperation(decision, runtime, context);
|
||||
daemon_restarting_ = false;
|
||||
DEBUG_LOGF("[App] Daemon restart complete — waiting for RPC...\n");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user