pub struct Command { /* private fields */ }Expand description
A description of a child process to launch: program, arguments, working directory, environment, stdin source, and an optional timeout.
A single builder for everything a run needs. Build it, then either drive it
to completion with a
helper (output_string, run, …) or
start it via a ProcessRunner for streaming/shared
groups.
Implementations§
Source§impl Command
impl Command
Sourcepub fn current_dir(self, dir: impl AsRef<Path>) -> Self
pub fn current_dir(self, dir: impl AsRef<Path>) -> Self
Set the working directory for the child process.
Relative-path programs and current_dir: if the program passed to
Command::new is a relative path (e.g. "./tool" or "../bin/x"),
it is resolved against the caller’s current directory at spawn time —
not against the directory set here. Use an absolute path for the program
when combining current_dir with a relative-path executable.
A bare-name program resolved via prefer_local
doesn’t share this footgun: a relative prefer_local directory is
always turned into an absolute path before being handed to the OS, so
it can’t be reinterpreted against the directory set here.
Sourcepub fn prefer_local(self, dir: impl Into<PathBuf>) -> Self
pub fn prefer_local(self, dir: impl Into<PathBuf>) -> Self
Probe dir for the program before the system PATH — for a
locally-installed tool (a project’s node_modules/.bin, target/debug,
a vendored toolchain) that a caller wants to run by bare name without
hand-rolling a PATH override.
Repeated calls accumulate, in priority order: the directory from the
first call is probed first, then the second, and so on, with the system
PATH tried last as the final fallback. Resolution reuses the exact
same PATHEXT-aware lookup as the PATH search (the same probe_dir
helper — no separate copy), so a .exe/.cmd/.bat on Windows is found
exactly as it would be on PATH.
Only affects a bare-name program. If the program passed to
Command::new is a path — absolute, or relative with a separator
("./tool", "../bin/x") — prefer_local has no effect and the
existing contract holds unchanged: such a program is never looked up
here or on PATH.
Does not touch the child’s own PATH. This only changes where the
parent looks to resolve the program for this one launch — the PATH
the child sees in its own environment (via inheritance, env,
or inherit_env) is neither rewritten nor extended.
When the program is found under one of these directories, the child is
simply spawned via that resolved absolute path instead of the bare name
(so the OS never has to search anything); a grandchild the program itself
spawns does not inherit this reach.
A relative dir here (e.g. "./node_modules/.bin") is probed against
the process’s actual current directory, not against whatever is set
via current_dir — and the resulting match is
always made absolute (by joining it onto that same current directory)
before being handed to the OS, so it can never be reinterpreted against
the child’s own working directory once current_dir
is also set.
If resolution fails everywhere, ErrorReason::NotFound’s
searched includes these directories — first, in priority order — ahead
of the PATH directories, so the diagnostic doesn’t hide that they were
checked too.
Sourcepub fn env(self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> Self
pub fn env(self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> Self
Set an environment variable for the child. To remove an inherited
variable, use env_remove — value here is always a
value, never None.
Secrets. env is the right channel for a token or password: env values
are redacted from this command’s Debug (only names appear) and are never
emitted via tracing or in cassette recordings, and the child receives the
value intact. Prefer it — or stdin, the strongest — over a
command-line arg: argv is reduced to a count in Debug too,
but is world-readable through the OS process table (/proc/<pid>/cmdline,
ps) and is exposed verbatim by command_line and
cassette recording. An env value is not world-readable, but is still
visible to the same user and root via /proc/<pid>/environ and is inherited
by every descendant process; stdin exposes the secret to neither.
processkit deliberately ships no Secret wrapper type — pair env with
the secrecy/zeroize crates for a typed, memory-scrubbed secret at
your own call sites and pass the exposed value here. Scrubbing only covers
your copy: once passed, processkit holds a plain OsString for the
command’s lifetime and the child receives cleartext (a core dump can expose
either). For a secret recomputed per operation — resolved when each command
is built and reused across that command’s retries, not regenerated per
attempt — use
CliClient::default_env_fn.
Sourcepub fn env_remove(self, key: impl AsRef<OsStr>) -> Self
pub fn env_remove(self, key: impl AsRef<OsStr>) -> Self
Remove an environment variable inherited from the parent.
Sourcepub fn envs<I, K, V>(self, vars: I) -> Self
pub fn envs<I, K, V>(self, vars: I) -> Self
Set multiple environment variables at once. Order is preserved; later entries win on a duplicated key.
use processkit::Command;
Command::new("tool").envs([("FOO", "1"), ("BAR", "2")]);Sourcepub fn env_clear(self) -> Self
pub fn env_clear(self) -> Self
Clear all inherited environment variables before applying any set here.
Opts out of client env defaults: a command that clears its environment
is treated as having taken full control of it, so a
CliClient’s default_env/
default_env_fn is not gap-filled
into it (a client default would otherwise pierce the clean slate). Set any
var you still want with an explicit env.
Sourcepub fn inherit_env<I, S>(self, names: I) -> Self
pub fn inherit_env<I, S>(self, names: I) -> Self
Inherit only the named variables from the parent environment —
an allow-list on top of an implied env_clear.
The named vars are copied from the parent environment at each spawn
(vars the parent lacks are skipped); explicit env /
env_remove overrides still apply afterwards.
Repeated calls extend the allow-list. Works on every platform.
A client default_env for an
allow-listed key is not applied — the command chose to inherit that
key from the parent, and a client default must not override it. A client
default for a key not in the list still fills (an explicit override
layered on top, orthogonal to parent inheritance) — so a client-wide safety
default reaches the command. Use env_clear instead to
opt out of client env defaults entirely.
Sourcepub fn uid(self, uid: u32) -> Self
pub fn uid(self, uid: u32) -> Self
Run the child as this user id (Unix privilege drop).
Applied by the OS between fork and exec; combine with
gid — the group id is set before the user id (once
the uid drops, changing gid is no longer permitted), an ordering the
standard library guarantees. On non-Unix targets the run fails with
ErrorReason::Unsupported — a requested
privilege drop is never silently skipped.
Linux cgroup caveat: under the cgroup v2 mechanism
(Mechanism::CgroupV2) the child joins
its cgroup after the OS has dropped the uid, by writing the
auto-created (and therefore not target-uid-writable) cgroup.procs —
so the spawn currently fails with a permission error rather than
producing an uncontained child. Privilege drop composes cleanly with
the POSIX process-group mechanism (macOS/BSD, or Linux without cgroup
delegation); making it compose with cgroups (e.g. chowning the cgroup
to the target uid) is tracked future work.
Sourcepub fn gid(self, gid: u32) -> Self
pub fn gid(self, gid: u32) -> Self
Run the child under this group id (Unix privilege drop) — see
uid for ordering and platform notes.
Sourcepub fn groups(self, gids: impl AsRef<[u32]>) -> Self
pub fn groups(self, gids: impl AsRef<[u32]>) -> Self
Set the child’s supplementary groups (Unix privilege drop), replacing the inherited set.
This is the missing third leg of a correct privilege drop: dropping the
uid/gid alone leaves the child holding the
parent’s supplementary groups (often root’s), so it could still reach
group-owned resources the target user shouldn’t. Pass the target user’s
groups (or [] to drop all extras) alongside uid/gid.
Ordering is handled for you: the OS applies setgroups → setgid →
setuid (groups and gid must be set while still privileged, before the
uid drops). On non-Unix targets the run fails with
ErrorReason::Unsupported — never silently
skipped. The Linux cgroup-v2 caveat from uid applies
unchanged.
Sourcepub fn setsid(self) -> Self
pub fn setsid(self) -> Self
Detach the child into a new session (Unix setsid()): no
controlling terminal, its own session and process group.
Containment is preserved: the group tracks the new session’s process
group (whose id is the child’s pid), so kill-on-drop and the teardown
verbs still reach it. On non-Unix targets the run fails with
ErrorReason::Unsupported.
Honored by the Command-driven launch paths (run/output_*/
start, ProcessGroup::start,
pipelines); the low-level raw-command
ProcessGroup::spawn escape hatch
bypasses these builders.
Sourcepub fn priority(self, priority: Priority) -> Self
pub fn priority(self, priority: Priority) -> Self
Launch this child at a lower (or higher) CPU-scheduling priority — for background/batch work that shouldn’t starve the foreground, or a task that should win over it.
Applied on both platforms via the existing spawn seams: Unix
setpriority in the same pre_exec hook that carries
uid/gid/setsid; Windows
a priority-class flag OR’d into creation_flags, the same seam as
create_no_window. Unlike the privilege
builders this never yields
ErrorReason::Unsupported — see
Priority for why both platforms cover every
variant, and the Unix caveat that lowering nice below its inherited
value — Priority::AboveNormal/
Priority::High always, and even
Priority::Normal under a positively-niced
parent — needs CAP_SYS_NICE/root there.
Last-write-wins with an earlier call, like timeout.
Sourcepub fn cpu_affinity(self, cpus: impl IntoIterator<Item = usize>) -> Self
pub fn cpu_affinity(self, cpus: impl IntoIterator<Item = usize>) -> Self
Restrict the child to the given logical CPU indices. Descendants inherit the mask, so this initially constrains the whole process tree (a child may still change its own affinity if the OS permits it).
On Linux the mask is applied with sched_setaffinity(2) in the pre-exec
child, before user code runs. On Windows it is applied with
SetProcessAffinityMask while the child is still suspended between job
assignment and resume; the ConPTY path uses the same ordering. macOS,
BSD, and other targets return ErrorReason::Unsupported rather than
silently inheriting the parent’s mask.
An empty set, a Linux index beyond cpu_set_t, or a Windows index beyond
the process mask width fails before the child runs. Windows’ mask API is
limited to one processor group; indices are therefore bounded by the
native pointer width, and the OS rejects processors unavailable to the
current group. Repeated calls are last-write-wins; duplicates are removed
and indices are stored in ascending order.
This owner-dependent configuration is refused by
spawn_detached. On Windows it is also unavailable
through to_tokio_command, because a raw command
has no post-spawn suspended-child configuration seam; use a high-level run
verb instead.
Sourcepub fn io_priority(self, priority: IoPriority) -> Self
pub fn io_priority(self, priority: IoPriority) -> Self
Set the Linux I/O-scheduling priority for this child, so background disk work can yield to foreground users.
Applied with ioprio_set(2) in the child before exec, on the same
pre_exec seam as priority and
umask. Linux is the only supported platform: Windows,
macOS, BSD, and other Unix targets fail with
ErrorReason::Unsupported rather than
silently inheriting the caller’s I/O priority. See IoPriority
for the Linux classes, data range, and privilege caveat.
This configuration is owner-dependent and is therefore refused by
spawn_detached. Last-write-wins with an earlier
call, like timeout.
Sourcepub fn umask(self, mask: u32) -> Self
pub fn umask(self, mask: u32) -> Self
Set the file-mode creation mask for the child (Unix umask(2)),
controlling the default permissions of files it creates.
Applied via pre_exec, alongside setsid/
groups — another knob on that same seam. On
non-Unix targets the run fails with
ErrorReason::Unsupported rather than
silently ignoring the requested mask. Only the low permission bits are
meaningful (as with the umask(2) syscall itself); pass the value you
would give the umask shell builtin, e.g. 0o022.
Sourcepub fn kill_on_parent_death(self) -> Self
pub fn kill_on_parent_death(self) -> Self
Kill the direct child if this process dies abruptly — including
a SIGKILL of the parent, where Drop never runs to tear the group
down. An opt-in hardening on top of the unconditional kill-on-drop
containment, best-effort by design:
| Platform | Effect |
|---|---|
| Windows | Already guaranteed regardless of this knob: the kernel closes the Job Object handle when the parent dies, and kill-on-close takes the whole tree. Documented no-op. |
| Linux | prctl(PR_SET_PDEATHSIG, SIGKILL) on the direct child only — grandchildren are not covered (with the parent gone, nothing tears the cgroup/pgroup down). |
| macOS / BSD / other | No pdeathsig equivalent — does nothing (the graceful-exit guarantee via Drop still holds). |
The reach of this hardening on the current platform is reported honestly
by kill_on_parent_death_scope as a
ParentDeathCleanup — WholeTree on
Windows, DirectChildOnly on
Linux, Unsupported on macOS/BSD — so
a caller can surface the real scope instead of overpromising a whole-tree
guarantee. This is best-effort hardening for abrupt owner death only;
ordinary graceful teardown (Drop) still kills the whole tree everywhere.
Two honest Linux caveats:
- The death signal fires when the spawning thread dies, not only the
process — on a multi-threaded tokio runtime, a worker thread retired
while the child lives would kill it early (for the strongest guarantee
spawn from a current-thread runtime). The parent-died-before-arming race
is closed in the child by re-checking
getppid()against the spawner’s pid captured before the fork — safe in containers where the spawner itself is PID 1. - The kernel clears
PR_SET_PDEATHSIGacross anexecveof a set-uid / set-gid binary (a security measure), so this is silently void for asudo …/ setuid child — it inherits the pdeathsig for the tiny window beforeexecve, then loses it. Contain such a child with the kill-on-drop group (the default) rather than relying on this knob.
(Idea borrowed from execa’s cleanup-on-exit, mapped to native
primitives.)
Sourcepub const fn kill_on_parent_death_scope() -> ParentDeathCleanup
pub const fn kill_on_parent_death_scope() -> ParentDeathCleanup
The scope of whole-tree cleanup kill_on_parent_death actually
achieves on this build’s target platform when the owner dies
abruptly — a ParentDeathCleanup capability report, so a caller
can state the real reach of the hardening rather than overpromising a
whole-tree guarantee the OS cannot keep.
WholeTree on Windows (the kernel
closes the Job Object handle on owner death and kill-on-close reaps the
whole tree), DirectChildOnly on
Linux (PR_SET_PDEATHSIG reaches only the direct child; grandchildren
survive), and Unsupported on macOS /
the BSDs (no pdeathsig equivalent). Fixed per target at build time — it
does not depend on whether kill_on_parent_death was called or on any
runtime state — so it is an associated function, not a method on a built
command. See ParentDeathCleanup for the full contract; note it
describes only the abrupt-death path, since ordinary graceful teardown
(owner exits/panics, so Drop runs) kills the whole tree everywhere.
Sourcepub fn create_no_window(self) -> Self
pub fn create_no_window(self) -> Self
Spawn without a console window (Windows CREATE_NO_WINDOW) — for a
GUI app launching a CLI tool without a flashing terminal.
On non-Windows targets this is a harmless no-op (purely cosmetic — no
console windows exist to suppress). Honored by the Command-driven
launch paths; the raw
ProcessGroup::spawn escape hatch still
overwrites creation flags (see its docs).
Sourcepub fn windows_graceful_ctrl_break(self) -> Self
pub fn windows_graceful_ctrl_break(self) -> Self
Opt in to a graceful Windows teardown via a console CTRL_BREAK
event, giving a console child a chance to shut down cleanly instead of
being hard-killed at once.
A graceful timeout (timeout_grace) or a group
shutdown already posts WM_CLOSE to any
top-level window a live member owns, so a windowed child (Electron app,
desktop tool, windowed service) drains cleanly with no opt-in. A console
child has no window, and Windows has no POSIX signal to reach it, so without
this opt-in its graceful teardown collapses to the atomic Job Object kill —
there is nothing to trigger a clean exit. With this opt-in
the direct child is spawned in its own console process group
(CREATE_NEW_PROCESS_GROUP), and at graceful teardown it is sent
GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid) before the grace window:
a child that installs a CTRL_BREAK handler (as many CLIs, Node, Python,
and Go services do) can flush and exit within the grace. Any survivor still
running when the grace elapses is then TerminateJobObject’d — the same
hard-kill fallback as before, so containment is never weakened.
§Boundaries (read these)
- Console-only. The event is delivered through the console this process
shares with the child. A child spawned
create_no_window(orDETACHED_PROCESS) does not share that console, so it never receives theCTRL_BREAKand simply rides the grace to theTerminateJobObjectfallback. A GUI / service parent with no console of its own can’t deliver the event either. CTRL_BREAK, notCTRL_C.CREATE_NEW_PROCESS_GROUPdisablesCTRL_Cfor the new group by default;CTRL_BREAKis always deliverable, which is why it is the event sent.timeout_signal(Unix’s signal choice) does not apply — Windows always sendsCTRL_BREAK.- Direct child only. Only the process launched by this run is a group
leader; its own descendants receive the event via the shared console and
group, but an
adopted child (not spawned here) is not addressed and falls back to the hard kill. - No-op off Windows. On Unix the graceful tier already sends a real signal, so this builder does nothing there.
Honored by the Command-driven launch paths (run helpers,
start, the run-level graceful
timeout_grace, and a shared
ProcessGroup’s
shutdown); the raw
ProcessGroup::spawn escape hatch, which
overwrites creation flags wholesale, does not participate.
Sourcepub fn use_pty(self) -> Self
Available on crate feature pty only.
pub fn use_pty(self) -> Self
pty only.Spawn the child under a pseudo-terminal (PTY) instead of three independent pipes, so tools that demand a controlling terminal work.
Off by default. When set, the child is launched over a single PTY master
— openpty on Unix, CreatePseudoConsole (ConPTY) on Windows — so
isatty() reports a terminal and a program that refuses to run (or that
hangs waiting for a prompt) without one behaves normally: an
isatty()-gated agentic CLI, an ssh/sudo password/passphrase
prompt, a credential helper. This is a minimal single-master-fd mode,
not a general terminal emulator.
§stdout and stderr are merged
A PTY has one master fd carrying the child’s combined output, so in this
mode stdout and stderr are merged and can no longer be separated. The
merged stream is delivered exactly where stdout normally is
(output_string, stdout_lines, on_stdout_line, stdout_tee, …);
on_stderr_line is never called and
stderr_tee never receives anything, because there is
no separate stderr to deliver. ProcessResult::stderr
is empty for a PTY run. If you need stdout and stderr apart, do not use
PTY mode.
§Interactive input
keep_stdin_open plus
take_stdin drive the master’s input
side exactly as with a pipe, and a configured stdin
source is written to it. On Unix the PTY line discipline’s terminal
echo is disabled so a written password is not echoed back into the
merged output (see ProcessStdin); the Windows
ConPTY has no portable per-write echo control, so that guarantee is
Unix-only.
§Line framing default (\r-aware) and output hygiene
A PTY child writes like a terminal: CRLF line endings, progress bars
redrawn in place with a bare \r (no \n until the end), and VT/ANSI
escape sequences (colors, cursor moves, alternate screen, OSC titles). Two
deliberate, coded decisions handle this — one automatic, one opt-in:
- Framing is auto
\r-aware. Underuse_ptythe effective defaultline_terminatorisCarriageReturninstead ofNewline, so each progress redraw surfaces as its own line rather than piling into one ever-growing string a naive consumer never sees framed. This is a non-destructive reframing (it only changes where lines split), so it is the sensible default for the mode; a\r\nstill counts as one terminator, so ordinary CRLF text reads unchanged. It is applied only when you have not pinned a terminator yourself — an explicitline_terminator(evenNewline) always wins, order-independently ofuse_pty. - Escape sanitization stays opt-in. Stripping VT/ANSI escapes is
destructive (it removes bytes from the captured output), so it is
not turned on automatically — reach for
sanitize_vtwhen you want the merged output de-escaped for line predicates and transcripts. Leaving it off keeps the PTY’s bytes verbatim in the backlog.
§Containment is unchanged
The PTY child is placed in the same Job Object / cgroup / process group as any other child, so whole-tree kill-on-drop, timeouts, and cancellation behave identically — the pseudo-terminal only changes the I/O wiring, never the teardown guarantee.
Available only with the pty crate feature. A note for callers who leave
it unset: without this the existing three-pipe behavior — including the
Newline framing default — is byte-for-byte unchanged.
§Terminal identity and environment overrides
At spawn, ProcessKit identifies the terminal it creates: Unix children
receive TERM=xterm-256color, and every PTY child receives COLUMNS and
LINES matching the initial PTY geometry (80×24 by default, or the size
set with pty_size). Windows does not synthesize
TERM: ConPTY exposes VT handling through the Windows console APIs, so
any inherited TERM remains governed by the normal environment rules.
These values are defaults, not forced overrides. Explicit
env or env_remove operations for
TERM, COLUMNS, or LINES always win, including with
env_clear or inherit_env.
Sourcepub fn pty_size(self, cols: u16, rows: u16) -> Self
Available on crate feature pty only.
pub fn pty_size(self, cols: u16, rows: u16) -> Self
pty only.Set the initial window size — cols columns by rows rows — of the
pseudo-terminal opened by use_pty.
The size matters to terminal-aware children: it drives line wrapping,
progress-bar/TUI layout, and pager behavior, and is the geometry a child
reads back with an isatty/TIOCGWINSZ-style query. Without this the PTY
opens at the conventional 80×24 default. At spawn, the child’s
COLUMNS and LINES environment defaults are set to the same values;
explicit env / env_remove operations
for either name win. A later
RunningProcess::resize_pty changes
the live terminal geometry but cannot rewrite an already-running
process’s environment.
§Only meaningful with use_pty
This is a PTY-only knob. On a command that is not
use_pty it is a documented no-op — the three-pipe
launch has no terminal to size, so the value is simply never read (it is
not silently applied anywhere, and it is not an error to set; it just
does nothing). Order-independent: pty_size(..).use_pty() and
use_pty().pty_size(..) are equivalent.
§Live resize
To change the size of an already-running session (e.g. propagating a host
window resize / SIGWINCH), use
RunningProcess::resize_pty.
Available only with the pty crate feature.
Sourcepub fn pipe(self, next: Command) -> Pipeline
pub fn pipe(self, next: Command) -> Pipeline
Chain this command’s stdout into next’s stdin — the first link of a
shell-free Pipeline. Keep chaining with
Pipeline::pipe (or the | operator), then
drive the whole thing with
Pipeline::output_string /
Pipeline::run.
Sourcepub fn unchecked_in_pipe(self) -> Self
pub fn unchecked_in_pipe(self) -> Self
Exempt this command, as a pipeline stage, from pipefail
attribution: its unclean exit (non-zero code, signal kill — including
SIGPIPE — or its own per-stage timeout kill) is
skipped when the chain decides what to report, and never shields a
checked stage’s failure. The motivating pattern is
producer | head -1: the consumer exits early, the producer dies of
SIGPIPE/EPIPE, and without this marker strict pipefail reports
that perfectly normal death as the chain’s failure. (Design borrowed
from duct’s unchecked() — the idea, not the code.)
Outside a Pipeline this is a no-op: a single
run’s status is already plain data in its
ProcessResult, and
ensure_success stays opt-in
— unchecked does not relax it, nor a whole-chain
Pipeline::timeout.
Sourcepub fn timeout(self, timeout: Duration) -> Self
pub fn timeout(self, timeout: Duration) -> Self
Kill the run if it exceeds timeout.
Clears a prior no_timeout — the last of the two wins.
Sourcepub fn no_timeout(self) -> Self
pub fn no_timeout(self) -> Self
Run without a timeout, and — unlike simply leaving the timeout unset —
opt out of any client-wide default_timeout
gap-fill. Use this to say “this one long-running command is deliberately
unbounded” against a client that otherwise imposes a deadline on every call
(a tail -f, a watch loop, an interactive session).
A plain Command (no client) is already unbounded by default, so this is
only meaningful when the command is run through a CliClient
with a default_timeout. Clears a prior timeout — the
last of the two wins.
Sourcepub fn inactivity_timeout(self, idle: Duration) -> Self
pub fn inactivity_timeout(self, idle: Duration) -> Self
Kill the run when neither stdout nor stderr produces bytes for idle.
Unlike timeout, this is a resettable deadline: every
successful read from either output stream grants a fresh idle window.
The initial window starts when the child is spawned, so a process that
never writes output is still bounded. Teardown uses the same
timeout_grace, timeout_signal (when the
process-control feature is enabled), and whole-tree containment path as
the absolute timeout.
The result is reported as
Outcome::InactivityTimedOut,
distinct from Outcome::TimedOut. A zero
duration is valid and expires as soon as the watchdog is polled.
Sourcepub fn timeout_opt(self, timeout: Option<Duration>) -> Self
pub fn timeout_opt(self, timeout: Option<Duration>) -> Self
Set the timeout from an optional Duration, folding the
timeout / no_timeout split into a
single composable verb for config-driven call sites. Some(d) is exactly
timeout(d); None is exactly
no_timeout().
Reach for it when you hold an Option<Duration> (a parsed config value, a
caller-supplied override) instead of the
match cfg { Some(d) => c.timeout(d), None => c.no_timeout() } dance. Mind
the None mapping: it means deliberately unbounded — opting out of a
client-wide default_timeout gap-fill,
not “leave the timeout unset for a default to fill”. Like the two verbs
it folds, it is last-write-wins with any earlier timeout call.
Sourcepub fn timeout_grace(self, grace: Duration) -> Self
pub fn timeout_grace(self, grace: Duration) -> Self
Make either timeout or
inactivity_timeout graceful: when the
winning watchdog fires the run’s
tree is sent SIGTERM (or the signal chosen via timeout_signal, with the
process-control feature), given up to grace to exit, then SIGKILLed.
Without it the watchdog hard-kills at once. No effect unless at least one
timeout is set.
Windows has no POSIX signal tier, but two best-effort soft triggers run
before the atomic kill when the tree can act on one: WM_CLOSE is posted to
every top-level window owned by a live member (a windowed child — Electron
app, desktop tool, windowed service — can then close and drain within
grace), and a child opted into
windows_graceful_ctrl_break is sent a
console CTRL_BREAK. A tree with neither a window nor that opt-in has
nothing to trigger a soft exit, so the deadline hard-kills the job at once,
grace unused — timings unchanged from before this soft tier. Any survivor
still running when grace elapses is TerminateJobObject’d. Either way
timed_out stays true (the deadline
was exceeded), graceful or not.
Sourcepub fn timeout_signal(self, signal: Signal) -> Self
Available on crate feature process-control only.
pub fn timeout_signal(self, signal: Signal) -> Self
process-control only.The signal sent at the start of a graceful
timeout_grace window (default
Signal::Term). Unix-only in effect; ignored on
Windows (no signal tier).
This builder lives behind the process-control feature because the
Signal type does. Without process-control the
graceful timeout always uses SIGTERM (the default); the feature is only
needed to choose a different teardown signal — promoting Signal into
the base API would enlarge the always-on surface for a niche knob.
Sourcepub fn ok_codes(self, codes: impl IntoIterator<Item = i32>) -> Self
pub fn ok_codes(self, codes: impl IntoIterator<Item = i32>) -> Self
Treat these exit codes (not just 0) as success for the checking verbs —
run (and run_unit/checked via
ProcessRunnerExt) and
ProcessResult::ensure_success / is_success.
For tools whose non-zero exit is a normal result — grep (1 = no match),
diff (1 = differs), rsync’s code families — so callers don’t hand-match.
An empty set is ignored — a no-op that leaves the previously configured
codes (or the default [0]) in place, rather than resetting to [0], since
an empty accepted-set would make every exit a failure. Does not change
exit_code (always the raw code) or
probe (always the 0/1 convention).
Sourcepub fn cancel_on(self, token: CancellationToken) -> Self
pub fn cancel_on(self, token: CancellationToken) -> Self
Tie this run to token: cancelling it kills the process tree and makes
every consuming path (run/output_string/output_bytes/wait/
exit_code/probe/profile/finish and the streamed
finishers) resolve to ErrorReason::Cancelled.
In a Pipeline, a token on any stage cancels that
stage and the cancellation errors the whole pipeline (the private
pipeline group tears the other stages down).
Unlike timeout — which is captured in the
ProcessResult (timed_out) without erroring on the non-checking
paths — a cancellation is always an error, on every path. When both
fire, cancellation wins (it is checked first — except in first_line’s
narrow tie where the deadline watchdog closes the stream in the same poll
the token fires, which surfaces as Timeout). An already-cancelled token
short-circuits before spawning. On a private group the whole tree is
killed; on a shared group
(ProcessGroup::start) only the direct
child is, like timeout. Both wait_any and
first_line surface a mid-run cancel as
Err(Cancelled) — their streaming race resolves the cancellation and
tears the child down — as does an already-cancelled token via the
pre-spawn short-circuit. A mid-run cancel during
wait_for_line, by contrast,
closes the stream and surfaces as that probe’s
ErrorReason::NotReady, not Cancelled — the
consuming finisher afterwards still reports Cancelled.
A cancelled run is never retried: retry policies and
Supervisor restarts both treat
ErrorReason::Cancelled as terminal — the token stays cancelled forever, so
another attempt could only fail the same way.
On a Command this replaces any previously set token (last write
wins) — contrast the gap-fill containers
Pipeline::cancel_on and
CliClient::default_cancel_on,
which leave an explicit per-element token intact.
Sourcepub fn retry(
self,
max_attempts: u32,
backoff: Duration,
retry_if: impl Fn(&Error) -> bool + Send + Sync + 'static,
) -> Self
pub fn retry( self, max_attempts: u32, backoff: Duration, retry_if: impl Fn(&Error) -> bool + Send + Sync + 'static, ) -> Self
Retry the run while retry_if accepts the error, up to max_attempts
total attempts, sleeping a fixed backoff between tries. For exponential
backoff + cap + jitter, use retry_with.
Applies to the success-checking helpers —
run/run_unit/checked/exit_code/probe/parse/try_parse — on
Command, on ProcessRunnerExt,
and on CliClient: the ones that surface failure as an
Error the classifier can inspect (e.g. a transient network failure in
stderr, or ErrorReason::Timeout). The non-erroring
output_string/output_bytes paths don’t retry.
Each attempt re-executes the whole command — a fresh process. Only
retry operations that are safe to repeat: a side effect that already landed
before the failure (a git push that reached the server, then dropped the
connection) will be replayed. Prefer to gate retries on a classifier that
matches pre-effect failures (DNS/connection errors, ErrorReason::Timeout
while still connecting) rather than any non-zero exit.
A timeout bounds each attempt, not the whole retried
operation — there is no total wall-clock ceiling across retries (worst case
≈ attempts × timeout + the sum of the backoffs). Bound the total with
cancel_on (a Cancelled is terminal — never retried).
A one-shot stdin source
(Stdin::from_reader /
from_lines) feeds a single run, so a retry
re-feeds it only when the failed attempt is guaranteed not to have
consumed it. The launch reserves that payload transactionally and
commits it only once a child exists, so a failure before any child was
spawned — NotFound,
Spawn (e.g. a transient ETXTBSY that
is_transient accepts), or
Unsupported — rolls the reservation back
and leaves the payload intact: such a command is retried (subject to
the classifier) and the next attempt feeds the untouched source. Any
other error may have reached a live child that already consumed the
source — a non-zero Exit,
Timeout, Signalled,
a stdin-write Stdin failure,
OutputTooLarge, or the ambiguous
Io (which arises both before and after a child) — so
the first attempt’s error is returned as-is, not retried (a retry
would either replay empty stdin or spuriously classify the re-consume).
Use a reusable source (from_string/from_bytes/from_file/
from_iter_lines) to retry unconditionally. (A one-shot source re-run
outside this retry loop — a Supervisor incarnation, a pipeline re-run —
does fail loud with ErrorReason::Io InvalidInput at
launch instead.)
Inert outside the success-checking verbs. A retry policy is
honored only by the verbs listed above. It is ignored by:
Supervisor— supervision is keep-alive restarting with its ownRestartPolicy/ backoff / storm handling, a different concern from replay-to-success; configure restarts there, not viaretry.output_all— a bounded fan-out that collects every outcome as data (no per-command retry); wrap each command’s verb yourself if a batch element must retry.- the raw
Pipelineverbs — a stage’sretrydoes not re-run that stage within the chain.
Counting: max_attempts is the total number of runs (so
retry(3, …) runs at most three times: the first plus two more).
max_attempts of 0 and 1 both mean a single run with no retry — a
command always runs at least once, so 0 does not mean “never run”. For
exponential backoff + cap + jitter instead of a fixed delay, use
retry_with, which takes a RetryPolicy — note that
a RetryPolicy counts max_retries (the runs after the first), so
retry(3, …) corresponds to RetryPolicy::new().max_retries(2).
Sourcepub fn retry_with(
self,
policy: RetryPolicy,
retry_if: impl Fn(&Error) -> bool + Send + Sync + 'static,
) -> Self
pub fn retry_with( self, policy: RetryPolicy, retry_if: impl Fn(&Error) -> bool + Send + Sync + 'static, ) -> Self
Retry on a rich RetryPolicy — exponential backoff + cap + jitter —
instead of the fixed (max_attempts, backoff) of retry.
The per-command analogue of
CliClient::default_retry, with the same
applicability and replay caveats as retry. Note
RetryPolicy counts max_retries (after the first attempt), whereas
retry counts max_attempts (total).
Sourcepub fn retry_never(self) -> Self
pub fn retry_never(self) -> Self
Opt out of retries entirely: run this command exactly once and
suppress any client-wide
default_retry gap-fill.
The explicit, symmetric counterpart to no_timeout:
a bare Command already retries nothing, so this is only meaningful
against a CliClient whose default_retry would
otherwise be filled in — it pins “run this one command once, whatever the
client policy”. Tidier than, and behaviorally identical to, the
retry(1, Duration::ZERO, |_| false) idiom (one attempt, a classifier that
accepts nothing). Last-write-wins with any earlier
retry / retry_with.
Sourcepub fn keep_stdin_open(self) -> Self
pub fn keep_stdin_open(self) -> Self
Leave stdin open after start so the child can be driven interactively via
RunningProcess::take_stdin.
Takes precedence over a stdin source — when set, that
source is ignored and the pipe is handed to the caller instead.
The open pipe lives until the caller takes it (take_stdin) or a
consuming verb runs: at consume time an untaken pipe is closed
(nothing could ever write to it again), so a stdin-reading child sees
EOF instead of blocking — combining keep_stdin_open with a bulk
helper (output_string, run, …) without ever taking the writer is
equivalent to not setting it. A writer the caller did take is
unaffected and keeps the pipe until dropped or
finished.
Mutually exclusive with inherit_stdin — a child
cannot both be handed an interactive stdin pipe and share the parent’s
stdin; setting both is rejected at launch (see inherit_stdin).
Sourcepub fn inherit_stdin(self) -> Self
pub fn inherit_stdin(self) -> Self
Give the child the parent’s own standard input — it reads directly from whatever this process’s stdin is connected to (a terminal, a file, a pipe) rather than from a crate-managed pipe.
This is the stdin counterpart of
stdout(StdioMode::Inherit) /
stderr(StdioMode::Inherit): the child shares the
parent stream instead of the crate mediating it. Reach for it when a
child must talk to the real terminal — git commit opening $EDITOR, a
tool prompting the user for a password or a yes/no, or simply forwarding
the parent’s piped stdin straight through to the child. Until a
pseudo-terminal exists (a future direction, not yet provided) this covers
the common non-tty-negotiating interactive cases without the crate having
to pump bytes.
Because the child reads the parent’s stdin directly, the crate neither
feeds nor captures that input, and there is no writer to
take_stdin (it returns None, as
for a non-keep_stdin_open run). stdout/stderr are unaffected — capture
and streaming of the child’s output keep working exactly as before.
§Mutually exclusive with a mediated stdin
Inheriting the parent’s stdin cannot be combined with either way the crate
would otherwise drive stdin — a configured stdin source
(Stdin::from_string/from_bytes/from_file/from_reader/from_lines,
or an explicit Stdin::empty()) or keep_stdin_open’s
interactive pipe. Setting inherit_stdin and one of those is a
contradiction (feed the child a source and let it read the terminal?),
so it is rejected at the launch boundary with a typed
ErrorReason::Io (InvalidInput) — the same failure mode
as the other stdin misconfiguration the crate refuses (re-running a
consumed one-shot source) — rather than silently letting one win. Drop the
other stdin knob to resolve it.
Sourcepub fn on_stdout_line<F>(self, handler: F) -> Self
pub fn on_stdout_line<F>(self, handler: F) -> Self
Invoke handler for each decoded stdout line as it is read (in addition
to capture/streaming). Runs on the pump task; keep it cheap. A handler
that panics is caught and disabled for the rest of the run — the
child is still drained and the result still carries every line (the
panic is reported as a tracing warn when that feature is on).
Ordering guarantees: invocations are FIFO within a stream; there
is no ordering between stdout and stderr handlers (two independent
pumps). On the consuming verbs (run/output_*/wait/profile/
finish) all handler invocations happen-before the awaited
future resolves — a progress bar can be finalized the moment the call
returns. (One documented exception: when a leaked pipe is held open
past the child’s death, teardown aborts the pump after a bounded
grace, cutting any not-yet-delivered lines along with their handler
calls.) On a streamed run, stdout handlers quiesce when the
stdout_lines stream ends.
At most one handler per stream: a repeat call replaces the previous one
(builder semantics, like timeout). To fan out, compose
inside a single closure.
Requires stdout to be Piped (the default):
the handler runs on the capture pump, so it never fires under
stdout(Inherit) / stdout(Null).
Byte cap caveat: a single line whose length exceeds a byte cap
(with_max_bytes) is never
assembled, so the handler never sees it either — it is silently skipped
for every sink (handler, tee, and capture buffer alike), counted only
via the truncation/dropped() signal. If every line matters, leave the
byte cap unset, or use a line cap instead.
Sourcepub fn on_stderr_line<F>(self, handler: F) -> Self
pub fn on_stderr_line<F>(self, handler: F) -> Self
Invoke handler for each decoded stderr line as it is read.
Same contract as on_stdout_line: runs on the
pump task, and a repeat call replaces the previous handler.
Sourcepub fn stdout(self, mode: StdioMode) -> Self
pub fn stdout(self, mode: StdioMode) -> Self
Set how the child’s standard output stream is connected (default:
StdioMode::Piped).
Piped(default) — captured into a pipe; all output-retrieval verbs (output_string,stdout_lines, …) read from it.Inherit— the child shares the parent’s stdout; output appears in the terminal/log but is not captured.Null— suppressed entirely (redirected to/dev/null).
With Inherit/Null there is no pipe to read, so the bulk capture
verbs (output_string/output_bytes) error rather than return
silently-empty output, and the streaming verbs (stdout_lines/
events) yield an empty stream. Use a discard verb (wait) to run
a command whose stdout you don’t want to capture. Calling this after
stdout_file restores a normal stdio mode.
Sourcepub fn stderr(self, mode: StdioMode) -> Self
pub fn stderr(self, mode: StdioMode) -> Self
Set how the child’s standard error stream is connected (default:
StdioMode::Piped).
Same semantics as stdout: Piped captures,
Inherit passes through, Null suppresses.
Sourcepub fn stdout_file(self, path: impl AsRef<Path>) -> Self
pub fn stdout_file(self, path: impl AsRef<Path>) -> Self
Redirect stdout directly to path, creating or truncating the file at
spawn time. The child owns the descriptor, so no parent-side pump or
output buffer is involved.
Capture and streaming verbs require a pipe and therefore reject this
configuration; use wait or another
discard verb. For a shared supervisor log across restarts, use
stdout_file_append.
Sourcepub fn stdout_file_append(self, path: impl AsRef<Path>) -> Self
pub fn stdout_file_append(self, path: impl AsRef<Path>) -> Self
Redirect stdout directly to path, creating it when absent and appending
on every spawn. This is useful for a Supervisor
whose incarnations should share one log file.
Sourcepub fn stdout_file_truncate(self, path: impl AsRef<Path>) -> Self
pub fn stdout_file_truncate(self, path: impl AsRef<Path>) -> Self
Explicit spelling of stdout_file, for code that
selects append versus truncate at the call site.
Sourcepub fn stderr_file(self, path: impl AsRef<Path>) -> Self
pub fn stderr_file(self, path: impl AsRef<Path>) -> Self
Redirect stderr directly to path, creating or truncating the file at
spawn time. The child owns the descriptor, so no parent-side pump or
output buffer is involved.
Sourcepub fn stderr_file_append(self, path: impl AsRef<Path>) -> Self
pub fn stderr_file_append(self, path: impl AsRef<Path>) -> Self
Redirect stderr directly to path, creating it when absent and appending
on every spawn. See stdout_file_append for
the restart-log use case.
Sourcepub fn stderr_file_truncate(self, path: impl AsRef<Path>) -> Self
pub fn stderr_file_truncate(self, path: impl AsRef<Path>) -> Self
Explicit spelling of stderr_file, for code that
selects append versus truncate at the call site.
Sourcepub fn stdout_tee<W>(self, writer: W) -> Self
pub fn stdout_tee<W>(self, writer: W) -> Self
Tee every decoded stdout line to writer as it is produced — capture
and stream to writer simultaneously.
writer is an async sink (tokio::io::AsyncWrite); each decoded line
is written to it followed by \n. The write is awaited on the capture
pump, so a slow sink applies backpressure (the pump slows, the OS pipe
fills, the child blocks on its next write) rather than blocking the
runtime. The sink must make forward progress, though: a destination
that blocks forever (not merely slow) stalls the pump — no further
lines are buffered and a live stdout_lines/events consumer
parks — until the run’s teardown grace aborts the pump. A write error
disables the tee for the rest of the run — surfaced as a tracing warn
under the tracing feature, not silently swallowed — and capture is
unaffected.
Runs independently of on_stdout_line: set
both and both fire per line (the tee no longer replaces the handler).
A second stdout_tee replaces an earlier one.
Shared across clones and attempts. The sink is held in an
Arc<Mutex<…>>, so cloning the Command shares one sink — and a
Command is cloned for every Pipeline stage, every
Supervisor incarnation, and every
retry attempt. Concurrent clones (pipeline stages running
at once) interleave their lines into it; sequential re-runs (retries,
restarts) append — a retried command’s sink accumulates the failed
attempt’s output followed by the successful one’s, with no delimiter. For
per-run or per-attempt separation, tee to distinct sinks (a fresh Command
per run) or have the sink write its own delimiters.
The tee fires before the buffer policy decides retention, so it sees
every decoded line — including ones the capture buffer then drops or
rejects, e.g. output past a fail_loud
line ceiling (that ceiling bounds retained memory, not what streams past).
One exception: a single line whose length exceeds a byte cap
(with_max_bytes) is never
assembled, so it is neither retained nor teed — nor delivered to
on_stdout_line: the byte cap silently skips
that line for every sink alike, counted only via the
truncation/dropped() signal. Leave the byte cap unset (or use a line
cap) if every line must reach the tee. The discard verbs
(wait / profile) apply a large internal
in-flight byte cap for the same memory bound, so a line exceeding it is
likewise not teed under those verbs;
drain instead honors this configured
byte cap, so a line exceeding the configured
max_bytes is the one skipped there.
Requires stdout to be Piped (the default):
the tee fires from the capture pump, so it is a no-op under
stdout(Inherit) / stdout(Null), which
run no pump. It is likewise inert under
output_bytes, which captures stdout raw (no
line pump) — reach for a stdout tee with the line verbs (output_string,
start + stdout_lines, events).
Sourcepub fn stderr_tee<W>(self, writer: W) -> Self
pub fn stderr_tee<W>(self, writer: W) -> Self
Tee every decoded stderr line to writer as it is produced.
Same contract as stdout_tee — an async
tokio::io::AsyncWrite sink, awaited on the pump (backpressure, not
runtime-blocking), independent of on_stderr_line,
and requiring stderr to be Piped.
Sourcepub fn stdout_raw_tee<W>(self, writer: W) -> Self
pub fn stdout_raw_tee<W>(self, writer: W) -> Self
Tee the child’s stdout to writer byte for byte, before any decoding or
line splitting — a transparent passthrough that hands the consumer the
child’s exact bytes.
Where stdout_tee writes decoded lines (each plus a
\n), this writes each chunk exactly as read from the pipe, ahead of
the decoder. That is the difference between a log tee and a faithful
wrapper: the raw tee neither loses nor invents a single byte, so a
consumer can forward the stream live and hash/capture the exact output.
Concretely, unlike the decoded tee it preserves:
- non-UTF-8 bytes — a child writing binary to stdout (
git archive,tar -cz -,ffmpeg … -) is teed verbatim, not mangled into U+FFFD replacement characters; - CRLF and lone
\r— no newline normalization, no line framing; - a missing final newline — the last bytes are teed as-is, never
given a fabricated
\n; - an unterminated prompt —
Password:(no newline) reaches the sink the moment it is read, rather than waiting in the decode buffer until EOF the way a decoded line does, so an interactive child does not read as hung; - a line the capture policy drops — a line past a
with_max_bytesbyte cap is skipped from every decoded sink (the buffer, the handler, andstdout_tee), but its bytes still reach the raw tee whole — the digest a wrapper computes over the raw tee covers the child’s actual output, not a truncated re-encoding.
Chunks arrive in FIFO order within the stream. This is strictly
additive: the decoded-line path — capture buffer, its
OutputBufferPolicy, the dropped() truncation accounting,
on_stdout_line, and
stdout_tee — is unchanged whether or not a raw tee is
set, and all configured sinks fire independently.
§Backpressure and memory
writer is an async tokio::io::AsyncWrite. Each raw write is awaited
on the capture pump, the same backpressure seam as
stdout_tee: a slow sink slows the pump, the OS pipe
fills, and the child blocks on its next write. Nothing is buffered in the
crate between the pipe and the sink, so a lagging raw consumer cannot
grow unbounded in-flight memory — the bound is the OS pipe, not a heap
queue. A destination that blocks forever (not merely slow) stalls the
pump until teardown’s grace aborts it, exactly as for the decoded tee. A
write error disables the raw tee for the rest of the run (a tracing warn
under the tracing feature, not silently swallowed); the decoded path is
unaffected. The sink is flushed once at stream end, so a buffering writer
(BufWriter, a file) commits its tail.
Shared across clones and attempts, like stdout_tee:
the sink is held in an Arc<Mutex<…>>, so cloned Commands (pipeline
stages, supervisor incarnations, retry attempts) share one sink —
concurrent clones interleave their bytes, sequential re-runs append. Tee to
distinct sinks for per-run separation. A second stdout_raw_tee replaces an
earlier one.
§Requires a piped stdout; inert on the raw-capture verb
The raw tee fires from the line capture pump, so — like
stdout_tee — it is a no-op under
stdout(Inherit) / stdout(Null) and a
stdout_file redirect (all of which run no capture
pump); the builder accepts the combination but simply never invokes the
sink, rather than rejecting it. It is likewise inert under
output_bytes, whose own return value already is
the exact raw stdout (a separate raw drain, no line pump) — reach for the
raw tee with the line/streaming verbs (output_string, start +
stdout_lines / events, wait / drain) when you need the raw
bytes alongside decoded lines.
§Record/replay caveat
On a live run the tee is byte-exact. On a
ScriptedRunner double or a cassette
replay there is no child: the scripted feeder writes the canned/recorded
text back as UTF-8, so the raw tee receives those bytes — byte-exact
only insofar as the recorded (already-decoded) text round-trips, the same
fidelity limit that makes output_bytes unsupported
on a cassette. Rely on byte accuracy only against a real process.
Sourcepub fn stderr_raw_tee<W>(self, writer: W) -> Self
pub fn stderr_raw_tee<W>(self, writer: W) -> Self
Tee the child’s stderr to writer byte for byte, before any decoding or
line splitting.
Same contract as stdout_raw_tee — verbatim bytes
(non-UTF-8, CRLF, missing final newline, and lines the buffer policy drops
all preserved), FIFO order, awaited on the pump (backpressure, bounded
memory), flushed at stream end, independent of
stderr_tee/on_stderr_line,
and requiring stderr to be Piped.
Sourcepub fn output_buffer(self, policy: OutputBufferPolicy) -> Self
pub fn output_buffer(self, policy: OutputBufferPolicy) -> Self
Cap the in-memory backlog of captured output lines (see
OutputBufferPolicy). The pump still drains the pipe; only retention is
bounded.
This policy governs the capturing verbs
(output_string and a streamed
finish) and, for its byte ceiling
only, the in-flight bound of
drain. The discard verbs
wait / profile ignore it entirely
(they pin a fixed internal in-flight cap); drain is the discard path that
honors this byte cap — retaining nothing, but bounding held memory by the
configured max_bytes rather than the
child’s output size. max_lines never affects the discard paths (they
retain nothing).
Sourcepub fn capture_policy(self, policy: impl CapturePolicy + 'static) -> Self
pub fn capture_policy(self, policy: impl CapturePolicy + 'static) -> Self
Install a CapturePolicy — a typed redaction-at-capture seam that
shapes each decoded line of both streams just before it is retained,
so the value the policy returns (not the raw line) is what lands in the
capture backlog and therefore in
output_string /
ProcessResult and the streaming verbs
(stdout_lines /
events).
This is the capture-shaping counterpart to the observing
on_stdout_line/on_stderr_line
handlers: those see each line but run alongside capture and cannot
change what is retained. Use it to scrub a secret a child echoes to its
output before it settles in the captured result — completing the crate’s
secret-hygiene posture (a cassette stores env names only; Debug
redacts env values).
A single whole-command knob (like output_buffer):
the policy is handed the OutputStream each line
came from, so one implementation can treat stdout and stderr differently.
A repeat call replaces the previous policy (builder semantics).
§Scope
The seam shapes only the capture backlog. The per-line handlers, the
decoded stdout_tee/stderr_tee, the byte-plane
stdout_raw_tee/stderr_raw_tee, and the raw
stdout of output_bytes are
independent and see the line un-redacted — if you also tee to a log,
redact in that sink too. A line past an
OutputBufferPolicy byte cap
(with_max_bytes) is never
assembled, so — like the handlers/tee — the policy never sees it. See
CapturePolicy for the full contract (including its fail-closed panic
behavior).
Sourcepub fn stdout_encoding(self, encoding: &'static Encoding) -> Self
pub fn stdout_encoding(self, encoding: &'static Encoding) -> Self
Decode stdout with encoding instead of UTF-8 (e.g.
encoding_rs::SHIFT_JIS).
Sourcepub fn stderr_encoding(self, encoding: &'static Encoding) -> Self
pub fn stderr_encoding(self, encoding: &'static Encoding) -> Self
Decode stderr with encoding instead of UTF-8.
Sourcepub fn encoding(self, encoding: &'static Encoding) -> Self
pub fn encoding(self, encoding: &'static Encoding) -> Self
Decode both stdout and stderr with encoding.
Sourcepub fn line_terminator(self, terminator: LineTerminator) -> Self
pub fn line_terminator(self, terminator: LineTerminator) -> Self
Choose where the line pump splits both streams into lines (see
LineTerminator). The default is LineTerminator::Newline — split on
\n only, unchanged from before this knob existed.
Pass LineTerminator::CarriageReturn to also treat a bare \r as a line
terminator, so carriage-return progress output (curl/pip/apt: a
bar redrawn in place with \r, no \n until the end) streams live, one
frame at a time instead of piling up as a single line that only surfaces
at EOF. In that mode each \r-delimited frame is a line for every line
sink alike — stdout_lines /
events, the
on_stdout_line/on_stderr_line
handlers, the stdout_tee/stderr_tee
sinks, and output_string — so there is a single, shared notion of a line.
A \r\n pair stays one terminator (no empty line between them), and the
OutputBufferPolicy byte cap now bounds an individual runaway frame
rather than dropping the whole stream.
Set it per stream with
stdout_line_terminator /
stderr_line_terminator when only one
stream carries progress output (progress usually lands on stderr, while
stdout stays newline-structured data).
§Interaction with use_pty
A PTY child writes progress as bare \r redraws, so use_pty (with the
pty feature) makes the effective default terminator
CarriageReturn instead of Newline,
so a naive PTY consumer gets framed progress rather than one ever-growing
line (see use_pty for the rationale). Calling this
method — with either variant, including an explicit Newline — opts
out of that auto-default and pins your choice, order-independently of
use_pty.
Sourcepub fn stdout_line_terminator(self, terminator: LineTerminator) -> Self
pub fn stdout_line_terminator(self, terminator: LineTerminator) -> Self
Choose where the line pump splits stdout into lines (see
LineTerminator); the stderr framing is left untouched. See
line_terminator for both streams at once (and
for how it interacts with the use_pty auto-default).
Sourcepub fn stderr_line_terminator(self, terminator: LineTerminator) -> Self
pub fn stderr_line_terminator(self, terminator: LineTerminator) -> Self
Choose where the line pump splits stderr into lines (see
LineTerminator); the stdout framing is left untouched. Handy when
progress output lands on stderr while stdout stays newline-structured.
Sourcepub fn sanitize_vt(self) -> Self
pub fn sanitize_vt(self) -> Self
Enable the opt-in VT/ANSI output sanitizer on both streams’ capture
backlog: each decoded line is stripped of terminal escape sequences and
lone control codes before it is retained, so a line-oriented consumer sees
readable text instead of \x1b[31m…-mucked strings.
The motivating case is a PTY agent CLI (use_pty, the pty feature) whose
merged output is full of colors, cursor moves, alternate-screen
switches, and OSC title/hyperlink escapes: with this on, the line predicates
(wait_for_line / first_line),
output_string /
ProcessResult, and the streaming verbs
(stdout_lines /
events) all carry the de-escaped text.
It drops CSI (ESC [ … final), OSC (ESC ] … BEL/ST), DCS/SOS/PM/APC
string escapes, other two-/n-byte ESC escapes, and lone C0 control bytes
/ DEL — keeping the horizontal tab \t.
§Scope (the same boundary as capture_policy)
Sanitization shapes only the capture backlog, exactly like
capture_policy. The observing per-line handlers
(on_stdout_line/on_stderr_line), the decoded
stdout_tee/stderr_tee, the byte-plane
stdout_raw_tee/stderr_raw_tee, and the raw
output_bytes stream are
independent and keep seeing the un-sanitized bytes — if you also tee to
a log and want it clean, sanitize in that sink. When combined with
capture_policy, sanitization runs first so a
secret-scrubbing policy matches on already-cleaned text (a token cannot
hide behind a color escape). A line past an
OutputBufferPolicy byte cap is judged on its
raw length (before this transform) and, if over-cap, is never assembled
— so, like the handlers/tee, sanitization never sees it.
Off by default and strictly additive: an existing run that never calls this
captures byte-for-byte as before. Set it per stream with
stdout_sanitize_vt /
stderr_sanitize_vt.
Sourcepub fn stdout_sanitize_vt(self) -> Self
pub fn stdout_sanitize_vt(self) -> Self
Enable the sanitize_vt VT/ANSI sanitizer on
stdout only; stderr capture is left verbatim.
Sourcepub fn stderr_sanitize_vt(self) -> Self
pub fn stderr_sanitize_vt(self) -> Self
Enable the sanitize_vt VT/ANSI sanitizer on
stderr only; stdout capture is left verbatim.
Sourcepub fn command_line(&self) -> String
pub fn command_line(&self) -> String
Render this command as a single shell-quoted line for display — logs, error messages, a dry-run echo. Quoting is per-platform (POSIX single-quote / Windows double-quote) and is for readability, not execution: the crate never invokes a shell, and the rendering is not guaranteed to round-trip through one. Do not feed the output back to a shell to re-run the command — the escaping targets human legibility, not any specific shell’s parsing rules.
The line includes the arguments, which may carry secrets (a --token=…
flag). Unlike the tracing feature — which never logs argv — this is
opt-in: render it only into a sink you control.
Sourcepub fn working_dir(&self) -> Option<&Path>
pub fn working_dir(&self) -> Option<&Path>
The working-directory override, if one was set.
Sourcepub fn env_overrides(&self) -> &[(OsString, Option<OsString>)]
pub fn env_overrides(&self) -> &[(OsString, Option<OsString>)]
The environment overrides, in order (a None value removes the variable).
Sourcepub fn stdin_source(&self) -> Option<&Stdin>
pub fn stdin_source(&self) -> Option<&Stdin>
The configured stdin source, if any.
Sourcepub fn configured_timeout(&self) -> Option<Duration>
pub fn configured_timeout(&self) -> Option<Duration>
The configured deadline, if any — Some(d) for a
timeout(d), None for both an unset timeout and an
explicitly no_timeout (neither imposes a deadline).
Sourcepub fn configured_inactivity_timeout(&self) -> Option<Duration>
pub fn configured_inactivity_timeout(&self) -> Option<Duration>
The configured output-inactivity window, if any.
Sourcepub fn configured_ok_codes(&self) -> Option<&[i32]>
pub fn configured_ok_codes(&self) -> Option<&[i32]>
The exit codes explicitly configured via ok_codes, if
any — None when unset, in which case the default {0} applies (see
ok_codes_vec for the always-populated effective
set). Mirrors configured_timeout: the raw
configured state, not a resolved default — lets ScriptedRunner::when
predicates and other inspection code (see the “Public accessors” note
above) tell “left at the default” apart from “explicitly set to {0}”.
Sourcepub fn configured_cpu_affinity(&self) -> Option<&[usize]>
pub fn configured_cpu_affinity(&self) -> Option<&[usize]>
The canonical logical CPU set configured via
cpu_affinity, if any. The slice is sorted and
deduplicated; Some([]) preserves an explicitly empty (invalid) request so
inspection can distinguish it from an unset affinity before launch.
Sourcepub fn to_tokio_command(&self) -> Result<Command>
pub fn to_tokio_command(&self) -> Result<Command>
Lower this builder to a raw tokio::process::Command — the escape hatch
for a platform knob ProcessKit deliberately doesn’t model.
Prefer the typed verbs. Almost every launch should go through
run/output_string/output_bytes/start
or a ProcessGroup: those drive the async output
pump, capture, timeouts/cancellation, and the graceful-teardown machinery
for you. This bridge exists for the rare case where you need to set
something on the OS command that the builder has no typed knob for (a niche
creation flag, your own pre_exec), without re-deriving the crate’s
launch wiring by hand.
The returned command carries everything this builder resolves at the OS
level: the (optionally prefer_local-resolved) program and arguments, the
working directory, the layered environment
(env_clear/inherit_env/
env/env_remove), the platform launch
hooks (Unix priority/cpu_affinity/umask/privilege-drop/
setsid pre_exec hooks; Windows creation flags), and stdio wired to match the
builder’s stdout/stdin configuration (piped for
capture by default). Mutate the returned command, then hand it to
ProcessGroup::spawn to keep containment.
What you keep, and what you give up. Spawning the result through
ProcessGroup::spawn still enrolls the child
in the group’s Job/cgroup/process-group, so containment is preserved
(kill-on-drop and the group-level teardown verbs still reach it). You
give up the high-level machinery keyed off this builder that lives
above the OS command: the async output pump and capture, the
ProcessResult/RunningProcess verbs, and the per-run
timeout/cancel_on/
timeout_grace/
windows_graceful_ctrl_break wiring —
you drive the bare tokio::process::Child (draining
its pipes, reaping it) yourself. On Windows,
ProcessGroup::spawn re-sets the child’s
creation flags to make containment race-free, so a creation flag left on this
command is overwritten by that path (see its docs) — reach for the typed
create_no_window on a high-level launch path
instead.
§Errors
The same preflight failures a normal launch would raise while resolving the
program / opening a stdout_file redirect
(ErrorReason::Io), plus
ErrorReason::Unsupported for a
Linux-only I/O-priority request on another platform, affinity on a target
other than Linux/Windows, or Windows affinity (which requires the typed
suspended-child launch seam and cannot be encoded in a raw command).
Sourcepub fn spawn_detached(&self) -> Result<DetachedChild>
pub fn spawn_detached(&self) -> Result<DetachedChild>
Spawn the child deliberately released from this crate’s kill-on-drop
containment, handing back a DetachedChild whose lifetime is entirely
yours: the crate will never kill, reap, time out, or capture it.
§Warning — this inverts the crate’s headline guarantee
Every other run/start verb keeps the child in a kill-on-drop container,
so nothing is orphaned. spawn_detached is the crate’s one deliberate
escape hatch for the legitimate handoff cases — daemonizing, a
nohup-style long-lived helper meant to outlive the launcher — where the
child must survive its owner. Reach for it only when you truly want that;
for everything else, start/run/output_* are what you want.
The returned DetachedChild is a separate, non-interchangeable type
(not a RunningProcess) carrying nothing but the
pid — no kill, no timeout, no capture, no
teardown — precisely because it is no longer contained.
§Detach happens at birth
- Unix — the child is launched into a new session (
setsid), so it has no controlling terminal and its own session/process group; it is not tracked by any of this crate’s kill-on-drop groups. - Windows — the child is not assigned to this crate’s Job Object, so closing/dropping any handle here cannot kill it. It is not made to break away from an external Job Object the OS already places it in — see below.
§Still bound by a host container (by design)
“Detached” means detached from this crate’s per-run containment — not
from a broader host container your process already lives in. If your process
runs under an external Windows Job Object or a Linux cgroup (a CI runner, a
systemd scope, this crate’s own supervisor), the child may still inherit
and be bound by it. spawn_detached deliberately does not attempt a job
breakaway or cgroup escape: that would be hostile to whoever set up the host
containment (and on Windows would simply fail the spawn where breakaway is
disallowed).
§stdio: null, or a file — never a pipe
A detached child has no owner draining its output, so a pipe would deadlock
it the moment the buffer fills after you go away (the classic daemon bug).
stdout/stderr are therefore null by default; the only alternative is a
file redirect (stdout_file /
stderr_file and their _append forms). stdin is
always null. A pipe or an inherited parent fd is rejected (see below).
§Rejected configuration — a loud, typed refusal
A detached child has no owner to enforce a timeout, no pump to capture
output, and no interactive stdin, so a Command carrying any of those knobs
is refused with ErrorReason::Unsupported
naming it — never silently ignored (the same “fail loud, don’t drop a
requested behavior” contract as uid/gid/umask off Unix). The refused
knobs are: timeout/timeout_grace,
retry/retry_with,
cancel_on,
kill_on_parent_death (its exact opposite),
windows_graceful_ctrl_break,
keep_stdin_open/inherit_stdin/
a configured stdin source, any capture wiring
(on_stdout_line/on_stderr_line,
the tee sinks, a capture_policy), an inherited
stdout/stderr connection, and (with the pty feature) use_pty.
Program/argument/env/working-directory and the
privilege-drop knobs (uid/gid/
groups/umask/priority)
are honored — a detached daemon may still drop privileges.
§Not async
Detaching is fire-and-forget: there is nothing to await and no tokio runtime
is required, so this is a plain synchronous spawn (like the low-level
ProcessGroup::spawn escape hatch), callable
from daemonizing code that runs before any runtime exists.
§Errors
ErrorReason::Unsupported— an incompatible knob (above), or a POSIX-only privilege primitive requested off Unix.ErrorReason::NotFound— the program could not be located.ErrorReason::Spawn— the program was located but the OS refused to start it (bad working directory, permission denied, a Windows.cmd/.batthat needscmd.exe, …).
use processkit::Command;
// Launch a long-lived helper that must outlive this process, logging to a
// file (never a pipe — there is no owner left to drain one).
let child = Command::new("my-daemon")
.arg("--serve")
.stdout_file("/var/log/my-daemon.log")
.spawn_detached()?;
println!("detached daemon pid = {}", child.pid());
// Dropping `child` does NOT kill the daemon — it keeps running.Sourcepub async fn start(&self) -> Result<RunningProcess>
pub async fn start(&self) -> Result<RunningProcess>
Start the command and return a live RunningProcess backed by a fresh
private group. Use this for streaming stdout
(RunningProcess::stdout_lines) or inspecting the process while it
runs; keep the handle in scope, as dropping it tears the tree down.
§Errors
The launch surface shared by every run verb on Command:
ErrorReason::NotFound— the program could not be located (not installed, not onPATH, or the given path does not resolve to an executable).ErrorReason::Spawn— the program was located but the OS refused to start it (permission denied, a missing or non-directory working directory, a Windows.cmd/.batthat needscmd.exe, …).ErrorReason::Unsupported— a requested POSIX-only primitive (running as another user/group, a new session viasetsid, or aumask) is not available on this platform.ErrorReason::Cancelled— thecancel_ontoken was already cancelled before the spawn.ErrorReason::Io— the privateProcessGroupbacking the run could not be created, or a one-shot streaming stdin source (Stdin::from_reader/Stdin::from_lines) was already consumed by a previous run.ErrorReason::ResourceLimit— a resource cap configured on the run’s group could not be enforced.
Sourcepub async fn output_string(&self) -> Result<ProcessResult<String>>
pub async fn output_string(&self) -> Result<ProcessResult<String>>
Run to completion and capture stdout as text, stderr, and the exit code.
A non-zero exit is reported, not raised — call
ProcessResult::ensure_success to turn it into an error.
§Errors
The launch failures listed on start. A non-zero exit, a
timeout, and a signal-kill are captured in the returned
ProcessResult rather than raised (call
ensure_success to promote them);
beyond launch, only ErrorReason::Cancelled (a cancellation is always
raised), ErrorReason::OutputTooLarge (a fail-loud buffer overflowed),
ErrorReason::Stdin (a non-broken-pipe stdin failure on an
otherwise-successful run), and ErrorReason::Io surface.
Sourcepub async fn output_bytes(&self) -> Result<ProcessResult<Vec<u8>>>
pub async fn output_bytes(&self) -> Result<ProcessResult<Vec<u8>>>
Run to completion and capture stdout as raw bytes (plus stderr/exit code).
§Errors
Identical to output_string — a non-zero exit, a
timeout, or a signal-kill is captured in the ProcessResult, not raised
— except that a fail-loud ErrorReason::OutputTooLarge applies to the raw
stdout byte ceiling.
Sourcepub async fn exit_code(&self) -> Result<i32>
pub async fn exit_code(&self) -> Result<i32>
Run to completion and return just the exit code (output is discarded). A
run that yields no code surfaces as an error — a timeout as
ErrorReason::Timeout, a signal-kill as
ErrorReason::Signalled — consistent with
ProcessRunnerExt::exit_code and
CliClient::exit_code.
§Errors
The launch failures listed on start, plus — when the run
produced no code — ErrorReason::Timeout (the deadline elapsed),
ErrorReason::Signalled (killed by a signal), or ErrorReason::Cancelled. A
non-zero exit is returned as the code, not raised.
Sourcepub async fn run(&self) -> Result<String>
pub async fn run(&self) -> Result<String>
Run to completion, requiring an accepted exit (0 by default, widened
by ok_codes), and return trimmed stdout. Any other
code is ErrorReason::Exit.
§Errors
The launch failures listed on start, plus the
success-checking failures: ErrorReason::Exit (a non-accepted exit code),
ErrorReason::Signalled (a signal-kill), ErrorReason::Timeout (the deadline
elapsed — raised here, unlike on
output_string), ErrorReason::Cancelled,
ErrorReason::OutputTooLarge (a fail-loud buffer truncated the presented
stdout), and ErrorReason::Stdin (a non-broken-pipe stdin failure on an
otherwise-successful run).
Sourcepub async fn checked(&self) -> Result<ProcessResult<String>>
pub async fn checked(&self) -> Result<ProcessResult<String>>
Run to completion, require an accepted exit, and return the full
captured ProcessResult (untrimmed stdout) — the building block when you
need the whole result after success-checking rather than trimmed stdout
(run) or the raw result (output_string).
Consistent with ProcessRunnerExt::checked
and CliClient::checked.
§Errors
The same success-checking surface as run —
ErrorReason::Exit / ErrorReason::Signalled / ErrorReason::Timeout /
ErrorReason::Cancelled / ErrorReason::Stdin, atop the launch failures on
start — except that, as the lenient building block,
checked does not fail loud on a bounded-buffer truncation (inspect
ProcessResult::truncated yourself), so
it never returns ErrorReason::OutputTooLarge.
Sourcepub async fn run_unit(&self) -> Result<()>
pub async fn run_unit(&self) -> Result<()>
Run for the side effect: require an accepted exit (0, or any code in
ok_codes) and discard the output. Consistent with
ProcessRunnerExt::run_unit and
CliClient::run_unit.
§Errors
The same surface as checked (the launch failures on
start plus ErrorReason::Exit / ErrorReason::Signalled /
ErrorReason::Timeout / ErrorReason::Cancelled / ErrorReason::Stdin); only the
captured output is discarded.
Sourcepub async fn probe(&self) -> Result<bool>
pub async fn probe(&self) -> Result<bool>
Run a predicate command and read its exit code as a boolean: exit 0 →
Ok(true), exit 1 → Ok(false), anything else → Err (any other code
as ErrorReason::Exit, a timeout as ErrorReason::Timeout,
a signal-kill as ErrorReason::Signalled). For tools
whose exit code is the answer —
git diff --quiet, git show-ref --verify --quiet, grep -q, …
§Errors
Any exit code other than 0/1 becomes ErrorReason::Exit, and — atop the
launch failures on start — a run that produced no code
errors as ErrorReason::Timeout, ErrorReason::Signalled, or
ErrorReason::Cancelled. The strict 0/1 contract holds regardless of the
command’s ok_codes.
Sourcepub async fn parse<T, F>(&self, parse: F) -> Result<T>
pub async fn parse<T, F>(&self, parse: F) -> Result<T>
Run (requiring an accepted exit) and feed stdout to an infallible
parse closure, returning the parsed value. Fails loud on a bounded-buffer
truncation so the parser never sees a clipped tail. Consistent with
ProcessRunnerExt::parse and
CliClient::parse.
§Errors
The success-checking surface of run (the launch failures on
start, plus ErrorReason::Exit / ErrorReason::Signalled /
ErrorReason::Timeout / ErrorReason::Cancelled / ErrorReason::Stdin), plus
ErrorReason::OutputTooLarge when a fail-loud buffer truncated the stdout the
parser would see. The parse closure is infallible, so it adds no error.
Sourcepub async fn try_parse<T, F>(&self, parse: F) -> Result<T>
pub async fn try_parse<T, F>(&self, parse: F) -> Result<T>
Run (requiring an accepted exit) and feed stdout to a fallible
parse closure (the JSON-deserialization shape; a failure becomes
ErrorReason::Parse or whatever the closure returns).
Fails loud on truncation. Consistent with
ProcessRunnerExt::try_parse and
CliClient::try_parse.
§Errors
Everything parse can return, plus whatever the fallible
parse closure yields on malformed output — typically
ErrorReason::Parse.
Sourcepub async fn first_line<F>(&self, predicate: F) -> Result<Option<String>>
pub async fn first_line<F>(&self, predicate: F) -> Result<Option<String>>
Return the first stdout line matching predicate (or the first line when
the predicate is trivial), then tear the process down.
§Errors
The launch failures listed on start, plus
ErrorReason::Timeout when a timeout is set and its
deadline elapses mid-stream (which tears the process down),
ErrorReason::Cancelled, or ErrorReason::Io while streaming. A stream that ends
with no match is Ok(None), not an error.
Sourcepub fn resolve_program(&self) -> Result<PathBuf>
pub fn resolve_program(&self) -> Result<PathBuf>
Resolve this command’s program to a concrete executable path without
launching it — a spawn-free preflight, for a doctor / early-diagnosis
check (“is git installed?”) that must have no side effects. Unlike
probe (which actually runs the tool), this only locates
it — no process is ever started.
Resolution is byte-for-byte the same as the one the real launch performs,
because it reuses the same internal logic — not a second copy: a bare
name is resolved against this command’s prefer_local
directories first (in priority order), then the PATH, honoring PATHEXT on
Windows and the execute bit on Unix; a path-form program (absolute, or
relative with a separator) is probed directly, exactly as the OS receives
it. When the command has relocated the child’s PATH
(env/env_remove of PATH,
env_clear, or inherit_env),
the lookup runs against that effective child PATH, so preflight never
disagrees with which list the spawn searches.
A resolved hit is exactly what a run spawns, at that same path — on
Windows including a bare name found only through a non-.exe PATHEXT
extension (.cmd/.bat/.com/…): the launch substitutes the resolved
absolute path (the OS’s own bare-name search appends only .exe), so such
a hit spawns instead of raising ErrorReason::Spawn. The one residual
asymmetry is a preflight miss on Windows: the OS can still locate a
bare name through the application directory, the current directory, or the
system directories — routes this PATH-based model doesn’t cover — so a
miss there is not proof a run couldn’t launch it. Unix (execvp,
PATH-only) has no such gap.
On success returns the resolved absolute path. This is a synchronous,
cheap filesystem probe (a few stats) — no async runtime is required.
§Errors
ErrorReason::NotFound when the program can’t be
located — not installed, not on PATH, or a path that doesn’t resolve to
an executable. Its searched field lists the directories that were
checked (prefer_local first, then PATH) for a bare-name lookup, and is
None for a path-form program; is_not_found
classifies it, exactly as it would for the same missing program on a real
run.
Trait Implementations§
Source§impl BitOr for Command
a | b — sugar for Command::pipe. Parenthesize the chain before a
terminal verb, since method calls bind tighter than |.
impl BitOr for Command
a | b — sugar for Command::pipe. Parenthesize the chain before a
terminal verb, since method calls bind tighter than |.
Source§impl BitOr<Command> for Pipeline
pipeline | c — sugar for Pipeline::pipe, so a | b | c chains
left-associatively into one pipeline.
impl BitOr<Command> for Pipeline
pipeline | c — sugar for Pipeline::pipe, so a | b | c chains
left-associatively into one pipeline.