fix(daemon): never restart, delete or swap under a node that hasn't exited

Every operation that stops the node (restart, rescan, repair, delete
chain data, reinstall, bootstrap, wallet switch/adopt/restore/rebuild,
wizard restart, encrypt, decrypt) now:

- refuses up front while the node is rebuilding witnesses or has not
  answered for 30s, with a "try again when it's done" warning, and sends
  no stop;
- goes ahead only once the node's PROCESS has exited, not its RPC port.
  A stop that doesn't finish within 120s cancels the operation (warning;
  a cancelled rescan/repair disarms its flags); nothing is started over
  the old node, chain data isn't deleted, wallet.dat isn't swapped.

Specifics:
- stopDaemonForWalletSwitch() no longer reports "stopped" when the app
  is quitting (adopt could swap wallet.dat under a live node).
- Decrypt waited a fixed 3s after "stop" before renaming wallet.dat; it
  now waits for the process to exit, and scrubs the key export if not.
- Bootstrap waits for the process before cleanChainData and stops the
  node through the stop routine instead of the shared RPC client.
- StopScope::Connected lets switch/reinstall/decrypt stop an adopted or
  external node through the same never-kill routine.
- A bounded stop that can't reach the node (a foreign node refusing our
  credentials) gives up after 5s instead of 120s.
- macOS: isDaemonProcessRunning() checks processes (libproc), not the
  RPC port.
- rpcBusyWhileAlive() also reads the busy gate: since the gate, the
  first timeout usually lands on another lane and the 30s clock never
  started (also weakened the quit prompt for a silent rebuild).
- Notifications render during the first-run wizard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 11:22:39 -05:00
co-authored by Claude Opus 5.5
parent 1215ded1a0
commit e20b70d73f
22 changed files with 320 additions and 122 deletions
+20 -6
View File
@@ -200,6 +200,12 @@ public:
// 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;
// The node is rebuilding witnesses (our own node's log says so) or has been too busy to answer for a
// while: stopping it now either waits up to an hour or makes it start over. UI thread only.
bool nodeBusyForStop() const;
// For an operation that would stop the node (restart, rescan, switch, reinstall, bootstrap, ...): when
// nodeBusyForStop(), tell the user to try again later and return true; the caller then does nothing.
bool refuseIfNodeBusy();
// 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);
@@ -497,12 +503,16 @@ public:
Background, // restart/rescan/switch/...: up to 120s, and let go as soon as the app starts quitting
Fallback, // the synchronous shutdown fallback on the UI thread: up to 20s
};
daemon::StopOutcome stopEmbeddedDaemon(StopMode mode = StopMode::Background);
// Stop the node specifically for a wallet switch, which MUST free the RPC port to relaunch on
// -wallet=<name>. Unlike stopEmbeddedDaemon() (whose DisconnectOnly policy leaves an adopted/external
// daemon running), this sends a graceful RPC "stop" — using our own connection creds, so only our
// daemon obeys it — even to an adopted daemon, then waits (bounded) for the port to actually release.
// Returns true once the RPC port is free (or we're shutting down).
// Which node it may stop. Policy: our own, or an external one only when the "stop external daemon"
// setting says so. Connected: also the node we're connected to (adopted or external), with the
// credentials we connected with — a wallet switch, a reinstall and decrypt must free the port, the binary
// and wallet.dat. Only a node that accepts those credentials obeys.
enum class StopScope { Policy, Connected };
daemon::StopOutcome stopEmbeddedDaemon(StopMode mode = StopMode::Background, StopScope scope = StopScope::Policy);
// Stop the node for a wallet switch / adopt / restore / rebuild, which swap wallet.dat or relaunch on
// -wallet=<name>. Unlike stopEmbeddedDaemon()'s default policy (which leaves an adopted/external node
// running) it stops the node we're connected to too (StopScope::Connected). True only once the process
// has exited and the RPC port is free; false if it is still running, or the app started quitting.
bool stopDaemonForWalletSwitch();
bool isEmbeddedDaemonRunning() const;
bool isUsingEmbeddedDaemon() const { return supportsEmbeddedDaemon() && use_embedded_daemon_; }
@@ -1078,6 +1088,10 @@ private:
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
std::atomic<int> pending_delete_result_{-1};
std::atomic<bool> pending_reinstall_node_busy_{false}; // reinstall gave up: the old node never exited
// A worker stopped the node for an operation and it didn't exit: the operation was cancelled. Told (and,
// for a rescan/repair, its armed state undone) on the UI thread. Bits: kStopFailed*.
static constexpr int kStopFailedNotify = 1, kStopFailedUndoRescan = 2;
std::atomic<int> pending_stop_failed_{0};
// Encryption state check timeout
float encryption_check_timer_ = 0.0f;