feat(daemon): deliver stop reliably and never kill the node unasked
Closing the wallet sent "stop" at most twice, gated on a getinfo probe that needs the daemon's cs_main, then killed the node after 20 s (Windows) or 30 s (POSIX) whether or not stop had arrived. A node busy with an hour-long witness rebuild therefore never got stop, was killed mid- rebuild, and replayed everything since its last flush on every start. Delivery (src/daemon/stop_delivery.h, RPCClient::open/trySendStop): - stop goes over a dedicated client with no probe, so it never waits on rpc_'s lock or a cs_main-bound getinfo - every attempt is classified, and StopDeliveryPolicy decides the next: Accepted / 503 = delivered; Queued (reply timeout) is resent at most every 30 s; QueueFull / not listening are retried every 1.5 s; a refusal after we were heard means the node is shutting down; a 401 on our own node tries the next credential source - POSIX: a group SIGTERM goes out as soon as an attempt fails, which bypasses the HTTP queue entirely Waiting (DaemonController::executeDaemonStop, pure, virtual-clock tested): - never kills on its own. Quitting waits as long as the node needs; the shutdown screen offers "Close wallet — leave node running" after 3 s, and "Kill node (unsafe)" (confirmed) only once stop was delivered and the node showed no CPU/I-O progress for ~10 min outside a rebuild - other callers (restart, rescan, reinstall, switch, ...) wait up to 120 s, let go once the app starts quitting, and keep watching a node that is still running; wallet switch refuses to go ahead then - EmbeddedDaemon gains stop-session primitives (beginStop, pollOwnedProcess, signalGracefulStop, forceKillOwnedProcess, ownedResourceUsage over the Linux process group / Windows handle, endStop) and only one stop session runs at a time - the Force Quit button and its stall heuristic are gone; "Leave running" aborts a pending stop attempt and closes at once Verified against the fake daemon with the real GUI: closing mid-rebuild now delivers stop (or SIGTERM 2 ms after a queue-full refusal) and the node exits cleanly after its rebuild, where it was SIGKILLed before. The shutdown screen, its "Leave running" button and the idle Kill offer (after ~11 min) were checked by screenshot. New strings are English- only for now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+274
-140
@@ -5378,105 +5378,231 @@ bool App::startEmbeddedDaemon()
|
||||
return daemon_controller_->start(settings_.get());
|
||||
}
|
||||
|
||||
void App::stopEmbeddedDaemon()
|
||||
// Drives one stop session for DaemonController::executeDaemonStop: a dedicated RPC client for "stop" (not
|
||||
// rpc_, which another thread may hold, and whose getinfo probe needs the daemon's cs_main), the owned
|
||||
// process's exit / signal / CPU, and the shutdown screen's choices.
|
||||
class AppStopRuntime final : public daemon::DaemonController::StopRuntime {
|
||||
public:
|
||||
using DC = daemon::DaemonController;
|
||||
|
||||
AppStopRuntime(App& app, daemon::EmbeddedDaemon* owned, std::vector<rpc::ConnectionConfig> credentials)
|
||||
: app_(app), owned_(owned), creds_(std::move(credentials)), t0_(std::chrono::steady_clock::now())
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(app_.stop_client_mutex_);
|
||||
app_.active_stop_client_ = &client_;
|
||||
}
|
||||
~AppStopRuntime() override
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(app_.stop_client_mutex_);
|
||||
app_.active_stop_client_ = nullptr;
|
||||
}
|
||||
|
||||
double nowSec() override
|
||||
{
|
||||
return std::chrono::duration<double>(std::chrono::steady_clock::now() - t0_).count();
|
||||
}
|
||||
bool processAlive() override
|
||||
{
|
||||
if (owned_) return owned_->pollOwnedProcess();
|
||||
// Not ours: we can only watch for it to go (port and process), never wait on or signal it.
|
||||
return daemon::EmbeddedDaemon::isRpcPortInUse() || daemon::EmbeddedDaemon::isDaemonProcessRunning();
|
||||
}
|
||||
daemon::StopSendResult trySendStop() override
|
||||
{
|
||||
if (creds_.empty()) return daemon::StopSendResult::Other;
|
||||
if (!clientOpen_) {
|
||||
const auto& c = creds_[credIndex_];
|
||||
clientOpen_ = client_.open(c.host, c.port, c.rpcuser, c.rpcpassword, c.use_tls);
|
||||
if (!clientOpen_) return daemon::StopSendResult::Other;
|
||||
}
|
||||
const rpc::RpcFailureKind kind = client_.trySendStop(2);
|
||||
const daemon::StopSendResult r = daemon::stopSendResultFromKind(kind);
|
||||
++attempts_;
|
||||
if (attempts_ == 1 || r != lastLogged_) { // log changes, not every retry
|
||||
DEBUG_LOGF("[stop] attempt %d at %.1fs: %s (%s)\n", attempts_, nowSec(),
|
||||
daemon::stopSendResultName(r), rpc::rpcFailureKindName(kind));
|
||||
lastLogged_ = r;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
bool nextCredentials() override
|
||||
{
|
||||
if (credIndex_ + 1 >= creds_.size()) return false;
|
||||
++credIndex_;
|
||||
clientOpen_ = false;
|
||||
DEBUG_LOGF("[stop] HTTP 401 — retrying with the next credential source (%zu of %zu)\n",
|
||||
credIndex_ + 1, creds_.size());
|
||||
return true;
|
||||
}
|
||||
bool sendGracefulSignal() override { return owned_ && owned_->signalGracefulStop(); }
|
||||
double cpuSeconds() override
|
||||
{
|
||||
usage_ = owned_ ? owned_->ownedResourceUsage() : daemon::EmbeddedDaemon::ResourceUsage{};
|
||||
return usage_.cpuSeconds;
|
||||
}
|
||||
double ioBytes() override { return usage_.ioBytes; } // sampled with cpuSeconds()
|
||||
bool rebuildActive() override { return false; } // the witness-rebuild tracker is not wired yet
|
||||
daemon::StopUserChoice userChoice() override
|
||||
{
|
||||
return static_cast<daemon::StopUserChoice>(app_.stop_user_choice_.load());
|
||||
}
|
||||
void offerForceKill(bool offered) override
|
||||
{
|
||||
DEBUG_LOGF("[stop] %s\n", offered ? "no CPU/I-O progress for a long time: offering Kill"
|
||||
: "progress resumed: Kill offer withdrawn");
|
||||
app_.stop_kill_offered_ = offered;
|
||||
}
|
||||
void forceKill() override
|
||||
{
|
||||
DEBUG_LOGF("[stop] user chose Kill after %.0fs\n", nowSec());
|
||||
if (owned_) owned_->forceKillOwnedProcess();
|
||||
}
|
||||
void setStatus(const DC::StopStatus& s) override
|
||||
{
|
||||
if (s.delivered && !loggedDelivered_) {
|
||||
DEBUG_LOGF("[stop] stop delivered at %.1fs (%s)\n", s.elapsedSec,
|
||||
s.signalled ? "SIGTERM to the process group" : daemon::stopSendResultName(s.lastSend));
|
||||
loggedDelivered_ = true;
|
||||
}
|
||||
std::string text = TR(s.delivered ? "shutdown_stop_delivered"
|
||||
: s.heard ? "shutdown_stop_queued"
|
||||
: "shutdown_stop_asking");
|
||||
if (s.cpuPercent >= 0) {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), TR("shutdown_stop_cpu_suffix"), static_cast<int>(s.cpuPercent + 0.5));
|
||||
text += buf;
|
||||
}
|
||||
app_.shutdown_status_ = text;
|
||||
app_.stop_idle_minutes_ = static_cast<int>(s.idleSec / 60.0);
|
||||
}
|
||||
|
||||
private:
|
||||
App& app_;
|
||||
daemon::EmbeddedDaemon* owned_ = nullptr;
|
||||
std::vector<rpc::ConnectionConfig> creds_;
|
||||
size_t credIndex_ = 0;
|
||||
rpc::RPCClient client_;
|
||||
bool clientOpen_ = false;
|
||||
int attempts_ = 0;
|
||||
daemon::StopSendResult lastLogged_ = daemon::StopSendResult::Other;
|
||||
bool loggedDelivered_ = false;
|
||||
daemon::EmbeddedDaemon::ResourceUsage usage_;
|
||||
std::chrono::steady_clock::time_point t0_;
|
||||
};
|
||||
|
||||
class AppStopTaskContext final : public daemon::DaemonController::LifecycleTaskContext {
|
||||
public:
|
||||
AppStopTaskContext(App& app, bool watchShutdown) : app_(app), watchShutdown_(watchShutdown) {}
|
||||
bool cancelled() const override { return false; }
|
||||
bool shuttingDown() const override { return watchShutdown_ && app_.shutting_down_.load(); }
|
||||
void sleepForMs(int ms) override { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); }
|
||||
|
||||
private:
|
||||
App& app_;
|
||||
bool watchShutdown_ = false;
|
||||
};
|
||||
|
||||
void App::chooseStopAction(daemon::StopUserChoice choice)
|
||||
{
|
||||
if (!daemon_controller_) return;
|
||||
stop_user_choice_ = static_cast<int>(choice);
|
||||
if (choice == daemon::StopUserChoice::Detach) {
|
||||
// End a pending stop attempt now rather than after its timeout.
|
||||
std::lock_guard<std::mutex> lk(stop_client_mutex_);
|
||||
if (active_stop_client_) active_stop_client_->requestAbort();
|
||||
}
|
||||
}
|
||||
|
||||
daemon::StopOutcome App::stopEmbeddedDaemon(StopMode mode)
|
||||
{
|
||||
if (!daemon_controller_) return daemon::StopOutcome::Exited;
|
||||
|
||||
auto shutdownDecision = daemon_controller_->shutdownDecision(
|
||||
false, settings_ && settings_->getStopExternalDaemon());
|
||||
if (shutdownDecision.action == daemon::DaemonController::ShutdownAction::DisconnectOnly) {
|
||||
DEBUG_LOGF("stopEmbeddedDaemon: %s — skipping\n", shutdownDecision.logReason);
|
||||
return;
|
||||
return daemon::StopOutcome::StillRunning;
|
||||
}
|
||||
|
||||
// Do we hold a live process handle for this node? Capture it BEFORE stop() reaps the pid. If owned,
|
||||
// daemon_controller_->stop() below BLOCKS until the process actually exits. If NOT owned (external /
|
||||
// adopted / direct-connected / a lock-gated start that never spawned), stop() returns at once on every
|
||||
// platform — it only ever waits on (or kills) a process this app launched, never one found by name —
|
||||
// so we can only ask it over RPC and then watch for it to disappear (handled after the stop() call).
|
||||
const bool owned = daemon_controller_->isRunning();
|
||||
// One stop at a time: a second caller (say the shutdown thread while a restart task is stopping the
|
||||
// node) waits for the first. Background stops let go as soon as the app starts quitting, so the
|
||||
// shutdown never waits long behind one.
|
||||
std::lock_guard<std::mutex> single(stop_mutex_);
|
||||
daemon::EmbeddedDaemon* d = daemon_controller_->daemon();
|
||||
// beginStop() also marks the stop as requested (an early exit is not a crash) and makes this session
|
||||
// the only reaper. Only a process we launched and still hold is "owned": a lock-gated start, an
|
||||
// external node and a node we already let go of are not, and are never waited on, signalled or killed.
|
||||
const bool owned = d && d->beginStop();
|
||||
if (d && !owned) d->endStop(/*keepWatching=*/false);
|
||||
|
||||
// Any exit from here on is the stop we are asking for, not a crash — even one that lands before
|
||||
// daemon_controller_->stop() below gets to reap it (crash_count_ drives auto-restart / give-up).
|
||||
daemon_controller_->markStopRequested();
|
||||
if (!owned && !daemon::EmbeddedDaemon::isRpcPortInUse()) {
|
||||
// Nothing listening for a stop: either it is gone, or it is another dragonxd we can't talk to (one
|
||||
// still finishing its own shutdown trips the start() lock gate this way). Leave it be.
|
||||
const bool running = daemon::EmbeddedDaemon::isDaemonProcessRunning();
|
||||
DEBUG_LOGF("stopEmbeddedDaemon: no node of ours and no RPC listener%s\n",
|
||||
running ? " — another dragonxd is running; leaving it alone" : "");
|
||||
return running ? daemon::StopOutcome::StillRunning : daemon::StopOutcome::Exited;
|
||||
}
|
||||
|
||||
// Send RPC "stop" command — this is the graceful path that lets the
|
||||
// daemon flush state, save block indexes, close sockets, etc.
|
||||
bool stop_sent = false;
|
||||
|
||||
// Try the existing RPC connection first. beginShutdown() called rpc_->requestAbort() to unblock any
|
||||
// in-flight request — but that sticky abort (cleared only by connect()) would make THIS "stop" self-
|
||||
// abort mid-flight (CURLE_ABORTED_BY_CALLBACK). The old async rpc_->stop() swallowed that error yet
|
||||
// set stop_sent=true, so the daemon never heard "stop" and the working temp-connection fallback was
|
||||
// skipped — which is why "Stop external daemon" left an external dragonxd running (it only died via
|
||||
// the 20s force-kill). Clear the abort and send SYNCHRONOUSLY so real success is surfaced.
|
||||
if (rpc_ && rpc_->isConnected()) {
|
||||
DEBUG_LOGF("Sending stop command via existing RPC connection...\n");
|
||||
rpc_->clearAbort();
|
||||
if (sendStopCommandSafely(*rpc_, "existing connection stop"))
|
||||
stop_sent = true;
|
||||
// Credentials for the stop: our own node's conf (and, if those are stale, our last working
|
||||
// connection, then the .cookie); a node we don't own, the exact credentials we connected with.
|
||||
std::vector<rpc::ConnectionConfig> creds;
|
||||
const rpc::ConnectionConfig detected = rpc::Connection::autoDetectConfig();
|
||||
const bool haveSaved = !saved_config_.rpcuser.empty() || !saved_config_.rpcpassword.empty();
|
||||
if (owned) {
|
||||
creds.push_back(detected);
|
||||
if (haveSaved && (saved_config_.rpcuser != detected.rpcuser ||
|
||||
saved_config_.rpcpassword != detected.rpcpassword || saved_config_.port != detected.port))
|
||||
creds.push_back(saved_config_);
|
||||
rpc::ConnectionConfig cookie;
|
||||
if (rpc::Connection::buildCookieAuthConfig(detected, cookie)) creds.push_back(cookie);
|
||||
} else {
|
||||
creds.push_back(haveSaved ? saved_config_ : detected);
|
||||
}
|
||||
|
||||
// If the main connection wasn't established (e.g. daemon was still
|
||||
// starting up when user closed the window), create a temporary
|
||||
// RPC connection just to send the stop command.
|
||||
if (!stop_sent) {
|
||||
DEBUG_LOGF("Main RPC not connected — creating temporary connection for stop...\n");
|
||||
auto config = rpc::Connection::autoDetectConfig();
|
||||
if (!config.rpcuser.empty() && !config.rpcpassword.empty()) {
|
||||
auto tmp_rpc = std::make_unique<rpc::RPCClient>();
|
||||
// Use a short timeout — if daemon isn't listening yet, don't block
|
||||
if (tmp_rpc->connect(config.host, config.port,
|
||||
config.rpcuser, config.rpcpassword,
|
||||
config.use_tls)) {
|
||||
DEBUG_LOGF("Temporary RPC connected, sending stop...\n");
|
||||
if (sendStopCommandSafely(*tmp_rpc, "Temporary daemon stop")) {
|
||||
stop_sent = true;
|
||||
}
|
||||
tmp_rpc->disconnect();
|
||||
} else {
|
||||
DEBUG_LOGF("Could not establish temporary RPC connection\n");
|
||||
}
|
||||
} else {
|
||||
DEBUG_LOGF("No RPC credentials available (DRAGONX.conf missing?)\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (stop_sent) {
|
||||
DEBUG_LOGF("Waiting for daemon to flush block index and shut down...\n");
|
||||
shutdown_status_ = "Waiting for daemon to flush block index...";
|
||||
// Give the daemon time to flush LevelDB to disk before we
|
||||
// escalate to SIGTERM. On macOS/APFS, LevelDB compaction +
|
||||
// fsync can take 15-20s on a large chain. The stop() method
|
||||
// will wait this long for a *natural* exit (via the RPC stop
|
||||
// we already sent) before falling back to SIGTERM.
|
||||
}
|
||||
|
||||
shutdown_status_ = "Waiting for dragonxd process to exit...";
|
||||
// 20s grace period for the RPC "stop" to complete (LevelDB flush).
|
||||
// Only after that does stop() escalate to SIGTERM, then SIGKILL.
|
||||
daemon_controller_->stop(20000);
|
||||
|
||||
// EXTERNAL / adopted node during app shutdown: we hold no process handle, so the stop() above returned
|
||||
// immediately (it can only wait on a node WE spawned). But the user turned on "Stop external daemon",
|
||||
// so keep the window on the shutdown screen and poll until the node is actually gone — surfacing a live
|
||||
// status so they can SEE it stop — rather than closing while it's still flushing. Bounded ~120s (a
|
||||
// graceful full-node shutdown can flush LevelDB for 60-90s). Scoped to real shutdown; the shutdown
|
||||
// screen's Force Quit stays available and flips shutdown_complete_, which breaks us out at once.
|
||||
if (stop_sent && !owned && shutting_down_) {
|
||||
auto stillUp = []() {
|
||||
return daemon::EmbeddedDaemon::isRpcPortInUse() || daemon::EmbeddedDaemon::isDaemonProcessRunning();
|
||||
};
|
||||
// Set the phase text ONCE, then just poll — the shutdown screen already renders a live "N seconds"
|
||||
// elapsed counter on the UI thread, so the user still sees time passing. (shutdown_status_ is now a
|
||||
// GuardedStatus, so per-iteration writes would be race-safe; the single write is just a UX choice.)
|
||||
shutdown_status_ = "Waiting for the external node to stop...";
|
||||
for (int i = 0; i < 1200 && stillUp() && !shutdown_complete_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
shutdown_status_ = stillUp() ? "External node still stopping — closing anyway..."
|
||||
: "External node stopped";
|
||||
DEBUG_LOGF("stopEmbeddedDaemon: external node %s\n", stillUp() ? "still up (timed out)" : "confirmed stopped");
|
||||
daemon::DaemonController::StopPolicy policy;
|
||||
policy.owned = owned;
|
||||
#ifndef _WIN32
|
||||
policy.gracefulSignal = owned; // dragonxd treats SIGTERM like stop; Windows has no such channel
|
||||
#endif
|
||||
switch (mode) {
|
||||
case StopMode::AppShutdown:
|
||||
policy.userChoices = true;
|
||||
policy.maxWaitSec = owned ? 0.0 : 120.0; // a node we don't own: watch it go for up to 2 min
|
||||
break;
|
||||
case StopMode::Background:
|
||||
policy.maxWaitSec = 120.0;
|
||||
policy.abandonOnShutdown = true;
|
||||
stop_user_choice_ = 0;
|
||||
break;
|
||||
case StopMode::Fallback:
|
||||
policy.maxWaitSec = 20.0;
|
||||
stop_user_choice_ = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("stopEmbeddedDaemon: %s node, %s\n", owned ? "our own" : "an external",
|
||||
mode == StopMode::AppShutdown ? "quitting (no time limit; the user may leave it running)"
|
||||
: mode == StopMode::Background ? "background (up to 120s, never killed)"
|
||||
: "fallback (up to 20s, never killed)");
|
||||
stop_kill_offered_ = false;
|
||||
stop_session_active_ = true;
|
||||
daemon::StopOutcome outcome;
|
||||
{
|
||||
AppStopRuntime runtime(*this, owned ? d : nullptr, std::move(creds));
|
||||
AppStopTaskContext task(*this, policy.abandonOnShutdown);
|
||||
outcome = daemon::DaemonController::executeDaemonStop(policy, runtime, task);
|
||||
}
|
||||
stop_session_active_ = false;
|
||||
stop_kill_offered_ = false;
|
||||
// Still running after a background wait: keep watching it (its exit is booked as a clean stop). Left
|
||||
// running on purpose ("Leave running"), exited or killed: let go of it.
|
||||
if (owned) d->endStop(/*keepWatching=*/outcome == daemon::StopOutcome::StillRunning);
|
||||
DEBUG_LOGF("stopEmbeddedDaemon: %s\n",
|
||||
outcome == daemon::StopOutcome::Exited ? "the node exited"
|
||||
: outcome == daemon::StopOutcome::Detached ? "left running at the user's request"
|
||||
: outcome == daemon::StopOutcome::Killed ? "killed at the user's request"
|
||||
: "still running (stopped waiting; never killed)");
|
||||
return outcome;
|
||||
}
|
||||
|
||||
bool App::isEmbeddedDaemonRunning() const
|
||||
@@ -5493,10 +5619,10 @@ bool App::stopDaemonForWalletSwitch()
|
||||
// not start the node. Gate on the handle instead.
|
||||
const bool owned = daemon_controller_ && isEmbeddedDaemonRunning();
|
||||
if (owned) {
|
||||
// We spawned it — stopEmbeddedDaemon() BLOCKS for full process exit (handle wait + SIGTERM/SIGKILL
|
||||
// escalation), so the datadir lock + RPC port are released by the time it returns; a short port
|
||||
// check just confirms.
|
||||
stopEmbeddedDaemon();
|
||||
// We spawned it — stopEmbeddedDaemon() waits for the process to exit (up to 120s, never killing
|
||||
// it), so on Exited the datadir lock + RPC port are released; a short port check just confirms.
|
||||
// Still running means the switch must not go ahead: the old node holds the wallet and the lock.
|
||||
if (stopEmbeddedDaemon() != daemon::StopOutcome::Exited) return shutting_down_;
|
||||
for (int i = 0; i < 200 && daemon::EmbeddedDaemon::isRpcPortInUse() && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
return shutting_down_ || !daemon::EmbeddedDaemon::isRpcPortInUse();
|
||||
@@ -5808,6 +5934,8 @@ void App::beginShutdown()
|
||||
shutting_down_ = true;
|
||||
quit_requested_ = true;
|
||||
shutdown_timer_ = 0.0f;
|
||||
stop_user_choice_ = 0;
|
||||
stop_kill_offered_ = false;
|
||||
shutdown_start_time_ = std::chrono::steady_clock::now();
|
||||
async_tasks_.cancelAll();
|
||||
// The seed-migration adopt task drives daemon stop/start and swaps wallet.dat; let it finish
|
||||
@@ -5883,8 +6011,9 @@ void App::beginShutdown()
|
||||
shutdown_thread_ = std::thread([this]() {
|
||||
DEBUG_LOGF("shutdown thread: calling stopEmbeddedDaemon()\n");
|
||||
shutdown_status_ = TR("appx_sending_stop_command_to_daemon");
|
||||
// Send RPC stop command
|
||||
stopEmbeddedDaemon();
|
||||
// Deliver stop and wait for the node to exit: no time limit, never killed unasked. The shutdown
|
||||
// screen offers "Leave running" (and "Kill" if the node goes idle), which end the wait at once.
|
||||
stopEmbeddedDaemon(StopMode::AppShutdown);
|
||||
|
||||
DEBUG_LOGF("shutdown thread: daemon stopped, disconnecting RPC\n");
|
||||
shutdown_status_ = TR("appx_cleaning_up");
|
||||
@@ -6055,17 +6184,10 @@ void App::renderShutdownScreen()
|
||||
};
|
||||
shutdown_timer_ += ImGui::GetIO().DeltaTime;
|
||||
|
||||
// Track how long the status text has been unchanged. A normal shutdown keeps updating the status
|
||||
// ("Stopping pool miner…" -> "Flushing…" -> "Cleaning up…"); a genuine hang stalls it. Offer Force
|
||||
// Quit only once the status has STALLED (likely hung), not on a bare timer, with a hard ceiling so
|
||||
// it's never impossible to escape.
|
||||
static std::string s_lastShutStatus;
|
||||
static float s_shutStallTimer = 0.0f;
|
||||
const std::string curShut = shutdown_status_.get(); // one consistent snapshot per frame (M-05)
|
||||
if (curShut != s_lastShutStatus) { s_lastShutStatus = curShut; s_shutStallTimer = 0.0f; }
|
||||
else s_shutStallTimer += ImGui::GetIO().DeltaTime;
|
||||
const bool shutdownStalled = s_shutStallTimer >= 8.0f;
|
||||
const bool allowForceQuit = shutdownStalled || shutdown_timer_ >= 20.0f;
|
||||
// After a few seconds, offer to close the wallet and leave the node running: stopping can take as long
|
||||
// as the node needs (a witness rebuild can hold it for an hour), and it is never killed unasked.
|
||||
const bool allowChoices = shutdown_timer_ >= 3.0f;
|
||||
|
||||
// Use the main viewport so the overlay covers the primary window
|
||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
@@ -6086,9 +6208,8 @@ void App::renderShutdownScreen()
|
||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
||||
ImGuiWindowFlags_NoSavedSettings;
|
||||
// Allow input only once Force Quit is offered (status stalled / hard ceiling), so the screen stays
|
||||
// click-through while a normal shutdown is progressing.
|
||||
if (!allowForceQuit)
|
||||
// Allow input only once the choices are offered, so the screen stays click-through for a quick exit.
|
||||
if (!allowChoices)
|
||||
shutdownFlags |= ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav;
|
||||
ImGui::Begin("##ShutdownOverlay", nullptr, shutdownFlags);
|
||||
|
||||
@@ -6189,51 +6310,64 @@ void App::renderShutdownScreen()
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// 4b. Force Quit button — appears once the shutdown appears stalled (or a hard-ceiling backstop)
|
||||
// 4b. "Leave running" (always, after a few seconds) and "Kill" (only once the node looks stuck)
|
||||
// -------------------------------------------------------------------
|
||||
if (allowForceQuit) {
|
||||
if (allowChoices) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
// State-aware caution: while the status is a daemon flush/exit step, force-quitting risks the
|
||||
// chainstate; say so instead of a bare button.
|
||||
if (shutdownStalled && !curShut.empty()) {
|
||||
std::string stalledMsg = std::string(TR("appx_still_status_prefix")) + curShut +
|
||||
TR("appx_still_status_suffix");
|
||||
ImVec2 ms = ImGui::CalcTextSize(stalledMsg.c_str());
|
||||
ImGui::SetCursorPosX(cx - ms.x * 0.5f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
|
||||
ImGui::TextUnformatted(stalledMsg.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
}
|
||||
const char* forceLabel = TR("force_quit");
|
||||
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f * dp, 0);
|
||||
ImGui::SetCursorPosX(cx - btnSize.x * 0.5f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
|
||||
if (ui::material::TactileButton(forceLabel, btnSize)) {
|
||||
force_quit_confirm_ = true;
|
||||
const char* leaveLabel = TR("shutdown_leave_running");
|
||||
ImVec2 leaveSize(ImGui::CalcTextSize(leaveLabel).x + 32.0f * dp, 0);
|
||||
ImGui::SetCursorPosX(cx - leaveSize.x * 0.5f);
|
||||
if (ui::material::TactileButton(leaveLabel, leaveSize)) {
|
||||
DEBUG_LOGF("Shutdown: user chose to leave the node running after %.0fs\n", shutdown_timer_);
|
||||
chooseStopAction(daemon::StopUserChoice::Detach);
|
||||
// No stop session (yet): nothing to wait for, so close now. A session that starts later sees
|
||||
// the choice and returns at once.
|
||||
if (!stop_session_active_) shutdown_complete_ = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ui::material::Tooltip("%s", TR("force_quit_warning"));
|
||||
ui::material::Tooltip("%s", TR("shutdown_leave_running_tip"));
|
||||
}
|
||||
|
||||
if (stop_kill_offered_) {
|
||||
ImGui::Spacing();
|
||||
char idleMsg[160];
|
||||
snprintf(idleMsg, sizeof(idleMsg), TR("shutdown_stop_idle"), stop_idle_minutes_.load());
|
||||
ImVec2 ms = ImGui::CalcTextSize(idleMsg);
|
||||
ImGui::SetCursorPosX(cx - ms.x * 0.5f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
|
||||
ImGui::TextUnformatted(idleMsg);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
const char* killLabel = TR("shutdown_kill_node");
|
||||
ImVec2 killSize(ImGui::CalcTextSize(killLabel).x + 32.0f * dp, 0);
|
||||
ImGui::SetCursorPosX(cx - killSize.x * 0.5f);
|
||||
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)));
|
||||
if (ui::material::TactileButton(killLabel, killSize)) {
|
||||
stop_kill_confirm_ = true;
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ui::material::Tooltip("%s", TR("shutdown_kill_node_tip"));
|
||||
}
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
|
||||
// Force Quit confirmation popup
|
||||
if (force_quit_confirm_) {
|
||||
ImGui::OpenPopup("##ForceQuitConfirm");
|
||||
force_quit_confirm_ = false;
|
||||
// Kill confirmation
|
||||
if (stop_kill_confirm_) {
|
||||
ImGui::OpenPopup("##KillNodeConfirm");
|
||||
stop_kill_confirm_ = false;
|
||||
}
|
||||
ImVec2 popupCenter(wp.x + vp_size.x * 0.5f, wp.y + vp_size.y * 0.5f);
|
||||
ImGui::SetNextWindowPos(popupCenter, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||
if (ImGui::BeginPopupModal("##ForceQuitConfirm", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
||||
if (ImGui::BeginPopupModal("##KillNodeConfirm", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
||||
ImGui::PushFont(Type().subtitle1());
|
||||
ImGui::TextUnformatted(TR("force_quit_confirm_title"));
|
||||
ImGui::TextUnformatted(TR("shutdown_kill_confirm_title"));
|
||||
ImGui::PopFont();
|
||||
ImGui::Spacing();
|
||||
ImGui::TextUnformatted(TR("force_quit_confirm_msg"));
|
||||
ImGui::TextUnformatted(TR("shutdown_kill_confirm_msg"));
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
|
||||
@@ -6249,9 +6383,9 @@ void App::renderShutdownScreen()
|
||||
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)));
|
||||
if (ui::material::TactileButton(TR("force_quit_yes"), ImVec2(btnW, 0))) {
|
||||
DEBUG_LOGF("Force quit confirmed by user after %.0fs\n", shutdown_timer_);
|
||||
shutdown_complete_ = true;
|
||||
if (ui::material::TactileButton(TR("shutdown_kill_confirm_yes"), ImVec2(btnW, 0))) {
|
||||
DEBUG_LOGF("Shutdown: user confirmed Kill after %.0fs\n", shutdown_timer_);
|
||||
chooseStopAction(daemon::StopUserChoice::Kill);
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
@@ -6767,7 +6901,7 @@ void App::shutdown()
|
||||
settings_->save();
|
||||
}
|
||||
if (daemon_controller_) {
|
||||
stopEmbeddedDaemon();
|
||||
stopEmbeddedDaemon(StopMode::Fallback); // on the UI thread: bounded, never killed
|
||||
}
|
||||
if (rpc_) {
|
||||
rpc_->disconnect();
|
||||
|
||||
Reference in New Issue
Block a user