feat(ui): say "Node busy" instead of "Launching dragonxd"; honest quit copy
A node rebuilding note witnesses for an hour was shown as "Starting DragonX node... Launching dragonxd", which invited closing it. - Overlay, a new NodeBusy banner and the status bar now say what the node is doing (rebuilding witnesses / catching up on blocks / up but not answering), with the elapsed time and Phase-1 block progress. A rebuild during startup warm-up replaces the stale "-28 Activating best chain" text, and the overlay re-applies its text once a second instead of only on a connect attempt (30s apart while the node is busy). - Quit prompt: elapsed time, "Keep node running & quit" is the default, "Stop anyway & quit" becomes "Stop node & wait" (it no longer kills). - A reinstall skipped because the node is still running now notifies. - Status bar: don't repeat the warm-up status on the right, and don't print "Connected" there in non-English locales. - i18n: 31 new keys in all 8 languages; 10 dead force-quit/old-modal keys removed; CJK subset rebuilt for the new glyphs. - Tests: chooseDaemonStatus, NodeBusy banner priority, and every new key present in all 8 JSONs with matching printf conversions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+114
-10
@@ -946,6 +946,18 @@ void App::update()
|
||||
pumpRpcBusyCanary();
|
||||
const bool rpcBusy = rpc_busy_gate_->busy();
|
||||
|
||||
// Keep the "node starting / warming up / busy" overlay current between connect attempts and polls,
|
||||
// unless something else has replaced the status (see init_status_title_, applyWarmupText).
|
||||
if (ImGui::GetTime() >= status_refresh_at_) {
|
||||
status_refresh_at_ = ImGui::GetTime() + 1.0;
|
||||
if (state_.warming_up && !warmup_rpc_message_.empty() && connection_status_ == state_.warmup_status) {
|
||||
applyWarmupText(warmup_rpc_message_);
|
||||
} else if (state_.daemon_initializing && !state_.connected && !init_status_title_.empty() &&
|
||||
connection_status_ == init_status_title_) {
|
||||
applyDaemonInitStatus(init_status_busy_);
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -1629,6 +1641,8 @@ void App::render()
|
||||
snprintf(deletedMsg, sizeof(deletedMsg), TR("appx_blockchain_data_deleted"), deletedN);
|
||||
ui::Notifications::instance().success(deletedMsg);
|
||||
}
|
||||
if (pending_reinstall_node_busy_.exchange(false, std::memory_order_relaxed))
|
||||
ui::Notifications::instance().warning(TR("appx_daemon_reinstall_node_busy"), 12.0f);
|
||||
}
|
||||
|
||||
|
||||
@@ -2315,6 +2329,9 @@ void App::renderNodeStatusBanner()
|
||||
in.lite_open_error = lite_open_error_;
|
||||
in.tip_known = state_.sync.tip_known;
|
||||
in.blocks = state_.sync.blocks;
|
||||
const NodeBusyText busy = nodeBusyText();
|
||||
in.node_busy = busy.busy;
|
||||
in.node_busy_detail = busy.compact; // one line; the overlay and the quit prompt explain more
|
||||
|
||||
const ui::NodeBannerState banner = ui::evaluateNodeStatusBanner(in);
|
||||
if (!banner.show) return;
|
||||
@@ -2349,6 +2366,8 @@ void App::renderNodeStatusBanner()
|
||||
title = TR("node_banner_lite_open_failed"); icon = ICON_MD_ERROR; break;
|
||||
case ui::NodeBannerReason::NoPeers:
|
||||
title = TR("node_banner_no_peers_title"); icon = ICON_MD_CLOUD_OFF; break;
|
||||
case ui::NodeBannerReason::NodeBusy:
|
||||
title = busy.title.c_str(); icon = ICON_MD_HOURGLASS_EMPTY; break;
|
||||
case ui::NodeBannerReason::FullNodeOffline:
|
||||
default:
|
||||
title = TR("node_banner_offline_title"); icon = ICON_MD_CLOUD_OFF; break;
|
||||
@@ -2672,6 +2691,9 @@ void App::renderStatusBar()
|
||||
const char* dotStr = (dots == 0) ? "." : (dots == 1) ? ".." : (dots == 2) ? "..." : "";
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "%s%s", TR("sb_rescanning"), dotStr);
|
||||
}
|
||||
} else if (rpc_busy_gate_->busy()) {
|
||||
// The node holds back our requests (a long operation holds its cs_main): data may be stale.
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(ui::material::Warning()), "%s", TR("sb_node_busy"));
|
||||
} else if (state_.sync.syncing) {
|
||||
int chainTip = state_.longestchain > 0 ? state_.longestchain : state_.sync.headers;
|
||||
int blocksLeft = chainTip - state_.sync.blocks;
|
||||
@@ -2803,7 +2825,11 @@ void App::renderStatusBar()
|
||||
// with a small gap.
|
||||
float gap = sbSectionGap;
|
||||
float occupiedX = versionX; // leftmost X used by the version + connection status so far
|
||||
if (!connection_status_.empty() && connection_status_ != "Connected") {
|
||||
// The left of the bar already shows the warmup / "node busy" status; don't print it twice.
|
||||
const bool shownOnLeft = (state_.warming_up || state_.daemon_initializing) &&
|
||||
connection_status_ == state_.warmup_status;
|
||||
if (!connection_status_.empty() && connection_status_ != "Connected" &&
|
||||
connection_status_ != TR("connected") && !shownOnLeft) {
|
||||
// During a post-repair rescan the raw status is a scary "RPC request failed: Timeout" — show a
|
||||
// calm line instead (the rescan legitimately can't answer RPC yet).
|
||||
const std::string statusBase = post_recovery_rescan_ ? std::string(TR("sb_finishing_repair"))
|
||||
@@ -5833,6 +5859,7 @@ void App::reinstallBundledDaemon()
|
||||
if (stillUp()) {
|
||||
DEBUG_LOGF("[App] Reinstall bundled daemon: aborted — a dragonxd process is still running; "
|
||||
"binaries left untouched and no new node started\n");
|
||||
pending_reinstall_node_busy_.store(true, std::memory_order_relaxed); // told on the UI thread
|
||||
return;
|
||||
}
|
||||
// Overwrite the installed dragonx binaries (dragonxd/cli/tx) with the bundled ones.
|
||||
@@ -6084,6 +6111,70 @@ void App::feedDaemonBusy(const std::string& chunk)
|
||||
if (daemon_busy_reset_pending_.exchange(false)) daemon_busy_.reset();
|
||||
daemon_busy_.feed(chunk);
|
||||
node_rebuilding_ = daemon_busy_.rebuilding();
|
||||
if (!daemon_busy_.busy()) busy_first_seen_ = {};
|
||||
else if (busy_first_seen_ == std::chrono::steady_clock::time_point{}) busy_first_seen_ = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
static std::string formatBusyDuration(double secs)
|
||||
{
|
||||
const long s = secs > 0 ? static_cast<long>(secs) : 0;
|
||||
char buf[32];
|
||||
if (s >= 3600) snprintf(buf, sizeof(buf), "%ld:%02ld:%02ld", s / 3600, (s / 60) % 60, s % 60);
|
||||
else snprintf(buf, sizeof(buf), "%ld:%02ld", s / 60, s % 60);
|
||||
return buf;
|
||||
}
|
||||
|
||||
App::NodeBusyText App::nodeBusyText() const
|
||||
{
|
||||
NodeBusyText out;
|
||||
// The tracker reads OUR node's output; once that process is gone its last state means nothing.
|
||||
const bool ours = daemon_controller_ && daemon_controller_->isRunning();
|
||||
const bool rebuilding = ours && daemon_busy_.rebuilding();
|
||||
const bool replaying = ours && daemon_busy_.replaying() && !rebuilding;
|
||||
const bool rpcBusy = rpc_busy_gate_->busy();
|
||||
if (!rebuilding && !replaying && !rpcBusy) return out;
|
||||
out.busy = true;
|
||||
out.rebuilding = rebuilding;
|
||||
|
||||
// Running time: from the node's own (UTC) log timestamp when there is one, else since we first saw it.
|
||||
double secs = rpc_busy_gate_->busySeconds();
|
||||
if (rebuilding || replaying) {
|
||||
const std::int64_t startUtc = rebuilding ? daemon_busy_.rebuildStartUtc() : daemon_busy_.replayStartUtc();
|
||||
if (startUtc > 0) {
|
||||
secs = static_cast<double>(static_cast<std::int64_t>(std::time(nullptr)) - startUtc);
|
||||
} else if (busy_first_seen_ != std::chrono::steady_clock::time_point{}) {
|
||||
secs = std::chrono::duration<double>(std::chrono::steady_clock::now() - busy_first_seen_).count();
|
||||
}
|
||||
}
|
||||
out.elapsed = formatBusyDuration(secs);
|
||||
|
||||
char buf[512];
|
||||
std::string progress;
|
||||
if (rebuilding) {
|
||||
out.title = TR("node_busy_rebuild_title");
|
||||
snprintf(buf, sizeof(buf), TR("node_busy_rebuild_desc"), out.elapsed.c_str());
|
||||
out.detail = buf;
|
||||
// Phase 1 logs a line per 5000 blocks read; Phase 2 (extending the witnesses) logs nothing.
|
||||
const long done = daemon_busy_.readDone(), total = daemon_busy_.readTotal();
|
||||
const bool reading = total > 0 && done + 5000 < total;
|
||||
if (reading) snprintf(buf, sizeof(buf), TR("node_busy_rebuild_reading"), done, total);
|
||||
else snprintf(buf, sizeof(buf), "%s", TR("node_busy_rebuild_extending"));
|
||||
out.detail += " ";
|
||||
out.detail += buf;
|
||||
if (reading) progress = buf;
|
||||
} else if (replaying) {
|
||||
out.title = TR("node_busy_replay_title");
|
||||
snprintf(buf, sizeof(buf), TR("node_busy_replay_desc"), out.elapsed.c_str());
|
||||
out.detail = buf;
|
||||
} else {
|
||||
out.title = TR("node_busy_rpc_title");
|
||||
snprintf(buf, sizeof(buf), TR("node_busy_rpc_desc"), out.elapsed.c_str());
|
||||
out.detail = buf;
|
||||
}
|
||||
snprintf(buf, sizeof(buf), TR("node_busy_running"), out.elapsed.c_str());
|
||||
out.compact = buf;
|
||||
if (!progress.empty()) out.compact += " · " + progress;
|
||||
return out;
|
||||
}
|
||||
|
||||
void App::resetDaemonBusy()
|
||||
@@ -6142,17 +6233,35 @@ void App::renderDaemonStopConfirm()
|
||||
bool proceed = false;
|
||||
if (ImGui::BeginPopupModal("##DaemonStopConfirm", nullptr,
|
||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
||||
// Honest about what each choice does now that the wallet never kills the node: stopping waits for
|
||||
// it to finish or abort the rebuild (Phase 1 aborts and is redone on the next start; Phase 2 can't
|
||||
// be interrupted). Keeping it running loses nothing, so it is the recommended, focused default.
|
||||
const NodeBusyText busy = nodeBusyText();
|
||||
std::string elapsed = busy.elapsed;
|
||||
if (!busy.busy && rpc_busy_since_ != std::chrono::steady_clock::time_point{})
|
||||
elapsed = formatBusyDuration(std::chrono::duration<double>(
|
||||
std::chrono::steady_clock::now() - rpc_busy_since_).count());
|
||||
const bool rebuilding = busy.rebuilding;
|
||||
if (Type().subtitle1()) ImGui::PushFont(Type().subtitle1());
|
||||
ImGui::TextUnformatted(TR("appx_node_rebuilding_witness_cache"));
|
||||
ImGui::TextUnformatted(TR(rebuilding ? "stopconfirm_rebuild_title" : "stopconfirm_busy_title"));
|
||||
if (Type().subtitle1()) ImGui::PopFont();
|
||||
ImGui::Spacing();
|
||||
char body[1024];
|
||||
snprintf(body, sizeof(body), TR(rebuilding ? "stopconfirm_rebuild_body" : "stopconfirm_busy_body"),
|
||||
elapsed.empty() ? "0:00" : elapsed.c_str());
|
||||
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 26.0f);
|
||||
ImGui::TextUnformatted(TR("appx_stopping_node_discards_rebuild"));
|
||||
ImGui::TextUnformatted(body);
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
|
||||
if (TactileButton(TR("appx_keep_node_running_and_quit"), ImVec2(0, 0))) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Primary(), 210)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Primary()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(WithAlpha(Primary(), 160)));
|
||||
const bool keep = TactileButton(TR("appx_keep_node_running_and_quit"), ImVec2(0, 0));
|
||||
ImGui::PopStyleColor(3);
|
||||
ImGui::SetItemDefaultFocus();
|
||||
if (keep) {
|
||||
shutdown_keep_daemon_override_ = true;
|
||||
shutdown_confirmed_ = true;
|
||||
daemon_stop_confirm_open_ = false;
|
||||
@@ -6160,12 +6269,7 @@ void App::renderDaemonStopConfirm()
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 210)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Error()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 160)));
|
||||
const bool stopAnyway = TactileButton(TR("appx_stop_anyway_and_quit"), ImVec2(0, 0));
|
||||
ImGui::PopStyleColor(3);
|
||||
if (stopAnyway) {
|
||||
if (TactileButton(TR("stopconfirm_stop_wait"), ImVec2(0, 0))) {
|
||||
shutdown_confirmed_ = true;
|
||||
daemon_stop_confirm_open_ = false;
|
||||
proceed = true;
|
||||
|
||||
Reference in New Issue
Block a user