fix(wallet): route switch stop by process-handle ownership, not the external latch
Live logs showed the app usually just DIRECT-CONNECTS to an already-running dragonxd (config found → connect; EmbeddedDaemon::start() never called). Two consequences broke the switch: no process handle, and externalDaemonDetected() stays false (it's only latched inside start()). So a direct-connected node was treated as "owned" → stopEmbeddedDaemon() → an autoDetectConfig() temp RPC stop that never reached the daemon → the node never stopped → the ~40s port poll timed out → "the running node didn't release its connection in time" revert. Gate on the real ownership signal — whether we hold a live process handle (isEmbeddedDaemonRunning()) — instead of the unreliable externalDaemonDetected(): - stopDaemonForWalletSwitch: owned (we spawned it) → stopEmbeddedDaemon() with SIGTERM/SIGKILL; NOT owned (adopted or direct-connect, no handle) → RPC "stop" over the exact creds we're connected with (saved_config_), which is guaranteed to reach our node. Then the unchanged port-free poll. - switchToWallet confirm gate: show "Stop the running node?" when connected to a node this session didn't spawn (state_.connected && !isEmbeddedDaemonRunning()). - Fixes beginAdoptSeedWallet's direct-connect case identically (shared helper). Adversarially verified (6/6): ownership signal, saved_config_ delivery incl. cookie auth, foreign-daemon safety, confirm gate, seed-adopt, re-ownership/revert. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
29
src/app.cpp
29
src/app.cpp
@@ -4171,14 +4171,22 @@ bool App::isEmbeddedDaemonRunning() const
|
||||
|
||||
bool App::stopDaemonForWalletSwitch()
|
||||
{
|
||||
const bool external = daemon_controller_ && daemon_controller_->externalDaemonDetected();
|
||||
if (external) {
|
||||
// Adopted daemon: stopEmbeddedDaemon()'s policy would DisconnectOnly and leave it running, but a
|
||||
// switch MUST free the RPC port to relaunch on -wallet=<name>. Send a graceful RPC "stop" using the
|
||||
// creds we actually connected with (saved_config_) — only OUR daemon accepts them, so a foreign
|
||||
// dragonxd (which would have a different rpcpassword) is a safe no-op: its port never frees and the
|
||||
// caller aborts without starting. No PID/name kill is ever issued at an adopted daemon. switchToWallet
|
||||
// already disconnected rpc_, so build a fresh temporary connection from the known-good creds.
|
||||
// "Owned" means we spawned the node this session and hold a live process handle. That is the ONLY
|
||||
// reliable ownership signal here: externalDaemonDetected() is latched only inside EmbeddedDaemon::start(),
|
||||
// so when the app just direct-connects to an already-running, config-provided node (the common
|
||||
// keep-node-running case) start() is never called and that flag stays false even though we clearly did
|
||||
// not start the node. Gate on the handle instead.
|
||||
const bool owned = daemon_controller_ && isEmbeddedDaemonRunning();
|
||||
if (owned) {
|
||||
// We spawned it — the normal policy stop (RPC stop + bounded SIGTERM/SIGKILL escalation).
|
||||
stopEmbeddedDaemon();
|
||||
} else {
|
||||
// Adopted OR direct-connected: we have NO process handle, so RPC "stop" is the only lever, and
|
||||
// stopEmbeddedDaemon()'s temp connection (autoDetectConfig) can't be trusted here. Send a graceful
|
||||
// RPC "stop" over the exact creds we're connected with (saved_config_) — guaranteed to reach our
|
||||
// node, and only OUR node accepts them, so a foreign dragonxd (different rpcpassword) is a safe
|
||||
// no-op: its port never frees and the caller aborts without starting. No PID/name kill is issued.
|
||||
// switchToWallet disconnects rpc_ before this worker runs, so build a fresh temporary connection.
|
||||
bool sent = false;
|
||||
auto tmp = std::make_unique<rpc::RPCClient>();
|
||||
if (tmp->connect(saved_config_.host, saved_config_.port, saved_config_.rpcuser,
|
||||
@@ -4186,10 +4194,7 @@ bool App::stopDaemonForWalletSwitch()
|
||||
sent = sendStopCommandSafely(*tmp, "wallet-switch stop");
|
||||
tmp->disconnect();
|
||||
}
|
||||
DEBUG_LOGF("[App] wallet-switch stop of adopted daemon: %s\n", sent ? "sent" : "FAILED");
|
||||
} else {
|
||||
// We own the process — the normal policy stop (RPC stop + bounded SIGTERM/SIGKILL escalation).
|
||||
stopEmbeddedDaemon();
|
||||
DEBUG_LOGF("[App] wallet-switch stop of unowned node (saved_config_): %s\n", sent ? "sent" : "FAILED");
|
||||
}
|
||||
|
||||
// Gate on the RPC port ACTUALLY releasing (mirrors reinstallBundledDaemon). isEmbeddedDaemonRunning()
|
||||
|
||||
@@ -1056,11 +1056,11 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed
|
||||
ui::Notifications::instance().warning("Finish or cancel the pending send before switching wallets.");
|
||||
return;
|
||||
}
|
||||
// The node we'd restart may be one this session ADOPTED — a dragonxd already running when the wallet
|
||||
// launched (left running by "keep node running", or started by the user). Switching must stop+restart
|
||||
// it on the new wallet, so confirm first rather than killing a node the user may have kept up on
|
||||
// purpose. (An owned node we started this session restarts silently, as before.)
|
||||
if (!stopDaemonConfirmed && daemon_controller_ && daemon_controller_->externalDaemonDetected()) {
|
||||
// If we're connected to a node this session did NOT spawn (no live process handle — it was left
|
||||
// running by "keep node running", started by the user, or we just direct-connected to a config-provided
|
||||
// one), confirm before stopping it: switching must stop+restart it on the new wallet, but the user may
|
||||
// have kept it running on purpose. A node we started ourselves (handle held) restarts silently.
|
||||
if (!stopDaemonConfirmed && state_.connected && !isEmbeddedDaemonRunning()) {
|
||||
pending_switch_wallet_file_ = walletFile;
|
||||
show_switch_stop_daemon_confirm_ = true;
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user