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