diff --git a/src/app.cpp b/src/app.cpp index c1b50e4..ff1625b 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -649,6 +649,9 @@ void App::update() // capture_mode_ is only set for the offline full sweep, so the live tab-only sweep is unaffected. if (capture_mode_) return; + // If a wallet switch's daemon failed to start, revert to the previous wallet (main-thread-safe). + processWalletSwitchRevert(); + // Track user interaction for auto-lock if (io.MouseDelta.x != 0 || io.MouseDelta.y != 0 || io.MouseClicked[0] || io.MouseClicked[1] || @@ -4150,6 +4153,11 @@ void App::rescanBlockchain() ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress."); return; } + // Don't race a wallet switch / seed-adopt / encryption restart, which drive their own daemon stop/start. + if (daemon_restarting_) { + ui::Notifications::instance().warning("The node is busy restarting — try again in a moment."); + return; + } DEBUG_LOGF("[App] Starting blockchain rescan - stopping daemon first\n"); ui::Notifications::instance().info("Restarting daemon with -rescan flag..."); @@ -4197,6 +4205,10 @@ void App::repairWallet() ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress."); return; } + if (daemon_restarting_) { + ui::Notifications::instance().warning("The node is busy restarting — try again in a moment."); + return; + } DEBUG_LOGF("[App] Starting wallet repair (-zapwallettxes=2) - stopping daemon first\n"); ui::Notifications::instance().info("Restarting daemon with -zapwallettxes=2 (wallet repair)..."); @@ -4370,6 +4382,10 @@ void App::beginShutdown() // the adopt task will NOT restart the daemon (it checks shutting_down_ before startEmbeddedDaemon). if (async_tasks_.isRunning("Adopt seed wallet")) async_tasks_.join("Adopt seed wallet"); + // The wallet-switch task also drives daemon stop/start — let it finish before shutdown touches the + // daemon so it can't orphan a freshly-started dragonxd (it checks shutting_down_ before starting). + if (async_tasks_.isRunning("Switch wallet")) + async_tasks_.join("Switch wallet"); // Signal the RPC worker to stop accepting new tasks (non-blocking), and abort any call // already in flight so the later join() doesn't wait out a request timeout. diff --git a/src/app.h b/src/app.h index 281559b..58b4086 100644 --- a/src/app.h +++ b/src/app.h @@ -475,6 +475,8 @@ public: // (rescan only if it was never synced in this datadir). Per-wallet data follows automatically // via the identity-scoped caches (P1). No-op if that wallet is already active. void switchToWallet(const std::string& walletFile); + // Main-thread continuation: if a wallet switch's daemon failed to start, revert active_wallet_file. + void processWalletSwitchRevert(); // Wallet encryption helpers void encryptWalletWithPassphrase(const std::string& passphrase); @@ -729,6 +731,13 @@ private: // Daemon restart (e.g. after changing debug log categories) std::atomic daemon_restarting_{false}; + // Wallet-switch failure recovery: the switch worker sets wallet_switch_failed_ when the new + // wallet's daemon won't start/stay up; the main loop then reverts active_wallet_file to + // 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}; + std::string wallet_switch_prev_file_; + // Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast // and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it. std::atomic pending_delete_result_{-1}; diff --git a/src/app_network.cpp b/src/app_network.cpp index 6d0952b..cab54c5 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -1029,6 +1029,16 @@ void App::switchToWallet(const std::string& walletFile) ui::Notifications::instance().warning("The node is busy restarting — try again in a moment."); return; } + // Don't race the other daemon-lifecycle operations. rescan/repair set state_.sync.rescanning; the + // seed-migration flow drives its own daemon stop/start; an encryption restart sets daemon_restarting_. + if (state_.sync.rescanning) { + ui::Notifications::instance().warning("A rescan or repair is in progress — try again once it finishes."); + return; + } + if (show_seed_migration_) { + ui::Notifications::instance().warning("Finish or cancel the seed migration before switching wallets."); + return; + } if (!isUsingEmbeddedDaemon()) { ui::Notifications::instance().warning("Switching wallets needs the embedded daemon — stop any external dragonxd first."); return; @@ -1043,8 +1053,13 @@ void App::switchToWallet(const std::string& walletFile) bool needRescan = true; if (const auto* e = wallet_index_.find(walletFile)) needRescan = !e->syncedHere; + const std::string prevWallet = settings_->getActiveWalletFile(); // for revert if the new one fails + wallet_switch_prev_file_ = prevWallet; + wallet_switch_failed_.store(false); settings_->setActiveWalletFile(walletFile); settings_->save(); + // A prior crash-wedge shouldn't block reconnecting to the freshly-launched wallet daemon. + if (daemon_controller_) daemon_controller_->resetCrashCount(); // Re-scope the PIN vault to the new wallet (its own vault-.dat), and clear any carried-over // lock-screen attempt/lockout state + wipe the passphrase/PIN entry buffers — the previous wallet's @@ -1076,11 +1091,39 @@ void App::switchToWallet(const std::string& walletFile) for (int i = 0; i < 60 && isEmbeddedDaemonRunning(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(500)); if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true); - if (!shutting_down_) startEmbeddedDaemon(); - daemon_restarting_ = false; // re-arm reconnect once the new daemon has been launched + bool 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; + } + if (ok) { + daemon_restarting_ = false; // re-arm reconnect once the new daemon is up + } else { + // Leave the reconnect GATED (daemon_restarting_ stays true) and hand the revert to the main + // thread — settings writes + vault re-scope must not run on this worker thread. + wallet_switch_failed_.store(true); + } }); } +// Main-thread continuation for a failed wallet switch: restore the previous wallet file so a broken +// active_wallet_file isn't persisted, re-scope the vault back, and re-arm the reconnect so the connect +// loop brings the (good) previous wallet's daemon back up. Called each tick from update(). +void App::processWalletSwitchRevert() +{ + if (!wallet_switch_failed_.exchange(false)) return; + if (settings_ && !wallet_switch_prev_file_.empty() && + settings_->getActiveWalletFile() != wallet_switch_prev_file_) { + settings_->setActiveWalletFile(wallet_switch_prev_file_); + settings_->save(); + if (vault_) vault_->setWalletScope(wallet_switch_prev_file_); + } + 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 +} + void App::wipePendingTransactionHistoryCachePassphrase() { if (!pending_transaction_history_cache_passphrase_.empty()) { diff --git a/src/app_security.cpp b/src/app_security.cpp index 52ab03e..cb7f65c 100644 --- a/src/app_security.cpp +++ b/src/app_security.cpp @@ -241,16 +241,19 @@ void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestar // the daemon is restarting. connection_status_ = TR("restarting_after_encryption"); } + // Gate the main-loop reconnect while the daemon is down (so tryConnect can't hit the stopped + // node or start a duplicate) and block a concurrent wallet switch/rescan, which check this flag. + daemon_restarting_ = true; // Give daemon a moment to shut down, then restart // (do this off the main thread to avoid stalling the UI) async_tasks_.submit(taskName, [this](const util::AsyncTaskManager::Token& token) { for (int i = 0; i < 20 && !token.cancelled() && !shutting_down_; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(100)); - if (token.cancelled() || shutting_down_) return; + if (token.cancelled() || shutting_down_) { daemon_restarting_ = false; return; } stopEmbeddedDaemon(); - if (token.cancelled() || shutting_down_) return; + if (token.cancelled() || shutting_down_) { daemon_restarting_ = false; return; } startEmbeddedDaemon(); - // tryConnect will be called by the update loop + daemon_restarting_ = false; // re-arm reconnect (tryConnect runs from the update loop) }); } else { ui::Notifications::instance().warning(