procman
A foreman-like process supervisor written in Rust. Reads a procman.yaml, spawns all listed commands, multiplexes their output with name prefixes, and tears everything down cleanly when any child exits or a signal arrives.
Usage
cargo install --path .
procman run — run all commands
Bare procman with no subcommand is equivalent to procman run.
procman serve — accept dynamic commands via a FIFO
&
& # custom config path
Runs all procman.yaml commands and listens on a named FIFO for dynamically added commands. The FIFO path is derived automatically from the config file path, so you never need to specify it. The FIFO is created automatically and removed on exit.
procman start — send a command to a running server
Opens the FIFO for writing and sends a JSON message. Fails immediately if no server is listening. The FIFO path is derived from the config path, matching the running server.
procman stop — gracefully shut down a running server
Sends a shutdown command to the server via the FIFO. The server logs the request and terminates cleanly.
Scripted service bringup
The serve/start pattern enables imperative orchestration — start a supervisor, wait for dependencies to become healthy, then add dependent services:
&
while ; do ; done
An advisory flock on procman.yaml prevents multiple instances from managing the same file simultaneously.
procman.yaml Format
web:
env:
PORT: "3000"
run: serve --port $PORT
migrate:
run: db-migrate up
once: true
api:
depends:
- process_exited: migrate
- url: http://localhost:3000/health
code: 200
poll_interval: 0.5
timeout_seconds: 30
run: api-server start
setup:
depends:
- path: /tmp/ready.flag
run: post-setup-task
db:
depends:
- tcp: "127.0.0.1:5432"
run: db-client start
nodes:
for_each:
glob: "/etc/nodes/*.yaml"
as: NODE_CONFIG
run: node-agent --config $NODE_CONFIG
once: true
- Each top-level key is a process name.
run(required): the command to execute (parsed with POSIX shell quoting). Supports${{ process.KEY }}templates to reference output values fromoncedependencies.env(optional): per-process environment variables (also supports${{ }}templates).once(optional): iftrue, the process exits cleanly on success (code 0) without triggering supervisor shutdown. Processes can write key-value pairs to$PROCMAN_OUTPUTfor downstream template resolution.for_each(optional): fan-out a template process across glob matches. Requiresglob(pattern) andas(variable name). Each match spawns an instance with the variable set in env and substituted in the run string.depends(optional): list of dependencies that must be satisfied before the process starts. Circular dependencies are detected at config parse time. Dependency paths support$VARand${VAR}environment variable expansion (including per-processenvoverrides); use$$for a literal$.- HTTP health check:
url+code(expected status), with optionalpoll_intervalandtimeout_seconds. - TCP connect:
tcp(address:port), with optionalpoll_intervalandtimeout_seconds. - File exists:
pathto a file that must exist. - File contains key:
file_containswithpath,format(json/yaml),key(dot-separated path), and optionalenv(variable name to extract the value into). With optionalpoll_intervalandtimeout_seconds. - Process exited:
process_exitednames aonce: trueprocess that must complete successfully before this process starts.
- HTTP health check:
Behavior
- Each child is signaled individually at shutdown.
- stderr is merged into stdout per-process.
- Output is prefixed with the process name, right-aligned and padded.
- Per-process logs are written to
./procman-logs/<name>.log(directory is recreated each run). - A combined
./procman-logs/procman.logcontains the full interleaved formatted output (same as stdout). - On SIGINT or SIGTERM, all children receive SIGTERM. After a 2-second grace period, remaining processes are sent SIGKILL.
- procman exits with the first child's exit code.
License
MIT