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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user