diff --git a/src/app.h b/src/app.h index 09910f8..276be60 100644 --- a/src/app.h +++ b/src/app.h @@ -507,6 +507,10 @@ private: std::string connection_status_ = "Disconnected"; bool connection_in_progress_ = false; bool remote_rpc_plaintext_warning_shown_ = false; + // Startup daemon-launch diagnostics: bound the "RPC port busy, no config" wait before warning, + // and show the embedded-daemon start failure (binary/params/spawn) only once. Reset on connect. + int daemon_wait_attempts_ = 0; + bool daemon_start_error_shown_ = false; float loading_timer_ = 0.0f; // spinner animation for loading overlay // Current page (sidebar navigation) diff --git a/src/app_network.cpp b/src/app_network.cpp index 53f1e23..ea6e1cd 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -165,6 +165,12 @@ static const char* const kDaemonInitPhases[] = { "Rescanning", "Rewinding", "Activating", "Verifying", "Loading", "Pruning", }; +// How many consecutive "RPC port busy but no config" connect attempts to wait through before +// warning the user that whatever owns the port isn't a usable DragonX node. The core retry runs +// roughly every few seconds, so this is on the order of ~20s — long enough for a real daemon to +// write its config, short enough not to leave the user guessing. +static constexpr int kDaemonWaitWarnAttempts = 4; + // ============================================================================ // Connection Management // ============================================================================ @@ -193,19 +199,32 @@ void App::tryConnect() VERBOSE_LOGF("[connect #%d] No valid config — DRAGONX.conf missing or no rpcuser/rpcpassword (looked at: %s)\n", connect_attempt, confPath.c_str()); - // If we already know an external daemon is on the port, just wait - // for the config file to appear (the daemon creates it on first run). - if (daemon_controller_ && daemon_controller_->externalDaemonDetected()) { + // Re-evaluate the RPC port LIVE rather than trusting a latched "external daemon detected" + // flag: EmbeddedDaemon::start() sets that latch whenever the port was busy at a prior + // attempt and then never re-checks it, so a stale socket (or a transient squatter that has + // since died) would strand us forever "waiting for config". If the port is genuinely busy, + // a real daemon writes its config shortly and we keep waiting; if it's free, we must start + // our own. + const bool portInUse = daemon::EmbeddedDaemon::isRpcPortInUse(); + if (portInUse) { connection_status_ = TR("sb_waiting_config"); - VERBOSE_LOGF("[connect #%d] External daemon detected on port, waiting for config file to appear\n", connect_attempt); + VERBOSE_LOGF("[connect #%d] RPC port in use but no config yet — waiting for the daemon to write it\n", + connect_attempt); + // After a bounded wait with no config appearing, whatever owns the port is not a usable + // DragonX node (a foreign process, or a stuck/half-dead daemon). Say so once, with the + // action, instead of leaving the user on a silent "waiting" spinner forever. + if (++daemon_wait_attempts_ == kDaemonWaitWarnAttempts) { + ui::Notifications::instance().warning(TR("daemon_port_busy_warn"), 20.0f); + } network_refresh_.setTimer(services::NetworkRefreshService::Timer::Core, services::RefreshScheduler::kCoreDefault - 1.0f); return; } - + daemon_wait_attempts_ = 0; // port is free — clear the bounded-wait counter + connection_status_ = TR("sb_no_conf"); - - // Try to start embedded daemon if enabled + + // Port is free → start our own embedded daemon (if enabled). if (isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) { connection_status_ = TR("sb_starting_daemon"); if (startEmbeddedDaemon()) { @@ -213,16 +232,24 @@ void App::tryConnect() VERBOSE_LOGF("[connect #%d] Embedded daemon starting, will retry connection...\n", connect_attempt); network_refresh_.setTimer(services::NetworkRefreshService::Timer::Core, services::RefreshScheduler::kCoreDefault - 1.0f); - } else if (daemon_controller_ && daemon_controller_->externalDaemonDetected()) { - connection_status_ = TR("sb_waiting_config"); - VERBOSE_LOGF("[connect #%d] External daemon detected but no config yet, will retry...\n", connect_attempt); + } else { + // The daemon couldn't be started (binary not found, Sapling params missing, spawn + // failure, …). Surface the actual reason instead of leaving the status stuck on + // "Starting dragonxd…": connection_status_ for the overlay/status bar, plus a + // one-time sticky notification with the full, actionable detail. + std::string detail = daemon_controller_ ? daemon_controller_->lastError() : std::string(); + VERBOSE_LOGF("[connect #%d] startEmbeddedDaemon() failed — lastError: %s, binary: %s\n", + connect_attempt, detail.empty() ? "(none)" : detail.c_str(), + daemon::EmbeddedDaemon::findDaemonBinary().c_str()); + connection_status_ = TR("sb_daemon_start_failed"); + if (!daemon_start_error_shown_) { + daemon_start_error_shown_ = true; + ui::Notifications::instance().error( + detail.empty() ? std::string(TR("sb_daemon_start_failed")) : detail, 30.0f); + } + // Keep retrying: a missing binary/params can be fixed without a restart. network_refresh_.setTimer(services::NetworkRefreshService::Timer::Core, services::RefreshScheduler::kCoreDefault - 1.0f); - } else { - VERBOSE_LOGF("[connect #%d] startEmbeddedDaemon() failed — lastError: %s, binary: %s\n", - connect_attempt, - daemon_controller_ ? daemon_controller_->lastError().c_str() : "(no daemon object)", - daemon::EmbeddedDaemon::findDaemonBinary().c_str()); } } else if (!isUsingEmbeddedDaemon()) { VERBOSE_LOGF("[connect #%d] Embedded daemon disabled (using external). No config found at %s\n", @@ -420,6 +447,8 @@ void App::onConnected() { state_.connected = true; state_.daemon_initializing = false; // RPC is answering now; clear the "initializing" overlay + daemon_wait_attempts_ = 0; // re-arm the port-busy / start-failure notifications + daemon_start_error_shown_ = false; connection_status_ = TR("connected"); // Reset crash counter on successful connection diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 269c4d5..d4fc79f 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -4,6 +4,7 @@ #include "i18n.h" #include "platform.h" +#include "../config/version.h" // DRAGONX_DEFAULT_RPC_PORT #include #include @@ -780,6 +781,11 @@ void I18n::loadBuiltinEnglish() strings_["sb_connecting_generic"] = "Connecting to daemon..."; strings_["sb_connecting_err"] = "Connecting to daemon — %s"; strings_["sb_daemon_crashed"] = "Daemon crashed %d times"; + strings_["sb_daemon_start_failed"] = "Couldn't start dragonxd"; + strings_["daemon_port_busy_warn"] = + "Port " DRAGONX_DEFAULT_RPC_PORT " is in use but isn't responding as a DragonX node. " + "Close the program using it (or free the port), then restart — the wallet can't start " + "its own node while the port is taken."; strings_["sb_extracting_sapling"] = "Extracting Sapling parameters..."; strings_["sb_sapling_failed"] = "Failed to extract Sapling parameters."; strings_["sb_sapling_not_found"] = "Sapling parameters not found.";