Logo ProcessKit API Reference

ProcessKit Namespace

Type/Module Description

CliClient (Module)

Pipe-friendly entry point for `CliClient`.

CliClient (Type)

A reusable handle to one command-line program with shared defaults applied to every invocation. The defaults live in a *template* `Command`, configured with the full `Command` builder via `WithDefaults` (timeout, working directory, environment, encoding, ok-codes, retry, logger, …) — no separate, partial setter API to learn. Build `Command`s for argument lists (`client.Command [ "status" ]`) or run them straight through the client's runner (`client.RunAsync [ "status" ]`).

Command (Module)

Pipe-friendly functions over `Command`, mirroring the instance **builder** methods. The run verbs (`RunAsync`/`OutputStringAsync`/`ParseAsync`/…) are instance methods only — end a pipeline with method syntax (`(cmd |> Command.arg "x").RunAsync()`), or go through `Runner.*` with an explicit runner.

Command (Type)

An immutable description of a process to run. Build it fluently — each method returns a new `Command`. The value is the *cold* description of a run; the process is launched only when a verb (`Runner.run`, `Command.start`, …) is invoked. Use the instance methods (`cmd.Arg "x"`) or the `Command` module's pipe-friendly functions (`cmd |> Command.arg "x"`).

CommandVerbs

Default-runner convenience verbs on `Command`, callable from F# and C# as `command.StartAsync()` / `command.RunAsync()` etc. They use a shared `JobRunner`; for a custom or injected runner, go through `Runner.*` or call the runner directly. The `cancellationToken` is optional and defaults to `CancellationToken.None`.

DelegatingProcessRunner

A pass-through `IProcessRunner` base for decorators: it forwards all three verbs to `inner`, so a wrapper — logging, retry, metrics, fault injection, fixed-latency, recording — overrides only the verb(s) it changes and inherits the rest. Each verb is an overridable member; the interface dispatches to it.

Exec

Top-level conveniences: run a program by name (without first building a `Command`), and run a whole batch of commands with bounded concurrency. The single-command verbs are zero-config one-liners (for cancellation, build a `Command` and use its verbs, or go through `Runner`); the batch verbs take an explicit `CancellationToken` so a long fan-out can be cancelled.

Finished

The result of finishing a streamed run: how it concluded plus its captured stderr. Returned by `RunningProcess.FinishAsync`, after stdout has been consumed as a stream. Sealed with an internal constructor so it can gain fields without breaking the frozen API.

IProcessRunner

The seam through which commands run to completion: three low-level primitives an implementer or mock provides. The public verb vocabulary (`RunAsync`/`OutputStringAsync`/`ExitCodeAsync`/… on a `Command`, a runner, a `CliClient`, or a `Pipeline`) is layered on top of these by the `Runner` module and its extension methods — so the primitives are named distinctly (`Capture*`/`Spawn`) and never collide with the verbs. (That collision used to force the capture verbs to silently drop the retry policy when called with a `CancellationToken`; with distinct names each verb is a single method taking an optional `CancellationToken` that retries uniformly whether or not a token is passed.) The default runner spawns real processes into a kill-on-drop group; test doubles (`ProcessKit.Testing.ScriptedRunner`) script replies with no subprocess. This interface is both the dependency-injection point and the test seam, so any .NET consumer can implement or mock it with plain `Task`-returning methods.

JobRunner

The default `IProcessRunner`: spawns each command into a fresh kill-on-dispose `ProcessGroup` (owned by the returned `RunningProcess`), then captures or streams its output and reaps the whole tree on completion, failure, or cancellation.

LineTerminator

How the output pump decides where one captured or streamed line ends — set per stream on `Command` via `Command.LineTerminator` (both streams at once) or `Command.StdoutLineTerminator` / `Command.StderrLineTerminator`. The default is `Lf`, which reproduces ProcessKit's original line-splitting behaviour exactly. This is one shared definition of "a line" for the whole line-pumped path: what `RunningProcess.StdoutLinesAsync` / `OutputEventsAsync` yield, what a `WaitForLineAsync` predicate sees, what the per-line handlers (`Command.OnStdoutLine` / `OnStderrLine`) receive, and what `OutputStringAsync` joins. Choosing a mode moves all of them together — there is never a per-sink disagreement about what a line is. It is orthogonal to the raw byte path: `OutputBytesAsync` and the tees (`Command.StdoutTee` / `StderrTee`) stay byte-exact and are unaffected by the mode. A `\r\n` pair is always treated as a **single** terminator in every mode (it never emits a spurious empty line between the `\r` and the `\n`), so ordinary CRLF text reads identically across the modes; the modes differ only in whether a *lone* `\n` or a *lone* `\r` ends a line.

Mechanism

The OS primitive a `ProcessGroup` uses to contain a process tree. Reported honestly (never a silent downgrade) so callers can reason about the containment guarantee on the current platform.

Outcome

How a process run concluded.

OutputBufferPolicy

Caps how many captured/streamed output lines are retained in memory. The pump always drains the OS pipe (the child never blocks on a full buffer); this only bounds the in-memory backlog. Line counters still count every line, so a count greater than the retained amount reveals that lines were dropped. Two independent ceilings — lines and bytes — either or both of which may be set.

OutputEvent

A single event in a merged stdout+stderr stream, tagged with its origin.

OutputLine

One line of captured output, with its terminating newline stripped. A sealed type (not a bare `string`) so the line can gain metadata — a timestamp, a monotonic sequence number — in a later minor version without breaking the frozen API.

OverflowMode

What to drop when a bounded output buffer is full.

Pipeline (Module)

Pipe-friendly functions over `Pipeline`, mirroring the instance methods.

Pipeline (Type)

An immutable left-to-right chain of commands wired stdout -> stdin, with **no shell** involved: each stage's standard output feeds the next stage's standard input directly. The whole chain runs inside one shared kill-on-dispose group, so cancelling, timing out, or disposing the run reaps every stage together. Build it by piping commands (`a.Pipe(b).Pipe(c)`), then run it to completion with the same run-and-capture verbs a single command exposes (`RunAsync`/`OutputStringAsync`/`OutputBytesAsync`/`ExitCodeAsync`/ `ProbeAsync`/`ParseAsync`/`TryParseAsync`). A pipeline runs as a whole, so the *streaming* verbs (`FirstLineAsync`, `StdoutLinesAsync`) are deliberately not offered — capture the last stage's output instead. The exit status follows shell **pipefail**: the rightmost stage that did not exit with an accepted code (its `Command.OkCodes`, `{0}` by default) determines the result, unless that stage opted out with `Command.UncheckedInPipe`. Per-stage I/O config that applies inside a pipeline: each stage's `OkCodes` (pipefail) and `UncheckedInPipe`, the last stage's `StdoutEncoding`, `StdoutTee`, and `OutputBuffer` **byte** cap (`MaxBytes` + `Overflow`, applied to the captured stdout — its `MaxLines` never applies to a raw byte capture), stage 0's `Stdin` source (feeding the whole chain), and the chain-level `Timeout` / `CancelOn`. Every stage's stderr is likewise drained under its OWN `OutputBuffer`'s **byte** cap (`MaxBytes` + `Overflow`; a stage without `MaxBytes` set keeps its stderr unbounded, as before), so a chatty stage can never exhaust memory regardless of its position in the chain. Per-stage *stdout/ stderr observation* hooks are still **not** applied — intermediate stages' `StdoutTee`, every stage's `StderrTee`, and `OnStdoutLine`/`OnStderrLine` — because the chain wires stdout into the next stage's stdin and captures only the final stage's output. Observe an individual command by running it on its own, not as a pipeline stage. Per-stage config a pipeline cannot honour is **rejected when the stage is piped** (an `ArgumentException` from `Pipe`, naming the field and stage index), rather than silently dropped: a `Stdin` source on any stage *after the first* (its stdin is always rewired to the previous stage's stdout — only stage 0 may set a source), a per-stage `Timeout` on any stage (only the chain-level `Pipeline.Timeout` bounds a pipeline; `Command.Timeout` on a stage never fires), a per-stage `IdleTimeout` on any stage (a pipeline captures only the last stage's output and does not monitor per-stage output activity, so a stage's own idle deadline can never fire), a per-stage `Retry` on any stage (retry is a verb-layer mechanism, and stages spawn directly, bypassing it), and a per-stage `CancelOn` on any stage (a stage's own `Command.CancelOn` token is likewise a verb-layer mechanism the direct stage spawn bypasses; only the chain-level `Pipeline.CancelOn` cancels a pipeline). Set the deadline on the pipeline, cancel the whole chain with `Pipeline.CancelOn`, feed stage 0, or run the command on its own. `MergeStderr` (a shell `2>&1`) is allowed only on the **last** stage — its stdout is the pipeline's captured output, so merging captures the final stage's combined stdout+stderr. On any earlier stage it is rejected (`ArgumentException`) the moment the stage stops being last (another stage is appended after it): a pipeline wires each stage's stdout into the next stage's stdin, so an OS-level merge on an intermediate stage would inject its stderr into the downstream stage's input data. Observability is whole-pipeline, not per-stage: running the chain emits one `Log.spawn`/`Log.exit` pair (plus `Log.timeout` on a timeout) and one `Diag.runStarted`/`runCompleted`/`runEnded` triple, all sharing a single run id — never one set per stage. Stage 0's `Logger` becomes the pipeline's logger (a per-stage `Logger` on any *other* stage has no effect — set it on stage 0, or observe an individual command by running it on its own); the `program` tag/label is a composite of every stage's name, joined `"a | b | c"` (built only from `Command.Program`, never argv/env, so the argv/env-never-logged invariant holds for a multi-stage run too). Per-stage config that is simply **inapplicable** inside a pipeline and has no effect: `StreamBuffer` (a policy for the streaming verbs, which a pipeline does not offer), and `KeepStdinOpen` / `RunningProcess.TakeStdin` (a pipeline exposes no live handle to write stdin into — it wires each stage after the first from the previous stage's stdout itself, and a `KeepStdinOpen` there is an internal wiring detail, not user-reachable).

PipelineExtensions

`Command.Pipe` builds a two-stage `Pipeline`; further `Pipeline.Pipe` calls extend it.

Priority

 A portable CPU-scheduling priority for a child process (see `Command.Priority`), mapped onto the
 native primitive at spawn time: a **Windows** process priority class OR'd into the `CreateProcess`
 creation flags (the same seam as `Command.CreateNoWindow`), or a **Unix** `nice` value applied via
 `setpriority` to the spawned process-group leader.

 Every variant is supported on **both** platform families — `setpriority` is plain POSIX (Linux,
 macOS, the BSDs alike) and every Windows edition has all five priority classes — so
 `Command.Priority` never yields `ProcessError.Unsupported`.

 **How far the priority reaches into the spawned tree depends on the platform and the level.** It
 always takes on the immediate child on both platforms; whether the child's own descendants
 (grandchildren) inherit it differs:

 - **Unix — whole tree, every level.** A `nice` value is inherited across `fork`, so every
   descendant the leader spawns runs at the requested priority. One honest divergence: the `nice`
   is applied to the group leader *immediately after* `posix_spawn` returns (there is no
   `posix_spawn` attribute for it), so a descendant the leader forks in the sub-millisecond window
   before that call lands keeps the inherited default — the same spawn→apply window the cgroup
   mechanism already documents.

 - **Windows — whole tree only for the lowered classes.** The priority class is set atomically at
   process creation (no spawn→apply window), but Windows only *inherits* a class to grandchildren
   when it is lowered. Per `CreateProcess`, a child spawned with no priority-class flag defaults to
   `NORMAL_PRIORITY_CLASS` *unless* its creator is `IDLE_PRIORITY_CLASS` or
   `BELOW_NORMAL_PRIORITY_CLASS`, in which case it inherits that class. So `Idle`/`BelowNormal`
   (and `Normal`) reach the whole tree, but for `AboveNormal`/`High` the grandchildren a child
   later spawns run at `Normal`, not the requested elevated class. The elevation is still honored
   on the immediate child for all five levels — only its inheritance by grandchildren is the
   platform limit, and it is never a silent downgrade of the child you launched.

 Only ordinary (non-real-time) priorities are exposed; `Priority` never raises a real-time class,
 and I/O scheduling is out of scope.

ProcessError (Module)

ProcessError (Type)

Structured failure type for ProcessKit operations. Named `ProcessError` (rather than just `Error`) to avoid colliding with the `Result.Error` constructor in F#. Honest-result verbs — `outputString`, `outputBytes`, `exitCode`, `probe` — return their value; only genuine failures surface as a `ProcessError` in the `Result` channel.

ProcessException

Raised by `Result.GetValueOrThrow()` when a run failed, carrying the structured `ProcessError`. The honest-result verbs return `Result<_, ProcessError>` and never raise this — it exists only for the exception-style convenience path, which reads naturally from C# (`string sha = (await cmd.RunAsync()).GetValueOrThrow();`).

ProcessGroup

A kill-on-dispose container for a process *tree*. Every process started into the group — and everything those processes spawn — is reaped when the group is disposed (deterministic under `use`) or, failing that, when the GC finalizes it. The OS primitive is chosen at creation and reported honestly by `Mechanism` — a Windows Job Object (`KILL_ON_JOB_CLOSE`), a Linux cgroup v2 (when resource limits are requested), or a POSIX process group (`killpg` teardown). All of that lives behind an `IContainmentBackend`; this type only orchestrates once-only teardown, the stdin/stream wiring, and the runner/disposable seams.

ProcessGroupOptions

Options applied when creating a `ProcessGroup`: the graceful-shutdown window and whole-tree resource limits.

ProcessGroupStats

A snapshot of a process group's resource usage. `TotalCpuTime` and `PeakMemoryBytes` are `None` when the platform can't report them — the POSIX process-group mechanism (macOS and the Linux fallback) has no kernel accumulator; the Linux cgroup v2 backend (the `limits` feature) supplies them. Sealed with an internal constructor so it can gain metrics without breaking the frozen API.

ProcessKitDiagnostics

The well-known names and identifiers of ProcessKit's `System.Diagnostics` / `Microsoft.Extensions.Logging` observability surface, so a consumer references them without a magic string or number — e.g. `builder.AddSource(ProcessKitDiagnostics.ActivitySourceName)` / `builder.AddMeter(ProcessKitDiagnostics.MeterName)`, or filter logs by `ProcessKitDiagnostics.Events.ProcessExited`. **Security:** neither a log message, a trace span tag, nor a metric tag ever carries argv or environment **values** — only the program *name* and non-secret facts (outcome, duration, exit code / signal, pid, run id).

ProcessResult

ProcessResult<'T>

The full outcome of a run: the exit code as data, captured stdout/stderr, and timing. A non-zero exit is **not** an error here — inspect `Code`/`IsSuccess`, or call `ProcessResult.ensureSuccess` to convert a failure into a `ProcessError`. `'T` is the captured-stdout type: `string` for the text verbs, `byte[]` for the bytes verbs.

ProcessRunnerExtensions

The full run-verb vocabulary on *any* `IProcessRunner`, layered over the three-method seam (`CaptureStringAsync`/`CaptureBytesAsync`/`SpawnAsync`). So a chosen or injected runner — a shared `ProcessGroup`, a `ScriptedRunner`, a `JobRunner` — gets `run`/`exitCode`/`probe`/`parse`/… uniformly (`group.RunAsync command`, `scripted.ProbeAsync command`), callable from F# and C#. The verb *logic* lives once in the `Runner` module; these are thin sugar over it. Retry contract: every capture verb here applies the command's `Retry` policy (it routes through the `Runner` module). The `cancellationToken` is optional and defaults to `CancellationToken.None`, so `runner.RunAsync command` and `runner.RunAsync(command, ct)` are the same method and retry identically. (Because the seam primitives are named `Capture*`/`Spawn`, the verb names never collide with them, so adding a token can't silently bypass retry.) For a raw, single, no-retry capture call the seam primitive directly: `runner.CaptureStringAsync(command, ct)`. Streaming verbs (`StartAsync`/`FirstLineAsync`) never retry.

ProcessStdin

A handle for writing to a running child's standard input interactively. Obtained from `RunningProcess.TakeStdin` when the command was built with `Command.Stdin` / `Command.KeepStdinOpen`. Call `FinishAsync` to close stdin (the child sees end-of-file). Each write accepts an optional `CancellationToken`: a child that stops reading fills the stdin pipe and blocks the write, so a token lets the caller bound how long it waits (a cancelled write throws `OperationCanceledException`, the .NET convention for a cancelled `Task`). As with any cancellable stream write, a cancelled write may already have delivered *some* of its bytes to the child, so the safe recovery from a timed-out interactive write is to abandon the session — not to retry the write, which would duplicate the delivered prefix.

ResourceLimits

Resource limits enforced on a process group **as a whole** (not per process), applied to the kernel container at creation time. Enforcement needs a real container — a **Windows Job Object** or a **Linux cgroup v2**. On macOS and the Linux process-group fallback there is no whole-tree limit primitive, so requesting *any* limit there fails fast with `ProcessError.ResourceLimit` rather than silently leaving the tree unbounded. On Linux the cgroup v2 controllers can only be enabled when this process runs at the real cgroup-v2 hierarchy root (not under a systemd scope, nor in an ordinary container); when they cannot, group creation fails fast for the same reason.

RestartPolicy

When the supervisor restarts an exited child. In every case `Supervisor.StopWhen` and `Supervisor.MaxRestarts` can end supervision first.

ResultExtensions

 C#-idiomatic consumption of the `Result<'T, ProcessError>` that every verb returns. F# matches
 `Ok`/`Error` directly; C# pattern-matches the result with no projection or helper call, using the
 result's own members:

 
 await cmd.OutputStringAsync() switch
 {
     { IsOk: true,  ResultValue: var run } => run.Stdout,
     { IsOk: false, ErrorValue: var err } => err.Message,
 }
 

 The match is exhaustive (the `IsOk` bool covers both arms — no discard needed); `ResultValue` is
 read only in the `IsOk: true` arm (the pattern short-circuits) and binds non-null, so no `!`. For
 non-`switch` styles these extensions give `Match` / `Switch`, `TryGetValue`, and `GetValueOrThrow`
 without touching the result's members. A `Result` is never `null`, and the Try out-parameters
 follow the standard .NET pattern.

Runner

The run verbs, expressed over any `IProcessRunner`. One verb, one meaning: - `run` — require a zero exit; return stdout, trailing whitespace trimmed. - `outputString` / `outputBytes` — the full `ProcessResult`; a non-zero exit is data. - `exitCode` — the exit code; a signal kill or timeout errors instead of inventing one. - `probe` — read the exit code as a yes/no: 0 -> true, 1 -> false, anything else errors.

RunningProcess

A live handle to a started process: stream its output, feed its stdin, wait for it, or collect it to completion. Disposing it reaps the whole process tree (kill-on-drop).

RunProfile

Resource summary of one finished run — produced by `RunningProcess.ProfileAsync`. CPU and memory come from the started child process (the same source as `RunningProcess.CpuTime` / `PeakMemoryBytes`), so they are `None` where per-process metrics are unavailable or when the run exited before the first sample landed. Sealed with an internal constructor.

Signal

A signal to broadcast to every process in a `ProcessGroup` via `ProcessGroup.Signal`. The curated variants map to the POSIX signal of the same name on Unix. On **Windows** `Kill` maps to the Job Object terminate (the same hard kill as `ProcessGroup.KillAll`), and `Int`/`Term` are delivered as a best-effort console CTRL+BREAK — but only to a child started with `Command.WindowsCtrlSignals()`; without one (or without a console to share) they yield `ProcessError.Unsupported`, never a silent downgrade. Every other variant yields `ProcessError.Unsupported` on Windows. `Other` is an escape hatch carrying a raw signal number on Unix (e.g. `SIGWINCH`); it is always unsupported on Windows. `SIGSTOP`/`SIGCONT` are deliberately absent from the curated set — pause and resume the whole tree with `ProcessGroup.Suspend` / `ProcessGroup.Resume`, which are portable (Windows included); `Signal.Other` with the raw `SIGSTOP` number remains available when the raw signal is specifically wanted on Unix.

Stdin

A source for a child process's standard input, attached with `Command.Stdin`. When set, the child's stdin is a pipe fed from this source; the pipe is closed (EOF) once the source is exhausted, unless `Command.KeepStdinOpen` is also set.

StdioMode

How a child's stdout or stderr stream is connected. Set per-stream on `Command` via `Stdout`/`Stderr`; the default is `Piped`.

StopReason

Why supervision ended.

StreamBufferPolicy

An opt-in bounded/backpressure policy for the streaming verbs (`StdoutLinesAsync` / `OutputEventsAsync` / `WaitForLineAsync`), set via `Command.StreamBuffer`. Unlike `OutputBufferPolicy` — which bounds an in-memory *buffer* a one-shot verb assembles — this bounds the *channel* between the background pump and your live consumer. Leaving it unset keeps today's unbounded channel: an unbounded, uncapped in-flight backlog, exactly as before this policy existed.

StreamFullMode

How a bounded *streaming* channel behaves once its capacity is reached — the streaming analogue of `OverflowMode`, but with a genuine backpressure option that only makes sense against a live consumer (a buffered one-shot verb has no such consumer to pace, which is why `OutputBufferPolicy` has no equivalent case).

SupervisionOutcome

What a finished supervision reports — the last run plus the keeper's telemetry. Sealed with an internal constructor so it can gain fields without breaking the frozen API.

Supervisor (Module)

Pipe-friendly entry points for `Supervisor`.

Supervisor (Type)

Keeps a `Command` alive: runs it, classifies every exit against the `RestartPolicy` and the `StopWhen` predicate, and restarts it after an exponential-backoff delay until supervision ends. `Command.Retry` answers "run this once, replaying on failure"; a supervisor answers the different question **"keep this alive"** — a minimal `runit`/`systemd`-style keeper on top of the runner layer. The two are distinct layers: a supervised command's own `Retry` is **not** applied per incarnation (supervision runs the bare runner), so use the supervisor's own restart policy and backoff instead. Runs go through an `IProcessRunner` (the default `JobRunner`); override with `WithRunner` to share a `ProcessGroup` or inject a test double. Defaults: `OnCrash`, unlimited restarts, backoff `200ms × 2.0` capped at 30 s, jitter on, failure-storm guard off (enable with `StormPause`; failure half-life 30 s, threshold 5.0). **Observability while supervision runs.** `RunAsync` only reports its `SupervisionOutcome` at the very end, which is unusable for a long-lived (potentially never-ending) supervised service — so two callback seams, `OnRestart` and `OnStormPause`, report restarts and storm pauses *live*, as they happen (e.g. for a health check or crash-loop alerting). Both callbacks are invoked synchronously, from the supervision loop itself (the same async context driving `RunAsync`), right before the corresponding delay is slept out — so a slow or blocking handler delays every restart/pause; keep handlers quick and non-blocking. Neither callback changes `SupervisionOutcome`'s semantics — `Restarts`/`StormPauses`/`Stopped` are unaffected and remain the authoritative final tally; the callbacks are an additive, best-effort live view.

SupervisorRestartEvent

A single restart, reported live from the supervision loop (see `Supervisor.OnRestart`) — not to be confused with the final `SupervisionOutcome.Restarts` count. Sealed with an internal constructor so it can gain fields without breaking the frozen API.

SupervisorStormPauseEvent

A single failure-storm pause, reported live from the supervision loop (see `Supervisor.OnStormPause`) — not to be confused with the final `SupervisionOutcome.StormPauses` count. Sealed with an internal constructor so it can gain fields without breaking the frozen API.

TryParser<'T>

A C#-friendly parser for the `TryParse` verbs: the standard .NET `bool TryX(string, out T)` shape, so C# can pass a BCL parser like `int.TryParse` / `DateTime.TryParse`. Give the verb an explicit type argument — `cmd.TryParseAsync<int>(int.TryParse)` — or assign the parser to a `TryParser<int>` first; the explicit `'T` is required because BCL `TryParse` methods are overloaded (and C# can't infer `'T` from a byref lambda parameter either). It returns `true` and sets `value` on success, `false` otherwise — a `false` result becomes `ProcessError.Parse`. For a custom error *message*, throw from a `Parse` parser, or use the `Result`-returning `Runner.tryParse` from F#.

WaitAnyResult

The result of `RunningProcess.WaitAnyAsync`: which started process finished first and how it concluded. A named type (rather than a tuple) so the fields read clearly from C#.

Type something to start searching.