feat(node): show "node initializing" feedback when the daemon isn't answering yet
When the full-node connect probe (getinfo) times out, the daemon is reachable
at the TCP level but busy initializing (loading the block index, verifying,
activating best chain, …) and won't answer RPC. The wallet only recognized the
JSON-RPC -28 warmup reply, so a raw socket timeout fell through to a bare,
alarming "Connection failed" retry with no indication of what the user was
waiting on.
Add a daemon-initializing UI state that drives the existing loading overlay:
- WalletState::daemon_initializing — daemon up/launching but not serving yet
(distinct from warming_up, which needs a -28 reply).
- App::applyDaemonInitStatus() infers the current phase from the daemon's own
console output (scanning recent lines for Loading/Verifying/Activating/
Rescanning/Rewinding/Pruning) and the latest block height, producing a
friendly title + description, e.g. "Processing blocks… (Block 123456)".
- The connect loop calls it from the daemon-starting and external-detected
branches: a timeout -> "reachable but initializing", a connect refusal ->
"launching, waiting to come online". Cleared on a real connect.
- The loading overlay now shows the description for daemon_initializing too,
and the status-bar amber indicator covers it (so Peers/Console tabs without
the overlay still explain the wait).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user