Changelog¶
0.2.1¶
Certification release. Adds the full critical-systems audit trail and the
v0.2.0 certification (audit-2026-07-23.md) to the repository, and removes an
unreachable code path in the client. No functional changes since 0.2.0.
0.2.0¶
Hardening release driven by a full critical-systems audit
(audit-2026-07-23.md). Every confirmed defect ships with a regression test
(tests/test_audit_regressions.py); the suite, mypy --strict, and ruff
are clean, and the fixes were verified end-to-end (daemon + CLI battery, race
stress runs).
Fixed¶
Config values containing
%no longer crash the parser. Interpolation is disabled, so the documented%(process_num)slog templating actually works and commands likedate +%sare configurable.A stop that lands mid-spawn now stops the child.
spawn()re-checks for a stop request after the fork;stop/stopgroupwait for the state to settle and report the state actually reached. Previously the child could be left running forever after an acknowledged stop.reloadapplies changed program configs. Edited settings take effect on the next restart, as the log message always claimed. Reloading an unchanged file no longer misreportsnumprocs > 1programs as changed on every reload.Starting a FATAL process works and reports the truth. Previously it always returned an INTERNAL_ERROR while starting the process anyway.
startreplies are truthful. “Started X” is only returned when the process actually reached RUNNING; otherwise an error names the state reached.Health-check restarts are paced and bounded. They now go through the backoff machinery and escalate to FATAL after
startretriesconsecutive health restarts (a passing check resets the counter). Previously a persistently failing check caused an unbounded kill/respawn storm.Transient spawn errors are retried. fd exhaustion, a briefly missing log directory, or a binary mid-deploy now retry under backoff/
startretriesinstead of instantly marking the process permanently FATAL. Permanent errors (unknown user, unparseable command, permission denied) still fail fast.Manual stops settle cleanly. No more transient/terminal BACKOFF states or polluted retry counters after an operator stop; an unkillable (D-state) process stays STOPPING instead of being reported STOPPED (preventing duplicate instances).
Startup binds the RPC socket before spawning children, so a conflicting instance is detected before any duplicate process is forked, and startup failures run the full shutdown path.
Supervision loops have an exception safety net — an unexpected internal error marks the process FATAL (visible, alertable) instead of silently freezing its supervision.
Changed¶
RUNNINGis now honest: a process staysSTARTINGuntil it survivesstartsecs(supervisord semantics). Exits inside the window count againststartretries; after reaching RUNNING, restarts are paced at 1s.Backoff delay is now the retry count in seconds (1s, 2s, …, capped 30s), no longer coupled to
startsecs.Default RPC socket moved out of
/tmp(squatting/impersonation risk) to$XDG_RUNTIME_DIR/supervice.sock,/run/supervice.sockfor root, else~/.supervice.sock. The daemon refuses to replace a socket it cannot prove stale and warns when the socket directory is world-writable.Default daemon
logfileis now empty: foreground mode logs to stdout (container-friendly); daemon mode falls back tosupervice.log.User switching no longer uses
preexec_fn. Users are resolved in the parent and switched viasubprocess’s nativeuser/group/extra_groups(thread-safe). The pdeathsig hook no longer dlopens libc post-fork.Script health checks run as the program’s
user, never as the (possibly root) daemon.
Added¶
Child log rotation: stdout/stderr are captured via pipes and rotated at
stdout_logfile_maxbytes/stderr_logfile_maxbyteswith*_logfile_backupskept.%(process_num)sexpansion incommandandenvironment(in addition to logfiles), sonumprocsinstances can bind distinct ports.pdeathsigprogram option to opt out of kill-children-on-supervisor-death.supervicectl --timeout(default 30s) so a wedged daemon cannot hang the CLI; group commands report per-process failures.Load-time validation: unparseable
commandvalues and[group:*]members that reference unknown programs are rejected at config load.
0.1.1¶
Bug-fix release addressing issues found in a full source audit. All fixes ship
with regression tests; mypy --strict and ruff remain clean.
Fixed¶
Exit codes 126/127 no longer force
FATAL. A supervised program that legitimately exits with code 126 or 127 was permanently markedFATALand never restarted, because those codes were overloaded as preexec-failure sentinels. Preexec (user-switch) failures are now detected out-of-band via a dedicatedCLOEXECstatus pipe, so the program’s real exit code is always honoured.reloadnow reconciles process groups. Adding, removing, or renaming a[group:*]section — or moving a program between groups — is now reflected in the live group table, sostartgroup/stopgroupwork after a reload. Previously only program add/remove was handled and group changes were ignored.startno longer races the supervision loop. The state-change signal used aset()/clear()anti-pattern that could makesupervicectl startblock up to 5 seconds under load; it now uses a race-free clear-then-check-then-wait.Refuse to hijack a live RPC socket. Startup previously unlinked any existing socket unconditionally; it now probes for a live instance first and refuses to start if one responds (relevant when the pidfile lock is disabled).
PID file is only removed if it holds our PID, so a foreign pidfile is never deleted.
PID file lock is released last during shutdown, after all children stop, so a restarting instance can’t orphan the old daemon’s children.
TCP health check no longer leaks a socket fd when the check is cancelled.
Config logger setup consolidated into the entry point (no longer performed inside
load_config), fixing fragile fd/handler ordering.Unreadable config files raise instead of being silently ignored.
Empty RPC requests return a clear
EMPTY_REQUESTerror instead of a confusing “Invalid JSON”.Warn when
numprocs > 1is combined with a non-templated log file (output would silently interleave).
Security¶
Documented that
healthcheck_commandruns through a shell and executes arbitrary code as the daemon user; treat the config file as trusted input.
0.1.0 (Unreleased)¶
Initial release.
Features¶
Async process supervision built on Python
asyncioZero external dependencies — pure Python stdlib
INI-based configuration compatible with Supervisor conventions
Process lifecycle management with state machine (STOPPED, STARTING, RUNNING, BACKOFF, STOPPING, EXITED, FATAL, UNHEALTHY)
Multiple process instances via
numprocsProcess groups with batch start/stop operations
TCP and script-based health checks with configurable intervals, timeouts, and retries
Auto-restart on process exit and health check failure
Configurable start retries with FATAL state on exhaustion
Hot configuration reload (add/remove programs without restart)
Unix socket RPC with length-prefixed JSON protocol
CLI control tool (
supervicectl) with status, start, stop, restart, reload commandsGraceful and forced restart (
--forceflag for SIGKILL)Process group kill (kills entire process tree, not just main PID)
Double-fork daemonization with PID file locking (
fcntl.flock)Log rotation via
RotatingFileHandlerPer-process uptime tracking and display
User switching with
setuid/setgid(requires root)Linux orphan prevention via
prctl(PR_SET_PDEATHSIG)Log file path substitution with
%(process_num)sQuote-aware environment variable parsing
SIGHUP handling (logged and ignored)
Bounded event queue with backpressure (prevents memory exhaustion)
Restrictive Unix socket permissions (
0o600)Full
mypy --strictcomplianceComprehensive test suite (63 tests)