fix(wallet): let a switch restart an adopted (external) daemon
Switching to a named wallet failed every time when the app had connected to a
pre-existing dragonxd at startup: the daemon is flagged externalDaemonDetected,
so stopEmbeddedDaemon()'s policy is DisconnectOnly ("not ours to stop") and the
switch skips the stop entirely. The old daemon keeps the RPC port, and the
relaunch fast-fails on EmbeddedDaemon::start()'s isPortInUse check — misread as a
bad wallet, reverting after a ~40s hang on the process-handle-only run-wait
(which reads false immediately for an adopted daemon).
A switch legitimately needs to restart the node, so:
- App::stopDaemonForWalletSwitch(): for an adopted daemon, send a graceful RPC
"stop" using the creds we actually connected with (saved_config_) — only our
own daemon obeys it, so a foreign dragonxd is a safe no-op — then wait (bounded
~40s) for the RPC port to actually free (isRpcPortInUse, the same gate start()
uses). Owned daemons take the normal stopEmbeddedDaemon() path. No PID/name kill
is ever issued at an adopted daemon.
- switchToWallet: gate start() on the port actually freeing; if it doesn't,
abort with a distinct switch_stop_failed_ reason instead of starting into a busy
port. Clear the external latch before relaunch so the fresh process is owned.
- EmbeddedDaemon::clearExternalDaemonDetected() (+ controller forwarder).
- Accurate revert message: "the running node didn't release its connection in
time" vs. the bad-wallet message.
Root-caused from live Windows logs; design + implementation adversarially
verified. Note: beginAdoptSeedWallet has the identical pattern and is left for a
separate fund-critical review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
32
src/app.cpp
32
src/app.cpp
@@ -4132,6 +4132,38 @@ bool App::isEmbeddedDaemonRunning() const
|
||||
return daemon_controller_ && daemon_controller_->isRunning();
|
||||
}
|
||||
|
||||
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.
|
||||
bool sent = false;
|
||||
auto tmp = std::make_unique<rpc::RPCClient>();
|
||||
if (tmp->connect(saved_config_.host, saved_config_.port, saved_config_.rpcuser,
|
||||
saved_config_.rpcpassword, saved_config_.use_tls)) {
|
||||
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();
|
||||
}
|
||||
|
||||
// Gate on the RPC port ACTUALLY releasing (mirrors reinstallBundledDaemon). isEmbeddedDaemonRunning()
|
||||
// is process-handle-only, so it reports "not running" immediately for an adopted daemon — the port is
|
||||
// the correct readiness signal, and it's the same check start() gates on (isRpcPortInUse). Bounded
|
||||
// ~40s: an adopted daemon has no SIGKILL fallback and a large-chain LevelDB flush can take 15-20s.
|
||||
for (int i = 0; i < 400 && daemon::EmbeddedDaemon::isRpcPortInUse() && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
return shutting_down_ || !daemon::EmbeddedDaemon::isRpcPortInUse();
|
||||
}
|
||||
|
||||
void App::rescanBlockchain()
|
||||
{
|
||||
if (!supportsFullNodeLifecycleActions()) {
|
||||
|
||||
@@ -415,6 +415,12 @@ public:
|
||||
// Embedded daemon control
|
||||
bool startEmbeddedDaemon();
|
||||
void stopEmbeddedDaemon();
|
||||
// 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).
|
||||
bool stopDaemonForWalletSwitch();
|
||||
bool isEmbeddedDaemonRunning() const;
|
||||
bool isUsingEmbeddedDaemon() const { return supportsEmbeddedDaemon() && use_embedded_daemon_; }
|
||||
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use && supportsEmbeddedDaemon(); }
|
||||
@@ -748,6 +754,9 @@ private:
|
||||
// wallet_switch_prev_file_ (settings writes stay on the main thread) so a broken wallet isn't
|
||||
// persisted across restarts. daemon_restarting_ stays set until the revert re-arms the reconnect.
|
||||
std::atomic<bool> wallet_switch_failed_{false};
|
||||
// Distinguishes the failure reason for the revert message: true when the switch failed because the
|
||||
// running node wouldn't release the RPC port in time (not because the target wallet is bad).
|
||||
std::atomic<bool> switch_stop_failed_{false};
|
||||
// True from a switch until the new wallet's daemon actually connects (onConnected). If the daemon
|
||||
// instead crash-wedges (a wallet that fails LATE in init, past the fast start grace), the connect
|
||||
// loop flags a revert so a broken wallet still can't stick.
|
||||
|
||||
@@ -1065,6 +1065,7 @@ void App::switchToWallet(const std::string& walletFile)
|
||||
const std::string prevWallet = settings_->getActiveWalletFile(); // for revert if the new one fails
|
||||
wallet_switch_prev_file_ = prevWallet;
|
||||
wallet_switch_failed_.store(false);
|
||||
switch_stop_failed_.store(false); // reason latch for the revert message; a stale one must not leak
|
||||
wallet_switch_pending_confirm_.store(true); // cleared on a successful connect (onConnected)
|
||||
settings_->setActiveWalletFile(walletFile);
|
||||
settings_->save();
|
||||
@@ -1099,19 +1100,30 @@ void App::switchToWallet(const std::string& walletFile)
|
||||
async_tasks_.submit("Switch wallet", [this, needRescan](const util::AsyncTaskManager::Token&) {
|
||||
bool ok = false;
|
||||
try {
|
||||
// Stop the node fully so it releases the datadir .lock + RPC port before we relaunch.
|
||||
stopEmbeddedDaemon();
|
||||
// Break the wait promptly if shutdown starts, so beginShutdown's join() doesn't freeze the
|
||||
// UI for the full 30s (shutdown then stops the daemon itself; we won't start a new one).
|
||||
for (int i = 0; i < 60 && isEmbeddedDaemonRunning() && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true);
|
||||
ok = shutting_down_ ? true : startEmbeddedDaemon();
|
||||
// A missing/corrupt wallet makes dragonxd exit during init — confirm the process survived a
|
||||
// moment; if not, the wallet is bad. (A valid launch keeps the process alive while it syncs.)
|
||||
if (ok && !shutting_down_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
|
||||
if (!isEmbeddedDaemonRunning()) ok = false;
|
||||
// Stop the node so it releases the datadir .lock + RPC port before we relaunch. For an adopted
|
||||
// (external) daemon this sends a graceful RPC "stop" — stopEmbeddedDaemon()'s policy would leave
|
||||
// it running — and then waits for the RPC PORT to actually free, the correct readiness signal
|
||||
// (isEmbeddedDaemonRunning() is process-handle-only and reads false immediately for an adopted
|
||||
// daemon). The wait breaks promptly on shutdown so beginShutdown's join() doesn't freeze the UI.
|
||||
const bool port_free = stopDaemonForWalletSwitch();
|
||||
if (!port_free) {
|
||||
// The old node wouldn't release the port in time — don't start() into a busy port (that
|
||||
// fast-fails on the isPortInUse check and gets misread as a bad wallet). Revert with an
|
||||
// accurate, distinct reason instead.
|
||||
switch_stop_failed_.store(true);
|
||||
ok = false;
|
||||
} else {
|
||||
// The relaunch is ours — clear the adopted-external latch so the fresh process is treated
|
||||
// as owned (stop/isRunning/exit behave normally for the next switch and app exit).
|
||||
if (daemon_controller_) daemon_controller_->clearExternalDaemonDetected();
|
||||
if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true);
|
||||
ok = shutting_down_ ? true : startEmbeddedDaemon();
|
||||
// A missing/corrupt wallet makes dragonxd exit during init — confirm the process survived a
|
||||
// moment; if not, the wallet is bad. (A valid launch keeps the process alive while it syncs.)
|
||||
if (ok && !shutting_down_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
|
||||
if (!isEmbeddedDaemonRunning()) ok = false;
|
||||
}
|
||||
}
|
||||
} catch (...) {
|
||||
ok = false; // a throw during stop/start is a failed switch — revert rather than wedge
|
||||
@@ -1142,7 +1154,15 @@ void App::processWalletSwitchRevert()
|
||||
}
|
||||
// Clear the crash-wedge so the (good) previous wallet's daemon is allowed to start again.
|
||||
if (daemon_controller_) daemon_controller_->resetCrashCount();
|
||||
ui::Notifications::instance().error("Couldn't open that wallet — reverted to the previous one.");
|
||||
// Accurate reason: "port wouldn't free" (the running node kept the connection) vs. a genuinely bad
|
||||
// target wallet. The former is the adopted-external-daemon case; the latter a daemon that exited in init.
|
||||
if (switch_stop_failed_.exchange(false)) {
|
||||
ui::Notifications::instance().error(
|
||||
"Couldn't switch wallets — the running node didn't release its connection in time. "
|
||||
"It's still on the previous wallet.");
|
||||
} else {
|
||||
ui::Notifications::instance().error("Couldn't open that wallet — reverted to the previous one.");
|
||||
}
|
||||
daemon_restarting_ = false; // the connect loop now restarts the previous wallet's daemon
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,11 @@ bool DaemonController::externalDaemonDetected() const
|
||||
return daemon_->externalDaemonDetected();
|
||||
}
|
||||
|
||||
void DaemonController::clearExternalDaemonDetected()
|
||||
{
|
||||
daemon_->clearExternalDaemonDetected();
|
||||
}
|
||||
|
||||
DaemonController::State DaemonController::state() const
|
||||
{
|
||||
return daemon_->getState();
|
||||
|
||||
@@ -93,6 +93,7 @@ public:
|
||||
|
||||
bool isRunning() const;
|
||||
bool externalDaemonDetected() const;
|
||||
void clearExternalDaemonDetected();
|
||||
State state() const;
|
||||
const std::string& lastError() const;
|
||||
int crashCount() const;
|
||||
|
||||
@@ -142,6 +142,10 @@ public:
|
||||
* When true the wallet should connect to it instead of showing an error.
|
||||
*/
|
||||
bool externalDaemonDetected() const { return external_daemon_detected_; }
|
||||
// Clear the adopted-external latch before relaunching our OWN process (e.g. a wallet switch that
|
||||
// stopped the adopted daemon), so the freshly spawned daemon is treated as owned. start() also
|
||||
// clears it on the port-free fall-through, but only if not short-circuited by an early guard.
|
||||
void clearExternalDaemonDetected() { external_daemon_detected_ = false; }
|
||||
|
||||
/**
|
||||
* @brief Set callback for state changes
|
||||
|
||||
Reference in New Issue
Block a user