fix(daemon): make the "keep the node running?" prompt actually appear

Quitting during a witness rebuild is supposed to ask before stopping the
node, because a stop throws the rebuild away and a killed node replays
and redoes it. The guard almost never fired:
- it returned false for any daemon >= v1.3.0, on the false premise that
  v1.3.0 checkpoints a rebuild (it does not; nothing survives a kill)
- it read a 16 KB debug.log tail and compared the last progress line with
  the newest line, so any unrelated line during the rebuild's silent
  second phase turned it off; and on POSIX that file is stale
- daemon_version, which the bypass needed, is zeroed when a busy node
  drops the connection

Now a DaemonBusyTracker (src/daemon/daemon_busy_detect.h, pure) follows
dragonxd's own markers in order: rebuilding (Phase-1 / legacy markers,
ended by the completion line, an abort line or the next UpdateTip) and
replaying (Activating best chain ... Done Loading), reset by a new banner
or "Shutdown: done". It is fed from the owned output on the UI thread,
including the backlog skipped after a restore. The quit guard prompts
when the quit would stop the node and it is rebuilding, or has been
alive but only timing out / refusing as busy for >= 30 s. A replay alone
never prompts: stopping it is safe. The stop loop uses the same signal
to never offer Kill during a rebuild.

Also:
- daemon output offsets are absolute, so a reader that falls behind a
  trim of the 1 MB buffer no longer skips new output (which could hide a
  start or end marker)
- a quit deferred while minimized restores and raises the window, and
  the wizard renders the prompt, so it is never waiting unseen

Verified live with the fake daemon: quitting mid-rebuild now shows the
prompt for a v1.3.0 node and leaves the node alone; a silent busy node
prompts after 30 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 00:02:27 -05:00
co-authored by Claude Opus 5.5
parent 821bebd13e
commit 6b14098bb2
8 changed files with 395 additions and 54 deletions
+22 -4
View File
@@ -22,6 +22,7 @@
#include "data/wallet_state.h"
#include "rpc/connection.h"
#include "daemon/stop_delivery.h"
#include "daemon/daemon_busy_detect.h"
#include "services/network_refresh_service.h"
#include "services/wallet_security_controller.h"
#include "services/wallet_security_workflow.h"
@@ -191,11 +192,19 @@ public:
*/
std::vector<std::string> tailDaemonDebugLog(int maxLines) const;
// True when the daemon's debug.log shows an in-progress Sapling witness-cache rebuild (best-effort
// heuristic). Stopping the daemon during one discards it and forces a multi-minute redo next launch.
bool daemonWitnessRebuildActive() const;
// Whether beginShutdown() should pause and confirm before stopping the daemon (rebuild in progress).
// Whether beginShutdown() should pause and confirm before stopping the daemon: this quit would stop it,
// and it is rebuilding witnesses (its own log says so) or has been alive but too busy to answer RPC for
// a while. Stopping mid-rebuild throws the rebuild away, and a node killed then replays and redoes it.
bool shouldConfirmDaemonStop() const;
// The node is alive and its RPC has only timed out / been refused as busy for >= 30s (a rebuild that
// logged no marker, e.g. an external node whose output we can't read).
bool rpcBusyWhileAlive() const;
// Record how an RPC to the node just went, for rpcBusyWhileAlive(): busy kinds (ReplyTimeout /
// QueueFull) start the clock, an answer stops it, anything else leaves it alone.
void noteDaemonRpcResult(bool answered, rpc::RpcFailureKind kind);
// Called by main.cpp: brings the window back (restore + raise) when a quit is deferred for the confirm
// modal while minimized or behind other windows — the modal only renders in a visible frame.
void setRaiseWindowCallback(std::function<void()> cb) { raise_window_ = std::move(cb); }
// The "node is rebuilding — stop anyway / keep running / cancel" modal, rendered from render().
void renderDaemonStopConfirm();
@@ -1152,6 +1161,15 @@ private:
void setDaemonStatus(std::string s);
mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
// What the node's own output says it is doing (witness rebuild / startup replay). Fed on the UI thread
// from the captured output; node_rebuilding_ is its snapshot for the shutdown thread's stop loop.
daemon::DaemonBusyTracker daemon_busy_;
std::atomic<bool> node_rebuilding_{false};
std::atomic<bool> daemon_busy_reset_pending_{false}; // set from any thread; applied by feedDaemonBusy()
std::chrono::steady_clock::time_point rpc_busy_since_{}; // epoch = not busy
std::function<void()> raise_window_;
void feedDaemonBusy(const std::string& chunk);
void resetDaemonBusy();
// Export/Import state
char export_result_[256] = {0}; // SECRET exported key — fixed buffer so it can be sodium_memzero'd