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:
@@ -54,6 +54,7 @@
|
||||
#include "util/amount_format.h"
|
||||
#include "util/payment_uri.h"
|
||||
#include "util/platform.h"
|
||||
#include "util/i18n.h"
|
||||
#include "util/xmrig_updater.h"
|
||||
#include "util/pool_registry.h"
|
||||
#include "util/daemon_updater.h"
|
||||
@@ -83,6 +84,7 @@
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
@@ -2987,6 +2989,31 @@ void testNodeStatusBanner()
|
||||
{
|
||||
using namespace dragonx::ui;
|
||||
|
||||
// S9: a busy node gets its own banner, ahead of every other full-node state. It must show even while
|
||||
// daemon_initializing is set (the busy overlay sets it), and offers no action (a reconnect would only
|
||||
// queue more requests behind the busy node).
|
||||
{
|
||||
NodeBannerInputs in;
|
||||
in.daemon_initializing = true;
|
||||
in.node_busy = true;
|
||||
in.node_busy_detail = "Running for 12:03.";
|
||||
NodeBannerState s = evaluateNodeStatusBanner(in);
|
||||
EXPECT_TRUE(s.show);
|
||||
EXPECT_TRUE(s.reason == NodeBannerReason::NodeBusy);
|
||||
EXPECT_TRUE(s.action == NodeBannerAction::None);
|
||||
EXPECT_TRUE(s.severity == NodeBannerSeverity::Warning);
|
||||
EXPECT_EQ(s.detail, std::string("Running for 12:03."));
|
||||
}
|
||||
{ // ...over "no peers" while connected, and over "offline" when not.
|
||||
NodeBannerInputs connected; connected.connected = true; connected.tip_known = false; connected.blocks = 5;
|
||||
connected.node_busy = true;
|
||||
EXPECT_TRUE(evaluateNodeStatusBanner(connected).reason == NodeBannerReason::NodeBusy);
|
||||
NodeBannerInputs offline; offline.node_busy = true;
|
||||
EXPECT_TRUE(evaluateNodeStatusBanner(offline).reason == NodeBannerReason::NodeBusy);
|
||||
NodeBannerInputs lite; lite.lite = true; lite.node_busy = true; // lite has no node to be busy
|
||||
EXPECT_FALSE(evaluateNodeStatusBanner(lite).show);
|
||||
}
|
||||
|
||||
// Connected full node → no banner.
|
||||
{
|
||||
NodeBannerInputs in; in.connected = true;
|
||||
@@ -4321,6 +4348,64 @@ void testConnectRetryBackoff()
|
||||
EXPECT_TRUE(attempts <= 6);
|
||||
}
|
||||
|
||||
// S9: the startup overlay says what the node is really doing, most specific first.
|
||||
void testChooseDaemonStatus()
|
||||
{
|
||||
using dragonx::daemon::DaemonBusyTracker;
|
||||
using dragonx::daemon::DaemonStatusKind;
|
||||
using dragonx::daemon::chooseDaemonStatus;
|
||||
DaemonBusyTracker idle, rebuilding, replaying;
|
||||
rebuilding.feedLine("Reading blocks for witness rebuild: 60000 / 62583");
|
||||
replaying.feedLine("init message: Activating best chain...");
|
||||
// The incident: a full queue during a rebuild showed "Launching dragonxd…".
|
||||
EXPECT_TRUE(chooseDaemonStatus(false, true, rebuilding) == DaemonStatusKind::Rebuilding);
|
||||
EXPECT_TRUE(chooseDaemonStatus(true, true, rebuilding) == DaemonStatusKind::Rebuilding);
|
||||
EXPECT_TRUE(chooseDaemonStatus(true, false, replaying) == DaemonStatusKind::Replaying);
|
||||
EXPECT_TRUE(chooseDaemonStatus(true, true, idle) == DaemonStatusKind::InitPhase); // "Loading block index"
|
||||
EXPECT_TRUE(chooseDaemonStatus(false, true, idle) == DaemonStatusKind::Busy); // reachable, no answer
|
||||
EXPECT_TRUE(chooseDaemonStatus(false, false, idle) == DaemonStatusKind::Launching); // not bound yet
|
||||
}
|
||||
|
||||
// S10: every string added by the daemon-stop work exists in English and in all 8 languages, with the same
|
||||
// printf arguments (a mismatch is rejected at runtime and would fall back to English, or worse).
|
||||
void testDaemonStopStringsTranslated()
|
||||
{
|
||||
const char* keys[] = {
|
||||
"shutdown_leave_running", "shutdown_leave_running_tip", "shutdown_kill_node", "shutdown_kill_node_tip",
|
||||
"shutdown_kill_confirm_title", "shutdown_kill_confirm_msg", "shutdown_kill_confirm_yes",
|
||||
"shutdown_stop_asking", "shutdown_stop_queued", "shutdown_stop_delivered", "shutdown_stop_cpu_suffix",
|
||||
"shutdown_stop_idle", "node_busy_rebuild_title", "node_busy_rebuild_desc", "node_busy_rebuild_reading",
|
||||
"node_busy_rebuild_extending", "node_busy_replay_title", "node_busy_replay_desc", "node_busy_rpc_title",
|
||||
"node_busy_rpc_desc", "node_starting_title", "node_starting_busy_desc", "node_starting_launch_desc",
|
||||
"sb_node_busy", "stopconfirm_rebuild_title", "stopconfirm_rebuild_body", "stopconfirm_busy_title",
|
||||
"stopconfirm_busy_body", "stopconfirm_stop_wait", "appx_daemon_reinstall_node_busy", "node_busy_running",
|
||||
};
|
||||
// printf conversions in order, "%%" excluded: what must survive translation.
|
||||
auto conversions = [](const std::string& s) {
|
||||
std::string sig;
|
||||
for (size_t i = 0; i < s.size(); ++i) {
|
||||
if (s[i] != '%' || ++i >= s.size()) continue;
|
||||
if (s[i] == '%') continue;
|
||||
while (i < s.size() && std::strchr("-+ #0123456789.*lhLzjtq", s[i])) ++i;
|
||||
if (i < s.size()) sig.push_back(s[i]);
|
||||
}
|
||||
return sig;
|
||||
};
|
||||
const fs::path langDir = fs::path(fixtureDir()) / ".." / ".." / "res" / "lang";
|
||||
for (const char* key : keys) {
|
||||
const std::string english = dragonx::util::I18n::instance().translate(key);
|
||||
EXPECT_TRUE(english != key); // present in the built-in English table
|
||||
for (const char* lang : {"de", "es", "fr", "pt", "ru", "zh", "ja", "ko"}) {
|
||||
std::ifstream in(langDir / (std::string(lang) + ".json"));
|
||||
const auto j = nlohmann::json::parse(in, nullptr, false);
|
||||
const bool has = !j.is_discarded() && j.contains(key) && j[key].is_string();
|
||||
if (!has) std::cerr << "missing translation: " << lang << "." << key << "\n";
|
||||
EXPECT_TRUE(has);
|
||||
if (has) EXPECT_EQ(conversions(j[key].get<std::string>()), conversions(english));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// S3: on Linux a node in its final shutdown phase reads comm "hush-shutoff"; its executable still says
|
||||
// dragonxd, so it must still count as running (it holds the datadir lock until it exits).
|
||||
void testDaemonProcessIdentity()
|
||||
@@ -8917,6 +9002,8 @@ int main()
|
||||
testDaemonBusyTracker();
|
||||
testShouldConfirmStop();
|
||||
testOutputSinceAcrossTrim();
|
||||
testChooseDaemonStatus();
|
||||
testDaemonStopStringsTranslated();
|
||||
#ifdef _WIN32
|
||||
testWindowsStopNeverKillsForeignDaemon();
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user