fix(daemon): never kill a dragonxd the wallet didn't launch

On Windows, stop() with no live process of its own looked up the first
"dragonxd.exe" by name and TerminateProcess'd it after the grace period:
an old node still shutting down (how an in-progress shutdown was killed),
a node on another datadir, or the user's main node. It was reached on a
lock-gated close, for an external node, after the tracked process had
exited, and from ~EmbeddedDaemon's stop(3000).

- Windows stop(): drop the by-name branch; with no tracked process it
  logs and returns. findProcessByName is detection only.
- ~EmbeddedDaemon no longer calls stop(); it stops watching and releases
  its handles. stop() returns whether the process exited on its own.
- SeedWalletCreator never sent its isolated node an RPC stop, so on
  Windows that node was always force-killed holding the wallet.dat it
  had just created, and the stack object's destructor could then kill
  the main node by name. It now stops the node over the node's own RPC
  on every path. A node that still had to be force-killed fails the
  creation (seed wiped; nothing has been swept into it yet), so an
  uncleanly closed wallet is never adopted.
- Reinstall waited only for the RPC port, which closes long before the
  process exits (locked exe on Windows, held datadir lock everywhere).
  It now waits for the process to exit and otherwise aborts without
  touching the binaries or starting a second node.
- Linux isDaemonProcessRunning also matches /proc/<pid>/exe, so a node
  whose main thread is already renamed "hush-shutoff" (still holding the
  datadir lock) counts as running.

Tests: a Windows real-process test (a fake dragonxd.exe on an isolated
datadir survives stop() and the destructor), a POSIX test that the
destructor leaves the process running, and the pure identity rule.
scripts/test-windows.sh passes the stub path to the Windows suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 19:44:35 -05:00
co-authored by Claude Opus 5.5
parent d8175b7dc1
commit a091bbc52f
6 changed files with 232 additions and 51 deletions
+28 -7
View File
@@ -17,6 +17,21 @@ namespace daemon {
namespace {
// Asks the isolated node to stop over its own RPC (port + throwaway credentials, never the main node's)
// and waits for it to exit. Without the RPC stop it was only ever force-killed, holding the wallet.dat
// it had just created. Returns false if it still had to be force-killed.
bool stopIsolatedNode(EmbeddedDaemon& node, rpc::RPCClient& cli, int port, const std::string& user,
const std::string& pass, int waitMs)
{
node.markStopRequested();
// connect() also succeeds against a node still in warmup, which accepts stop too.
if (cli.isConnected() || cli.connect("127.0.0.1", std::to_string(port), user, pass, /*useTls=*/false)) {
try { cli.call("stop", nlohmann::json::array(), 10L); } catch (const std::exception&) {}
}
cli.disconnect();
return node.stop(waitMs);
}
// Random alphanumeric token for the isolated node's throwaway RPC credentials (libsodium CSPRNG;
// sodium_init() has already run at app startup for the chat crypto).
std::string randomToken(int n)
@@ -113,7 +128,7 @@ SeedWalletResult SeedWalletCreator::create(bool keepDatadir,
}
if (!ready) {
r.error = "The isolated node did not become ready in time.";
temp.stop(20000);
stopIsolatedNode(temp, cli, port, user, pass, 20000); // datadir is thrown away: a forced stop is harmless
fs::remove_all(base, ec);
return r;
}
@@ -146,18 +161,24 @@ SeedWalletResult SeedWalletCreator::create(bool keepDatadir,
}
}
// 7. Stop the isolated node (graceful; it flushes its tiny empty chain quickly). Adopt later copies
// this wallet.dat, so it must have been closed cleanly: a node that had to be force-killed
// fails the creation (nothing has been swept into it yet, so nothing is lost by retrying).
if (!stopIsolatedNode(temp, cli, port, user, pass, 60000) && r.ok) {
r.ok = false;
r.error = "The isolated node had to be force-stopped, so its new wallet can't be trusted. "
"Please try again.";
}
// W1-2: never hand back a live seed on a failure path. If the mnemonic was exported but a
// later step failed (empty address, or z_getnewaddress threw), the caller discards this
// result without wiping it, which would leave the seed resident. Success keeps it deliberately.
// later step failed (empty address, z_getnewaddress threw, or an unclean stop), the caller
// discards this result without wiping it, which would leave the seed resident. Success keeps
// it deliberately.
if (!r.ok && !r.seedPhrase.empty()) {
sodium_memzero(&r.seedPhrase[0], r.seedPhrase.size());
r.seedPhrase.clear();
}
// 7. Stop the isolated node (graceful; it flushes its tiny empty chain quickly).
cli.disconnect();
temp.stop(20000);
// 8. Keep the temp wallet for a later sweep/adopt step, or scrub it. tempDatadir is the
// migration root `base`; the new wallet.dat lives in <base>/DRAGONX.
r.tempDatadir = base;