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:
2026-04-29 12:47:57 -05:00
parent ee8a08e569
commit 9edab31728
95 changed files with 8776 additions and 37563 deletions

View File

@@ -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");
});