diff --git a/src/app.cpp b/src/app.cpp index cab4384..78489bf 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -4178,32 +4178,45 @@ bool App::stopDaemonForWalletSwitch() // 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). + // We spawned it — stopEmbeddedDaemon() BLOCKS for full process exit (handle wait + SIGTERM/SIGKILL + // escalation), so the datadir lock + RPC port are released by the time it returns; a short port + // check just confirms. 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; + for (int i = 0; i < 200 && daemon::EmbeddedDaemon::isRpcPortInUse() && !shutting_down_; ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + return shutting_down_ || !daemon::EmbeddedDaemon::isRpcPortInUse(); + } + + // Adopted OR direct-connected: 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. switchToWallet + // disconnects rpc_ before this worker runs, so build a fresh temporary connection. + 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 unowned node (saved_config_): %s\n", sent ? "sent" : "FAILED"); } + 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() - // 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) + // CRITICAL: wait for the node to FULLY EXIT before the caller starts the replacement — NOT just for the + // RPC port. On Windows isRpcPortInUse() is a connect() probe that reads "free" the moment the daemon + // stops accepting RPC (early in shutdown), but the process keeps the DATADIR LOCK until it exits, and a + // graceful shutdown can take 60-90s when the node's network threads block on peer timeouts. Starting a + // replacement into a still-locked datadir fails ("Cannot obtain a lock…") and wedges the switch. So gate + // on the PROCESS being gone (isDaemonProcessRunning) AND the port free. Bounded ~120s while stopping; if + // we couldn't even send the stop (foreign node / bad creds → its port never frees), only a brief grace. + const int maxTicks = sent ? 1200 : 50; + auto stillUp = []() { + return daemon::EmbeddedDaemon::isRpcPortInUse() || daemon::EmbeddedDaemon::isDaemonProcessRunning(); + }; + for (int i = 0; i < maxTicks && stillUp() && !shutting_down_; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(100)); - return shutting_down_ || !daemon::EmbeddedDaemon::isRpcPortInUse(); + return shutting_down_ || !stillUp(); } void App::rescanBlockchain() diff --git a/src/app_network.cpp b/src/app_network.cpp index 43ee5c8..3edc525 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -1104,7 +1104,9 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed // can't surface (or sign outgoing chat) under the new wallet. It re-provisions from the new // wallet's seed once it connects. onDisconnected alone doesn't cover chat. resetChatSession(); - ui::Notifications::instance().info("Switching wallet — the node will restart…"); + ui::Notifications::instance().info( + "Switching wallet — stopping the node and restarting on the new wallet. " + "A graceful shutdown can take up to a minute.", 60.0f); async_tasks_.submit("Switch wallet", [this, needRescan](const util::AsyncTaskManager::Token&) { bool ok = false; diff --git a/src/daemon/embedded_daemon.cpp b/src/daemon/embedded_daemon.cpp index 26d0234..dfc33ce 100644 --- a/src/daemon/embedded_daemon.cpp +++ b/src/daemon/embedded_daemon.cpp @@ -1247,5 +1247,28 @@ bool EmbeddedDaemon::tcpPortInUse(int port) return isPortInUse(port); } +bool EmbeddedDaemon::isDaemonProcessRunning() +{ +#ifdef _WIN32 + return findProcessByName("dragonxd.exe") != 0; +#elif defined(__linux__) + // Scan /proc for a process whose comm is exactly "dragonxd". Iterate with an error_code so a proc + // entry vanishing mid-scan (a process exiting) can't throw. + std::error_code ec; + fs::directory_iterator it("/proc", ec), end; + for (; !ec && it != end; it.increment(ec)) { + const std::string pid = it->path().filename().string(); + if (pid.empty() || pid[0] < '0' || pid[0] > '9') continue; // numeric pid dirs only + std::ifstream f((it->path() / "comm").string()); + std::string comm; + if (f && std::getline(f, comm) && comm == "dragonxd") return true; + } + return false; +#else + // macOS has no /proc; fall back to the RPC-port probe (best-effort). + return isPortInUse(std::atoi(DRAGONX_DEFAULT_RPC_PORT)); +#endif +} + } // namespace daemon } // namespace dragonx diff --git a/src/daemon/embedded_daemon.h b/src/daemon/embedded_daemon.h index c620a4b..682ed8d 100644 --- a/src/daemon/embedded_daemon.h +++ b/src/daemon/embedded_daemon.h @@ -221,6 +221,15 @@ public: */ void setSkipPortCheck(bool v) { skip_port_check_ = v; } + /** + * @brief True while ANY dragonxd process is running (by process name), regardless of who started it. + * Unlike isRpcPortInUse()/isRunning(), this reflects the actual PROCESS still being alive — a + * graceful shutdown stops accepting RPC (port reads "free") but keeps the datadir lock until the + * process exits, which can take up to ~90s. Use this to know a stopped node has FULLY released + * the datadir before starting a replacement. Matches the daemon binary name on all platforms. + */ + static bool isDaemonProcessRunning(); + /** @brief Is an arbitrary TCP port currently in use on localhost? (used to pick a free port) */ static bool tcpPortInUse(int port);