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:
@@ -62,13 +62,16 @@ export DRAGONX_TEST_FIXTURE_DIR="$repo/tests/fixtures"
|
||||
sandbox="$bd/test-appdata"
|
||||
mkdir -p "$sandbox"
|
||||
export APPDATA="$sandbox" LOCALAPPDATA="$sandbox"
|
||||
# The fake dragonxd.exe plays a "foreign" node (isolated datadir/port) for the never-kill-by-name test.
|
||||
export DRAGONX_TEST_FAKE_DAEMON="$bd/fake_dragonxd/dragonxd.exe"
|
||||
if grep -qi microsoft /proc/version 2>/dev/null && [[ -e /proc/sys/fs/binfmt_misc/WSLInterop ]]; then
|
||||
# /p: translate each to a Windows path for the Windows process
|
||||
export WSLENV="${WSLENV:+$WSLENV:}DRAGONX_TEST_FIXTURE_DIR/p:APPDATA/p:LOCALAPPDATA/p"
|
||||
export WSLENV="${WSLENV:+$WSLENV:}DRAGONX_TEST_FIXTURE_DIR/p:APPDATA/p:LOCALAPPDATA/p:DRAGONX_TEST_FAKE_DAEMON/p"
|
||||
cd "$bd/bin"
|
||||
exec "$exe"
|
||||
elif command -v wine >/dev/null 2>&1; then
|
||||
export DRAGONX_TEST_FIXTURE_DIR="$(winepath -w "$DRAGONX_TEST_FIXTURE_DIR")"
|
||||
export DRAGONX_TEST_FAKE_DAEMON="$(winepath -w "$DRAGONX_TEST_FAKE_DAEMON")"
|
||||
cd "$bd/bin"
|
||||
exec wine "$exe"
|
||||
else
|
||||
|
||||
+16
-5
@@ -5391,8 +5391,9 @@ void App::stopEmbeddedDaemon()
|
||||
|
||||
// Do we hold a live process handle for this node? Capture it BEFORE stop() reaps the pid. If owned,
|
||||
// daemon_controller_->stop() below BLOCKS until the process actually exits. If NOT owned (external /
|
||||
// adopted / direct-connected), stop() returns at once — we can only ask it over RPC and then watch
|
||||
// for it to disappear (handled after the stop() call).
|
||||
// adopted / direct-connected / a lock-gated start that never spawned), stop() returns at once on every
|
||||
// platform — it only ever waits on (or kills) a process this app launched, never one found by name —
|
||||
// so we can only ask it over RPC and then watch for it to disappear (handled after the stop() call).
|
||||
const bool owned = daemon_controller_->isRunning();
|
||||
|
||||
// Any exit from here on is the stop we are asking for, not a crash — even one that lands before
|
||||
@@ -5677,11 +5678,21 @@ void App::reinstallBundledDaemon()
|
||||
sendStopCommandSafely(*rpc_, "reinstall-daemon");
|
||||
}
|
||||
runtime.stopDaemonWithPolicy();
|
||||
// Wait (bounded, ~12s) for the process to exit and release the binary before overwriting.
|
||||
for (int i = 0; i < 120 && daemon::EmbeddedDaemon::isRpcPortInUse()
|
||||
&& !ctx.cancelled() && !ctx.shuttingDown(); ++i) {
|
||||
// Wait (bounded, ~120s) for the old node's PROCESS to exit, not just its RPC port: dragonxd unbinds
|
||||
// RPC first and can take a minute or more to flush and close its wallet. Until it exits its exe is
|
||||
// locked on Windows (the overwrite would fail) and it holds the datadir lock (a restart would fail).
|
||||
auto stillUp = [] {
|
||||
return daemon::EmbeddedDaemon::isRpcPortInUse() || daemon::EmbeddedDaemon::isDaemonProcessRunning();
|
||||
};
|
||||
for (int i = 0; i < 1200 && stillUp() && !ctx.cancelled() && !ctx.shuttingDown(); ++i) {
|
||||
ctx.sleepForMs(100);
|
||||
}
|
||||
if (ctx.cancelled() || ctx.shuttingDown()) return;
|
||||
if (stillUp()) {
|
||||
DEBUG_LOGF("[App] Reinstall bundled daemon: aborted — a dragonxd process is still running; "
|
||||
"binaries left untouched and no new node started\n");
|
||||
return;
|
||||
}
|
||||
// Overwrite the installed dragonx binaries (dragonxd/cli/tx) with the bundled ones.
|
||||
const bool ok = resources::reextractBundledDaemon();
|
||||
DEBUG_LOGF("[App] Reinstall bundled daemon: %s\n", ok ? "ok" : "FAILED");
|
||||
|
||||
@@ -51,7 +51,30 @@ EmbeddedDaemon::EmbeddedDaemon() = default;
|
||||
|
||||
EmbeddedDaemon::~EmbeddedDaemon()
|
||||
{
|
||||
stop(3000); // Wait up to 3 seconds for clean shutdown
|
||||
// Never stop() from here: a destructor must not kill a node. On Windows stop() also used to fall
|
||||
// back to killing ANY dragonxd.exe by name, so destroying an object that had already stopped its own
|
||||
// process (seed migration's isolated node) could take down the user's main node. Owners call stop()
|
||||
// explicitly (the app's own daemon is never destroyed: main() ends in _Exit). Just stop watching and
|
||||
// release what we hold; a process still running is left to finish on its own.
|
||||
should_stop_ = true;
|
||||
if (monitor_thread_.joinable()) {
|
||||
monitor_thread_.join();
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (process_handle_ != nullptr) {
|
||||
CloseHandle(process_handle_);
|
||||
process_handle_ = nullptr;
|
||||
}
|
||||
if (stdout_read_ != nullptr) {
|
||||
CloseHandle(stdout_read_);
|
||||
stdout_read_ = nullptr;
|
||||
}
|
||||
#else
|
||||
if (stdout_fd_ >= 0) {
|
||||
close(stdout_fd_);
|
||||
stdout_fd_ = -1;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string EmbeddedDaemon::findDaemonBinary()
|
||||
@@ -856,8 +879,9 @@ void EmbeddedDaemon::drainOutput()
|
||||
CloseHandle(hFile);
|
||||
}
|
||||
|
||||
void EmbeddedDaemon::stop(int wait_ms)
|
||||
bool EmbeddedDaemon::stop(int wait_ms)
|
||||
{
|
||||
stop_requested_ = true;
|
||||
should_stop_ = true;
|
||||
setState(State::Stopping, "Stopping dragonxd...");
|
||||
|
||||
@@ -883,6 +907,7 @@ void EmbeddedDaemon::stop(int wait_ms)
|
||||
}
|
||||
};
|
||||
|
||||
bool exitedOnItsOwn = true;
|
||||
if (tracked_alive) {
|
||||
// Our tracked process (dragonxd.exe launched directly) is still alive.
|
||||
// The RPC "stop" was already sent by the caller — wait for it to exit.
|
||||
@@ -891,40 +916,21 @@ void EmbeddedDaemon::stop(int wait_ms)
|
||||
DEBUG_LOGF("Timeout — forcing daemon termination...\n");
|
||||
TerminateProcess(process_handle_, 1);
|
||||
WaitForSingleObject(process_handle_, 2000);
|
||||
exitedOnItsOwn = false;
|
||||
}
|
||||
drainOutput();
|
||||
CloseHandle(process_handle_);
|
||||
process_handle_ = nullptr;
|
||||
} else {
|
||||
// Tracked handle is dead (batch file case: cmd.exe already exited).
|
||||
// The real dragonxd.exe may still be running as an orphan.
|
||||
// No live process of OUR OWN to stop. Never go looking for one by name: findProcessByName returns
|
||||
// the first dragonxd.exe of any kind (an old node still shutting down, a node on another datadir,
|
||||
// an isolated seed-migration node), and killing it is how an in-progress shutdown got
|
||||
// TerminateProcess'd. Nodes we didn't launch only ever get an RPC stop from the caller.
|
||||
if (process_handle_ != nullptr) {
|
||||
CloseHandle(process_handle_);
|
||||
process_handle_ = nullptr;
|
||||
}
|
||||
|
||||
// Find the real dragonxd.exe process by name
|
||||
DWORD dragonxd_pid = findProcessByName("dragonxd.exe");
|
||||
if (dragonxd_pid != 0) {
|
||||
DEBUG_LOGF("Found orphaned dragonxd.exe (PID %lu) — waiting for RPC stop to take effect...\n", dragonxd_pid);
|
||||
HANDLE hProc = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, dragonxd_pid);
|
||||
if (hProc) {
|
||||
// RPC stop was already sent — wait for graceful exit
|
||||
if (!pollWait(hProc, wait_ms)) {
|
||||
DEBUG_LOGF("Timeout — forcing dragonxd.exe (PID %lu) termination...\n", dragonxd_pid);
|
||||
TerminateProcess(hProc, 1);
|
||||
WaitForSingleObject(hProc, 2000);
|
||||
}
|
||||
drainOutput();
|
||||
CloseHandle(hProc);
|
||||
DEBUG_LOGF("dragonxd.exe stopped\n");
|
||||
} else {
|
||||
DEBUG_LOGF("Could not open dragonxd.exe process (PID %lu), error %lu\n",
|
||||
dragonxd_pid, GetLastError());
|
||||
}
|
||||
} else {
|
||||
DEBUG_LOGF("No dragonxd.exe process found — daemon may have already exited\n");
|
||||
}
|
||||
DEBUG_LOGF("No tracked daemon process to stop (any other dragonxd.exe is not ours and is left alone)\n");
|
||||
}
|
||||
|
||||
if (stdout_read_ != nullptr) {
|
||||
@@ -939,6 +945,7 @@ void EmbeddedDaemon::stop(int wait_ms)
|
||||
}
|
||||
|
||||
setState(State::Stopped, "dragonxd stopped");
|
||||
return exitedOnItsOwn;
|
||||
}
|
||||
|
||||
// Translate Windows NTSTATUS / exit codes to human-readable descriptions
|
||||
@@ -1336,10 +1343,11 @@ bool EmbeddedDaemon::reapWithin(pid_t pid, int timeout_ms, int& status)
|
||||
}
|
||||
}
|
||||
|
||||
void EmbeddedDaemon::stop(int wait_ms)
|
||||
bool EmbeddedDaemon::stop(int wait_ms)
|
||||
{
|
||||
stop_requested_ = true;
|
||||
should_stop_ = true;
|
||||
bool exitedOnItsOwn = true;
|
||||
|
||||
// Join the monitor FIRST (it sees should_stop_ within one 100ms tick) so that from here on this is
|
||||
// the only reaper. isRunning() is state-based and stays true until we set Stopped below, so it can't
|
||||
@@ -1371,6 +1379,7 @@ void EmbeddedDaemon::stop(int wait_ms)
|
||||
DEBUG_LOGF("Forcing dragonxd termination with SIGKILL (group -%d)...\n", pid);
|
||||
kill(-pid, SIGKILL);
|
||||
while (waitpid(pid, &status, 0) < 0 && errno == EINTR) {}
|
||||
exitedOnItsOwn = false;
|
||||
}
|
||||
DEBUG_LOGF("dragonxd exited (%s)\n", describeExitStatus(status).c_str());
|
||||
}
|
||||
@@ -1384,6 +1393,7 @@ void EmbeddedDaemon::stop(int wait_ms)
|
||||
}
|
||||
|
||||
setState(State::Stopped, "dragonxd stopped");
|
||||
return exitedOnItsOwn;
|
||||
}
|
||||
|
||||
void EmbeddedDaemon::monitorProcess()
|
||||
@@ -1447,8 +1457,8 @@ 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.
|
||||
// Scan /proc for a dragonxd process (by comm or by executable: see isDaemonProcessIdentity). 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)) {
|
||||
@@ -1456,7 +1466,10 @@ bool EmbeddedDaemon::isDaemonProcessRunning()
|
||||
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;
|
||||
if (f) std::getline(f, comm);
|
||||
std::error_code linkEc; // another user's process: EACCES, leaving the name empty
|
||||
const std::string exe = fs::read_symlink(it->path() / "exe", linkEc).filename().string();
|
||||
if (isDaemonProcessIdentity(comm, exe)) return true;
|
||||
}
|
||||
return false;
|
||||
#else
|
||||
|
||||
@@ -55,10 +55,12 @@ public:
|
||||
bool start(const std::string& binary_path = "");
|
||||
|
||||
/**
|
||||
* @brief Stop the running daemon
|
||||
* @brief Stop the daemon process this object launched (never any other dragonxd)
|
||||
* @param wait_ms Maximum milliseconds to wait for graceful shutdown
|
||||
* @return true if it exited on its own (or there was nothing to stop), false if it had to be
|
||||
* force-killed — then its wallet/chain files may not have been closed cleanly
|
||||
*/
|
||||
void stop(int wait_ms = 5000);
|
||||
bool stop(int wait_ms = 5000);
|
||||
|
||||
/**
|
||||
* @brief Record that we are about to ask the daemon to stop (e.g. over RPC)
|
||||
@@ -254,6 +256,22 @@ public:
|
||||
*/
|
||||
static bool isDaemonProcessRunning();
|
||||
|
||||
/**
|
||||
* @brief Does a /proc entry (its comm + its exe link's basename) belong to a dragonxd? (Linux)
|
||||
*
|
||||
* comm alone misses a node in its final shutdown phase: Shutdown() renames the main thread to
|
||||
* "hush-shutoff" while the process still holds the datadir lock. The exe link keeps the binary
|
||||
* name, with " (deleted)" appended once the binary was replaced underneath it (reinstall/update).
|
||||
*/
|
||||
static bool isDaemonProcessIdentity(const std::string& comm, std::string exeName)
|
||||
{
|
||||
static const std::string kDeleted = " (deleted)";
|
||||
if (exeName.size() > kDeleted.size() &&
|
||||
exeName.compare(exeName.size() - kDeleted.size(), kDeleted.size(), kDeleted) == 0)
|
||||
exeName.erase(exeName.size() - kDeleted.size());
|
||||
return comm == "dragonxd" || exeName == "dragonxd";
|
||||
}
|
||||
|
||||
/** Decision returned by evaluateDatadirLockGate(): whether start() may spawn now. */
|
||||
struct StartLockGateDecision {
|
||||
bool proceed = true; // false => bail before spawning
|
||||
|
||||
@@ -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;
|
||||
|
||||
+120
-5
@@ -1,3 +1,12 @@
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <signal.h>
|
||||
#include <sys/wait.h>
|
||||
#endif
|
||||
#include "chat/chat_crypto.h"
|
||||
#include "chat/chat_identity.h"
|
||||
#include "chat/chat_service.h"
|
||||
@@ -135,6 +144,11 @@ void removeTree(const fs::path& path)
|
||||
fs::remove_all(path, ec);
|
||||
}
|
||||
|
||||
double secondsSince(std::chrono::steady_clock::time_point t0)
|
||||
{
|
||||
return std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
|
||||
}
|
||||
|
||||
// A named path under the platform temp dir (/tmp on Linux, %TEMP% on Windows, where "/tmp/..."
|
||||
// would mean C:\tmp).
|
||||
fs::path testTmpPath(const std::string& name)
|
||||
@@ -3305,11 +3319,6 @@ std::filesystem::path writeFakeDaemonScript(const std::string& name, const std::
|
||||
return script;
|
||||
}
|
||||
|
||||
double secondsSince(std::chrono::steady_clock::time_point t0)
|
||||
{
|
||||
return std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
|
||||
}
|
||||
|
||||
// S4: stop() must see a clean exit and return at once. Before the fix the monitor thread exited
|
||||
// without reaping and the state-based isRunning() stayed true, so every POSIX stop waited out the
|
||||
// whole grace period, then sent SIGTERM, then SIGKILL (here ~5 + 10 = 15s).
|
||||
@@ -3384,8 +3393,109 @@ void testPosixStopSigtermIsGraceful()
|
||||
fsn::remove(script, ec);
|
||||
fsn::remove(events, ec);
|
||||
}
|
||||
|
||||
// S3: destroying an EmbeddedDaemon must not stop (let alone kill) the process. The destructor used to
|
||||
// call stop(3000), which on Windows fell back to killing any dragonxd.exe by name; owners now stop()
|
||||
// explicitly and the destructor only stops watching. Before the fix this took ~3s and SIGTERMed it.
|
||||
void testPosixDestructorLeavesProcessRunning()
|
||||
{
|
||||
using dragonx::daemon::EmbeddedDaemon;
|
||||
namespace fsn = std::filesystem;
|
||||
const fsn::path pidFile = fsn::temp_directory_path() / "od_fake_daemon_dtor.pid";
|
||||
std::error_code ec;
|
||||
fsn::remove(pidFile, ec);
|
||||
const auto script = writeFakeDaemonScript("od_fake_daemon_dtor.sh",
|
||||
"echo $$ > \"$OD_TEST_PIDFILE\"\nsleep 30\n");
|
||||
setenv("OD_TEST_PIDFILE", pidFile.string().c_str(), 1);
|
||||
|
||||
pid_t pid = 0;
|
||||
auto d = std::make_unique<EmbeddedDaemon>();
|
||||
d->setSkipPortCheck(true);
|
||||
EXPECT_TRUE(d->start(script.string()));
|
||||
for (int i = 0; i < 300 && pid <= 0; ++i) {
|
||||
std::ifstream in(pidFile);
|
||||
long v = 0;
|
||||
if (in >> v) pid = static_cast<pid_t>(v);
|
||||
else std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
EXPECT_TRUE(pid > 0);
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
d.reset();
|
||||
EXPECT_TRUE(secondsSince(t0) < 1.0);
|
||||
EXPECT_TRUE(pid > 0 && kill(pid, 0) == 0); // still running
|
||||
if (pid > 0) {
|
||||
kill(-pid, SIGKILL); // the test's own child (its own process group)
|
||||
waitpid(pid, nullptr, 0);
|
||||
}
|
||||
unsetenv("OD_TEST_PIDFILE");
|
||||
fsn::remove(script, ec);
|
||||
fsn::remove(pidFile, ec);
|
||||
}
|
||||
#endif // !_WIN32
|
||||
|
||||
#ifdef _WIN32
|
||||
// S3: EmbeddedDaemon must never kill a dragonxd.exe it didn't launch. Before the fix, stop() (and the
|
||||
// destructor's stop(3000)) with no live process of its own looked up "dragonxd.exe" by name and
|
||||
// TerminateProcess'd the first match: an old node still shutting down, or the user's real node. A fake
|
||||
// dragonxd.exe (tools/fake_dragonxd) on an isolated datadir and port plays the foreign node.
|
||||
void testWindowsStopNeverKillsForeignDaemon()
|
||||
{
|
||||
const char* stub = std::getenv("DRAGONX_TEST_FAKE_DAEMON");
|
||||
if (!stub || !*stub) {
|
||||
std::printf("[skip] testWindowsStopNeverKillsForeignDaemon (set DRAGONX_TEST_FAKE_DAEMON; "
|
||||
"scripts/test-windows.sh does)\n");
|
||||
return;
|
||||
}
|
||||
const fs::path root = makeTempDir();
|
||||
const fs::path dir = root / "DRAGONX";
|
||||
fs::create_directories(dir);
|
||||
writeTestFile(dir / "DRAGONX.conf", "rpcuser=u\nrpcpassword=p\nrpcport=39769\n");
|
||||
_putenv_s("FAKE_REBUILD_SECS", "0");
|
||||
_putenv_s("FAKE_WARMUP_SECS", "0");
|
||||
std::string cmd = "\"" + std::string(stub) + "\" -datadir=\"" + dir.string() + "\"";
|
||||
STARTUPINFOA si{};
|
||||
si.cb = sizeof(si);
|
||||
PROCESS_INFORMATION pi{};
|
||||
const BOOL started = CreateProcessA(nullptr, &cmd[0], nullptr, nullptr, FALSE, CREATE_NO_WINDOW,
|
||||
nullptr, nullptr, &si, &pi);
|
||||
_putenv_s("FAKE_REBUILD_SECS", "");
|
||||
_putenv_s("FAKE_WARMUP_SECS", "");
|
||||
EXPECT_TRUE(started);
|
||||
if (!started) return;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
EXPECT_TRUE(WaitForSingleObject(pi.hProcess, 0) == WAIT_TIMEOUT); // the foreign node is up
|
||||
|
||||
{
|
||||
dragonx::daemon::EmbeddedDaemon d;
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
EXPECT_TRUE(d.stop(2000)); // nothing of its own to stop: returns at once and kills nothing
|
||||
EXPECT_TRUE(secondsSince(t0) < 1.0);
|
||||
} // ...nor does the destructor
|
||||
EXPECT_TRUE(WaitForSingleObject(pi.hProcess, 500) == WAIT_TIMEOUT); // the foreign node survived
|
||||
|
||||
TerminateProcess(pi.hProcess, 0); // the test's own child, by its own handle
|
||||
WaitForSingleObject(pi.hProcess, 5000);
|
||||
CloseHandle(pi.hThread);
|
||||
CloseHandle(pi.hProcess);
|
||||
removeTree(root);
|
||||
}
|
||||
#endif // _WIN32
|
||||
|
||||
// S3: on Linux a node in its final shutdown phase reads comm "hush-shutoff"; its executable still says
|
||||
// dragonxd, so it must still count as running (it holds the datadir lock until it exits).
|
||||
void testDaemonProcessIdentity()
|
||||
{
|
||||
using dragonx::daemon::EmbeddedDaemon;
|
||||
EXPECT_TRUE(EmbeddedDaemon::isDaemonProcessIdentity("dragonxd", "dragonxd"));
|
||||
EXPECT_TRUE(EmbeddedDaemon::isDaemonProcessIdentity("hush-shutoff", "dragonxd"));
|
||||
EXPECT_TRUE(EmbeddedDaemon::isDaemonProcessIdentity("hush-shutoff", "dragonxd (deleted)")); // binary replaced
|
||||
EXPECT_TRUE(EmbeddedDaemon::isDaemonProcessIdentity("dragonxd", "")); // another user's: no exe link
|
||||
EXPECT_FALSE(EmbeddedDaemon::isDaemonProcessIdentity("hush-shutoff", "hushd")); // a different Hush daemon
|
||||
EXPECT_FALSE(EmbeddedDaemon::isDaemonProcessIdentity("hushd", "hushd"));
|
||||
EXPECT_FALSE(EmbeddedDaemon::isDaemonProcessIdentity("bash", "bash"));
|
||||
EXPECT_FALSE(EmbeddedDaemon::isDaemonProcessIdentity("dragonx-cli", "dragonx-cli"));
|
||||
}
|
||||
|
||||
void testDatadirLockGate()
|
||||
{
|
||||
using dragonx::daemon::EmbeddedDaemon;
|
||||
@@ -7953,12 +8063,17 @@ int main()
|
||||
testWalletSecurityWorkflowExecutor();
|
||||
testDaemonShutdownPolicy();
|
||||
testDatadirLockGate();
|
||||
testDaemonProcessIdentity();
|
||||
#ifdef _WIN32
|
||||
testWindowsStopNeverKillsForeignDaemon();
|
||||
#endif
|
||||
#ifndef _WIN32
|
||||
testExecFailureReported();
|
||||
testDaemonCrashDetected();
|
||||
testPosixStopReturnsAfterCleanExit();
|
||||
testPosixRequestedStopIsNotACrash();
|
||||
testPosixStopSigtermIsGraceful();
|
||||
testPosixDestructorLeavesProcessRunning();
|
||||
#endif
|
||||
testPlatformEnsureDirectory();
|
||||
testVerifySaplingParams();
|
||||
|
||||
Reference in New Issue
Block a user