diff --git a/src/app.cpp b/src/app.cpp index 108f346..063cb76 100644 --- a/src/app.cpp +++ b/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=. 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(); + 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()) { diff --git a/src/app.h b/src/app.h index 95f4f37..7b5b426 100644 --- a/src/app.h +++ b/src/app.h @@ -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=. 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 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 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. diff --git a/src/app_network.cpp b/src/app_network.cpp index 32340f7..f65f094 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -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 } diff --git a/src/daemon/daemon_controller.cpp b/src/daemon/daemon_controller.cpp index 1ebd8de..927745d 100644 --- a/src/daemon/daemon_controller.cpp +++ b/src/daemon/daemon_controller.cpp @@ -61,6 +61,11 @@ bool DaemonController::externalDaemonDetected() const return daemon_->externalDaemonDetected(); } +void DaemonController::clearExternalDaemonDetected() +{ + daemon_->clearExternalDaemonDetected(); +} + DaemonController::State DaemonController::state() const { return daemon_->getState(); diff --git a/src/daemon/daemon_controller.h b/src/daemon/daemon_controller.h index 0f78348..8e67ec1 100644 --- a/src/daemon/daemon_controller.h +++ b/src/daemon/daemon_controller.h @@ -93,6 +93,7 @@ public: bool isRunning() const; bool externalDaemonDetected() const; + void clearExternalDaemonDetected(); State state() const; const std::string& lastError() const; int crashCount() const; diff --git a/src/daemon/embedded_daemon.h b/src/daemon/embedded_daemon.h index a16af1e..c620a4b 100644 --- a/src/daemon/embedded_daemon.h +++ b/src/daemon/embedded_daemon.h @@ -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