fix(wallet): wait for the old node to fully exit before restarting on switch
After the stop, the switch started the replacement dragonxd too early — while the old (direct-connected, unowned) node was still doing a slow graceful shutdown (~70s; its network threads block on peer TLS timeouts) and holding the DATADIR LOCK. The replacement couldn't acquire the lock, failed repeatedly, and the crash-wedge left it stuck "starting". Root cause: the readiness poll used isRpcPortInUse(), which on Windows is a connect() probe that reads "free" the moment the daemon stops ACCEPTING RPC — early in shutdown, long before the process exits and releases the datadir. - EmbeddedDaemon::isDaemonProcessRunning(): true while any dragonxd process is alive (Windows findProcessByName; Linux /proc/<pid>/comm scan; macOS port fallback) — reflects the PROCESS, not just RPC acceptance. - stopDaemonForWalletSwitch: for an UNOWNED node (no handle), after the RPC stop wait until BOTH the port is free AND isDaemonProcessRunning() is false, bounded ~120s (or ~5s if the stop couldn't be sent). Owned nodes are unchanged (stopEmbeddedDaemon() blocks for exit via the handle). - Switch notification reworded to set the up-to-a-minute expectation (60s toast). daemon_restarting_ stays set across the wait so the connect loop can't spawn a competing daemon. Adversarially verified 6/6; fixes the seed-adopt path too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
45
src/app.cpp
45
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<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 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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user