diff --git a/src/app.cpp b/src/app.cpp index 8ab89d1..bd4bdf8 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1675,7 +1675,9 @@ void App::renderStatusBar() // Connection status float dotOpacity = S.drawElement("components.status-bar", "connection-dot").opacity; if (dotOpacity < 0.0f) dotOpacity = 1.0f; - if (state_.warming_up) { + if (state_.warming_up || state_.daemon_initializing) { + // Both states mean "daemon reachable/launching but not serving yet" — show the same amber + // status so tabs without the overlay (Peers, Console) still tell the user what's happening. ImGui::PushFont(ui::material::Type().iconSmall()); ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.0f, dotOpacity), ICON_MD_CIRCLE); ImGui::PopFont(); @@ -3305,7 +3307,7 @@ void App::renderLoadingOverlay(float contentH) // ------------------------------------------------------------------- // 2b. Warmup description (subtitle explaining what's happening) // ------------------------------------------------------------------- - if (state_.warming_up && !state_.warmup_description.empty()) { + if ((state_.warming_up || state_.daemon_initializing) && !state_.warmup_description.empty()) { const char* descText = state_.warmup_description.c_str(); ImFont* capFont = Type().caption(); if (!capFont) capFont = ImGui::GetFont(); diff --git a/src/app.h b/src/app.h index 0e27f28..09910f8 100644 --- a/src/app.h +++ b/src/app.h @@ -701,6 +701,12 @@ private: void tryConnect(); void onConnected(); void onDisconnected(const std::string& reason); + // Set the "node is initializing" UI state (status line + overlay description) from the + // embedded/external daemon's launch state and its own console output (current phase + block + // height), so a connect probe that times out while the daemon loads shows WHAT it's doing. + // `reachableButBusy` is true when the probe connected but got no RPC reply (a timeout), + // false when the daemon is merely launching (not bound yet). Returns the status title. + std::string applyDaemonInitStatus(bool reachableButBusy); // Tear down a connection that died mid-session (daemon crash / restart / dropped // socket) so update()'s reconnect branch re-enters tryConnect(). Unlike onDisconnected // alone, this also rpc_->disconnect()s so rpc_->isConnected() actually flips to false. diff --git a/src/app_network.cpp b/src/app_network.cpp index f635bcf..53f1e23 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -158,6 +158,13 @@ static WarmupText translateWarmup(const std::string& raw) return {raw.c_str(), ""}; } +// Phrases dragonxd prints to its console while initializing, in the order translateWarmup() +// understands them. The most recent matching console line tells us which stage the node is in +// even when the RPC probe just times out (no -28 reply to read). +static const char* const kDaemonInitPhases[] = { + "Rescanning", "Rewinding", "Activating", "Verifying", "Loading", "Pruning", +}; + // ============================================================================ // Connection Management // ============================================================================ @@ -344,21 +351,25 @@ void App::tryConnect() "Restart the daemon or correct the credentials."); } else if (daemonStarting) { state_.connected = false; - // Show the actual RPC error alongside the waiting message so - // auth mismatches and timeouts aren't silently hidden. - if (!connectErr.empty()) { - char buf[256]; snprintf(buf, sizeof(buf), TR("sb_waiting_daemon_err"), connectErr.c_str()); - connection_status_ = buf; - } else { - connection_status_ = TR("sb_waiting_daemon"); - } + // The daemon is launched but RPC isn't answering yet. A *timeout* means it + // connected but the node is busy initializing (loading the block index, etc.); + // a connect refusal means it hasn't bound the RPC port yet. Either way, show a + // clear "node initializing" overlay (status + phase + block height from the + // daemon's own console output) instead of a bare technical error. + const bool reachableButBusy = connectErr.find("Timeout") != std::string::npos; + applyDaemonInitStatus(reachableButBusy); VERBOSE_LOGF("[connect #%d] RPC connection failed (%s) — daemon still starting, will retry...\n", attempt, connectErr.c_str()); network_refresh_.setTimer(services::NetworkRefreshService::Timer::Core, services::RefreshScheduler::kCoreDefault - 1.0f); } else if (externalDetected) { state_.connected = false; - if (!connectErr.empty()) { + // An external daemon is on the RPC port but not answering. A timeout means it's + // up and busy initializing; surface that as the init overlay (we can't read its + // console since we didn't launch it, so no phase line — just a clear message). + if (connectErr.find("Timeout") != std::string::npos) { + applyDaemonInitStatus(/*reachableButBusy=*/true); + } else if (!connectErr.empty()) { char buf[256]; snprintf(buf, sizeof(buf), TR("sb_connecting_err"), connectErr.c_str()); connection_status_ = buf; } else { @@ -408,6 +419,7 @@ void App::tryConnect() void App::onConnected() { state_.connected = true; + state_.daemon_initializing = false; // RPC is answering now; clear the "initializing" overlay connection_status_ = TR("connected"); // Reset crash counter on successful connection @@ -518,6 +530,47 @@ void App::onDisconnected(const std::string& reason) } } +std::string App::applyDaemonInitStatus(bool reachableButBusy) +{ + state_.daemon_initializing = true; + + // Find the most recent console line that names an init phase, so we can tell the user exactly + // what the node is doing (loading the block index, verifying, activating best chain, …). + std::string phaseLine; + if (daemon_controller_) { + const auto lines = daemon_controller_->recentLines(40); + for (auto it = lines.rbegin(); it != lines.rend() && phaseLine.empty(); ++it) { + for (const char* phase : kDaemonInitPhases) { + if (it->find(phase) != std::string::npos) { phaseLine = *it; break; } + } + } + } + + WarmupText wt; + if (!phaseLine.empty()) { + wt = translateWarmup(phaseLine); + } else if (reachableButBusy) { + // The probe connected but got no RPC reply within the timeout: the node is up but busy + // initializing (it isn't printing a recognizable phase, or we didn't launch it). + wt = {"Starting DragonX node…", + "The node is reachable but still initializing and isn't answering yet. " + "This is normal after an update or on first launch — it can take a few minutes."}; + } else { + // The daemon is launching but hasn't bound its RPC port yet. + wt = {"Starting DragonX node…", + "Launching dragonxd and waiting for it to come online…"}; + } + + std::string title = wt.title; + const int h = daemon_controller_ ? daemon_controller_->lastBlockHeight() : -1; + if (h > 0) title += " (Block " + std::to_string(h) + ")"; + + state_.warmup_status = title; + state_.warmup_description = wt.description ? wt.description : ""; + connection_status_ = title; + return title; +} + void App::handleLostConnection(const std::string& reason) { DEBUG_LOGF("[Connection] %s — tearing down for reconnect\n", reason.c_str()); diff --git a/src/data/wallet_state.h b/src/data/wallet_state.h index cc7da2a..08c8745 100644 --- a/src/data/wallet_state.h +++ b/src/data/wallet_state.h @@ -189,6 +189,12 @@ struct WalletState { // Connection bool connected = false; bool warming_up = false; // daemon reachable but in RPC warmup (error -28) + // True when the daemon is up/launching but not yet answering RPC (e.g. the connect probe + // times out because the node is loading the block index). Distinct from warming_up, which + // needs a JSON-RPC -28 reply; here getinfo never returns, so we infer the state from the + // daemon's launch state + its own console output. Drives the same loading overlay so the + // user sees WHAT the node is doing instead of a bare "Connection failed". + bool daemon_initializing = false; std::string warmup_status; // user-friendly title, e.g. "Processing blocks..." std::string warmup_description; // subtitle explaining the stage int daemon_version = 0; @@ -262,6 +268,7 @@ struct WalletState { void clear() { connected = false; warming_up = false; + daemon_initializing = false; warmup_status.clear(); warmup_description.clear(); daemon_version = 0;