From 70d8b0f687c6a004317aab9de3aa5fdc246a39b7 Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 12 Oct 2026 13:22:00 -0500 Subject: [PATCH] fix(daemon): supervise a daemon restarted after a crash After an unexpected exit the monitor thread returned on its own but stayed joinable, and start() only spawned a monitor when none was joinable. A node restarted after a crash (no stop() in between) then ran unwatched: a second crash was never detected, its output was never drained, and the dead run's stdout pipe leaked into the next fork. - start() joins a monitor that returned on its own (only when joinable, at the top, after the "already running" early returns), closes the dead run's stdout pipe (Windows: process and pipe handles), and always starts a new monitor for the process it launched. crash_count_ is kept. - New lifecycle_mutex_ serializes start(), beginStop(), stop()'s flag writes and joins, and endStop(), so two threads can never join the same monitor. The monitor thread and the state callback never take it; a beginStop() racing a start() waits for it to return (its longest wait is the ~300 ms datadir-lock poll). - start() is refused (returns false, state unchanged) while a stop is in progress, i.e. between beginStop() and endStop(). - The daemon tests now start the stand-in only in isolation (temp -datadir override, port check skipped): they no longer write the real ~/.hush/DRAGONX/obsidiandragon-dragonxd.pid or tail the real debug.log. New tests: restart after a crash is supervised, a restarted run stops cleanly, no fd leak over crash+restart cycles (Linux), and start() is refused while stopping. Co-Authored-By: Claude Opus 5.5 --- src/daemon/embedded_daemon.cpp | 66 ++++++++-- src/daemon/embedded_daemon.h | 6 +- tests/test_phase4.cpp | 219 +++++++++++++++++++++++++++++++-- 3 files changed, 269 insertions(+), 22 deletions(-) diff --git a/src/daemon/embedded_daemon.cpp b/src/daemon/embedded_daemon.cpp index f013dd4..5a1f189 100644 --- a/src/daemon/embedded_daemon.cpp +++ b/src/daemon/embedded_daemon.cpp @@ -500,6 +500,8 @@ static bool isPortInUse(int port) bool EmbeddedDaemon::start(const std::string& binary_path) { + std::lock_guard life(lifecycle_mutex_); + // Don't start if already running or starting if (state_ == State::Starting || state_ == State::Running) { DEBUG_LOGF("[INFO] Daemon already %s\n", state_ == State::Starting ? "starting" : "running"); @@ -511,6 +513,27 @@ bool EmbeddedDaemon::start(const std::string& binary_path) return true; } + // A stop session owns this object until endStop() sets Stopped; never spawn under it (its endStop would + // adopt and abandon our new pid, and it still uses stdout_fd_/process_handle_). + if (state_ == State::Stopping) { + DEBUG_LOGF("[INFO] start(): a stop is still in progress, not starting\n"); + return false; + } + // A monitor that returned on its own (unexpected exit, or a requested stop that landed before stop()) is + // still joinable. Join it, then release what its run left behind. Only in this case: if a stop session + // already joined it, the fd/handle belong to that session and endStop() closes them. + if (monitor_thread_.joinable()) { + should_stop_ = true; // a monitor somehow still looping ends within one tick + monitor_thread_.join(); // normally immediate (it already returned) +#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; } // also stops it leaking into the next fork +#endif + } + should_stop_ = false; + // Check if something is already listening on the RPC port. An isolated instance (migrate-to- // seed) runs on its own non-default port alongside the main daemon, so it skips this bail. int rpc_port = std::atoi(DRAGONX_DEFAULT_RPC_PORT); @@ -691,11 +714,12 @@ bool EmbeddedDaemon::start(const std::string& binary_path) if (pid > 0 && !rec.startToken.empty()) writeOwnershipRecord(ownershipRecordPath(), rec); } - // Start monitor thread (if not already running) + // Supervise the process just launched. A monitor left over from an earlier run was joined above. should_stop_ = false; - if (!monitor_thread_.joinable()) { - monitor_thread_ = std::thread(&EmbeddedDaemon::monitorProcess, this); + if (monitor_thread_.joinable()) { // unreachable under lifecycle_mutex_; never join a live monitor unsignalled + should_stop_ = true; monitor_thread_.join(); should_stop_ = false; } + monitor_thread_ = std::thread(&EmbeddedDaemon::monitorProcess, this); return true; } @@ -924,8 +948,11 @@ void EmbeddedDaemon::drainOutput() bool EmbeddedDaemon::stop(int wait_ms) { - stop_requested_ = true; - should_stop_ = true; + { + std::lock_guard life(lifecycle_mutex_); + stop_requested_ = true; + should_stop_ = true; + } setState(State::Stopping, "Stopping dragonxd..."); // Check if our tracked process handle is still alive @@ -983,8 +1010,11 @@ bool EmbeddedDaemon::stop(int wait_ms) // Final drain of debug.log drainOutput(); - if (monitor_thread_.joinable()) { - monitor_thread_.join(); + { + std::lock_guard life(lifecycle_mutex_); + if (monitor_thread_.joinable()) { + monitor_thread_.join(); + } } setState(State::Stopped, "dragonxd stopped"); @@ -993,6 +1023,7 @@ bool EmbeddedDaemon::stop(int wait_ms) bool EmbeddedDaemon::beginStop() { + std::lock_guard life(lifecycle_mutex_); // before the flags: a racing start() would reset should_stop_ stop_requested_ = true; should_stop_ = true; if (monitor_thread_.joinable()) { @@ -1042,8 +1073,12 @@ EmbeddedDaemon::ResourceUsage EmbeddedDaemon::ownedResourceUsage() const void EmbeddedDaemon::endStop(bool keepWatching) { + std::lock_guard life(lifecycle_mutex_); if (keepWatching && isRunning()) { // Still running and still ours: watch it again. Its exit is booked as a clean stop. + if (monitor_thread_.joinable()) { // unreachable after beginStop()'s join; never join a live monitor unsignalled + should_stop_ = true; monitor_thread_.join(); + } should_stop_ = false; monitor_thread_ = std::thread(&EmbeddedDaemon::monitorProcess, this); return; @@ -1504,16 +1539,20 @@ bool EmbeddedDaemon::reapWithin(pid_t pid, int timeout_ms, int& status) 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 // tell us the child exited: poll waitpid() on a cached pid instead. If the child exited inside that // tick, the monitor already reaped it and, seeing stop_requested_, recorded a clean stop (pid now 0). - if (monitor_thread_.joinable()) { - monitor_thread_.join(); + // lifecycle_mutex_ covers the flags and the join only, never the reap waits below. + { + std::lock_guard life(lifecycle_mutex_); + stop_requested_ = true; + should_stop_ = true; + if (monitor_thread_.joinable()) { + monitor_thread_.join(); + } } const pid_t pid = process_pid_; @@ -1557,6 +1596,7 @@ bool EmbeddedDaemon::stop(int wait_ms) bool EmbeddedDaemon::beginStop() { + std::lock_guard life(lifecycle_mutex_); // before the flags: a racing start() would reset should_stop_ stop_requested_ = true; should_stop_ = true; // The monitor sees should_stop_ within one 100ms tick. If the child exited in that window the @@ -1649,9 +1689,13 @@ EmbeddedDaemon::ResourceUsage EmbeddedDaemon::ownedResourceUsage() const void EmbeddedDaemon::endStop(bool keepWatching) { + std::lock_guard life(lifecycle_mutex_); const pid_t pid = process_pid_; if (pid > 0 && keepWatching) { // Still running and still ours: watch it again. Its exit is booked as a clean stop. + if (monitor_thread_.joinable()) { // unreachable after beginStop()'s join; never join a live monitor unsignalled + should_stop_ = true; monitor_thread_.join(); + } should_stop_ = false; monitor_thread_ = std::thread(&EmbeddedDaemon::monitorProcess, this); return; diff --git a/src/daemon/embedded_daemon.h b/src/daemon/embedded_daemon.h index 8cfcf23..6b65194 100644 --- a/src/daemon/embedded_daemon.h +++ b/src/daemon/embedded_daemon.h @@ -53,7 +53,8 @@ public: /** * @brief Start the embedded dragonxd daemon * @param binary_path Path to dragonxd binary (auto-detect if empty) - * @return true if process started successfully + * @return true if process started successfully (or is already running); false while a stop + * session (beginStop() .. endStop()) is still in progress */ bool start(const std::string& binary_path = ""); @@ -501,6 +502,9 @@ private: std::thread monitor_thread_; std::atomic should_stop_{false}; std::atomic stop_requested_{false}; // a stop was asked for: the next exit is not a crash + // Serializes start/beginStop/stop/endStop over monitor_thread_, should_stop_ and stop_requested_ (two + // threads joining one std::thread is undefined). Never taken by the monitor thread or the state callback. + std::mutex lifecycle_mutex_; std::set debug_categories_; int max_connections_ = 0; // 0 = daemon default bool stratum_enabled_ = false; // -stratum: host a RandomX pool (v1.3.0+; older daemons ignore it) diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 52733e3..5cbd2f5 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -3896,6 +3896,27 @@ void testPlatformEnsureDirectory() #ifndef _WIN32 // Integration tests that drive the REAL EmbeddedDaemon fork/exec/waitpid paths (POSIX only). + +// Every start() in these tests is isolated (X13). The skipped port check also skips the datadir-lock gate; +// the -datadir override (a temp dir) means no ownership record (the user's real +// ~/.hush/DRAGONX/obsidiandragon-dragonxd.pid) is written and /debug.log is tailed instead of the real +// one. The override is one-shot: set it again before every start(). +void isolateNextDaemonStart(dragonx::daemon::EmbeddedDaemon& d, const fs::path& dir) +{ + d.setSkipPortCheck(true); + d.setNextStartOverride(dir.string(), {}); +} + +bool waitUntilTrue(const std::function& cond, double seconds) +{ + const auto t0 = std::chrono::steady_clock::now(); + while (!cond()) { + if (secondsSince(t0) >= seconds) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + return true; +} + void testExecFailureReported() { using dragonx::daemon::EmbeddedDaemon; @@ -3907,13 +3928,15 @@ void testExecFailureReported() { std::ofstream(bin.string(), std::ios::binary) << "this is not an executable"; } fsn::permissions(bin, fsn::perms::owner_read, fsn::perm_options::replace); // 0400, no +x + const fs::path tmp = makeTempDir(); EmbeddedDaemon d; - d.setSkipPortCheck(true); // bypass the port + datadir-lock gates so we reach startProcess() + isolateNextDaemonStart(d, tmp); // bypass the port + datadir-lock gates so we reach startProcess() EXPECT_TRUE(!d.start(bin.string())); EXPECT_TRUE(d.getLastError().find("not executable or wrong architecture") != std::string::npos); EXPECT_TRUE(!d.isRunning()); std::error_code ec; fsn::remove(bin, ec); + removeTree(tmp); } void testDaemonCrashDetected() @@ -3927,8 +3950,9 @@ void testDaemonCrashDetected() { std::ofstream(script.string()) << "#!/bin/bash\nsleep 0.2\nexit 7\n"; } fsn::permissions(script, fsn::perms::owner_all, fsn::perm_options::replace); // +x + const fs::path tmp = makeTempDir(); EmbeddedDaemon d; - d.setSkipPortCheck(true); + isolateNextDaemonStart(d, tmp); EXPECT_TRUE(d.start(script.string())); EXPECT_TRUE(d.isRunning()); // reads the atomic state_, not a racy waitpid() @@ -3946,6 +3970,7 @@ void testDaemonCrashDetected() d.stop(); // join the monitor thread cleanly EXPECT_EQ(d.getCrashCount(), crashes); // a later stop never erases a real crash std::error_code ec; fsn::remove(script, ec); + removeTree(tmp); } // Writes an executable bash stand-in for dragonxd (EmbeddedDaemon runs it via /bin/bash); returns its path. @@ -3958,6 +3983,158 @@ std::filesystem::path writeFakeDaemonScript(const std::string& name, const std:: return script; } +// The same, in `dir` (a per-test temp dir). +std::filesystem::path writeFakeDaemonScript(const fs::path& dir, const std::string& name, const std::string& body) +{ + const fs::path script = dir / name; + { std::ofstream(script.string()) << "#!/bin/bash\n" << body; } + fs::permissions(script, fs::perms::owner_all, fs::perm_options::replace); // +x + return script; +} + +size_t countSubstring(const std::string& hay, const std::string& needle) +{ + size_t n = 0; + for (size_t pos = hay.find(needle); pos != std::string::npos; pos = hay.find(needle, pos + needle.size())) ++n; + return n; +} + +// C101: start() after an unexpected exit, with no stop() in between, must supervise the new run. The +// crashed run's monitor returned on its own but stayed joinable, and start() only spawned a monitor when +// none was joinable: the restarted node ran unwatched (its crash never seen, its output never drained). +void testDaemonRestartAfterCrashIsSupervised() +{ + using dragonx::daemon::EmbeddedDaemon; + const fs::path tmp = makeTempDir(); + const auto script = writeFakeDaemonScript(tmp, "od_fake_daemon_restart.sh", "echo RUN\nsleep 0.2\nexit 7\n"); + + EmbeddedDaemon d; + isolateNextDaemonStart(d, tmp); + EXPECT_TRUE(d.start(script.string())); + EXPECT_TRUE(waitUntilTrue([&d] { return d.getCrashCount() == 1; }, 3.0)); + isolateNextDaemonStart(d, tmp); + EXPECT_TRUE(d.start(script.string())); // no stop() in between + EXPECT_TRUE(waitUntilTrue([&d] { + return d.getCrashCount() == 2 && d.getState() == EmbeddedDaemon::State::Error; + }, 3.0)); + EXPECT_EQ(countSubstring(d.getOutput(), "RUN"), static_cast(2)); // both runs' output drained + + const auto t0 = std::chrono::steady_clock::now(); + d.stop(500); + EXPECT_TRUE(secondsSince(t0) < 2.0); + EXPECT_EQ(d.getCrashCount(), 2); + removeTree(tmp); +} + +// C101: a run restarted after a crash is watched by a new monitor, and a later stop joins it: the stop is +// prompt, recorded as Stopped and not as a crash, and nothing is left running. +void testRestartAfterCrashThenStop() +{ + using dragonx::daemon::EmbeddedDaemon; + const fs::path tmp = makeTempDir(); + const std::string runs = (tmp / "runs").string(); + // Run 1 exits 3 at once; every later run prints RUN and sleeps 30 s. + const auto script = writeFakeDaemonScript(tmp, "od_fake_daemon_restart_stop.sh", + "n=$(cat '" + runs + "' 2>/dev/null || echo 0)\nn=$((n+1))\necho $n > '" + runs + "'\n" + "if [ \"$n\" -eq 1 ]; then exit 3; fi\necho RUN$n\nsleep 30\n"); + + EmbeddedDaemon d; + isolateNextDaemonStart(d, tmp); + EXPECT_TRUE(d.start(script.string())); + EXPECT_TRUE(waitUntilTrue([&d] { return d.getCrashCount() == 1; }, 3.0)); + isolateNextDaemonStart(d, tmp); + EXPECT_TRUE(d.start(script.string())); // run 2 (sleeps 30 s) + EXPECT_TRUE(waitUntilTrue([&d] { return d.getOutput().find("RUN2") != std::string::npos; }, 3.0)); // drained + EXPECT_TRUE(d.isRunning()); + + d.markStopRequested(); + const auto t0 = std::chrono::steady_clock::now(); + d.stop(500); // 0.5 s grace, then the group SIGTERM ends bash and its sleep + EXPECT_TRUE(secondsSince(t0) < 2.0); + EXPECT_TRUE(d.getState() == EmbeddedDaemon::State::Stopped); + EXPECT_FALSE(d.isRunning()); + EXPECT_EQ(d.getCrashCount(), 1); + removeTree(tmp); +} + +#ifdef __linux__ +size_t countOpenFds() +{ + size_t n = 0; + std::error_code ec; + for (fs::directory_iterator it("/proc/self/fd", ec), end; !ec && it != end; it.increment(ec)) ++n; + return n; +} + +// C101 (Linux): crash + restart cycles leak no descriptor. Each run's stdout pipe used to be overwritten by +// the next start() without being closed (and inherited by the next child). +void testDaemonRestartCyclesLeakNoFds() +{ + using dragonx::daemon::EmbeddedDaemon; + const fs::path tmp = makeTempDir(); + const auto script = writeFakeDaemonScript(tmp, "od_fake_daemon_fdleak.sh", "sleep 0.1\nexit 9\n"); + const size_t baseline = countOpenFds(); + { + EmbeddedDaemon d; + isolateNextDaemonStart(d, tmp); + EXPECT_TRUE(d.start(script.string())); + EXPECT_TRUE(waitUntilTrue([&d] { return d.getCrashCount() == 1; }, 3.0)); + for (int cycle = 2; cycle <= 4; ++cycle) { // 3 crash + restart cycles + isolateNextDaemonStart(d, tmp); + EXPECT_TRUE(d.start(script.string())); + EXPECT_TRUE(waitUntilTrue([&d, cycle] { return d.getCrashCount() == cycle; }, 3.0)); + } + d.stop(500); + EXPECT_EQ(countOpenFds(), baseline); + } + removeTree(tmp); +} +#endif + +// C101 D4: start() is refused while a stop session is in progress. After beginStop() and a reap (here +// forceKillOwnedProcess(): pid 0, state still Stopping) start() used to spawn a process that the session's +// endStop() then abandoned. +void testStartRefusedWhileStopping() +{ + using dragonx::daemon::EmbeddedDaemon; + const fs::path tmp = makeTempDir(); + const fs::path launches = tmp / "launches"; + const auto script = writeFakeDaemonScript(tmp, "od_fake_daemon_stopping.sh", + "echo $$ >> '" + launches.string() + "'\nsleep 30\n"); + auto launchPids = [&launches] { + std::vector v; + std::ifstream in(launches); + long p = 0; + while (in >> p) v.push_back(p); + return v; + }; + + EmbeddedDaemon d; + isolateNextDaemonStart(d, tmp); + EXPECT_TRUE(d.start(script.string())); + EXPECT_TRUE(waitUntilTrue([&launchPids] { return launchPids().size() == 1; }, 3.0)); + EXPECT_TRUE(d.beginStop()); + d.forceKillOwnedProcess(); + EXPECT_FALSE(d.isRunning()); + EXPECT_TRUE(d.getState() == EmbeddedDaemon::State::Stopping); + + isolateNextDaemonStart(d, tmp); + EXPECT_FALSE(d.start(script.string())); + EXPECT_TRUE(d.getState() == EmbeddedDaemon::State::Stopping); // unchanged + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + const auto pids = launchPids(); + EXPECT_EQ(pids.size(), static_cast(1)); // nothing spawned + d.clearNextStartOverride(); // the refused start didn't consume it + + d.endStop(/*keepWatching=*/false); + EXPECT_TRUE(d.getState() == EmbeddedDaemon::State::Stopped); + EXPECT_EQ(d.getCrashCount(), 0); + // Before the fix a second run was spawned and endStop() left it running: end it (its own process group). + for (size_t i = 1; i < pids.size(); ++i) + if (pids[i] > 0) kill(-static_cast(pids[i]), SIGKILL); + removeTree(tmp); +} + // 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). @@ -3966,8 +4143,9 @@ void testPosixStopReturnsAfterCleanExit() using dragonx::daemon::EmbeddedDaemon; const auto script = writeFakeDaemonScript("od_fake_daemon_clean.sh", "sleep 1\nexit 0\n"); + const fs::path tmp = makeTempDir(); EmbeddedDaemon d; - d.setSkipPortCheck(true); + isolateNextDaemonStart(d, tmp); EXPECT_TRUE(d.start(script.string())); const auto t0 = std::chrono::steady_clock::now(); d.stop(5000); @@ -3976,6 +4154,7 @@ void testPosixStopReturnsAfterCleanExit() EXPECT_TRUE(d.getState() == EmbeddedDaemon::State::Stopped); EXPECT_TRUE(!d.isRunning()); std::error_code ec; std::filesystem::remove(script, ec); + removeTree(tmp); } // S4: an exit after markStopRequested() is the stop we asked for, even when the monitor reaps it @@ -3985,8 +4164,9 @@ void testPosixRequestedStopIsNotACrash() using dragonx::daemon::EmbeddedDaemon; const auto script = writeFakeDaemonScript("od_fake_daemon_requested.sh", "sleep 0.3\nexit 0\n"); + const fs::path tmp = makeTempDir(); EmbeddedDaemon d; - d.setSkipPortCheck(true); + isolateNextDaemonStart(d, tmp); EXPECT_TRUE(d.start(script.string())); d.markStopRequested(); for (int i = 0; i < 300 && d.getState() == EmbeddedDaemon::State::Running; ++i) @@ -3999,6 +4179,7 @@ void testPosixRequestedStopIsNotACrash() EXPECT_TRUE(secondsSince(t0) < 1.0); EXPECT_EQ(d.getCrashCount(), 0); std::error_code ec; std::filesystem::remove(script, ec); + removeTree(tmp); } // C103: EmbeddedDaemon::memoryProbe (POSIX: place after testPosixRequestedStopIsNotACrash, it uses @@ -4012,8 +4193,9 @@ void testEmbeddedDaemonMemoryProbe() EXPECT_NEAR(idle.getMemoryUsageMB(), 0.0, 1e-9); } const auto script = writeFakeDaemonScript("od_fake_daemon_mem.sh", "sleep 2\nexit 0\n"); + const fs::path tmp = makeTempDir(); EmbeddedDaemon d; - d.setSkipPortCheck(true); + isolateNextDaemonStart(d, tmp); EXPECT_TRUE(d.start(script.string())); auto probe = d.memoryProbe(); EXPECT_TRUE(static_cast(probe)); @@ -4023,6 +4205,7 @@ void testEmbeddedDaemonMemoryProbe() EXPECT_TRUE(mb > 0.0); // the bash stand-in's process group d.stop(5000); std::error_code ec; std::filesystem::remove(script, ec); + removeTree(tmp); } // S4: SIGTERM is graceful on POSIX (dragonxd treats it like RPC stop), and stop() reaps the clean @@ -4040,8 +4223,9 @@ void testPosixStopSigtermIsGraceful() "while :; do sleep 0.1; done\n"); setenv("OD_TEST_EVENTS", events.string().c_str(), 1); + const fs::path tmp = makeTempDir(); EmbeddedDaemon d; - d.setSkipPortCheck(true); + isolateNextDaemonStart(d, tmp); EXPECT_TRUE(d.start(script.string())); const auto t0 = std::chrono::steady_clock::now(); d.stop(1000); // nothing asked it to stop, so SIGTERM goes out at +1s @@ -4055,6 +4239,7 @@ void testPosixStopSigtermIsGraceful() unsetenv("OD_TEST_EVENTS"); fsn::remove(script, ec); fsn::remove(events, ec); + removeTree(tmp); } // S3: destroying an EmbeddedDaemon must not stop (let alone kill) the process. The destructor used to @@ -4072,8 +4257,9 @@ void testPosixDestructorLeavesProcessRunning() setenv("OD_TEST_PIDFILE", pidFile.string().c_str(), 1); pid_t pid = 0; + const fs::path tmp = makeTempDir(); auto d = std::make_unique(); - d->setSkipPortCheck(true); + isolateNextDaemonStart(*d, tmp); EXPECT_TRUE(d->start(script.string())); for (int i = 0; i < 300 && pid <= 0; ++i) { std::ifstream in(pidFile); @@ -4093,6 +4279,7 @@ void testPosixDestructorLeavesProcessRunning() unsetenv("OD_TEST_PIDFILE"); fsn::remove(script, ec); fsn::remove(pidFile, ec); + removeTree(tmp); } // executeDaemonStop over a REAL EmbeddedDaemon child: steady clock, real waitpid/signals/CPU sampling. @@ -4156,8 +4343,9 @@ void testPosixStopSessionDetach() "trap '' TERM\necho $$ > \"$OD_TEST_PIDFILE\"\nwhile :; do :; done\n"); // pid file = trap is set setenv("OD_TEST_PIDFILE", pidFile.string().c_str(), 1); + const fs::path tmp = makeTempDir(); EmbeddedDaemon d; - d.setSkipPortCheck(true); + isolateNextDaemonStart(d, tmp); EXPECT_TRUE(d.start(script.string())); const pid_t pid = readPidFile(pidFile); EXPECT_TRUE(pid > 0); @@ -4189,6 +4377,7 @@ void testPosixStopSessionDetach() unsetenv("OD_TEST_PIDFILE"); fsn::remove(script, ec); fsn::remove(pidFile, ec); + removeTree(tmp); } // S1/S2 on POSIX: with no RPC answer, the group SIGTERM stops a node that handles it, promptly and @@ -4199,8 +4388,9 @@ void testPosixStopSessionSigterm() using DC = dragonx::daemon::DaemonController; const auto script = writeFakeDaemonScript("od_fake_daemon_session_term.sh", "trap 'exit 0' TERM\nwhile :; do sleep 0.1; done\n"); + const fs::path tmp = makeTempDir(); EmbeddedDaemon d; - d.setSkipPortCheck(true); + isolateNextDaemonStart(d, tmp); EXPECT_TRUE(d.start(script.string())); EXPECT_TRUE(d.beginStop()); RealStopRuntime rt(d); @@ -4214,6 +4404,7 @@ void testPosixStopSessionSigterm() EXPECT_TRUE(d.getState() == EmbeddedDaemon::State::Stopped); EXPECT_EQ(d.getCrashCount(), 0); std::error_code ec; std::filesystem::remove(script, ec); + removeTree(tmp); } // S1: a bounded wait that gives up keeps watching the node: its later exit is still seen, and booked @@ -4230,8 +4421,9 @@ void testPosixStopSessionGiveUpKeepsWatching() const auto script = writeFakeDaemonScript("od_fake_daemon_session_slow.sh", "trap '' TERM\necho $$ > \"$OD_TEST_PIDFILE\"\nsleep 1.5\nexit 0\n"); setenv("OD_TEST_PIDFILE", readyFile.string().c_str(), 1); + const fs::path tmp = makeTempDir(); EmbeddedDaemon d; - d.setSkipPortCheck(true); + isolateNextDaemonStart(d, tmp); EXPECT_TRUE(d.start(script.string())); EXPECT_TRUE(readPidFile(readyFile) > 0); EXPECT_TRUE(d.beginStop()); @@ -4252,6 +4444,7 @@ void testPosixStopSessionGiveUpKeepsWatching() std::error_code ec; fsn::remove(script, ec); fsn::remove(readyFile, ec); + removeTree(tmp); } #endif // !_WIN32 @@ -16103,6 +16296,12 @@ int main() #ifndef _WIN32 testExecFailureReported(); testDaemonCrashDetected(); + testDaemonRestartAfterCrashIsSupervised(); + testRestartAfterCrashThenStop(); +#ifdef __linux__ + testDaemonRestartCyclesLeakNoFds(); +#endif + testStartRefusedWhileStopping(); testPosixStopReturnsAfterCleanExit(); testPosixRequestedStopIsNotACrash(); testEmbeddedDaemonMemoryProbe();