Skip to content

Commit ce4db5f

Browse files
committed
Fix GitHub Pages overview, tables, and navigation
1 parent 54a8adb commit ce4db5f

15 files changed

Lines changed: 1213 additions & 1207 deletions

docs/README.md

Lines changed: 63 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,63 +5,58 @@
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* spawneda
40+
build tool's compiler children, the real payload behind a wrapper (`cmd /c …`, `sh -c …`), a
41+
test's helper serverssurvive 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 primitivea **Job
45+
Object** on Windows, a **cgroup v2** on Linux (with a process-group fallback), a **POSIX process
46+
group** on macOS/BSDso 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 pretendingnever a silent
52+
downgrade.
53+
- **Async-first.** Run-and-capture, line streaming, interactive stdin, readiness probes,
54+
shell-free pipelines, supervisionall 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 cassettesno 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

9394
See 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
194195
guides on the same sitereach for it when you want a member-by-member lookup instead of a
195196
task-oriented guide.
197+
198+
---
199+
200+
Next: [Comparison and migration guide](comparison.md)

docs/commands.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,4 @@ carry that field (e.g. `.Code` is set only on `Exit`, `.Stdout`/`.Stderr`/`.Comb
10771077

10781078
---
10791079

1080-
Next: [Streaming & interactive I/O](streaming.md) ·
1081-
[Timeouts, retries & cancellation](timeouts-and-cancellation.md) ·
1082-
[Process groups](process-groups.md)
1080+
Next: [Process groups](process-groups.md)

docs/comparison.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,8 +315,12 @@ equivalent to CliWrap's event-stream callback style, without giving up buffered
315315

316316
## See also
317317

318-
- [READMEWhy ProcessKit?](../README.md#why-processkit) — the elevator pitch and the
319-
single-column differentiator table.
318+
- [OverviewWhy ProcessKit?](README.md#why-processkit) — the elevator pitch and the
319+
core differentiators.
320320
- [Running commands](commands.md), [Streaming & interactive I/O](streaming.md),
321321
[Pipelines](pipelines.md), [Supervision](supervision.md),
322322
[Testing your code](testing.md) — the full guide set each recipe above links into.
323+
324+
---
325+
326+
Next: [Cookbook](cookbook.md)

docs/containers.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,3 @@ matters, and `ProcessGroupOptions` limits are for the narrower case of bounding
257257
*within* a container's broader budget — a build step's compiler, an untrusted subprocess, a fork
258258
bomb guard on a supervised worker — where the container-level cap alone can't distinguish between
259259
that one process and the rest of the workload sharing the container.
260-
261-
---
262-
263-
Next: [Platform support](platform-support.md) · [Process groups](process-groups.md) ·
264-
[Dependency injection](dependency-injection.md) · [docs index](README.md)

docs/cookbook.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,3 +890,7 @@ Cassettes also cover the `byte[]` capture and streaming (`SpawnAsync`) verbs,
890890
`RecordReplayRunner.Auto` grows a cassette by recording on a miss, and
891891
`RecordReplayOptions` adds arg-normalizer / redaction / file-content-stdin matching
892892
see [testing.mdRecord and replay](testing.md#record-and-replay).
893+
894+
---
895+
896+
Next: [Running commands](commands.md)

docs/dependency-injection.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,7 @@ services.AddHealthChecks().Add(
148148
failureStatus: null,
149149
tags: null));
150150
```
151+
152+
---
153+
154+
Next: [Platform support](platform-support.md)

docs/observability.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,7 @@ and disposed. Only the first of those two events counts toward `runs.completed`/
9898
span: a handle disposed without a terminal verb is still not counted as completed ("an abandoned run
9999
simply isn't counted as completed"), so `runs.started`/`runs.completed` can legitimately diverge even
100100
though `runs.active` is exact.
101+
102+
---
103+
104+
Next: [Dependency injection](dependency-injection.md)

0 commit comments

Comments
 (0)