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)s log templating actually works and commands like date +%s are configurable.

  • A stop that lands mid-spawn now stops the child. spawn() re-checks for a stop request after the fork; stop/stopgroup wait for the state to settle and report the state actually reached. Previously the child could be left running forever after an acknowledged stop.

  • reload applies changed program configs. Edited settings take effect on the next restart, as the log message always claimed. Reloading an unchanged file no longer misreports numprocs > 1 programs 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.

  • start replies 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 startretries consecutive 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/startretries instead 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

  • RUNNING is now honest: a process stays STARTING until it survives startsecs (supervisord semantics). Exits inside the window count against startretries; 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.sock for 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 logfile is now empty: foreground mode logs to stdout (container-friendly); daemon mode falls back to supervice.log.

  • User switching no longer uses preexec_fn. Users are resolved in the parent and switched via subprocess’s native user/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_maxbytes with *_logfile_backups kept.

  • %(process_num)s expansion in command and environment (in addition to logfiles), so numprocs instances can bind distinct ports.

  • pdeathsig program 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 command values 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 marked FATAL and never restarted, because those codes were overloaded as preexec-failure sentinels. Preexec (user-switch) failures are now detected out-of-band via a dedicated CLOEXEC status pipe, so the program’s real exit code is always honoured.

  • reload now reconciles process groups. Adding, removing, or renaming a [group:*] section — or moving a program between groups — is now reflected in the live group table, so startgroup/stopgroup work after a reload. Previously only program add/remove was handled and group changes were ignored.

  • start no longer races the supervision loop. The state-change signal used a set()/clear() anti-pattern that could make supervicectl start block 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_REQUEST error instead of a confusing “Invalid JSON”.

  • Warn when numprocs > 1 is combined with a non-templated log file (output would silently interleave).

Security

  • Documented that healthcheck_command runs 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 asyncio

  • Zero 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 numprocs

  • Process 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 commands

  • Graceful and forced restart (--force flag 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 RotatingFileHandler

  • Per-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)s

  • Quote-aware environment variable parsing

  • SIGHUP handling (logged and ignored)

  • Bounded event queue with backpressure (prevents memory exhaustion)

  • Restrictive Unix socket permissions (0o600)

  • Full mypy --strict compliance

  • Comprehensive test suite (63 tests)