ProcessKit Namespace
| Type/Module | Description |
|
One completed command from a completion-ordered batch stream (`Exec.outputStream` / `Exec.outputStreamBytes`): the command's **input index** — its position in the `commands` sequence handed to the verb — paired with that command's own independent `Result`. The index is what keeps a result traceable to its source once items no longer arrive in input order, and it is always the position in the ORIGINAL sequence, never a counter of how many items the stream has yielded so far. The `Result` means exactly what one element of `Exec.outputAll` means: an `Error` is a genuine run failure (`ProcessError`: couldn't start, timed out, was cancelled, exhausted its `Retry` budget, …), while a non-zero exit stays `Ok` data whose `Code` you inspect. |
|
|
How `Exec.outputAllWithPolicy` / `outputAllBytesWithPolicy` behave once any command in the batch produces an `Error` — a genuine run failure (`ProcessError`: couldn't start, timed out, was cancelled, exhausted its `Retry` budget, …), never a non-zero exit, which stays data under `Ok` exactly as it does for `outputString`/`outputBytes` themselves. |
|
|
What this host can do on ONE axis of the containment contract — the honest three-valued answer the capability snapshot reports instead of a bare `bool`. A boolean `false` says nothing about *why*, or about what would make it true, so a caller can neither explain the refusal to an operator nor act on it. Every value here carries that missing half: a `Qualified` capability states the qualification it holds under, and an `Unsupported` one states the precondition that is missing. This mirrors the rest of the library, where an unavailable operation is a typed `ProcessError` naming what it needed — never a silent downgrade. The verb itself stays the authority at the moment it runs (see `ContainmentCapabilities`): this is a point-in-time report, not a promise about a later call. |
|
|
Which of a child's two captured streams a decoded line came from. Handed to `ICapturePolicy.OnCapture` so one policy can treat the two streams differently (redact only the stream a passphrase prompt is echoed on, say). It mirrors `OutputEvent`'s stdout/stderr split, but is a bare discriminator carrying no line: the capture seam should not force a consumer that only wants to shape text to depend on the streaming event type. Deliberately carries no stable machine identifier (no `Name`/`FromName`, and no entry in `spec/identifiers.json`), unlike `RlimitResource` or `IoPriorityClass`: nothing serializes it — it is an argument passed to a callback within one process, never a value ProcessKit writes to a report, a log field, or a cassette. |
|
|
Pipe-friendly entry point for `CliClient`. |
|
|
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" ]`). |
|
|
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. |
|
|
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"`). |
|
|
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`. |
|
|
The encoding a **legacy console program** actually writes its output in on this host — the one answer needed to read a pre-UTF-8 Windows tool without mojibake. ProcessKit decodes captured output as UTF-8 by default, and that default is deliberately not changed by anything here: it is right for every modern tool, on every platform, and guessing a code page for a child that emits UTF-8 would corrupt output that reads correctly today. But a Windows console program written before UTF-8 — `ping`, `netstat`, `chkdsk`, most of the built-in tooling, and any application still built against the ANSI/OEM CRT — writes its non-ASCII text in a code page instead, so a UTF-8 decode turns every accented or Cyrillic character into `U+FFFD`. This module resolves **which** code page that is, so the fix is one call rather than a research project (`GetOEMCP` vs `GetConsoleOutputCP`, `chcp`, `CodePagesEncodingProvider`). Use it through `Command.ConsoleEncoding()`, which applies the result to text stdin and both captured streams, or take the `Encoding` directly from `current ()` for a `Pipeline`/`CliClient`/single-stream case. |
|
|
A point-in-time, side-effect-free snapshot of what process containment can actually do on **this** host for a given `ProcessGroupOptions` — obtained from `ProcessGroup.Capabilities`, without creating a group, spawning a process, or touching any container. `ProcessGroup.Mechanism` already reports the primitive a group ended up with, but only once one exists; until now a caller who needed to pick a portable policy *before* spawning had to create a group and try each operation to find out what it would get. This is the same honesty contract answered up front: for every axis either a real availability on this platform and these options, or a typed `Capability` that names the missing precondition — never a bare `false`. **A snapshot, not a promise.** Every value is read from the platform facts in force at the moment of the call: a mounted cgroup v2 hierarchy, a helper binary present in a trusted directory, the ConPTY entry point exported by this Windows build. A host can gain or lose any of them afterwards — a package installed, a filesystem unmounted — so the answer here is the answer for *now*, and the verb itself remains the authority at the moment it runs, still returning its own typed `ProcessError`. It is deliberately not cached for that reason. What it never does is create a process, a group, or a container, and it neither reads nor reports any argv or environment value. |
|
|
A full-duplex Content-Length framed transport over a live process's stdin/stdout, suitable for LSP, DAP, BSP, and similar protocols. Construct it over a command started with `Command.KeepStdinOpen()`, enumerate `FramesAsync()`, and send raw payload bytes with `SendAsync`. The session owns the run's stdout consumption; other output verbs on the same handle are refused. `Command.StreamBuffer` bounds the unread frame backlog; leaving it unset preserves the default unbounded backlog. Only its two LOSSLESS full modes are honoured here: `Backpressure` paces the parser (and, through the pipe, the child) against your consumer, and `Error` faults the frame stream at the cap. The two DROP modes are refused at construction with a typed `ProcessError.Unsupported` — a framed transport carries protocol messages a peer correlates with its requests, so quietly deleting a queued frame is a corruption no consumer could detect, and this library refuses inapplicable configuration rather than downgrading it silently. With a bounded backlog, drain `FramesAsync()` concurrently with your sends rather than awaiting a send first: backpressure deliberately stops the parser (and the child) once the backlog is full, so a consumer that only starts reading after some other await can stall the child it is waiting on. The constructor itself never waits on the child — a `Command.Stdin(source)` feeder is awaited by the first `SendAsync`/`FinishInputAsync` instead, so the caller always gets the session back and can start draining frames. |
|
|
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. |
|
|
A child launched **outside** every containment primitive by `Command.LaunchDetached` / `Exec.detach` — the library's single, deliberate opt-out from the kill-on-dispose guarantee, for the spawn-and-forget cases containment makes impossible: a self-updater that must outlive the process it replaces, a restart-myself relaunch, a daemon/agent handed off to the OS. **This is a diagnostic snapshot, not a handle.** It owns nothing, holds no OS handle, and has no `Dispose`: the child is no longer ProcessKit's to manage, so there is deliberately nothing here to wait on, stream, signal, or kill. Everything a contained run gives you — `RunningProcess`, the `Outcome`, the Job Object / cgroup / process-group teardown — is what you traded away by calling the detached verb; go through `StartAsync`/`RunAsync` (or a `ProcessGroup`) if you want any of it back. If you later need to reach this process anyway, do it through the OS with the identity below (`System.Diagnostics.Process.GetProcessById`, a pid file, a service manager), accepting the pid-reuse risk that ProcessKit's own containment exists to eliminate. **`Pid` alone is not an identity.** A pid is reused by the OS once the process is gone, so a bare pid read later can name an unrelated process. `StartTime` is the standard disambiguator: the pair (`Pid`, `StartTime`) identifies this specific incarnation, and re-reading a live process's start time is how you check that a pid still refers to *this* child before acting on it. The pair is captured at launch, while the pid was still pinned (Windows: our own open process handle; POSIX: the child is unreaped), so it can never describe an already-recycled pid. Sealed with an internal constructor so it can gain fields without breaking the frozen API. Like `MemberInfo`, it deliberately carries **no command line and no environment** — only the program name already present on `ProcessError` — since argv routinely carries secrets. |
|
|
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. |
|
|
What a `PtySession.ExpectAsync` pattern matched, plus everything the child emitted before it — the output a prompt was preceded by (a banner, a menu, an error line), which is usually what the script wants to inspect or log. Both are consumed from the session's window, so the next `ExpectAsync` starts after this match and can never match the same prompt twice. |
|
|
The result of finishing a streamed run: how it concluded, its captured stderr, and whether output exposed by the streaming session was truncated. 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. |
|
A consumer-supplied, typed seam that shapes each decoded line **just before it enters the capture
backlog** — the redaction-at-capture extension point, set with `Command.CapturePolicy`.
For every framed line handed to a capture, `OnCapture` runs *before* the line is retained, so the
text it returns — never the raw line — is what lands in the backlog, and therefore in
`ProcessResult.Stdout`/`Stderr` and `Finished.Stderr`. That is what the observing
`Command.OnStdoutLine`/`OnStderrLine` handlers cannot do: they run *alongside* capture and can see
a line, not shape what is kept.
**Scope and boundaries (read before relying on it for secret hygiene).** This seam shapes the
in-memory capture backlog and nothing else. It deliberately does **not** reach the independent
observation sinks, each of which keeps its existing contract and sees the line **unshaped**:
- the per-line handlers `Command.OnStdoutLine`/`OnStderrLine` and the tees
`Command.StdoutTee`/`StderrTee` — they exist to observe the real output; if you also write to a
log or a file there, redact in that sink too;
- the **streaming** verbs, which hand each line to a live consumer rather than retaining it:
`RunningProcess.StdoutLinesAsync`, `OutputEventsAsync`, `WaitForLineAsync`, the byte-chunk
streams, `PtySession`'s window/transcript, `ContentLengthSession`'s frames, and the stderr
readiness probes. (`FinishAsync`'s retained *stderr* on those same sessions **is** backlog, and
is shaped.)
- a **raw byte** capture, which has no decoded line to shape: `OutputBytesAsync`'s stdout. A bytes
run's line-pumped **stderr** is still shaped.
A `Pipeline` captures nothing but raw bytes — its final stdout and every stage's stderr — so it
cannot shape anything at all; a stage carrying a policy is therefore **rejected** by `Pipe`
(`ArgumentException`) rather than run with the seam quietly inactive. Run such a command on its own.
**A failing policy fails closed.** If `OnCapture` throws — or returns `null` — the offending line
is retained **empty**, never the raw line it was meant to scrub, and the policy stays active for
the lines that follow. A redactor that throws blanks its output rather than leaking it. Nothing
else reports that failure, so prefer a policy that cannot throw.
**It must be thread-safe.** One policy instance serves both of a run's streams, whose pumps are
independent tasks, so `OnCapture` can be called for a stdout line and a stderr line at the same
time (and for several concurrent runs sharing one policy). A pure transform of its argument — the
shape this seam is for — needs nothing extra; a policy that keeps mutable state has to guard it.
**What it does not decide.** *How much* is retained, and what is evicted on overflow, remain
`OutputBufferPolicy`'s job; the two compose orthogonally — this decides each retained line's
content, that decides how many survive. Retention bookkeeping (the retained-byte total, the
`DropNewest` seal, `Truncated`/`TooLarge`) is computed from the text you return, while the
cumulative line counters and the raw-pipe byte counters (`RunningProcess.StdoutBytesSeen`) are
taken before this seam and are untouched by it.
|
|
|
Directional disk I/O rate limits for one device or volume. Linux uses `Target` as a cgroup v2 `major:minor` device key. Windows uses it as the NT device name of one volume; Windows Job Objects enforce one aggregate read/write bandwidth and IOPS ceiling for that volume. |
|
|
One Linux I/O-scheduling priority as configured through `Command.IoPriority`: an `IoPriorityClass` and, for the two levelled classes, the level within it. The pair `ioprio_set(2)` itself encodes into a single value. Built only through the three validating factories below — `Idle`, `BestEffort level`, `RealTime level` — which reject an out-of-range level at that boundary with `ArgumentOutOfRangeException` rather than clamping it into range or handing the kernel a value it would refuse later. **Lower levels mean higher priority**, which is the kernel's own convention and the opposite of how `Priority` reads: `BestEffort 0` is the most aggressive best-effort setting and `BestEffort 7` the politest. |
|
|
The **Linux I/O-scheduling class** a child's disk work runs in — the class half of an `IoPriority`, and the axis `ionice(1)`'s `-c` option selects. A separate dimension from the portable CPU-scheduling `Priority`: that one decides how much *processor* the child gets, this one how its *block-device* requests are ordered against everyone else's. Neither implies the other, and a background job usually wants both. **Linux-only, and honestly so.** `ioprio_set(2)` is a Linux system call with no POSIX or Win32 equivalent, so a spawn carrying an `IoPriority` fails with `ProcessError.Unsupported` on Windows, macOS, and the BSDs rather than running the child at the inherited priority as if the request had been honoured (see `Command.IoPriority`). |
|
|
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. |
|
|
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. |
|
|
One JSON-RPC message the peer sent that is **not** an answer to a request this session made: a notification (no `id`), or a request the peer wants answered (`id` present, so `IsRequest` is true — LSP servers use these for `workspace/configuration`, `window/showMessageRequest`, and similar call-backs). Answers to this session's own requests never appear here: they are routed straight to the `RequestAsync` call waiting on them. The message is kept as raw JSON text rather than a deserialized value, because only the caller knows what type a given `method` carries. Use `ParamsAs<'T>` for a typed read, or `ParamsJson`/`Payload` to handle it by hand. |
|
A typed JSON-RPC 2.0 conversation with a child process that speaks `Content-Length`-framed JSON — a
language server (LSP), a build server (BSP), or an MCP-style tool. It is the layer above
`ContentLengthSession`: that type frames bytes, this one serializes values, allocates and correlates
request ids, separates answers from notifications, and bounds a call end to end.
```fsharp
task {
let command = (Command.create "language-server").KeepStdinOpen()
match! command.StartAsync() with
| Error err -> eprintfn $"{err.Message}"
| Ok proc ->
use proc = proc
let session = JsonRpcSession(proc)
match! session.RequestAsync
|
|
|
The *scope* of cleanup a platform actually guarantees when the parent process of a `Command.KillOnParentDeath` child dies **suddenly** (SIGKILL, a crash, `TerminateProcess`) — reported honestly, fixed per platform, and **independent of whether `KillOnParentDeath()` was called**. This is the same "report the real guarantee rather than paper over a downgrade" principle as `ProcessGroup.Mechanism`: a caller can reason about what actually happens on the current OS instead of assuming a uniform whole-tree kill everywhere. |
|
|
Post-run evidence of whether the resource caps a `ProcessGroup` carried actually fired — one `LimitVerdict` per axis, read from the container the group itself owns. See `ProcessGroup.LimitEvidence()` for when it becomes available and what each axis means. Deliberately per-axis rather than one whole-group verdict: folding three honest three-valued verdicts into one would have to merge `NotTripped` and `Unknown` together, turning "we have no evidence" into "no". Sealed with an internal constructor — built only by the backend that reads it. Covers only `MemoryMax`, `MaxProcesses`, and `CpuQuota` (the `Cpu` axis is additionally guarded against `CpuTimeMax` — see `CappedAxes.GuardCpuVerdict`). `IoMax` and `CpuAffinity` have **no** corresponding axis at all: no containment mechanism here keeps a post-mortem "this whole-tree I/O or affinity cap engaged" counter, so there is nothing honest to report for them, ever — not even `Unknown`. `WindowsUiRestrictions` and `OomGroupKill` are policy toggles rather than caps that "fire", so they are likewise out of scope. |
|
|
The post-run verdict for **one** resource-limit axis — did a cap this group carried actually **engage** while the tree ran? Read from `ProcessGroup.LimitEvidence()`; see that member for exactly when it becomes available. This answers a different question than `ProcessError.ResourceLimit`: that error is about **admission** — "could the cap you asked for even be applied?" This verdict is about what only the container itself can answer afterwards — "did a cap on this axis then actually fire?" A `Tripped` verdict is returned only on **authoritative kernel/OS evidence** recorded by the group's own container; exit codes and signals are never consulted, because a cap-driven kill and a self-inflicted crash can look identical from the outside, and inferring from them would manufacture exactly the false verdict this type exists to avoid. |
|
|
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. |
|
|
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. |
|
|
An enriched, point-in-time snapshot of one member of a `ProcessGroup` (see `ProcessGroup.MembersInfo`). `Pid` is always present — it is how the group enumerated the member; every other field is `option` and is `None` wherever the platform cannot honestly report it, never a fabricated value. The member's **command line and environment are deliberately absent on every platform** — argv routinely carries secrets and redaction/hashing is the consumer's policy, so this snapshot excludes them by construction, the same exclusion the logging / tracing / metrics paths enforce. Sealed with an internal constructor so it can gain fields without breaking the frozen API. |
|
|
A point-in-time resource snapshot of one live `ProcessGroup` member. `Pid` is the member identity from the group's native membership snapshot. CPU time and resident memory are optional because the operating system may not expose them for every process/platform; I/O counters are optional as a group because the per-process interface is not available everywhere. Missing values remain `None` rather than being represented by fabricated zeroes. A member that exits while it is being sampled is omitted from the returned list. |
|
|
How a process run concluded. |
|
|
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. |
|
|
A single event in a merged stdout+stderr stream, tagged with its origin. |
|
|
One line of captured output, with its terminating newline stripped, its UTC capture time, and a per-run sequence number shared by stdout and stderr. |
|
|
What to drop when a bounded output buffer is full. |
|
|
Pipe-friendly functions over `Pipeline`, mirroring the instance methods. |
|
|
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`). To stream the final stage's stdout as it arrives instead of buffering the whole chain, start a live session with `StartAsync` (→ `PipelineSession`), the pipeline analogue of `Command.StartAsync` → `RunningProcess`. 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`. When no checked stage failed, the final stage's real outcome and diagnostics are returned; an unchecked voluntary exit remains successful because its actual code is included in the result's accepted codes, not because the outcome is rewritten. 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. A stage's `CapturePolicy` is bound by that same boundary — every capture a pipeline makes (the final stdout, and each stage's stderr) is a RAW BYTE capture with no line framing, which is also what leaves `OutputBuffer.MaxLines` inapplicable here — but it is **rejected** rather than left unapplied, because a redaction hook that quietly stops running hands back the secret it was installed to remove (see the rejected-config paragraph below). 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), `KeepStdinOpen` on any stage (the pipeline exposes no per-stage `RunningProcess.TakeStdin` handle through which the kept-open pipe could be used), a per-stage `CapturePolicy` on any stage (every capture a pipeline makes is a raw byte capture, so the seam has no decoded line to shape — run such a command on its own to scrub what it captured), `StdoutToFile` on any stage (stdout is instead pipeline wiring or final captured output), and `Stdout(StdioMode.Null|Inherit)` on any stage (the pipeline must use a pipe rather than silently override that destination). `StderrToFile` remains supported: it directs that stage's diagnostics to the requested file while the chain continues to carry stdout. 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). Stage 0 likewise owns `StopSignal`, because graceful shutdown broadcasts one soft signal to the whole chain — and, for the same reason, `CancelGrace`/`CancelSignal`: a cancelled chain (`Pipeline.CancelOn` or the verb's own token) is torn down through ONE soft signal and ONE grace window over the shared group, so setting either on stage 0 makes the whole chain's cancellation graceful, and setting it on a later stage is rejected rather than ignored. The chain-level `Pipeline.Timeout` is untouched by that knob and keeps its immediate hard kill. `StreamBuffer` depends on how the pipeline is run. For buffered verbs (`RunAsync`, `OutputStringAsync`, `OutputBytesAsync`, `ExitCodeAsync`, and similar verbs used without `StartAsync`), it is inapplicable because there is no streaming consumer to receive the policy. `StartAsync`, however, returns a live `PipelineSession`: the last stage's `StreamBuffer` policy is applied to the session's stdout channel, including its bounded-buffer/backpressure behavior. `KeepStdinOpen` is rejected with `ArgumentException`, not accepted as a silent no-op. Although `PipelineSession` is a live handle, it does not expose interactive stdin: the pipeline wires each stage after the first from the previous stage's stdout itself, and exposes no user-reachable `RunningProcess.TakeStdin` handle. |
|
|
`Command.Pipe` builds a two-stage `Pipeline`; further `Pipeline.Pipe` calls extend it. |
|
|
A live streaming session over a whole pipeline — the multi-stage analogue of `RunningProcess`, returned by `Pipeline.StartAsync`. It gives a pipeline the streaming layer a single command has long had: stream the **final** stage's stdout line by line as it arrives (`StdoutLinesAsync` / `StdoutJsonLinesAsync` / `OutputEventsAsync`), wait on a readiness line (`WaitForLineAsync`), wait for the whole chain to finish with the SAME pipefail classification the buffering verbs use (`FinishAsync`), or stop / reap the entire chain (`StopAsync` / `Kill` / dispose). Disposing it reaps every stage's tree (kill-on-drop), just like disposing a `RunningProcess`. **Single consumption** (the `RunningProcess` rule, [[K-031]]): the final stage's stdout is pumped exactly once, so `StdoutLinesAsync` and `OutputEventsAsync` are mutually exclusive — a second, different consumer throws `"already consumed by another verb"`. `FinishAsync` rejoins the SAME stdout-streaming session `StdoutLinesAsync` started (so it is the natural "wait for the rest" after streaming lines); after `OutputEventsAsync`, use `StopAsync` (or dispose) to reap. These hold because the session delegates every streaming/consuming verb to one underlying `RunningProcess`. **Whole-chain semantics.** The stream is the final stage's stdout, but `FinishAsync`/`StopAsync` reap and classify the ENTIRE chain: the returned `Finished.Outcome` is the pipefail representative (the rightmost checked stage that did not exit with an accepted code, or a `TimedOut`/`Cancelled` for the whole chain); when no checked stage failed, it is the final stage's real outcome, including an accepted unchecked non-zero exit. `Finished.Stderr` is that representative stage's stderr — identical to what `Pipeline.RunAsync` would report. Stopping or disposing tears down EVERY stage (including a partially started chain), never just the last. A genuine read failure in an upstream inter-stage relay is returned by `FinishAsync` as `ProcessError.Io`, even if the downstream stage observed the resulting EOF and exited successfully; a downstream broken pipe remains routine. That relay failure also tears the whole chain down the moment it is seen — it never waits out a still-running upstream stage that has simply stopped writing — so the streamed final-stage output ends wherever that kill lands. |
|
|
One external binary this platform's spawn paths must load, and whether this host actually holds it. ProcessKit shells out to a helper only where the OS offers no in-process equivalent — arming `PR_SET_PDEATHSIG` inside the child, establishing a controlling terminal, running a `.cmd` shim. A missing helper is never worked around silently: the affected verb fails with a typed `ProcessError` naming the helper. This entry is that same fact, available *before* the spawn. The POSIX security helpers are resolved only from a fixed list of trusted system directories and never from `PATH`, so a helper present on `PATH` alone is reported missing here — deliberately, because the spawn refuses it too (a `PATH`-hijackable binary would run with the caller's full privileges before any drop). See the hardening guide. |
|
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) CPU priorities are exposed here: `Priority` never raises a real-time scheduling class. And it is the CPU axis alone — `Priority` itself never touches **I/O** scheduling. How a child's block-device requests are ordered against everyone else's is the separate `Command.IoPriority` axis (Linux-only; see `IoPriority`). Neither axis implies the other, and background work usually wants both. |
|
|
|
|
|
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. |
|
|
Carries a structured `ProcessError` when an error must surface through an exception. Raised by `Result.GetValueOrThrow()`, streaming infrastructure (JSON-line parsing, pump faults, and bounded streams in `StreamFullMode.Error` mode), and ProcessKit group initialization through DI. Streaming APIs may surface pipeline faults this way even when their run-result counterparts return `Result<_, ProcessError>`. `Message` is exactly `Error.Message`, so it inherits that render's guarantees: one line, with every caller-, child-, or peer-controlled fragment sanitized and bounded — safe to print into a log or a terminal even when a hostile child chose its own stderr. The full, unmodified payload is on `Error` (`Detail`, `Stdout`, `Stderr`, `Data`, `Original`), never truncated. |
|
|
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), a FreeBSD `procctl(2)` process reaper (whole-tree, `setsid` escapees included), 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. |
|
|
Options applied when creating a `ProcessGroup`: the graceful-shutdown window and whole-tree resource limits. |
|
|
A snapshot of a process group's resource usage. Optional peak-process, CPU, memory, and I/O fields are `None` when the platform can't report them — the POSIX process-group mechanism (macOS and the Linux fallback) has no kernel accumulator, and neither does the FreeBSD process reaper, which contains a tree but accounts for nothing in it; the Linux cgroup v2 backend (the `limits` feature) supplies the controller metrics available to it. Sealed with an internal constructor so it can gain metrics without breaking the frozen API. |
|
|
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). |
|
|
Standalone, identity-safe process lookup and reuse-safe liveness for a pid the caller holds **outside** any `ProcessGroup` (T-385) — a pid saved to disk across runs, a launch registry, or an external probe watching a process this library never itself contained. `ProcessGroup.MembersInfo` answers the same questions for a group's own membership; this module is the companion for a bare pid nothing here has ever tracked, so it needs no group and creates nothing. Reuses exactly the same per-platform readers `ProcessGroup.MembersInfo` uses (`Native.Windows` / `Native.Posix`) — no second, parallel identity-reading mechanism for this entry point — and keeps every standing rule those readers already enforce: never reads a process's argv or environment, and every enriching `MemberInfo` field stays an honest `option`, `None` wherever the platform cannot report it, never fabricated. |
|
|
|
|
|
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. |
|
|
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. |
|
|
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. |
|
|
Initial terminal geometry and behaviour flags for an opt-in pseudo-terminal (PTY) run — see `Command.Pty`. A PTY gives the child a real controlling terminal (`isatty` true) on a single merged stdout+stderr stream, for tools that demand a tty (an interactive `ssh`/`sudo` prompt, a credential helper, a TUI, a progress bar that switches to "dumb" line-buffered output when it detects a pipe). The default (no PTY) is byte-identical to a plain pipe run. **Secret-safety (echo footgun).** A terminal echoes typed input back into its *output* by default (cooked-mode `ECHO`), so bytes written to the child's stdin through the PTY — including an interactively typed password — are echoed into the captured merged output. This is standard terminal behaviour, not a bug, but it means a credential can appear in captured output (or a recorded cassette). Set `Echo = false` to disable the terminal echo: on **POSIX** ProcessKit clears the pty slave's cooked-mode `ECHO` bit (`termios`) before the child adopts it, so a password typed to the child through the PTY is not echoed into the captured merged stream (proven by test). On **Windows** the echo of a ConPTY is governed by the child's own console mode (`ENABLE_ECHO_INPUT`/`ENABLE_LINE_INPUT` on `CONIN$`), which has no supported parent-side pre-spawn override; ProcessKit therefore does not force echo off there — a documented platform divergence, not a silent claim (an interactive prompt on Windows should suppress its own echo, as `ssh`/credential helpers do). As everywhere else in the library, argv and environment **values** — and any PTY credentials — are never logged or traced; the record/replay redaction hook still governs what a cassette persists. |
|
|
What `PtySession.SendLineAsync` appends to the text it sends — the "Enter key" of an interactive session. A real terminal sends a carriage return when Enter is pressed, which is why `Auto` picks it for a `Command.Pty` run; a plain pipe has no line discipline to translate it, so `Auto` picks a line feed there instead. Override it when a particular child disagrees. |
|
|
An expect-style conversation with a live child: wait for a pattern in its terminal output, send it input, repeat — the classic automation loop for an interactive program (`ssh`, a REPL, an installer, a credential prompt). Built over a started `RunningProcess`, and designed for one from a `Command.Pty` run, which is what makes an interactive child prompt at all. **Why not `WaitForLineAsync`.** A terminal prompt is not a line: `Password: `, `> `, `(y/N) ` carry no line terminator, so a line-framed wait cannot see them until a newline finally arrives — often never, because the child is waiting for the very input the prompt is asking for. A session therefore reads the child's merged terminal output as **raw text** and matches patterns against a sliding window of it, framing nothing. **One conversation, in order.** The verbs are meant to be called sequentially, the way the script they automate reads: expect, send, expect. Two `ExpectAsync` calls racing on one session are safe (the window is guarded, so exactly one of them consumes a given match) but arbitrary — which of the two sees the prompt is a coin toss, the same as concurrently driving any other single handle. **This session owns the output pipes.** Creating it claims the handle exactly like `OutputEventsAsync`/`StdoutLinesAsync` do, so a capturing or streaming verb on the same handle afterwards is refused ("already consumed by another verb"), and constructing a session over a handle another verb already claimed throws `InvalidOperationException`. Use `WaitForExitAsync` for the child's outcome, and dispose the `RunningProcess` (or its owning `ProcessGroup`) to reap the tree — do not layer a second consuming verb on top. **Line handlers do not fire here.** `Command.LineTerminator`, `OnStdoutLine`/`OnStderrLine` and the per-stream line counters describe a framed stream, and this session deliberately does not frame one; the byte-exact tees (`Command.StdoutTee`/`StderrTee`) are still fed, exactly as they are on the line paths. On a plain (non-PTY) run a session still works, but the child decides whether you ever see a prompt: without a terminal most programs switch their stdout to block buffering, so the prompt sits in the child's own buffer — that is the child's behaviour, and precisely what `Command.Pty` exists to change. **`StreamBuffer` is inapplicable here.** A session has no queued line/frame backlog: raw output feeds its sliding match window directly. `PtySessionOptions.WindowChars` and `TranscriptChars` are its explicit bounded-memory policy, so a `Command.StreamBuffer` setting has no effect. **Secret-safety.** Sent input is never logged, traced, or added to `Transcript` — but a terminal echoes input back into its OUTPUT by default, so with `PtyConfig.Echo = true` a sent password arrives in the child's output stream and therefore in the transcript. For a credential exchange use `Echo = false` (POSIX), or turn `CaptureTranscript` off, or both. |
|
|
Tuning for a `PtySession`: how much output a pattern may be matched against, whether the session keeps a transcript for diagnostics, and what `SendLineAsync` sends as the line ending. Both sizes are hard memory bounds in **characters**, so a long-lived session over a chatty child can never grow without limit; both bound by dropping the OLDEST text, since the tail is what the next pattern will match and what a failed session's diagnosis needs. |
|
AOT-safe `System.Text.Json` metadata for the opt-in JSONL report serializer: one self-describing JSON
object per line for `Outcome`, `ProcessResult<string>`/`ProcessResult<byte[]>`,
`ProcessGroupStats`, `RunProfile`, `MemberInfo`, and `LimitEvidence` — the shapes this port has today.
Ports the **shape**, not the code, of ProcessKit-rs's `report-serde` feature; see
`docs/jsonl-reports.md` for the full schema, the versioning promise, and C#/F# consumer examples
reading a JSONL stream.
**Opt-in.** Nothing on `ProcessResult`/`ProcessGroupStats`/`RunProfile`/`MemberInfo`/`LimitEvidence`
themselves changed — this is a separate serializer you reach for explicitly, via
`JsonSerializer.Serialize(value, ReportJson.OutcomeTypeInfo)` or the `ToReportJson()` extension methods
on `ReportJsonExtensions`.
**Four rules, matching the source feature:**
1. **A tagged shape carries a `"kind"` identifier**, spelled as this schema's own stable, documented
machine name — never a raw union-case ordinal or a BCL `.ToString()`. `Outcome` is `exited` /
`signalled` / `timed_out` / `unobserved`; each report line's own envelope is `process_result` /
`process_group_stats` / `run_profile` / `member_info` / `limit_evidence`.
2. **`Serialize` only — deliberately no `Deserialize`.** These are values the library *reports*, never
values a caller supplies back to it; every converter's `Read` throws `NotSupportedException`. Every
`JsonTypeInfo<'T>` below is still safe to pass to a `JsonSerializer.Deserialize` call by a
caller who ignores this and does so anyway — it simply throws instead of fabricating a value.
3. **Reports *about* processes, never what a process produced.** No converter here ever reads captured
stdout/stderr content, argv, or environment values — `ProcessResult.Stdout`/`Stderr`/`Combined` are
never touched, whatever `'T` is.
4. **Fields are additive; a field's spelling and unit are frozen.** Every one of these report types is
`[<Sealed>]` with an internal constructor and grows fields across minor releases without
breaking this schema's readers — a JSONL consumer must ignore keys it does not recognize, the same
discipline any self-describing format needs. Time is always a number of fractional seconds
(`duration_secs`, `total_cpu_time_secs`, `cpu_time_secs`, …); a measurement the platform cannot
report is `null`, never a fabricated `0`.
|
|
|
C#-friendly `ToReportJson()` overloads over `ReportJson`'s metadata — one compact JSON object per call, with no embedded newline, so appending `Environment.NewLine` (or `\n`) after it is a valid JSONL line. F# callers can use these too, or call `JsonSerializer.Serialize(value, ReportJson.OutcomeTypeInfo)` (etc.) directly. |
|
|
What this host can enforce, per `ResourceLimits` dimension. **These answer for the HOST, not for the mechanism the snapshot's options happen to select.** On Linux, asking for a whole-tree cap is itself what selects the cgroup v2 mechanism, so reporting "unsupported" for a limit-free options set would understate what the host can do — the honest question each member answers is "if this cap were added to the options, could this host enforce it?". `Creation` and `Mechanism` are the members that answer for the options as they stand. |
|
|
Resource limits applied at group creation. Most are enforced on the group **as a whole** by its kernel container; `CpuTimeMax` is the deliberate POSIX exception and is applied per spawned process through `RLIMIT_CPU` before exec. Whole-tree enforcement needs a real container — a **Windows Job Object** or a **Linux cgroup v2**. On macOS and the Linux process-group fallback, requesting memory/process/quota/affinity limits fails fast with `ProcessError.ResourceLimit`; CPU-time alone remains available through the per-child rlimit. 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. |
|
|
Why the supervisor is restarting an incarnation — the `SupervisorRestartEvent.Cause` a live `OnRestart` handler can branch on to tell an ordinary exit/crash restart apart from one the liveness probe forced. |
|
|
When the supervisor restarts an exited child. In every case `Supervisor.StopWhen` and `Supervisor.MaxRestarts` can end supervision first. |
|
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:
|
|
|
One per-process rlimit as configured on a `Command`: the resource, and the soft and hard values to apply to it — the pair `setrlimit(2)` itself takes. Read back from `Command.Rlimits`; built by `Command.Rlimit`, which validates the pair at the builder boundary. The **soft** value is the limit actually in force (exceeding it raises this resource's signal or fails the operation); the **hard** value is the ceiling the child may raise its own soft value back up to. Both are in the resource's native unit (see `RlimitResource`), and `Soft` never exceeds `Hard`. |
|
|
A Unix **per-process** resource governed by `setrlimit(2)` and requested through `Command.Rlimit` — the per-child complement of the whole-tree `ResourceLimits` below. The two are different instruments and neither replaces the other. A `ResourceLimits` cap is enforced by the group's kernel container on every process in the tree AT ONCE (one memory budget shared by all of them); an rlimit is applied to the direct child before its program starts and is then INHERITED individually by each descendant, so ten descendants each get their own copy of the cap rather than a shared one. A descendant may lower its own limits further, and may raise its soft value back up as far as the hard value it inherited — an rlimit is a robustness bound, not a containment boundary (that is what the group is for). Every value is in the resource's own native unit, exactly as the syscall takes it: **bytes** for `Core`/`Data`/`FileSize`/`Stack`, **seconds** for `Cpu`, a **count** for `NoFile`. There is no "unlimited" value — this API exists to LOWER a limit the child inherited, and raising a hard limit needs privilege the kernel refuses to an ordinary caller (the refusal is honest: the child never runs). **Unix-only**, and honestly so: Windows has no `setrlimit` analogue, so a spawn carrying any rlimit fails there with `ProcessError.Unsupported` rather than running the child uncapped. On POSIX the limits are applied before the child's own program starts by the util-linux `prlimit` helper, loaded only from a trusted system directory; a host that holds it in none of them (macOS/BSD, a minimal image) fails with `ProcessError.ResourceLimit` — see `Command.Rlimit` for the full mechanism. |
|
|
A caller-owned write-only stream that rotates a file by size for use with `Command.StdoutTee` or
`Command.StderrTee`. The active file is `path`; archives are `path.1` (newest) through
`path. |
|
The run verbs, expressed over any `IProcessRunner`. One verb, one meaning: - `run` — require a zero/accepted exit; return stdout, trailing whitespace trimmed. Output a bounded `OutputBuffer` policy truncated is refused (`OutputTooLarge`), never passed off as whole. - `outputString` / `outputBytes` — the full `ProcessResult`; a non-zero exit is data, and so is a truncated capture (read `ProcessResult.Truncated`). - `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. |
|
|
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). |
|
|
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`). I/O comes from the run's private containment tree and stays `None` for a shared group, where an aggregate would include sibling runs. Sealed with an internal constructor. |
|
|
The observed facts of one graceful `ProcessGroup` teardown, returned by `ProcessGroup.ShutdownReportAsync`. Where the fire-and-forget `ShutdownAsync` reports only success or a thrown exception, this carries what the teardown **actually observed**: which soft signal was attempted and whether it landed, how many members were alive before and after, whether the tree drained within the grace or had to be hard-killed, and how long it really took. A consumer that owns its own end-of-run race (a deadline that is not a fixed timeout, but a timeout x Ctrl-C x control-socket race) can report the *observed* tier instead of re-deriving it from `ShutdownAsync`'s bare success. # Point-in-time member counts `MembersBefore`/`MembersAfter` count the same member set `ProcessGroup.Members` reports — the whole tree on the Windows Job Object and Linux cgroup v2 mechanisms and on the FreeBSD process reaper (which reads the live tree from `PROC_REAP_GETPIDS`, zombies excluded), the tracked group **leaders** on the POSIX process-group fallback (macOS / the other BSDs / Linux without cgroup v2). Each is `None` only if that membership read failed (an unreadable `cgroup.procs`, a failed Job Object query), never a fabricated `0`. # Unconditional teardown — same guarantee as `ShutdownAsync` This still tears the group down exactly like `ShutdownAsync`: the soft signal, then the grace, then an unconditional hard kill of any survivor, then release. There is no "spare the survivors, keep the group usable" mode here — see `ProcessGroup.ShutdownReportAsync`'s own doc comment for why this port does not offer one. `Escalated` reports whether that hard kill actually fired; it is never a choice the caller can suppress. # Sealed, accessor-only A read-only snapshot the library produces: sealed with an internal constructor so it can gain fields across minor releases without a breaking change, and each fact is exposed through a property (documenting its own platform caveats) rather than a public field. |
|
|
A signal delivered to one run via `RunningProcess.Signal` or broadcast through `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 a best-effort soft stop: a console CTRL+BREAK to each child started with `Command.WindowsCtrlSignals()` and a `WM_CLOSE` posted to the top-level windows of every member that has one (a GUI child). They yield `ProcessError.Unsupported` only when the group has neither a CTRL-capable child nor a windowed member — nothing to soft-signal — 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. |
|
|
What `ProcessGroup.Signal` / `RunningProcess.Signal` can actually deliver through the mechanism the snapshot's options select. The three rows are the signal vocabulary's platform divergence: the unconditional hard kill, the soft stop, and everything else. |
|
|
The fate of a graceful teardown's best-effort **soft-signal** tier — what actually happened to the polite "please exit" request `ProcessGroup.ShutdownReportAsync` issues before the grace window, as opposed to what it *tried* to do. The soft signal is the group's configured `Options.StopSignal` (`Signal.Term` by default) on the Unix mechanisms, and a best-effort `WM_CLOSE` on the Windows soft tier. It is deliberately **not** the hard kill: escalation to `SIGKILL` / the atomic Job terminate is reported separately, by `ShutdownReport.Escalated`. |
|
|
How far a **soft stop** — a `Signal.Term`/`Signal.Int`-class request that asks a tree to exit cleanly rather than hard-killing it — reaches on *this* group, right now: the honest answer to "if I call `group.Signal(Signal.Term)` this instant, which of its members have a live target for that request?", read with `ProcessGroup.SoftStopScope()` *before* the signal is attempted. This is a capability report, not a delivery guarantee — see below. This is a **capability report, not an action**: querying it delivers no signal, posts no `WM_CLOSE`, spawns nothing, and does not mutate the group — asking never changes the answer a later `Signal` call gets. A caller (a CLI wrapping this library, an orchestrator deciding whether a soft stop is worth attempting at all) can state the real reach to its own operator instead of firing `Signal(Term)`, parsing a `ProcessError.Unsupported` back, and guessing at the scope from a hard-coded platform assumption. # It reports the *soft* tier only This describes only the graceful `Signal.Int`/`Signal.Term` soft-stop tier. The unconditional **hard** kill — `Signal.Kill`, `ProcessGroup.KillAll`, and disposing the group — always tears the whole tree down on every platform regardless of this value; that guarantee is unchanged and never `Unsupported`. # Runtime, per-group — not a fixed platform constant Unlike `KillOnParentDeathScope` (fixed per platform at build time), this is read from the group's **live membership** on every call, so the *same* build reports different scopes for different groups — most visibly on Windows, where a group with a live console-CTRL leader (a child started with `Command.WindowsCtrlSignals()`) or a live windowed member reports `OptInMembers`, while a group with neither reports `Unsupported`. "Live" is checked, not assumed: a CTRL-capable leader whose handle is still open (and so still registered) but has already exited does NOT count, because `GenerateConsoleCtrlEvent` on its now-torn-down console process group would then fail. The read is side-effect-free — it posts no `WM_CLOSE`, sends no CTRL+BREAK, and mutates nothing — but it is a capability report, not a delivery guarantee: `OptInMembers` says a live target for the soft stop exists, not that a later `Signal(Int/Term)` is certain to reach it (`GenerateConsoleCtrlEvent` can still fail for reasons this read does not probe, such as the caller having no console to share). | Mechanism | Scope | Why | |---|---|---| | Linux cgroup v2 | `WholeTree` | `Signal(Int/Term)` writes to every process in the cgroup. | | FreeBSD process reaper | `WholeTree` | `PROC_REAP_KILL` reaches every process in every subtree the group owns, with no escapee at all — not even a child that `setsid`s away: this is the strongest form of the promise on any unix. | | POSIX process group (macOS / the other BSDs / Linux without cgroup v2) | `WholeTree` | `killpg` reaches every tracked group leader and its descendants — a child that `setsid`s away escapes, the same documented weakness the kill-on-drop guarantee already has, not new to the soft stop. | | Windows Job Object | `OptInMembers` or `Unsupported` | A Job Object has no POSIX signal; a soft stop reaches only members it can *trigger* — a console-CTRL leader (opted in via `Command.WindowsCtrlSignals()`) or any live windowed member (`WM_CLOSE`). `OptInMembers` when at least one such member is live, else `Unsupported`. | |
|
|
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, in which case the pipe is left open after the source is drained so the caller can keep writing to it interactively via `RunningProcess.TakeStdin` (which becomes available once the source feed has finished, so the source and the interactive writer never write the pipe at the same time). |
|
|
How a child's stdout or stderr stream is connected. Set per-stream on `Command` via `Stdout`/`Stderr`; the default is `Piped`. |
|
|
Why supervision ended. |
|
|
An opt-in bounded/backpressure policy for the streaming verbs (`StdoutLinesAsync` / `OutputEventsAsync` / `WaitForLineAsync`) and `ContentLengthSession.FramesAsync` (which honours the lossless full modes and refuses the two lossy ones), set via `Command.StreamBuffer`. It is inapplicable to `PtySession`, whose raw match window and transcript have their own character bounds. 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. |
|
|
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). |
|
|
One typed transition of a live supervision, delivered by `SupervisionSession.EventsAsync` — the stream counterpart of the `OnRestart`/`OnStormPause` callbacks and the `Status` snapshot, which it adds to rather than replaces. Read `Kind` first: it says which transition this is, and therefore which of the payload properties below carry a value (each is `None` for every other kind). `Name` is the same fact as a stable lowercase machine identifier — `incarnation_started`, `restart_scheduled`, … — suitable as a log field or metric label, in the same `snake_case` style as the `"kind"` identifiers `ReportJson` writes. **Non-secret by construction.** An event carries lifecycle facts only: counters, a pid, an `Outcome`, durations, the program name, and coarse failure/stop classifications. It never carries argv, environment values, captured stdout/stderr, or a `ProcessError`'s message — the same taxonomy `MemberInfo` and the library's logging already follow, so a consumer can forward the whole stream to a log or metrics sink without auditing it for secrets. Sealed with an internal constructor so it can gain fields without breaking the frozen API. |
|
|
Which supervision transition a `SupervisionEvent` reports — the discriminator to branch on when consuming `SupervisionSession.EventsAsync`. A plain .NET enum on purpose: this taxonomy is meant to *grow*, and a new kind must not break a consumer that was written against an earlier version. Existing kinds are never renumbered, renamed, or repurposed; new ones are appended with the next free value. Both languages already force the habit that makes that safe — F# requires a wildcard branch when matching an enum, and a C# `switch` expression needs a discard arm — so treat an unrecognized kind as "something newer happened" (the event's `Name` still identifies it) rather than as an error. |
|
|
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. |
|
|
A live handle to a running supervision, returned by `Supervisor.StartAsync`. Unlike `RunAsync` — which only reports its `SupervisionOutcome` at the very end — a session lets a caller watch supervision *while it runs* (`Status`), ask it to stop *gracefully* (`StopAsync`), and await its eventual outcome (`Completion`). This is the primitive for building daemons / process managers on top of the runner layer without pulling in `Microsoft.Extensions.Hosting`. Thread-safe: `Status` is read under the same lock the supervision loop uses to publish each state change, so a concurrent read never races an update nor throws; `StopAsync` is idempotent and race-safe against the loop (and against a repeat call). Sealed with an internal constructor — build one via `Supervisor.StartAsync`. |
|
|
A consistent, point-in-time snapshot of a `SupervisionSession`'s live state — read atomically, so every field agrees with the others (no torn read across a concurrent update from the supervision loop). Only non-secret facts are exposed (activity, counts, the current child's pid/start time); argv and environment values never appear here, matching `ProcessKitDiagnostics`'s taxonomy. Sealed with an internal constructor so it can gain fields without breaking the frozen API. |
|
|
Pipe-friendly entry points for `Supervisor`. |
|
|
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.
**Interactive supervision.** For a poll-and-control view — a live `Status` snapshot (activity,
restart count, storm-pause flag, the current child's pid/start time), a graceful `StopAsync`, and a
`Completion` task — use `StartAsync`, which returns a live `SupervisionSession` handle. `RunAsync`
is a thin wrapper over `StartAsync` + awaiting `Completion`; the `Status` snapshot *adds* to the
`OnRestart`/`OnStormPause` callbacks without replacing them.
**Event stream.** Where the callbacks push two specific transitions into your code, `Events` opts in
to the whole lifecycle as a *pull*-based `IAsyncEnumerable |
|
|
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. |
|
|
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. |
|
|
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#. |
|
|
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#. |
|
|
The Windows *mandatory integrity level* a child's token is lowered to — see `Command.WindowsIntegrityLevel`. Windows labels every token (and every securable object) with an integrity level and enforces a no-write-up policy: a process may not modify an object labelled above its own level, whatever the DACL says. Lowering the level a child runs at therefore takes away write access to the user's own files, registry, and windows *without* changing who the child runs as — the closest Windows analogue to the Unix `Uid`/`Gid` drop, and the mechanism behind browser renderer sandboxes. Only levels at or below an ordinary process's own are offered: a token's integrity can be lowered but never raised (`SetTokenInformation` refuses), so an "elevate me" variant could only ever fail the spawn. Pair with `Command.WindowsRestrictedToken` for the full drop — the two are independent (integrity governs what may be *written to*, privileges govern what may be *done*). |
|
|
The Windows Job Object **UI restrictions** a process group can impose on its whole tree
(`JOBOBJECT_BASIC_UI_RESTRICTIONS`) — see `ProcessGroupOptions.WithUiRestrictions`.
These are the desktop-side counterpart of the resource caps: where the rest of `ResourceLimits`
bounds what a contained tree may *consume* (and which cores it may consume it on), these bound what
it may *do to the interactive session it happens to share with you* — read or overwrite the
clipboard, change display or system-wide parameters, create or switch desktops, or log the user
off / shut the machine down. A build plugin or a downloaded tool has no business doing any of that, and unlike a
resource cap there is no way to notice after the fact that it did.
A `[ |
ProcessKit API Reference