Daemon
Long-running process orchestration — supervisor wiring around b.appShutdown, foreground signal handling, detached-fork spawn via b.processSpawn, PID-file health probes, and a SIGTERM-then-SIGKILL restart policy on stop.
Two operator paths share one entry point:
1. Foreground service mode (no command): the current process acquires pidFile, redirects stdout/stderr to logFile, and installs signal handlers (defaults: SIGTERM, SIGINT, SIGHUP) that route through a b.appShutdown orchestrator the operator can extend with addPhase.
2. Detached fork mode (command + args): the parent spawns the child via b.processSpawn (filtered env), writes the child PID to pidFile, hands the log fd to the child's stdout/stderr, and returns immediately so the parent can exit.
Stale-PID handling — when pidFile exists but the recorded PID is no longer alive, start and stop clean up the sidecar and emit daemon.stale_pid_cleaned. Cross-process linkage uses b.appShutdown.pidLock, which layers O_EXCL atomic-create + signal-0 liveness probe + reap-on-stale.
On Windows a received signal can never reach a JS handler (process.kill maps it to TerminateProcess), so stop drives a cooperative stop-request sentinel () that start watches and routes into the same orchestrator, escalating to a hard TerminateProcess only after the stop timeout. status is a read-only liveness probe that never mutates the pidfile.
Audit events: daemon.started (pidFile + logFile + commandKind + pid), daemon.stopped (pidFile + signal + waitMs + escalated + mechanism: signal|cooperative|terminate), daemon.spawn_failed (pidFile + command) when a detached child fails to launch, and daemon.stale_pid_cleaned (pidFile + stalePid).
b.daemon.start(opts) #
{
pidFile: string, // absolute path of the PID sidecar (required)
logFile: string, // append-mode log; redirects stdout+stderr
signals: string[], // foreground signals; default: SIGTERM/SIGINT/SIGHUP
command: string, // executable for detached-fork mode
args: string[], // argv for the detached child
cwd: string, // cwd for the detached child
bootDeathWindowMs: number, // detached: keep the parent loop alive this long after spawn to observe a boot death (an abnormal exit in the window is audited as a spawn failure + reaps the pidfile); default 5000, 0 opts out (fire-and-forget)
}
Acquire pidFile, optionally redirect stdout/stderr to logFile, and either install signal handlers in the current process (foreground mode) or spawn a detached child (when command is supplied). Reaps a stale pidfile before acquire and emits daemon.stale_pid_cleaned when one is found.
Returns { pid, pidFile, logFile, mode }. In foreground mode the return value also exposes orchestrator (the underlying b.appShutdown handle), addPhase (operator-supplied shutdown phases), and shutdown (manual trigger). In detached mode mode is "detached"; in foreground mode it is "foreground".
Throws DaemonError("daemon/already-running") when the pidfile is held by a live PID, DaemonError("daemon/spawn-failed") when the detached spawn errors, and DaemonError("daemon/log-open-failed") when the log file cannot be opened in foreground mode.
var handle = b.daemon.start({
pidFile: "/tmp/blamejs-daemon-demo.pid",
signals: ["SIGTERM", "SIGINT"],
});
handle.mode; // → "foreground"
handle.pidFile; // → "/tmp/blamejs-daemon-demo.pid"
typeof handle.shutdown; // → "function"
await handle.shutdown();
b.daemon.stop(opts) #
{
pidFile: string, // absolute path of the PID sidecar (required)
signal: string, // initial signal; default "SIGTERM"
timeoutMs: number, // wait before SIGKILL escalation; default 30 s
pollMs: number, // liveness-probe interval; default 100 ms
abortSignal: AbortSignal, // forwarded to b.safeAsync.sleep
}
Read pidFile, send signal (default SIGTERM), poll for exit up to timeoutMs (default 30 s), then escalate to SIGKILL. Cleans up the pidfile on successful exit and emits daemon.stopped with escalated: true|false recording whether SIGKILL was needed.
Returns { stopped, pid, signal, escalated?, reason? }. reason is "no-pidfile" when nothing was running and "stale" when the pidfile pointed at a dead PID (the file is removed and a daemon.stale_pid_cleaned audit row lands).
var report = await b.daemon.stop({
pidFile: "/tmp/blamejs-daemon-demo.pid",
timeoutMs: b.constants.TIME.seconds(5),
});
report.stopped; // → false
report.reason; // → "no-pidfile"
b.daemon.status(opts) #
{
pidFile: string, // absolute path of the PID sidecar (required)
}
Read-only PID-liveness probe. Reads pidFile and reports whether the recorded process is alive, WITHOUT mutating anything — unlike stop(), a stale pidfile is reported but never unlinked, so a health check can't disturb the daemon's lifecycle state. A missing / malformed / symlinked / oversized pidfile reports running: false with reason: "no-pidfile" rather than throwing (the same fd-safe, symlink-refusing, 1 KiB-capped read that start and stop use). Bad opts throw daemon/bad-pid-file.
Returns { running, pid, reason? }. reason is "no-pidfile" when no live sidecar was found and "stale" when the pidfile pointed at a dead PID — the file is left in place for the operator to inspect or for stop to reap.
var s = b.daemon.status({ pidFile: "/tmp/blamejs-daemon-demo.pid" });
s.running; // → false
s.reason; // → "no-pidfile"
Last updated 2026-08-08T16:39:49.652Z by seeder.