### Install and Serve Documentation Locally Source: https://github.com/zelanton/processkit/blob/main/README.md Installs the mdbook tool and serves the documentation site with live reloading. Ensure you have Rust and Cargo installed. ```sh cargo install mdbook # once mdbook serve # live-reload at http://localhost:3000 ``` -------------------------------- ### Backoff Delay Example Source: https://github.com/zelanton/processkit/blob/main/docs/supervision.md Provides a concrete example of restart delays with base=200ms, factor=2.0, and cap=30s, showing how delays increase exponentially up to the cap. ```text base=200ms, factor=2.0, cap=30s: restart #0 → ~200ms #1 → ~400ms #2 → ~800ms … #7 → ~25.6s #8+ → 30s (cap) ``` -------------------------------- ### Start Server and Wait for Readiness Probes Source: https://github.com/zelanton/processkit/blob/main/docs/streaming.md Starts a server and uses various probes (stdout line, TCP port, async predicate) to determine readiness within a deadline. Probes fail fast if readiness is impossible and do not kill the child process. ```rust use processkit::Command; use std::time::Duration; let mut run = Command::new("my-server").start().await?; // 1. A line on stdout (returns the matching line): let banner = run .wait_for_line(|l| l.contains("listening on"), Duration::from_secs(10)) .await?; // 2. A TCP port accepting connections: run.wait_for_port("127.0.0.1:8080".parse().unwrap(), Duration::from_secs(10)) .await?; // 3. Any async predicate (an HTTP /health endpoint, a file appearing, …): run.wait_for(|| async { health_check().await }, Duration::from_secs(10)) .await?; // ready — use the server… ``` -------------------------------- ### Starting, Spawning, and Adopting Processes in a Group Source: https://github.com/zelanton/processkit/blob/main/docs/process-groups.md Demonstrates the three primary methods for managing child processes within a ProcessGroup: `start` for full command control, `spawn` for raw process handles, and `adopt` for external processes. ```rust use processkit::{Command, ProcessGroup}; let group = ProcessGroup::new()?; // 1. start(): the full Command experience (capture, streaming, timeouts) in a // SHARED group. The handle does not own the group — dropping the handle // kills that child, dropping the group kills everyone. let server = group.start(&Command::new("dev-server")).await?; // 2. spawn(): the raw escape hatch for a tokio::process::Command you already // have. You get the bare Child back; pipes and reaping are your problem. // spawn() takes the command BY VALUE (reuse would stack pre-exec hooks). let raw = tokio::process::Command::new("background-helper"); let child = group.spawn(raw)?; // 3. adopt(): contain a child that was spawned OUTSIDE the group. let external = tokio::process::Command::new("legacy-launcher").spawn()?; group.adopt(&external)?; ``` -------------------------------- ### Profile a command's execution statistics Source: https://github.com/zelanton/processkit/blob/main/docs/cookbook.md Illustrates how to start a command and then profile its execution, retrieving statistics like duration, peak memory usage, and average CPU cores. ```rust use std::time::Duration; // One run, summarized (requires the opt-in `stats` feature): let profile = Command::new("crunch").start().await?.profile(Duration::from_millis(100)).await?; println!("outcome={:?} took={:?} peak_rss={:?} avg_cpu_cores={:?}", profile.outcome, profile.duration, profile.peak_memory_bytes, profile.avg_cpu_cores()); ``` -------------------------------- ### Install processkit-py Source: https://github.com/zelanton/processkit/blob/main/docs/python-wrapper.md Install the processkit-py package using pip. This command installs the package for use in Python applications. ```bash pip install processkit-py ``` -------------------------------- ### Basic Supervisor Setup and Run Source: https://github.com/zelanton/processkit/blob/main/docs/supervision.md Demonstrates how to configure and run a Supervisor with a command. It shows setting restart policy, maximum restarts, backoff duration and multiplier, maximum backoff, jitter, and a custom stop condition. This is useful for ensuring a process stays running with defined failure handling. ```rust use processkit::{Command, RestartPolicy, Supervisor}; use std::time::Duration; #[tokio::main] async fn main() -> processkit::Result<()> { let outcome = Supervisor::new(Command::new("my-server").args(["--port", "8080"])) .restart(RestartPolicy::OnCrash) // default .max_restarts(5) // default: unlimited .backoff(Duration::from_millis(200), 2.0) // default: 200ms × 2.0 .max_backoff(Duration::from_secs(30)) // default: 30s cap .jitter(true) // default: on .stop_when(|res| res.code() == Some(0)) // optional exit condition .run() .await?; println!( "ended after {} restarts, reason: {:?}, last exit: {:?}", outcome.restarts, outcome.stopped, outcome.final_result.code(), ); Ok(()) } ``` -------------------------------- ### Wait for Child Process Readiness with Probes Source: https://github.com/zelanton/processkit/blob/main/docs/README.md Demonstrates how to start a server command and then use `wait_for_line`, `wait_for_port`, and `wait_for` to ensure it's ready. Each probe has a specified timeout. The probes do not kill the child process if they fail. ```rust use processkit::Command; use std::time::Duration; #[tokio::main] async fn main() -> processkit::Result<()> { let mut run = Command::new("my-server").start().await?; // Wait for the startup banner (returns the matching line)… let banner = run .wait_for_line(|l| l.contains("listening on"), Duration::from_secs(10)) .await?; println!("server says: {banner}"); // …or for the port to accept connections… let addr = "127.0.0.1:8080".parse().expect("valid socket address"); run.wait_for_port(addr, Duration::from_secs(10)).await?; // …or for any async health check to pass. run.wait_for(|| async { health_check().await }, Duration::from_secs(10)).await?; // ready — use the server … Ok(()) } async fn health_check() -> bool { // e.g. probe an HTTP /health endpoint true } ``` -------------------------------- ### Start a Running Process and Get Lifecycle Info Source: https://github.com/zelanton/processkit/blob/main/docs/streaming.md Initiates a child process and provides methods to access its PID, elapsed time, and wait for its outcome. Dropping the `RunningProcess` handle will terminate the child process tree. ```rust use processkit::Command; let mut run = Command::new("dev-server").start().await?; run.pid(); // Option — None once the child is reaped run.elapsed(); // time since spawn // Consume the handle exactly one way: // output_string() / output_bytes() → capture everything (same as the one-shot verbs) // wait() → just the Outcome; output is discarded // finish() → after streaming stdout (below) // profile(every) → resource samples; output discarded, like wait() (stats feature) let outcome = run.wait().await?; ``` -------------------------------- ### Start a Server and Wait for Readiness Source: https://github.com/zelanton/processkit/blob/main/docs/cookbook.md Launch a server process and wait for it to become ready using various readiness probes. The server is automatically killed when the `server` variable goes out of scope. ```rust use std::time::Duration; let mut server = Command::new("my-server").args(["--port", "8080"]).start().await?; // Pick the probe that matches how the server announces readiness: server.wait_for_line(|l| l.contains("listening"), Duration::from_secs(10)).await?; // server.wait_for_port("127.0.0.1:8080".parse().unwrap(), Duration::from_secs(10)).await?; // server.wait_for(|| async { http_health().await }, Duration::from_secs(10)).await?; // …use the server; dropping `server` kills its whole tree. ``` -------------------------------- ### Run Command and Capture Trimmed Output Source: https://github.com/zelanton/processkit/blob/main/docs/README.md Example of running a command and capturing its trimmed stdout. A non-zero exit code does not cause an error on its own. ```rust use processkit::{Command, ProcessGroup, Stdin}; #[tokio::main] async fn main() -> processkit::Result<()> { // Capture output; a non-zero exit does not error on its own. let result = Command::new("git").args(["rev-parse", "HEAD"]).output_string().await?; println!("HEAD is {}", result.stdout().trim()); // Require success and get trimmed stdout directly. let version = Command::new("cargo").arg("--version").run().await?; println!("{version}"); // Feed stdin. let sorted = Command::new("sort") .stdin(Stdin::from_string("banana\napple\n")) .output_string() .await?; println!("{}", sorted.stdout()); // Share one kill-on-drop group across several children; dropping the group // reaps the whole tree. let group = ProcessGroup::new()?; let _server = group.start(&Command::new("some-server")).await?; // ... work ... group.shutdown().await?; // graceful SIGTERM → wait → SIGKILL (Unix); atomic on Windows Ok(()) } ``` -------------------------------- ### Install processkit Crate Source: https://github.com/zelanton/processkit/blob/main/docs/README.md Add the processkit crate to your Cargo.toml file. This crate requires a tokio runtime and a minimum Rust version of 1.88. ```bash cargo add processkit ``` -------------------------------- ### Manage Processes with ProcessGroup (Synchronous) Source: https://github.com/zelanton/processkit/blob/main/docs/python-wrapper.md Use `ProcessGroup` as a synchronous context manager to start and manage multiple commands. All processes started within the `with` block share the same containment object and are torn down upon exiting the block. ```python from processkit import Command, ProcessGroup with ProcessGroup() as group: group.start(Command("dev-server")) group.start(Command("worker")) print(group.mechanism) # job_object | cgroup_v2 | process_group print(group.members()) ``` -------------------------------- ### Profile Child Process Over Time Source: https://github.com/zelanton/processkit/blob/main/docs/streaming.md Shows how to start a child process, profile its resource usage at a specified interval until it exits, and then print a summary of the collected telemetry. This is ideal for analyzing the performance characteristics of a process throughout its execution. ```rust let profile = Command::new("crunch") .start().await? .profile(Duration::from_millis(100)) .await?; println!( "outcome={{:?}} wall={{:?}} cpu={{:?}} peak_rss={{:?}} avg_cpu_cores={{:?}} ({} samples)", profile.outcome, // Exited(code) / Signalled(sig) / TimedOut profile.duration, profile.cpu_time, profile.peak_memory_bytes, profile.avg_cpu_cores(), // cpu / wall — e.g. Some(1.7) ≈ 1.7 cores busy profile.samples, ); ``` -------------------------------- ### Capture Child Process Resource Usage Source: https://github.com/zelanton/processkit/blob/main/docs/streaming.md Demonstrates how to start a child process and retrieve its CPU time and peak memory usage. This is useful for monitoring individual process resource consumption. ```rust use processkit::Command; use std::time::Duration; let run = Command::new("crunch").start().await?; run.cpu_time(); // Option — user+kernel so far run.peak_memory_bytes(); // Option ``` -------------------------------- ### Run a command and get its output Source: https://github.com/zelanton/processkit/blob/main/docs/cookbook.md Use `run()` to execute a command and capture its standard output. It expects a zero exit code and trims whitespace from the output. Non-zero exits or spawn failures result in a typed `Error`. ```rust let head = Command::new("git").args(["rev-parse", "HEAD"]).run().await?; ``` -------------------------------- ### Mocking Git Branch Command with ScriptedRunner Source: https://github.com/zelanton/processkit/blob/main/docs/testing.md This example demonstrates how to use ScriptedRunner to mock the 'git branch --show-current' command. It sets up a successful reply for the command and a failure reply for any command executed in a directory that is not a git repository. A fallback reply is also provided for any other unmatched commands. ```rust use processkit::Command; use processkit::testing::{Reply, ScriptedRunner}; #[tokio::test] async fn detects_the_branch() { let runner = ScriptedRunner::new() // Match by program + argument PREFIX (element-wise; first element is // the program name, in registration order): .on(["git", "branch", "--show-current"], Reply::ok("main\n")) // …or by any predicate over the full Command: .when( |cmd| cmd.working_dir().is_some(), Reply::fail(128, "fatal: not a git repository"), ) // …with an optional catch-all: .fallback(Reply::ok("")); assert_eq!(current_branch(&runner).await.unwrap(), "main"); } ``` -------------------------------- ### Sample Process Statistics Over Time Source: https://github.com/zelanton/processkit/blob/main/docs/README.md Demonstrates how to collect a series of statistics (CPU, memory, process count) for a ProcessGroup over a specified interval. Also shows how to profile a single command run for a summary. ```rust use processkit::{Command, ProcessGroup, StreamExt}; use std::time::Duration; #[tokio::main] async fn main() -> processkit::Result<()> { // A CPU/RSS/process-count series for a whole group: let group = ProcessGroup::new()?; let _worker = group.start(&Command::new("worker")).await?; let mut samples = group.sample_stats(Duration::from_millis(250)); if let Some(s) = samples.next().await { println!("procs={:?} rss={:?}", s.active_process_count, s.peak_memory_bytes); } drop(samples); // …or a one-shot summary of a single run: let profile = Command::new("crunch") .start().await? .profile(Duration::from_millis(100)).await?; println!( "outcome={{:?}} took={{:?}} peak_rss={{:?}} avg_cpu_cores={{:?}}", profile.outcome, profile.duration, profile.peak_memory_bytes, profile.avg_cpu_cores(), ); Ok(()) } ``` -------------------------------- ### Build and Run a Command with Arguments and Working Directory Source: https://github.com/zelanton/processkit/blob/main/docs/commands.md Use the `Command` builder to specify the program, arguments, and working directory before running the process. The `run()` method executes the command and returns its output. ```rust use processkit::Command; let out = Command::new("git") .arg("log") // one at a time… .args(["--oneline", "-n", "10"]) // …or in bulk .current_dir("/path/to/repo") // run there .run() .await?; ``` -------------------------------- ### Access OutputLine Text Using Accessor Source: https://github.com/zelanton/processkit/blob/main/docs/upgrading.md Use the `ev.text()` accessor to get the text content from an `OutputEvent` regardless of its stream, simplifying handling. ```rust if let Some(text) = ev.text() { println!("{text}"); } ``` -------------------------------- ### Run a Command and Capture Output Source: https://github.com/zelanton/processkit/blob/main/docs/README.md This snippet demonstrates how to execute a command and capture its output. Ensure you have the `processkit` and `tokio` crates added to your project. ```rust use processkit::Command; #[tokio::main] async fn main() -> processkit::Result<()> { let version = Command::new("cargo").arg("--version").run().await?; println!("{version}"); Ok(()) } ``` -------------------------------- ### Clearing All Environment Variables Source: https://github.com/zelanton/processkit/blob/main/docs/commands.md Starts a child process with a completely empty environment, ensuring no inherited variables affect its execution. This is useful for hermetic builds or testing. ```rust // Scorched earth: the child starts with an empty environment. Command::new("hermetic-tool").env_clear().run().await?; ``` -------------------------------- ### ProcessRunner Trait Definition Source: https://github.com/zelanton/processkit/blob/main/docs/testing.md Defines the core ProcessRunner trait with methods for running commands and capturing their output. `output_string` is required, while `output_bytes` and `start` have default implementations. ```rust #[async_trait] pub trait ProcessRunner: Send + Sync { async fn output_string(&self, command: &Command) -> Result>; // Defaulted (route through `start`); override for byte/streaming support: async fn output_bytes(&self, command: &Command) -> Result>>; async fn start(&self, command: &Command) -> Result; } ``` -------------------------------- ### Create a Process Group Source: https://github.com/zelanton/processkit/blob/main/docs/process-groups.md Demonstrates creating a new ProcessGroup with default options and with custom shutdown timeout and escalation behavior. Shows how to inspect the underlying kernel mechanism used for containment. ```rust use processkit::{ProcessGroup, ProcessGroupOptions}; use std::time::Duration; // Defaults: 2s graceful-shutdown grace, escalate to SIGKILL. let group = ProcessGroup::new()?; // Tuned: let group = ProcessGroup::with_options( ProcessGroupOptions::default() .shutdown_timeout(Duration::from_secs(10)) .escalate_to_kill(true), )?; // Which kernel mechanism is actually containing the tree? println!("{:?}", group.mechanism()); // JobObject | CgroupV2 | ProcessGroup ``` -------------------------------- ### Asynchronous Streaming Output Source: https://github.com/zelanton/processkit/blob/main/docs/python-wrapper.md Use `astart` to begin a process asynchronously and `stdout_lines` to stream its output line by line. This is useful for real-time monitoring of build processes or logs. ```python from processkit import Command async with await Command("my-build", ["--watch"]).astart() as proc: async for line in proc.stdout_lines(): print(line) ``` -------------------------------- ### Build and Run a Git Log Pipeline Source: https://github.com/zelanton/processkit/blob/main/docs/pipelines.md Demonstrates creating a pipeline to extract author names from git logs, sort them, and count unique occurrences. Use `run()` to execute the pipeline and capture the trimmed stdout. Requires `tokio::main` for async execution. ```rust use processkit::Command; #[tokio::main] async fn main() -> processkit::Result<()> { // git log --format=%an | sort | uniq -c let authors = Command::new("git").args(["log", "--format=%an"]) .pipe(Command::new("sort")) .pipe(Command::new("uniq").arg("-c")) .run() // require every stage to succeed .await?; println!("{authors}"); Ok(()) } ``` -------------------------------- ### List Process Group Members Source: https://github.com/zelanton/processkit/blob/main/docs/process-groups.md Starts two worker commands within a process group and then retrieves a list of their Process IDs (PIDs). The interpretation of 'members' varies by OS. ```rust use processkit::{Command, ProcessGroup}; let group = ProcessGroup::new()?; let _a = group.start(&Command::new("worker-a")).await?; let _b = group.start(&Command::new("worker-b")).await?; let pids: Vec = group.members()?; println!("live members: {pids:?}"); ``` -------------------------------- ### Execute Simple Commands with Free Functions Source: https://github.com/zelanton/processkit/blob/main/docs/commands.md For quick one-liners, use the free functions `run` and `output_string`. `run` returns trimmed stdout on success, while `output_string` returns the full `ProcessResult`. ```rust let version = processkit::run("cargo", ["--version"]).await?; // trimmed stdout, success required ``` ```rust let result = processkit::output_string("git", ["status", "-s"]).await?; // full ProcessResult ``` -------------------------------- ### Simulate Server Ready State Source: https://github.com/zelanton/processkit/blob/main/docs/testing.md This snippet demonstrates how to use `ScriptedRunner` to simulate a server process that boots up and becomes ready. It verifies that the process streams expected lines and exits successfully. ```rust use processkit::{Command, Outcome, ProcessRunner, StreamExt, Finished}; use processkit::testing::{Reply, ScriptedRunner}; use std::time::Duration; #[tokio::test] async fn server_becomes_ready() { let runner = ScriptedRunner::new() .on(["server", "serve"], Reply::lines(["booting", "listening on 8080"])); let mut run = runner.start(&Command::new("server").arg("serve")).await.unwrap(); run.wait_for_line(|l| l.contains("listening"), Duration::from_secs(5)) .await .unwrap(); // satisfied by the canned banner — no subprocess let Finished { outcome, .. } = run.finish().await.unwrap(); assert_eq!(outcome, Outcome::Exited(0)); } ``` -------------------------------- ### Generic Function Using ProcessRunner Source: https://github.com/zelanton/processkit/blob/main/docs/testing.md An example of production code that accepts any type implementing ProcessRunner, allowing for easy dependency injection of test doubles during testing. This function uses the `run` helper from `ProcessRunnerExt`. ```rust use processkit::{Command, ProcessRunner, ProcessRunnerExt, Result}; // Production code: generic over the runner. async fn current_branch(runner: &impl ProcessRunner) -> Result { runner .run(&Command::new("git").args(["branch", "--show-current"])) .await } ``` -------------------------------- ### Control Environment and Privileges Source: https://github.com/zelanton/processkit/blob/main/docs/README.md Configure environment variables, user/group IDs, and session IDs for spawned processes. Use platform-specific flags like `create_no_window` for Windows or `setsid` for POSIX systems. `kill_on_parent_death` ensures the child process terminates if the parent dies abruptly. ```rust use processkit::Command; #[tokio::main] async fn main() -> processkit::Result<()> { Command::new("worker") .inherit_env(["PATH", "HOME", "LANG"]) // allow-list on a cleared env .uid(1000).gid(1000) // Unix: drop privileges .setsid() // Unix: new session .run() .await?; Command::new("helper") .create_no_window() // Windows: no console window .run() .await?; Command::new("daemonish") .kill_on_parent_death() // die with a SIGKILLed parent .start() .await?; Ok(()) } ``` -------------------------------- ### Record and Replay Command Executions Source: https://github.com/zelanton/processkit/blob/main/docs/testing.md Demonstrates how to use RecordReplayRunner to record a command execution to a cassette and then replay it. The recording phase runs against the actual tool, while the replay phase uses the saved cassette for deterministic results. Ensure the cassette is saved after recording. ```rust use processkit::{Command, JobRunner, ProcessRunnerExt}; use processkit::testing::RecordReplayRunner; // Record once against the real tool (an opt-in `--record` test run, say): let runner = RecordReplayRunner::record("fixtures/git.json", JobRunner::new()); let version = runner.run(&Command::new("git").arg("--version")).await?; runner.save()?; // the error-surfacing flush // (best-effort on drop too) // Replay everywhere else: let runner = RecordReplayRunner::replay("fixtures/git.json")?; assert_eq!(runner.run(&Command::new("git").arg("--version")).await?, version); ``` -------------------------------- ### Stream stdout lines from a child process Source: https://github.com/zelanton/processkit/blob/main/docs/streaming.md Use `stdout_lines()` to get a stream of decoded lines from a child process's stdout. This allows processing output as it's generated. Ensure `stdout_lines()` is called only once per command. ```rust use processkit::{Command, Outcome, StreamExt, Finished}; #[tokio::main] async fn main() -> processkit::Result<()> { let mut run = Command::new("cargo") .args(["build", "--release"]) .start() .await?; let mut lines = run.stdout_lines()?; while let Some(line) = lines.next().await { println!("build: {line}"); } // The stream ended (stdout closed). Collect the outcome and stderr — // stderr was drained in the background the whole time, so a noisy child // could never block on a full pipe. let Finished { outcome, stderr, .. } = run.finish().await?; if outcome != Outcome::Exited(0) { eprintln!("build failed ({outcome:?}):\n{stderr}"); } Ok(()) } ``` -------------------------------- ### Probe Command Exit Code as Boolean Source: https://github.com/zelanton/processkit/blob/main/docs/commands.md Use the `probe()` verb to get a boolean result based on the command's exit code. This is useful for predicate commands where an exit code of 0 signifies true and 1 signifies false. ```rust use processkit::Command; // probe(): the exit code as a boolean. let clean = Command::new("git").args(["diff", "--quiet"]).probe().await?; ``` -------------------------------- ### Join and Fan-out Commands with ProcessKit Source: https://github.com/zelanton/processkit/blob/main/docs/README.md Use `wait_all` to join a fixed set of handles, returning all outcomes in order. `output_all` runs a batch of commands with a concurrency cap, preventing resource exhaustion. Pass `&group` instead of `&JobRunner` to keep children in a shared kill-on-drop group. ```rust use processkit::{Command, JobRunner, wait_all, output_all}; #[tokio::main] async fn main() -> processkit::Result<()> { let group = processkit::ProcessGroup::new()?; let mut a = group.start(&Command::new("worker-a")).await?; let mut b = group.start(&Command::new("worker-b")).await?; let outcomes = wait_all(&mut [&mut a, &mut b]).await?; // both, in input order // 200 conversions, but never more than 8 processes alive at once. let cmds = (0..200).map(|i| Command::new("convert").arg(format!("{i}.png"))); let results = output_all(cmds, 8, &JobRunner).await; let failed = results.iter().filter(|r| !matches!(r, Ok(o) if o.is_success())).count(); println!(‘{:?}; {failed} conversions failed‘, outcomes); Ok(()) } ``` -------------------------------- ### Run Command with Cancellation Source: https://github.com/zelanton/processkit/blob/main/docs/timeouts-and-cancellation.md Demonstrates how to spawn a command that can be cancelled using a CancellationToken. The token is wired into the command via `cancel_on`, and cancellation is triggered by calling `shutdown.cancel()`. ```rust use processkit::{CancellationToken, Command}; #[tokio::main] async fn main() -> processkit::Result<()> { let shutdown = CancellationToken::new(); // Wire the same parent token into many jobs via child tokens: let job = tokio::spawn({ let token = shutdown.child_token(); async move { Command::new("long-export").cancel_on(token).run().await } }); // Ctrl-C handler, sibling failure, UI button, … shutdown.cancel(); assert!(matches!( job.await.unwrap(), Err(processkit::Error::Cancelled { .. }) )); Ok(()) } ``` -------------------------------- ### Backoff Delay Formula Source: https://github.com/zelanton/processkit/blob/main/docs/supervision.md Illustrates the formula for calculating the delay before the n-th restart, incorporating base delay, exponential factor, maximum backoff, and jitter. ```text delay(n) = min(base × factor^n, max_backoff) × jitter ``` -------------------------------- ### Configure and Run Supervisor with Storm Guard Source: https://github.com/zelanton/processkit/blob/main/docs/supervision.md This snippet demonstrates how to configure and run a Supervisor with the storm guard enabled. It sets the storm pause duration, failure decay time, and failure threshold. The storm guard is off by default and must be explicitly enabled. ```rust use processkit::{Command, Supervisor}; use std::time::Duration; let outcome = Supervisor::new(Command::new("worker")) .storm_pause(Duration::from_secs(15)) // master switch — off by default .failure_decay(Duration::from_secs(30)) // score half-life (default 30s) .failure_threshold(5.0) // trip point (default 5.0) .run() .await?; println!("storm pauses taken: {}", outcome.storm_pauses); ``` -------------------------------- ### Record and Replay CLI Runs with RecordReplayRunner Source: https://github.com/zelanton/processkit/blob/main/docs/README.md Enable the 'record' feature to use RecordReplayRunner for deterministic testing. It records real runs into a JSON cassette and replays them, avoiding subprocesses in CI. ```rust use processkit::{Command, JobRunner, ProcessRunnerExt}; use processkit::testing::RecordReplayRunner; #[tokio::main] async fn main() -> processkit::Result<()> { // Record once against the real tool (e.g. an opt-in `--record` test run): let runner = RecordReplayRunner::record("fixtures/git.json", JobRunner::new()); let version = runner.run(&Command::new("git").arg("--version")).await?; runner.save()?; // or best-effort on drop // Replay everywhere else — no subprocess, identical results: let runner = RecordReplayRunner::replay("fixtures/git.json")?; assert_eq!(runner.run(&Command::new("git").arg("--version")).await?, version); Ok(()) } ``` -------------------------------- ### Setting Per-Stage and Pipeline Timeouts Source: https://github.com/zelanton/processkit/blob/main/docs/pipelines.md Demonstrates setting a timeout for an individual command stage and a separate timeout for the entire pipeline. The per-stage timeout kills only that command, while the pipeline timeout tears down the whole chain. ```rust use processkit::Command; use std::time::Duration; let out = Command::new("producer") .timeout(Duration::from_secs(10)) // per-STAGE: kills just `producer` .pipe(Command::new("consumer")) .timeout(Duration::from_secs(30)) // whole-CHAIN: Pipeline::timeout .output_string() .await?; ``` -------------------------------- ### Cancel a command tree on shutdown Source: https://github.com/zelanton/processkit/blob/main/docs/cookbook.md Demonstrates how to create a cancellation token and associate it with a spawned job. When the token is cancelled, the entire command tree is terminated. ```rust use processkit::CancellationToken; let token = CancellationToken::new(); let job = tokio::spawn({ let token = token.child_token(); async move { Command::new("long-job").cancel_on(token).run().await } }); // On Ctrl-C / shutdown signal / sibling failure: token.cancel(); // kills the tree; the run resolves to Error::Cancelled let outcome = job.await; // Err(Error::Cancelled { .. }) inside ``` -------------------------------- ### Inspect Process Tree Members and Wait for First Exit Source: https://github.com/zelanton/processkit/blob/main/docs/README.md Snapshot live member PIDs and race multiple running processes, reporting the first to exit. `members()` lists the whole tree on Windows/Linux; POSIX backends list group leaders. `wait_any` has no per-process timeout and does no output pumping. ```rust use processkit::{Command, ProcessGroup, wait_any}; #[tokio::main] async fn main() -> processkit::Result<()> { let group = ProcessGroup::new()?; let mut a = group.start(&Command::new("server-a")).await?; let mut b = group.start(&Command::new("server-b")).await?; println!("live pids: {:?}", group.members()?); let (idx, outcome) = wait_any(&mut [&mut a, &mut b]).await?; println!("contender #{idx} exited first with {outcome:?}"); Ok(()) } ``` -------------------------------- ### Verify ProcessKit Upgrade Source: https://github.com/zelanton/processkit/blob/main/docs/upgrading.md Run these commands to update the ProcessKit dependency and verify that your project builds and tests pass after the upgrade. ```sh cargo update -p processkit cargo build # both breaking changes are compiler-caught cargo test ``` -------------------------------- ### Setting and Unsetting Environment Variables Source: https://github.com/zelanton/processkit/blob/main/docs/commands.md Demonstrates how to set a single environment variable and unset an inherited one for a child process. This is useful for configuring specific aspects of the child's execution environment. ```rust use processkit::Command; Command::new("worker") .env("RUST_LOG", "debug") // set one variable .env_remove("GIT_DIR") // unset one inherited variable .run().await?; ``` -------------------------------- ### Enable Limits Feature in Cargo.toml Source: https://github.com/zelanton/processkit/blob/main/docs/README.md To use resource capping, you need to enable the `limits` feature in your `Cargo.toml` file. This adds the necessary functionality to your project's dependencies. ```toml processkit = { version = "…", features = ["limits"] } ``` -------------------------------- ### Define a Typed Git Client with `cli_client!` Macro Source: https://github.com/zelanton/processkit/blob/main/docs/testing.md This snippet demonstrates how to use the `cli_client!` macro to generate a typed `Git` client. It includes methods for retrieving the HEAD commit, checking if the work tree is clean, and listing branches with custom parsing logic. ```rust use processkit::{cli_client, Error, ProcessRunner, Result}; use std::path::Path; use std::time::Duration; cli_client!( /// A typed `git` client. pub struct Git => "git" ); impl Git { /// HEAD's commit id. pub async fn head(&self, repo: &Path) -> Result { self.core.run(self.core.command_in(repo, ["rev-parse", "HEAD"])).await } /// Is the work tree clean? (exit code IS the answer) pub async fn is_clean(&self, repo: &Path) -> Result { self.core.probe(self.core.command_in(repo, ["diff", "--quiet"])).await } /// Branch list, parsed — the parser is fallible and returns the crate's /// `Result`, typically an `Error::Parse` naming the program. pub async fn branches(&self, repo: &Path) -> Result> { self.core .try_parse( self.core.command_in(repo, ["branch", "--format=%(refname:short)"]), |out| { let list: Vec = out.lines().map(str::to_owned).collect(); if list.is_empty() { Err(Error::Parse { program: "git".into(), message: "no branches".into(), }) } else { Ok(list) } }, ) .await } } // Production: the real runner, with per-client defaults. let git = Git::new().default_timeout(Duration::from_secs(30)); let head = git.head(Path::new(".")).await?; ``` -------------------------------- ### Set default cancellation for a client Source: https://github.com/zelanton/processkit/blob/main/docs/cookbook.md Shows how to configure a `CliClient` to automatically cancel all its in-flight commands when a specific cancellation token is triggered. ```rust use processkit::{CancellationToken, CliClient}; let token = CancellationToken::new(); let gh = CliClient::new("gh").default_cancel_on(token.child_token()); // token.cancel() → every in-flight command of THIS client dies. ``` -------------------------------- ### Cancel a Long-Running Command Source: https://github.com/zelanton/processkit/blob/main/docs/README.md Demonstrates how to create a CancellationToken, spawn a command that listens to it, and then cancel the token to terminate the command. The result of the job will be an Error::Cancelled. ```rust use processkit::{CancellationToken, Command}; #[tokio::main] async fn main() -> processkit::Result<()> { let token = CancellationToken::new(); let job = tokio::spawn({ let token = token.child_token(); async move { Command::new("long-job").cancel_on(token).run().await } }); // elsewhere — a shutdown signal, a sibling failure, a UI button: token.cancel(); assert!(matches!(job.await.unwrap(), Err(processkit::Error::Cancelled { .. }))) Ok(()) } ``` -------------------------------- ### Set Resource Limits for a Process Group Source: https://github.com/zelanton/processkit/blob/main/docs/README.md Use `ProcessGroupOptions` to define memory, process count, and CPU quotas when creating a `ProcessGroup`. This prevents runaway child processes from exhausting host resources. The `with_options` function returns an error if limits cannot be enforced. ```rust use processkit::{Command, ProcessGroup, ProcessGroupOptions}; #[tokio::main] async fn main() -> processkit::Result<()> { let group = ProcessGroup::with_options( ProcessGroupOptions::default() .memory_max(512 * 1024 * 1024) // 512 MiB across the tree .max_processes(64) .cpu_quota(0.5), // half of one core )?; let _job = group.start(&Command::new("untrusted-tool")).await?; // ... work ... Ok(()) } ``` -------------------------------- ### Set Default Cancellation Token on Client Source: https://github.com/zelanton/processkit/blob/main/docs/timeouts-and-cancellation.md Shows how to configure a `CliClient` with a default cancellation token. Any command built by this client will automatically use this token for cancellation, simplifying cancellation management for multiple commands. ```rust use processkit::{CancellationToken, CliClient}; let token = CancellationToken::new(); let gh = CliClient::new("gh").default_cancel_on(token.child_token()); // ... controller cancels `token` → every in-flight command of THIS client // dies (whole tree), surfacing Error::Cancelled to the awaiting call. ``` -------------------------------- ### Wrap CLI Tool with Typed API using cli_client! Source: https://github.com/zelanton/processkit/blob/main/docs/cookbook.md Generate a typed API for a CLI tool using the `cli_client!` macro. This allows for cleaner, more idiomatic Rust code when interacting with external commands. ```rust use processkit::{cli_client, ProcessRunner, Result}; cli_client!(pub struct Git => "git"); impl Git { pub async fn current_branch(&self) -> Result { // A verb takes the args directly (D7); pass a built `command(..)` only // when you need to customize it (per-call timeout, stdin, …). self.core.run(["branch", "--show-current"]).await } pub async fn is_clean(&self) -> Result { self.core.probe(["diff", "--quiet"]).await } } ``` -------------------------------- ### Pipeline with Operator Syntax Source: https://github.com/zelanton/processkit/blob/main/docs/pipelines.md Shows an alternative syntax for creating pipelines using the `|` operator, which is sugar for `pipe()`. Parenthesize the chain before a terminal verb like `run()` because method calls bind tighter than the `|` operator. ```rust use processkit::Command; let authors = (Command::new("git").args(["log", "--format=%an"]) | Command::new("sort") | Command::new("uniq").arg("-c")) .run() .await?; ``` -------------------------------- ### Sandbox Untrusted Tool with Resource Limits Source: https://github.com/zelanton/processkit/blob/main/docs/cookbook.md Run an untrusted command within a sandboxed environment using ProcessGroupOptions to enforce resource limits such as memory, process count, and CPU quota. This requires the 'limits' feature and uses OS-specific mechanisms like Job objects on Windows or cgroups on Linux. ```rust use processkit::{ProcessGroup, ProcessGroupOptions}; // Cap the whole tree (requires the `limits` feature; Windows Job / Linux cgroup): let group = ProcessGroup::with_options( ProcessGroupOptions::default() .memory_max(512 * 1024 * 1024) .max_processes(64) .cpu_quota(0.5), )?; let result = group .start( &Command::new("untrusted-tool") .inherit_env(["PATH"]) // allow-list: everything else is cleared .timeout(std::time::Duration::from_secs(60)), ) .await?; .output_string() .await?; ``` -------------------------------- ### Supervise a Long-Lived Child Process Source: https://github.com/zelanton/processkit/blob/main/docs/README.md Illustrates how to use a Supervisor to keep a child process alive, restarting it according to a defined policy when it exits. Includes configuration for restart policy, backoff, and a storm pause. ```rust use processkit::{Command, RestartPolicy, Supervisor}; use std::time::Duration; #[tokio::main] async fn main() -> processkit::Result<()> { let outcome = Supervisor::new(Command::new("my-server").args(["--port", "8080"])) .restart(RestartPolicy::OnCrash) // Always | OnCrash | Never .max_restarts(5) .backoff(Duration::from_millis(200), 2.0) // base, multiplier (cap: .max_backoff) .storm_pause(Duration::from_secs(15)) // crash-loop guard (off by default) .stop_when(|res| res.code() == Some(0)) // a clean exit ends supervision .run() .await?; println!("ended after {} restarts: {:?}", outcome.restarts, outcome.stopped); Ok(()) } ```