55[ ![ License: MIT] ( https://img.shields.io/badge/License-MIT-blue.svg )] ( LICENSE )
66[ ![ .NET] ( https://img.shields.io/badge/.NET-8.0%20%7C%2010.0-512BD4.svg )] ( https://dotnet.microsoft.com/ )
77
8- # ProcessKit — documentation
9-
10- ProcessKit is a child-process toolkit for .NET in two layers:
11-
12- <div style =" display :flex ; gap :0.6em ; flex-wrap :wrap ; margin-bottom :2em ;" >
13- <a href =" https://www.nuget.org/packages/ProcessKit " style =" display :inline-block ; padding :0.35em 0.8em ; border :1px solid var (--table-border-color ); border-radius :4px ; font-size :0.88em ; text-decoration :none ; color :var (--links );" >NuGet ↗</a >
14- <a href =" https://zelanton.github.io/ProcessKit-fSharp/api/ " style =" display :inline-block ; padding :0.35em 0.8em ; border :1px solid var (--table-border-color ); border-radius :4px ; font-size :0.88em ; text-decoration :none ; color :var (--links );" >API Reference ↗</a >
15- <a href =" https://github.com/ZelAnton/ProcessKit-fSharp " style =" display :inline-block ; padding :0.35em 0.8em ; border :1px solid var (--table-border-color ); border-radius :4px ; font-size :0.88em ; text-decoration :none ; color :var (--links );" >GitHub ↗</a >
16- </div >
17-
18- ``` text
19- ┌─────────────────────────────────────────────────────────────────┐
20- │ Runner layer (async, Task) │
21- │ Command · RunningProcess · Pipeline · Supervisor · CliClient │
22- │ capture / streaming / interactive stdin / readiness probes │
23- │ testing seam: IProcessRunner → ScriptedRunner / RecordReplay… │
24- ├─────────────────────────────────────────────────────────────────┤
25- │ Group layer (kill-on-dispose containment) │
26- │ ProcessGroup: spawn / signal / suspend / members / stats / │
27- │ limits / shutdown │
28- ├─────────────────────────────────────────────────────────────────┤
29- │ OS mechanisms │
30- │ Windows Job Object · Linux cgroup v2 · POSIX process group │
31- └─────────────────────────────────────────────────────────────────┘
32- ```
8+ Async child-process management for .NET with a kernel-backed ** no-orphan guarantee** : every
9+ process you start — and everything * it* spawns — lives in a kill-on-dispose container (a
10+ ** Windows Job Object** , a ** Linux cgroup v2** , or a ** POSIX process group** ), so no descendant
11+ ever outlives your program.
3312
34- Every ` Command ` run gets containment for free: the one-shot verbs spawn into a fresh private
35- group that dies with the run, so an early return or an unhandled exception never leaks a process
36- tree. The layers are also usable independently — a raw ` ProcessGroup ` can contain children you
37- spawn yourself, and the runner's test doubles never touch the OS at all.
13+ Beyond spawning a subprocess: run-and-capture, line streaming, interactive stdin, shell-free
14+ pipelines, readiness probes, timeouts & cancellation, supervision with restart/backoff, and a
15+ mockable runner seam for subprocess-free tests.
3816
39- > ** Written in F#, built for both F# and C#.** ProcessKit is implemented in F#, but it is designed
40- > for first-class, idiomatic use from ** both F# and C#** — every public API is meant to be called
41- > naturally from either language, and every example in these guides is shown in both.
17+ ** F#**
4218
43- ## The orphan process problem
19+ ``` fsharp
20+ task {
21+ match! (Command.create "dotnet" |> Command.arg "--version").RunAsync() with
22+ | Ok version -> printfn $"{version}"
23+ | Error err -> eprintfn $"{err.Message}"
24+ }
25+ ```
4426
45- ` Process.Start() ` gives you a handle to the direct child, and a bare ` Process.Kill() ` ends that
46- child — not a durable containment boundary for its descendants. If a build tool starts compiler
47- workers, or a shell wrapper such as ` cmd /c <command> ` starts the real payload, those grandchildren
48- can outlive a timeout, exception, or early return. They become orphans that keep holding ports,
49- temporary files, handles, and other resources after the code that started them has moved on.
27+ ** C#**
5028
5129``` csharp
52- using var process = Process . Start ( new ProcessStartInfo ( " cmd " , " /c build.cmd " )
30+ Console . WriteLine ( await new Command ( " dotnet " ). Arg ( " --version " ). RunAsync () switch
5331{
54- UseShellExecute = false ,
55- })! ;
56-
57- await Task .Delay (TimeSpan .FromSeconds (5 ));
58- process .Kill (); // stops cmd.exe; a compiler worker started by build.cmd can keep running
32+ { IsOk : true , ResultValue : var version } => version ,
33+ { IsOk : false , ErrorValue : var err } => $" error: {err .Message }" ,
34+ });
5935```
6036
61- Cleaning that up correctly means tracking every descendant and handling races while the tree is
62- still spawning. ProcessKit makes that ownership explicit: a ` ProcessGroup ` contains the whole tree,
63- and disposing it reaps the group. The one-shot ` Command ` verbs create and dispose a private group
64- for each run, so the same guarantee applies without extra plumbing.
37+ ## Why ProcessKit?
38+
39+ `System .Diagnostics .Process ` reaches (at most ) the direct child . The processes *it * spawned — a
40+ build tool 's compiler children , the real payload behind a wrapper (`cmd /c …`, `sh -c …`), a
41+ test 's helper servers — survive a timeout , an exception , or a dropped task , and keep running as
42+ orphans .
43+
44+ ProcessKit spawns every child into the operating system 's own containment primitive — a **Job
45+ Object ** on Windows , a **cgroup v2 ** on Linux (with a process -group fallback ), a **POSIX process
46+ group ** on macOS /BSD — so teardown is a kernel operation over the whole tree , not a best -effort
47+ signal to one pid :
48+
49+ - ** Nothing escapes silently .** Disposing the handle or group reaps every descendant ,
50+ grandchildren included . Where a mechanism has a genuine weakness (a `setsid ` child escapes a
51+ POSIX process group ), the active `Mechanism ` is reported instead of pretending — never a silent
52+ downgrade .
53+ - **Async -first .** Run -and - capture , line streaming , interactive stdin , readiness probes ,
54+ shell -free pipelines , supervision — all return `Task <…>` and stream as `IAsyncEnumerable <…>`.
55+ - **Honest results .** A non -zero exit is data (`ProcessResult `) until you ask for success ; a
56+ timeout is *captured * in the result ; a cancellation is always an error ; every platform
57+ divergence is typed or documented .
58+ - **Testable .** One interface seam (`IProcessRunner `) swaps the real spawner for scripted doubles
59+ or record /replay cassettes — no subprocess in your tests .
6560
6661## OS-level containment mechanisms
6762
@@ -79,16 +74,22 @@ environment instead of assuming one. See [Process groups](process-groups.md) and
7974
8075## How it compares
8176
82- | Capability | ` System.Diagnostics.Process ` | CliWrap | Medallion.Shell | SimpleExec | ** ProcessKit** |
83- | ---| :---:| :---:| :---:| :---:| :---:|
84- | Whole-tree kill-on-dispose containment | — | partial | — | — | ** ✓** |
85- | Honest results (non-zero exit is not an exception by default) | partial | partial | partial | — | ** ✓** |
86- | Typed, pattern-matchable errors | — | — | — | — | ** ✓** |
87- | Line streaming + readiness probes | partial | partial | partial | — | ** ✓** |
88- | Shell-free pipelines | — | ✓ | ✓ | — | ** ✓** |
89- | Supervision (restart, backoff, jitter) | — | — | — | — | ** ✓** |
90- | Mockable test seam (` IProcessRunner ` ) | — | — | — | — | ** ✓** |
91- | Built-in observability (logging, tracing, metrics) | — | — | — | — | ** ✓** |
77+ The comparison is easier to scan as short capability cards than as a table that forces five
78+ columns onto a narrow screen :
79+
80+ - ** `System .Diagnostics .Process `** tracks only the direct child . It has no durable whole -tree
81+ containment , readiness probes , supervision , or injectable runner seam .
82+ - **CliWrap ** has an excellent fluent pipeline API and tree -aware cancellation , but no persistent
83+ `ProcessGroup ` for several commands , resource limits , readiness probes , supervision , or runner
84+ seam .
85+ - **Medallion .Shell ** offers straightforward synchronous /async commands and pipelines , but does
86+ not provide whole -tree containment , typed errors , readiness probes , supervision , or a formal test
87+ seam .
88+ - **SimpleExec ** is intentionally minimal and exception -first : useful for build -script glue , but
89+ without streaming , pipelines , containment , supervision , or runner substitution .
90+ - **ProcessKit ** combines kernel -backed whole -tree containment with honest typed outcomes , async
91+ streaming , readiness probes , shell -free pipelines , supervision , secret -safe observability , and
92+ the `IProcessRunner ` seam .
9293
9394See the [Comparison and migration guide ](comparison .md ) for details and migration recipes .
9495
@@ -193,3 +194,7 @@ The same XML docs also power a browsable, generated
193194**[API reference](https :// zelanton.github.io/ProcessKit-fSharp/api/)** — published alongside these
194195guides on the same site — reach for it when you want a member-by-member lookup instead of a
195196task -oriented guide .
197+
198+ ---
199+
200+ Next : [Comparison and migration guide ](comparison .md )
0 commit comments