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:
@@ -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