fix(daemon): harden startup process lifecycle (crash race, exec failure, datadir lock)
Three verified daemon-startup edge-case fixes in the embedded-daemon process
lifecycle (all in embedded_daemon.{cpp,h}):
- F1: EmbeddedDaemon::isRunning() (POSIX) now reads the atomic state_ instead of
calling waitpid(WNOHANG) from the UI thread, which raced monitorProcess()'s own
reap. waitpid is one-shot: whichever thread won consumed the exit status; if
isRunning() won, the monitor never saw the crash, so crash_count_/State::Error
and the 3-strike restart cap were silently lost. monitorProcess() is now the sole
reaper (predicate Running || Stopping keeps stop()'s wait loops correct). Mirrors
the existing XmrigManager::isRunning() fix.
- F2: startProcess() (POSIX) adds a close-on-exec self-pipe exec handshake. On a
non-executable / wrong-arch / corrupt binary, execv fails in the child and the
parent now learns synchronously (reads errno vs EOF), reaps the zombie, sets a
precise last_error_ ("not executable or wrong architecture"), and returns false
-- instead of reporting State::Running for a daemon that never started. Uses
pipe()+FD_CLOEXEC (not pipe2) so the branch stays shared with macOS. Parent-side
setpgid is now best-effort + logged.
- F4: start() gates on a lingering datadir lock after the port check. A graceful
shutdown releases the RPC port ~90s before the datadir .lock, so a rapid
stop->start spawned a daemon that died on the lock and, three times in ~12s,
tripped the 3-strike crash cap before the lock cleared. start() now polls
isDaemonProcessRunning() with a bounded ~300ms wait and bails with a distinct
non-crash Error (no crash_count_ bump) that the connect loop retries once the
lock clears. Isolated migrate-to-seed starts (skip_port_check_ / -datadir
override) are exempt.
Adds the testDatadirLockGate unit test (pure evaluateDatadirLockGate matrix) to
test_phase4.cpp. Plan and progress tracked in docs/daemon-startup-hardening.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
#include "chat/chat_service.h"
|
||||
#include "chat/chat_database.h"
|
||||
#include "daemon/daemon_controller.h"
|
||||
#include "daemon/embedded_daemon.h"
|
||||
#include "data/transaction_history_cache.h"
|
||||
#include "data/address_book.h"
|
||||
#include "data/wallet_index.h"
|
||||
@@ -2477,6 +2478,28 @@ void testDaemonShutdownPolicy()
|
||||
EXPECT_TRUE(bootstrap.disconnectRpc);
|
||||
}
|
||||
|
||||
void testDatadirLockGate()
|
||||
{
|
||||
using dragonx::daemon::EmbeddedDaemon;
|
||||
|
||||
// Normal start, no lingering daemon after the bounded wait → proceed.
|
||||
auto clear = EmbeddedDaemon::evaluateDatadirLockGate(false, false, false);
|
||||
EXPECT_TRUE(clear.proceed);
|
||||
|
||||
// A previous dragonxd still alive after the wait → bail with a distinct, non-crash msg.
|
||||
auto locked = EmbeddedDaemon::evaluateDatadirLockGate(false, false, true);
|
||||
EXPECT_TRUE(!locked.proceed);
|
||||
EXPECT_TRUE(std::string(locked.errorMessage).find("data directory lock") != std::string::npos);
|
||||
|
||||
// Isolated instance via skip_port_check_ is exempt even if a sibling dragonxd is running.
|
||||
auto skipPort = EmbeddedDaemon::evaluateDatadirLockGate(true, false, true);
|
||||
EXPECT_TRUE(skipPort.proceed);
|
||||
|
||||
// Isolated instance via -datadir override is exempt even if a sibling is running.
|
||||
auto isolated = EmbeddedDaemon::evaluateDatadirLockGate(false, true, true);
|
||||
EXPECT_TRUE(isolated.proceed);
|
||||
}
|
||||
|
||||
void testDaemonLifecycleExecution()
|
||||
{
|
||||
using dragonx::daemon::DaemonController;
|
||||
@@ -6619,6 +6642,7 @@ int main()
|
||||
testWalletSecurityWorkflow();
|
||||
testWalletSecurityWorkflowExecutor();
|
||||
testDaemonShutdownPolicy();
|
||||
testDatadirLockGate();
|
||||
testDaemonLifecycleExecution();
|
||||
testDaemonLifecycleAdapters();
|
||||
testConsoleTextLayout();
|
||||
|
||||
Reference in New Issue
Block a user