Expand description
processkit — async child-process management for Rust + tokio: whole-tree
kill-on-drop (no orphaned subprocesses), run-and-capture, streaming,
shell-free pipelines, timeouts & cancellation, and supervision.
Two layers:
ProcessGroup— a kill-on-drop container for a process tree. Every child spawned into the group, and everything those children spawn, dies with the group, so an exiting or panicking owner doesn’t leak subprocesses. Containment is a Windows Job Object, a Linux cgroup v2 (with a POSIX process-group fallback), a FreeBSDprocctl(2)process reaper, or a POSIX process group on macOS/the other BSDs — observable viaMechanism. A spawn-freehost_containmentreports whichMechanism(and the reach of soft stop / abrupt-owner-death cleanup) a group would get on this host, before any group exists. Two caveats theProcessGroup/Mechanismdocs spell out: the guarantee rides onDroprunning (apanic = "abort"process, or aSIGKILL/power-loss of the owner, skips it — Windows still reaps through Job Object handle close, while Linux’s opt-in parent-death signal reaches only the direct child and macOS/BSD have no equivalent), and on the process-group mechanism a child that callssetsidescapes containment. The one deliberate way out isCommand::spawn_detached— an explicit, loudly-named opt-in that hands back aDetachedChildfor which the crate exposes no public lifetime, kill, wait, timeout, capture, or control operations (Unix still reaps its exit status through a private background owner), and never contains (it inverts this guarantee on purpose; see its docs). The whole tree can be signalled (ProcessGroup::signal, seeSignal), paused/resumed (ProcessGroup::suspend/ProcessGroup::resume), and inspected (ProcessGroup::members);wait_anyraces several running processes and reports the first to exit.- runner — async run-and-capture built on the group. Describe a run with
Command, then drive it to completion (Command::output_string,Command::run, …) orstartit for streaming and interactive I/O. TheProcessRunnertrait runs commands to completion and is the mock seam (seeScriptedRunner). ASupervisorkeeps a command alive — restarting it per policy with backoff — whereCommand::retrymerely replays one run to success. Readiness probes (RunningProcess::wait_for_line/RunningProcess::wait_for_stderr_line/wait_for_port/wait_for) wait until a started child is actually ready instead of sleeping. APipeline(Command::pipe) chains commands stdout→stdin without a shell — each stage spawns into its own kill-on-dropProcessGroupsub-group, with chain-wide teardown fanning the kill across every sub-group, pipefail outcome.Command::cancel_onties a run to aCancellationToken: cancelling it kills the tree and every consuming path resolves toErrorReason::Cancelled. Spawn-time sandboxing knobs:Command::inherit_env(env allow-list),Command::uid/Command::gid(Unix privilege drop),Command::setsid,Command::create_no_window,Command::priority(CPU-scheduling priority, both platforms),Command::cpu_affinity(Linux/Windows CPU placement),Command::io_priority(Linux I/O scheduling),Command::umask(Unix file-creation mask).
Async throughout (tokio). Errors are the structured Error; a non-zero
exit is reported in ProcessResult, not raised, until you call
ProcessResult::ensure_success.
Stability. Since 1.0, processkit follows Semantic Versioning: the
public API is stable, and any breaking change lands only in a new major
version, so 2.x upgrades are backward-compatible (the last breaking release
was 2.1.0). (The lone exception is the mock feature’s mockall-generated
expect_* surface — see below.)
Stable machine identifiers. The reporting and configuration enums —
Mechanism, Outcome, ParentDeathCleanup, StopReason,
StdioMode, LineTerminator, OverflowMode, Priority,
RestartPolicy, plus the feature-gated LimitKind / LimitReason /
LimitVerdict
(limits) and Signal / SoftStopScope / SoftSignal
(process-control), given as bare
names here since this crate-root doc also builds with those features off —
each
expose a name() that returns a short, lowercase snake_case identifier
for machine-readable output (a CLI’s JSONL schema, a cross-language binding,
a structured log field), so a consumer publishing a contract over these
types has one canonical spelling per variant instead of a hand-maintained
table. These identifiers are a diagnostic surface — a stable vocabulary
rather than a frozen wire schema (see the report-serde feature below,
which puts exactly these identifiers on the wire) — and they carry the same
stability promise as the rest of the public API: a
new variant gets a new identifier, and an existing identifier is never
renamed without a major release. Every enum whose value can arrive from
outside (config, CLI, another language) also has a from_name(&str) inverse
that returns None — an honest miss, never a silent default — on an
unrecognized name. See the Errors guide’s “Stable machine identifiers”
section for the whole set.
Beyond this page, the repository ships a narrative guide set — a task-oriented cookbook (“I want to …” → snippet), a deep guide per capability, and every per-platform caveat collected in one place.
Run vocabulary — one verb, one meaning, at every layer (Command,
ProcessRunner/ProcessRunnerExt, CliClient):
run— require an accepted exit (0by default, widened byCommand::ok_codes) and return stdout as aString, trailing whitespace trimmed (trim_end: the final newline is noise, but leading whitespace can be significant).run_unit— the same, discarding the output.output_string/output_bytes— return the fullProcessResult(stdout as text / raw bytes); a non-zero exit is not an error here. (output_string, not a bareoutput, sincestd::process::Command::outputyields bytes — the explicit name avoids that footgun and is spelled the same on every layer.)exit_code— the exit code, with a missing code surfaced as an error. (On aProcessResult,codeis the plainOption<i32>accessor —Nonefor a timeout/signal kill, never a-1sentinel.)probe— run a predicate and read its exit code as abool:0→true,1→false, anything else is an error (git diff --quiet, …).parse/try_parse— run to a clean success and feed the captured stdout to a closure:parsefor an infallible closure,try_parsefor one returningResult(the JSON-deserialization shape).Send-contract exception:Command::parse/CliClient::parserequireF: Send(andT: Send), so the returned future isSendand movable intotokio::spawn;Pipeline::parsedeliberately does not requireSend— its closure runs inline on the awaiting task rather than across atokio::spawnboundary, so it accepts strictly more closures, but the resulting future isSendonly whenF/Thappen to be. If you need to move aPipeline::parse/Pipeline::try_parsecall intotokio::spawn, make sure your closure and its output areSendyourself; the compiler won’t require it for you the way it does forCommand/CliClient.output_json(featurejson) — the success-checking typed JSON form oftry_parse;RunningProcess::stdout_json_linesprovides strict line-wise NDJSON without buffering the complete stdout.
§Features
Every flag is additive and gates visibility only — the kill-on-drop tree guarantee is unconditional in every configuration.
-
stats— resource measurement:ProcessGroupStats,ProcessGroup::stats(plus thesample_statstime-series sampler and its owning'statictwinOwnedStatsSampler), the per-processRunningProcess::cpu_time/peak_memory_bytesdiagnostics, and theRunningProcess::profilerun summary. Opt-in for its specialized purpose (on Windows it calls the systemProcessStatus/PSAPI API — a link to an OS library, not an added crate dependency); enable withfeatures = ["stats"], orlimits, which implies it. (The features that do pull an extra crate aremock→mockall,tracing→tracing, andrecord/json→serde/serde_json.) -
process-control(default) — tree control beyond contain+kill:SignalandProcessGroup::{signal, suspend, resume, members, members_info, adopt, adopt_external}, the enrichedMemberInfomember snapshot, and the free-standingprocess_info/process_is_alivequeries for a pid held outside any group (reuse-safe liveness by the(pid, start time)pair). -
limits— whole-tree resource caps:ResourceLimits, themax_memory/max_processes/cpu_quotabuilders onProcessGroupOptions,ErrorReason::ResourceLimit(why a requested cap could not be applied), and the post-runProcessGroup::limit_evidencereport —LimitEvidence/LimitVerdict— saying whether a cap the group carried then actually fired. Impliesstats. -
mock— themockall-generatedtesting::MockRunnerfor consumers’ tests. Itsexpect_*surface is generated bymockalland is exempt from this crate’s semver guarantees — it tracks themockallversion (an implementation detail) rather than a frozen API. The first-class doubles (ScriptedRunner/RecordingRunner) are the stable, recommended seam; reach formockonly if you specifically want expectation-style mocking. -
tracing—tracingevents on theprocesskittarget: spawn and exit (program/pid/mechanism), timeout and cancellation firing, group terminate/shutdown, retry attempts, supervisor restarts and storm pauses, and teardown anomalies (stdin-writer failures, pump overruns). Never logs argv or environment values. -
metrics—metricscounters and histograms over data the crate already computes: run/spawn counters, run-duration histograms, an exit-code/timeout/cancel/signal tally, and retry / supervisor restart / storm-pause events. A thin façade — the crate emits into whatever global recorder (a Prometheus/OTel exporter) the consumer installs. Labels carry only program name / mechanism / outcome / exit code — never argv or environment values, and no unbounded-cardinality key like a pid. See thedocs/observability.mdguide. -
record— record/replay cassettes over theProcessRunnerseam:RecordReplayRunnerrecords realInvocation → ProcessResultpairs to a JSON fixture once, then replays them hermetically — no subprocess in CI. Pulls inserde+serde_json. -
json— typed JSON capture throughCommand::output_json,ProcessRunnerExt::output_json, andCliClient::output_json, plus line-wise NDJSON throughRunningProcess::stdout_json_lines. Parse failures carry bounded raw fragments and exact decoded-output locations. Pulls inserde+serde_json. -
report-serde—serde::Serializefor the crate’s report types, so a finished run, a graceful teardown, a stats tick or a supervision event can be emitted as one JSONL line (or any other self-describing serde format) without hand-copying fields and hand-callingname()per enum:ProcessResult,RunProfile,ProcessGroupStats,ShutdownReport,MemberInfo,LimitEvidence,SupervisionEvent/SupervisionOutcome/SupervisionStatus, and the enums those carry. Reuses the optionalserdedependencyrecord/jsonalready pull, and pulls no codec of its own — pickserde_json,serde_yaml,ciborium, … yourself. The schema targets self-describing formats; rule 4 says why a non-self-describing binary codec is out of scope. Four rules define the shape:- Every enum travels as its stable
name()identifier, never a serde-derived variant tag:{"kind": "exited", "code": 0, "signal_number": null}, not{"Exited": 0}. An enum carrying a payload is an object tagged under"kind"; one without is the bare identifier string ("restarts_exhausted"). The wire vocabulary is therefore exactly the dictionary described under Stable machine identifiers above — the same onespec/identifiers.jsonpublishes — never a second spelling.Signalis the one named exception, with a third form:Signal::Other(i32)is a raw OS number that deliberately has no curated identifier (Signal::name()answersNonethere rather than minting a spelling no dictionary defines and nofrom_nameparses back), so a signal travels as its identifier string when curated ("term") and as that bare number when not (37). The union stops there, because a key names one domain:signalis always aSignal(aShutdownReport’s soft tier), while the raw OS number anOutcomecarries is a different fact under its own key,signal_number— always a number ornull. Two keys, so a consumer folding both into one JSONL table never reconciles a string with an integer under one column. Serializeonly — deliberately noDeserialize. These types are reported by the crate and never supplied back to it: the same asymmetry that leavesOutcome/ErrorKind/SupervisionEventwithout afrom_nameinverse. Enums a caller genuinely does supply (Signal,RestartPolicy,Priority, …) keep theirfrom_name, so the missing direction leaves no gap.- Reports about processes — never what a process produced. Nothing
here serializes captured stdout/stderr content, argv, or environment
values, the same secret hygiene the
tracingandmetricsseams keep (a child’s output routinely carries tokens, and a capture can be multi-megabyte).ProcessResultreports the run — program name, outcome, timings, truncation totals — and leaves the streams to the caller, who already holds them. For the same reasonError/ErrorReason(captured streams, the searchedPATH),ProcessEventandFinished(captured output) deliberately have no impl: reportErrorKindand attach whatever bounded, redacted detail your own contract calls for. - The set of fields is not frozen; the spelling of each field is.
Every one of these types is
#[non_exhaustive], keeps its fields private, or both — they are grown, not frozen, and no downstream struct literal pins today’s set — and that stays true on the wire: a future minor may add a key, so a consumer must ignore unknown ones (the same discipline any JSONL reader already needs). That promise is also why the schema targets self-describing formats: “ignore the keys you don’t know”, and rule 1’s identifier-or-numberSignal, both need an encoding that carries names and types, whichbincode/postcarddeliberately do not — they will happily encode these values, but nothing here pins their layout across a minor release. What is held stable, like the rest of the public API, is everything already there — an identifier or a key is never renamed or repurposed without a major release. Time is always a number of seconds (duration_secs,elapsed_secs,delay_secs, …), the unit themetricshistograms record; a measurement a platform cannot report isnull, never a fabricated0.
- Every enum travels as its stable
§Other languages
Not on Rust? processkit-py is a
Python wrapper (PyO3 bindings) over this crate’s core, with an asyncio-facing
API. This crate remains the single source of truth for the containment/runner
logic underneath.
Modules§
- prelude
- Re-exports of small vocabulary types from the crate’s
0.xdependencies, kept out of the crate root souse processkit::*doesn’t pull them in (and so a future0.xmajor bump of either dependency stays contained to this module rather than the whole crate surface). - testing
- Test doubles for the
ProcessRunnerseam: aScriptedRunnerthat serves canned replies, aRecordingRunnerthat asserts on invocations, theInvocationit captures, aDryRunRunnerthat renders and echoes commands without spawning them, and (behind features) record/replay cassettes and amockallmock.
Macros§
- cli_
client - Scaffold a typed CLI-wrapper struct around a
CliClient.
Structs§
- Cancellation
Token - Re-exported so callers can
use processkit::CancellationToken;without a directtokio-utildependency. SeeCommand::cancel_on. A token which can be used to signal a cancellation request to one or more tasks. - CliClient
- Owns a CLI tool’s program name,
ProcessRunner, and default timeout, and builds + runsCommands against them. - Command
- A description of a child process to launch: program, arguments, working directory, environment, stdin source, and an optional timeout.
- Detached
Child - A minimal handle to a child spawned outside this crate’s kill-on-drop
containment via
Command::spawn_detached. - Error
- The crate’s error type: a pointer-sized handle to a structured
ErrorReason. - Finished
- The outcome of a run driven via
stdout_linesorevents: how the run ended plus the captured standard error. Returned byRunningProcess::finish. - Host
Containment - A spawn-free, side-effect-free report of how process containment behaves on
this host — the answer to a consumer’s preflight “what will I get here?”
without paying to create a real
ProcessGroup. - JobRunner
- The default runner: every run gets a fresh, private
ProcessGroupowned by the run, so its tree is torn down when the run finishes (or its handle drops). - Json
Lines json - A typed NDJSON stream returned by
RunningProcess::stdout_json_lines. - Limit
Evidence limits - Post-run evidence about a group’s resource caps: one
LimitVerdictperLimitKindaxis, read from the container the crate itself owns. - Member
Info process-control - An enriched snapshot of one member of a
ProcessGroup— its pid plus best-effort metadata (parent pid, image name, start time). - Output
Buffer Policy - Caps how many captured/streamed output lines are retained in memory.
- Output
Line - One decoded line carried by a
ProcessEvent. - Output
Overflow - The overflow counters carried by an
OutputTooLargefailure, read throughError::output_overflow/ErrorReason::output_overflowwithout destructuring the#[non_exhaustive]variant. - Owned
Stats Sampler stats - A periodic
ProcessGroupStatsseries that does not borrow the group by lifetime — the owning,'statictwin ofStatsSampler, for a group held behind a sharedArc. - Pipeline
- A chain of
Commands connected stdout→stdin — built withCommand::pipe, extended withpipe, driven with the same verb vocabulary as a singleCommand:output_string/output_bytesfor capture,run/run_unit/checkedfor success-checked runs,exit_code/probefor the code, andparse/try_parsefor typed output — each operating on the pipefail outcome. Bound the whole chain withtimeout/cancel_on. - Pipeline
Session - A live streaming session over a running
Pipeline— the multi-stage analogue of aRunningProcess, returned byPipeline::start. It streams the last stage’s stdout as it arrives while every inner stage drains in the background, then folds the same pipefail outcome as the buffering verbs atfinish. - Process
Events - A
Streamof a running child’sProcessEvents (seeRunningProcess::events). - Process
Group - A container that ties the lifetime of a child-process tree to its own.
- Process
Group Options - Tuning for a
ProcessGroup— graceful-shutdown timing and (with thelimitsfeature) resource limits. - Process
Group Stats stats - A snapshot of a process group’s resource usage.
- Process
Result - The captured result of running a process to completion.
- Process
Stdin - An interactive writer to a child’s standard input.
- Resource
Limits limits - Resource limits enforced on a process group as a whole.
- Retry
Policy - How a retryable run is re-attempted: a bounded number of retries with exponential backoff, a per-delay cap, and optional full jitter.
- RunProfile
stats - Resource summary of one finished run — produced by
RunningProcess::profile. - Running
Process - A handle to a process spawned by a runner.
- Shutdown
Report process-control - The observed facts of one graceful group teardown, returned by
ProcessGroup::stop. - Stats
Sampler stats - A periodic
ProcessGroupStatsseries — created byProcessGroup::sample_stats. - Stdin
- What to feed a child process on standard input.
- Stdout
Lines - A
Streamof the child’s standard-output lines (seeRunningProcess::stdout_lines). - Supervision
Events - A bounded stream of
SupervisionEventvalues from one live session. - Supervision
Outcome - What a finished supervision reports — the last run plus the keeper’s telemetry.
- Supervision
Session - A live handle to a running supervision, returned by
Supervisor::start. Unlikerun— which only reports itsSupervisionOutcomeat the very end — a session lets a caller watch supervision while it runs (status), ask it to stop gracefully (stop), and await its eventual outcome (wait). This is the primitive for building daemons / process managers on top of the runner layer. - Supervision
Status - A consistent, point-in-time snapshot of a live
SupervisionSession’s state — read atomically under the same lock the supervision loop publishes each change under, so every field agrees with the others (no torn read). Only non-secret facts appear here (activity, counts, the current child’s pid / start time); argv and environment values never do. - Supervisor
- Keeps a
Commandalive: runs it, classifies every exit against theRestartPolicyand thestop_whenpredicate, and restarts it after an exponential-backoff delay until supervision ends.
Enums§
- Error
Kind - The kind of an
Error— a total, compact classification of the failure into one bucket per operational disposition, reached throughError::kind/ErrorReason::kind. - Error
Reason - The structured failure mode behind an
Error— the enum you reach throughError::reason. - Give
UpAttempt - What the
give_up_whenclassifier inspects: a crashed run that produced aProcessResult, or a spawn/IO failure that prevented the child from ever starting (e.g.ENOENTfor a mistyped program name) and so never produced one. - IoPriority
- An I/O-scheduling priority for a child process (see
Command::io_priority). - Limit
Kind limits - Which
ResourceLimitsfield anErrorReason::ResourceLimitfailure is about —Memoryformax_memory,Processesformax_processes,Cpuforcpu_quota. - Limit
Reason limits - Why a requested resource limit could not be applied — the classification an
ErrorReason::ResourceLimitfailure carries so a caller (e.g. theprocesskit-pybinding) can branch on the kind of failure without parsing the Englishdetailtext. - Limit
Verdict limits - The post-run verdict for one limit axis — did a cap this group carried actually engage while the tree ran?
- Line
Terminator - How the output pump decides where one captured/streamed line ends.
- Mechanism
- The containment mechanism actually in effect for a process group.
- Outcome
- How a run ended — the explicit form of the
code()/timed_out()pair. - Output
Stream - Which of a child’s two captured streams a decoded line came from.
- Overflow
Mode - What to drop when a bounded output buffer is full.
- Parent
Death Cleanup - The reach of the parent-death hardening
(
Command::kill_on_parent_death) when the owning process dies abruptly — aSIGKILLof the owner or a crash, whereDropnever runs to tear the containment group down. - Priority
- A portable CPU-scheduling priority for a child process (see
Command::priority), mapped onto the native primitive at spawn time: Unixsetpriority/nice(applied through the samepre_execseam asuid/gid), Windows priority class (OR’d intocreation_flags, the same seam ascreate_no_window). - Process
Event - A lifecycle event produced by a running child process, yielded by
RunningProcess::events, which merges the process’s lifecycle transitions and its two output streams into a single ordered sequence: - Restart
Policy - When the supervisor restarts an exited child. See each variant; in every
case
stop_whenandmax_restartscan end supervision first. - Rlimit
Resource - A Unix per-process resource controlled by
Command::rlimit. - Signal
process-control - A signal to broadcast to every process in a
ProcessGroupviasignal. - Soft
Signal process-control - The fate of a graceful teardown’s best-effort soft-signal tier — what the
kernel actually observed of the polite “please exit” request
ProcessGroup::stopissues before the grace window, as opposed to what it tried to do. - Soft
Stop Scope process-control - How far a soft stop on the group axis reaches on this group, right now
— the honest answer to “if I ask this group to stop gracefully
(
signal(Signal::Term)/Signal::Int), which of its members will actually receive that request?” - Stdio
Mode - How a child process’s standard output or error stream is connected.
- Stop
Reason - Why supervision ended.
- Supervision
Event - A typed transition emitted by a live
SupervisionSession.
Traits§
- Capture
Policy - A consumer-supplied, typed seam that shapes each decoded line just before it enters the capture backlog — the redaction-at-capture extension point.
- Into
Command - What a
CliClientverb accepts: either an argument list — built into aCommandfor the client’s program with its defaults (timeout, env, cancellation) applied — or a ready-madeCommand, run as-is. - Process
Runner - Runs a
Command— to a captured result (output_string/output_bytes) or a live handle (start). - Process
Runner Ext - Convenience methods available on every
ProcessRunner(including&dyn ProcessRunner), layered overoutput_string.
Functions§
- host_
containment - Report how process containment behaves on this host without creating a
container or spawning anything — a spawn-free preflight (a doctor /
host-check command that must have no side effects) that answers what a
ProcessGroupwould otherwise only reveal after it exists: whichMechanisma group created here and now would use, how far a soft stop reaches, what the OS guarantees on abrupt owner death, and this crate’s version. - output_
all - Run every command in
commands, keeping at mostconcurrencyof them live at once, and collect all their results in input order. - output_
all_ bytes - The raw-bytes companion to
output_all: captures each command’s stdout asVec<u8>instead of decoded text. All other semantics are identical — seeoutput_all. The streaming counterpart isoutput_stream_bytes. - output_
stream - Run every command in
commandswith at mostconcurrencylive at once, yielding each result — an(input index,Result<ProcessResult<String>>)pair — the moment that command finishes. This is the streaming sibling ofoutput_all: the same bounded fan-out and the same per-command error semantics (anErris a spawn/I/O failure; a non-zero exit is anOk(ProcessResult); the fan-out never short-circuits), but presented as aStreamover completions instead of a singleVecat the end. - output_
stream_ bytes - The raw-bytes companion to
output_stream: each yieldedProcessResultcaptures stdout asVec<u8>instead of decoded text (for binary artifacts —git cat-file,tar -c, an image transcoder). Scheduling, completion ordering, input indexing, no-short-circuit, and cancellation/teardown are identical tooutput_stream; the buffering counterpart isoutput_all_bytes. - output_
string - Run
programwithargsinside a private job and capture the result without erroring on a non-zero exit — for commands whose exit code is meaningful. - process_
info process-control - Look up the identity and best-effort metadata of an arbitrary process by
pid — the standalone companion to
ProcessGroup::members_info, for a pid the caller holds outside any group (a pid saved to disk across runs, a launch registry, an e2e probe watching a process from outside its container). - process_
is_ alive process-control - Reuse-safe liveness: is the process at
pidstill the same instance you saw earlier — the one whosestart_timeyou saved? - run
- Run
programwithargsinside a private job and return trimmed stdout, or anErroron a non-zero exit / spawn failure / timeout. A thin shim overCommand; use the builder for a working directory, env, stdin, timeout, or the full verb vocabulary. - wait_
all - Wait for all of several running processes to exit, returning their
Outcomes in the same order asprocesses. The processes are only borrowed and stay usable afterwards (the exit status tokio caches remains re-readable). - wait_
any - Wait for whichever of several running processes exits first, returning
its index in
processesand itsOutcome(matchingRunningProcess::wait). - which
- Resolve
programto a concrete executable path without launching it — a spawn-free preflight for a doctor / early-diagnosis check (“isgitinstalled?”) that must have no side effects. A thin shim overCommand::new(program).resolve_program(); use the builder form when you needprefer_localdirectories or a relocatedPATHhonored (the client-levelCliClient::resolve_programdoes the same for a wrapped tool).
Type Aliases§
- Result
- Crate result alias.