procpilot
Production-grade subprocess runner for Rust — typed errors, retry with backoff, timeout, stdin piping, secret redaction.
Built for CLI tools that spawn external processes and need precise failure handling. Not intended for shell scripting (see xshell for that).
Why not std::process::Command?
Command::output() returns a status the caller must remember to check. Command::spawn() gives you a Child but no help with timeout, retry, or deadlock-safe pipe draining. Every production CLI ends up writing the same wrapping layer.
procpilot is that layer.
- Typed errors — [
RunError] distinguishesSpawn(couldn't start),NonZeroExit(command ran and failed, with captured stdout/stderr), andTimeout(killed after budget). - Retry with exponential backoff + jitter — [
Cmd::retry] / [Cmd::retry_when]. - Timeout + deadline — per-attempt timeout or overall wall-clock budget across retries.
- Stdin piping — owned bytes (reusable across retries) or a boxed
Read(one-shot streaming). - Stderr routing — capture / inherit / null / redirect-to-file via [
Redirection]. - Secret redaction — [
Cmd::secret] replaces args with<secret>in error output and logs.
Usage
[]
= "0.2"
use Duration;
use ;
let output = new
.args
.in_dir
.env
.timeout
.run?;
# Ok::
Error handling
use ;
match new.args.run
# ; Ok::
RunError is #[non_exhaustive]; include a wildcard arm. All variants carry a [CmdDisplay] that renders the command shell-style (with secret redaction if .secret() was set). NonZeroExit and Timeout include up to the last 128 KiB of stdout/stderr.
Retry
use ;
new
.args
.in_dir
.retry
.retry_when
.run?;
# Ok::
Deadline across retries
.timeout() bounds a single attempt; .deadline() bounds the whole operation (including retry backoff sleeps). Combine them when you want "retry up to 3× but never exceed 10 seconds total".
use ;
use ;
new
.args
.in_dir
.timeout
.deadline
.retry
.run?;
# Ok::
Inheriting stderr (live progress)
When the child prompts the user or should stream progress to the terminal, route stderr with Redirection::Inherit instead of capturing it.
use ;
new
.args
.stderr
.run?;
# Ok::
Stdin
use Cmd;
let manifest = "apiVersion: v1\nkind: ConfigMap\n...";
new.args.stdin.run?;
# Ok::
Secret redaction
use Cmd;
new.args.secret.run?;
// Error messages show `docker <secret>` instead of the token.
# Ok::
License
Licensed under either Apache-2.0 or MIT at your option.