Logo ProcessKit API Reference

ProcessGroup Type

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.

Instance members

Instance member Description

this.Adopt

Full Usage: this.Adopt

Parameters:
Returns: Result<unit, ProcessError>
 Adopt an already-running **external** process — one this group did not start (created by other
 code, inherited from another layer, or located by pid) — into the container, so from now on it
 obeys the same whole-tree rules as a process started with `StartAsync`: kill-on-dispose, and
 participation in `Signal`/`Suspend`/`Resume`/`Members`/`MembersInfo`/`Stats` and any resource
 limits. This restores the "kill the whole tree" guarantee for a wrapper whose child was not
 launched through ProcessKit.

 **The argument is a `System.Diagnostics.Process`, not a bare pid — deliberately.** A raw pid is
 subject to number reuse: between the caller obtaining it and the adopt landing, the OS may have
 recycled it onto an unrelated process, which the adopt would then contain by mistake. A live
 `Process` holds an open OS handle to the target, which on **Windows** pins the pid (the OS will not
 reuse it while a handle is open), so the adopt cannot race a recycle; this call keeps the `Process`
 alive across the native adopt for exactly that reason. On **Linux** there is no handle that pins a
 pid, so the `Process` gives the pre-adopt `HasExited` guard and a narrower window, but a pid
 recycled in the residual window cannot be fully ruled out by number alone — the honest limitation
 is documented, not hidden.

 A caller who has only a number — from a pidfile, a registry, an FFI or IPC boundary — cannot use
 this overload at all, and `AdoptByPid` is the door for that case: it takes an identity anchor of
 its own for whatever the number currently names (the process object on Windows, cgroup membership
 on cgroup v2, a re-verified start-time token on the POSIX process group) rather than trusting the
 number. It is a strictly weaker guarantee than the pinning handle THIS overload gets on Windows,
 because nothing can close the window before the call — so where a live `Process` is available, this
 one stays the better choice.

 **Per-platform behaviour** (honest and typed, never a silent no-op):
  * **Windows (Job Object)** — `AssignProcessToJobObject`; supported with or without limits.
  * **Linux cgroup v2** — writes the pid to the group's `cgroup.procs`; available only on a group
    created **with** resource limits (which is what selects the cgroup mechanism). A plain,
    limit-free Linux group uses the POSIX process-group mechanism and cannot adopt (below).
  * **POSIX process group (macOS and the other BSDs, or Linux without limits)** —
    `ProcessError.Unsupported`: `setpgid` only relocates our own children before they `exec`, so a
    foreign process cannot be moved into our group at all.
  * **FreeBSD process reaper** — `ProcessError.Unsupported`, refused by the POSIX process group it
    layers over: a reaper contains this process's own *descendants*, and no `procctl(PROC_REAP_*)`
    call turns a foreign process into one (`PROC_REAP_ACQUIRE` does not even re-attach children
    forked before it ran).

 **Edge cases**, each a distinct typed failure rather than a fabricated success:
  * a `null` argument throws `ArgumentNullException` (a programming error, surfaced eagerly);
  * a process that has already exited, or a pid that no longer exists (a TOCTOU race lost to the
    target's own exit), returns `ProcessError.Adopt`;
  * missing rights to the foreign process returns `ProcessError.Adopt` (a typed error, not an
    escaping exception);
  * a Windows process already assigned to a Job that does not permit nesting on this OS
    configuration returns `ProcessError.Adopt` — never a "looks like it worked" no-op.

 **The adopted process is not our child**, so ProcessKit does **not** reap it via `waitpid` and does
 **not** signal its process group (which it does not own): the container primitive alone contains and
 kills it — the Job's `KILL_ON_JOB_CLOSE` / a cgroup `cgroup.kill` at teardown — while its real
 parent (or `init`, once reparented) reaps it. The **exit-observation (wait) path is the caller's
 own `Process`** (`Process.WaitForExitAsync` / `HasExited`); this method adds containment, and returns
 `Result` rather than a `RunningProcess`, because the external process's stdio is not ours
 to stream.

 Routed through the same lifecycle gate as the other control verbs: adopting into a released group
 returns a non-transient `ProcessError.Unsupported` before touching the closed/removed native
 container, never a use-after-teardown.
externalProcess : Process
Returns: Result<unit, ProcessError>

this.AdoptByPid

Full Usage: this.AdoptByPid

Parameters:
    pid : int

Returns: Result<unit, ProcessError>
 Adopt an already-running **external** process into the container from a **bare pid** — the door for
 a process this library did not start and for which the caller holds no `System.Diagnostics.Process`
 at all: one an outside supervisor launched, one whose id came from a pidfile, a registry or an
 FFI/IPC caller. `Adopt(Process)` covers only the case where a live `Process` object is in hand; this
 takes the one identifier every such caller does have. Both are unchanged in what containment means:
 kill-on-dispose, and participation in `Signal`/`Suspend`/`Resume`/`Members`/`MembersInfo`/`Stats`
 and any resource limits.

 ### A pid is an address, not a handle

 The number is used to *find* the process; it is not what the group keeps afterwards. Once a process
 is reaped the OS may give its number to an unrelated one, so this call captures an **identity
 anchor of its own** for whatever the number names at the moment it runs, and binds the group to
 that:

  * **Windows (Job Object)** — the process **object**. The number is used exactly once, by this
    call's `OpenProcess`; `AssignProcessToJobObject` then puts that object into the Job, and the
    kernel keeps membership per object. Nothing later resolves the number again.
  * **Linux cgroup v2** — kernel-maintained **cgroup membership**, per task. A `/proc//stat`
    start-time read on either side of the `cgroup.procs` write *detects* a number that changed hands
    across it (see the failure list below for what the call then does about it) — detection, not
    prevention.
  * **POSIX process group (macOS, or Linux without limits)** — the tracked pid **plus** the
    start-time token read here, **re-read before every later probe, signal, suspend/resume and
    teardown kill**, never once at adoption and then trusted. A number whose token no longer matches
    is dropped from the group and receives nothing.
  * **FreeBSD process reaper** — whatever the POSIX process group underneath answers, since this
    verb is delegated to it verbatim: a reaper is a membership mechanism for this process's own
    *descendants*, not an identity mechanism for an arbitrary number, so it adds no anchor of its
    own. In practice FreeBSD ships no start-time reader this library can verify, so the refusal
    below is what a caller gets there.

 So a process that recycles the number *after* this call is rejected rather than signalled. What no
 library can check for you is the window *before* it — whether `pid` still named the process you
 meant by the time you passed it here. Look the number up as late as you can; where you can hold a
 `Process`, `Adopt(Process)` closes even that window on Windows, because the caller's own open handle
 pins the pid. The start-time token also carries one residual the other two anchors do not: its
 resolution (a clock tick on Linux, a microsecond on macOS) cannot tell apart two processes that
 occupied the number within the same tick.

 ### What the group covers

 Processes the adopted one had **already** spawned keep their original containment. What happens to
 the ones it spawns *afterwards* follows the mechanism (`Mechanism`): on the **Job Object** and
 **cgroup v2** a later fork joins the container with its parent, so the subtree grown from here is
 contained; on the **POSIX process group** the process is tracked **individually** — signalled and
 killed with the group, but its future forks are not, because POSIX has no primitive that moves a
 foreign, already-`exec`ed process into another process group.

 ### Refusals — each typed, never a silent success

  * `pid <= 0` and this process's **own** pid are refused with `ProcessError.Adopt` before any
    mechanism is consulted. Neither is an adoptable target and both are actively dangerous as a
    number: `0` means "the caller's own process group" to `kill`, a negative number addresses a
    process group rather than a process, and adopting this very process would enlist the caller in
    its own group's teardown.
  * a platform with **no start-time identity reader** on the POSIX process-group mechanism (the
    BSDs) returns `ProcessError.Unsupported`: there is no anchor to capture, and tracking a bare
    number would mean SIGKILLing whatever holds it at teardown. Never a silent downgrade to raw
    by-number containment.
  * a process this caller may **not signal** — another user's, or a protected/system one — is
    refused with `ProcessError.Adopt` on the POSIX process-group mechanism, which checks it with a
    `kill(pid, 0)` probe at adoption. Reading a start-time anchor proves only that the process can be
    *identified* (on Linux `/proc//stat` is world-readable), never that it can be controlled,
    and a member this group could not signal or SIGKILL would be containment reported but not held.
    The Job Object and cgroup v2 mechanisms refuse the same case by construction, through the denied
    `OpenProcess`/`cgroup.procs` write below. The check is about the moment of adoption: a process
    that changes credentials afterwards is no more foreseeable here than one that exits afterwards.
  * a pid that names nothing, an identity that cannot be read (a `hidepid` `/proc` mount, another
    user's process on macOS), a denied `OpenProcess`/`cgroup.procs` write, a Windows assign the
    kernel refuses, or a number that changed hands *while this call ran*, all return
    `ProcessError.Adopt` with the specific cause. On cgroup v2 that last case has already written the
    stranger into this group's cgroup, so the call moves it back out into the parent cgroup and says
    so; where even that is refused, the message says the process stays a member of this group and
    will be killed by its teardown.

 ### Ownership: this group never reaps it

 Exactly as with `Adopt(Process)`, an adopted process is **not** this library's child: nothing here
 `waitpid`s it, and no exit status for it is ever reported through this API — there is no
 `RunningProcess` and no result to await, which is why this returns `Result`. The group can
 *contain*, *signal*, *list* and *kill* it; its exit belongs to its real parent (an outside
 supervisor, or `init` once it is reparented), and observing that exit is the caller's own business
 — `Process.WaitForExitAsync()`/`HasExited` where a `Process` is available. One consequence to plan
 for on the POSIX mechanism: a process that exits and is not reaped by its own parent becomes a
 zombie, and a zombie still answers the identity probe, so a graceful `ShutdownAsync` waits out its
 full grace on it and no kill can clear it — only its parent's `wait` can.

 Routed through the same lifecycle gate as the other control verbs: adopting into a released group
 returns a non-transient `ProcessError.Unsupported` before touching the closed/removed native
 container, never a use-after-teardown.
pid : int
Returns: Result<unit, ProcessError>

this.KillAll

Full Usage: this.KillAll

Returns: Result<unit, ProcessError>

Immediately hard-kill every process currently in the group (the honest name for the tree kill — no graceful signal). Idempotent; after an `Ok` return the group stays usable for further spawns. Returns `Result` for parity with the other tree-control verbs. On Linux cgroup v2, both the atomic `cgroup.kill` path and its legacy per-member fallback explicitly thaw and verify the reusable cgroup before returning `Ok`. A freezer that still reports frozen or cannot be read returns `ProcessError.Io`; an already-unfrozen or removed freezer remains a best-effort success. On a kernel without `cgroup.kill` (< 5.14), the legacy fallback can additionally report `ProcessError.Io` when a member could not be signalled AND the cgroup is still populated — or its membership unreadable — once the sweep ends. A cgroup that did drain is not reported as a kill failure, because the drain check is the authority on whether the tree died. That delivery case includes a kernel without pidfd (< 5.3), where the identity-safe sweep kills nothing at all rather than downgrading to a raw `kill(pid, ...)` that a recycled pid could land on an unrelated process. An error makes no reuse guarantee; final disposal still runs its bounded best-effort drain and reclaim attempt, recording a cgroup it could not remove. **Windows** reports `ProcessError.Io` when `TerminateJobObject` is REFUSED and the Job still holds live members (or its accounting cannot be read at all, which is never read as "drained"). A refusal on an already-empty Job stays a successful no-op, so repeating `KillAll` on a reaped tree — or racing the tree's own exit — is idempotent as before. Closing the Job handle (`Dispose`) still kills the tree through `KILL_ON_JOB_CLOSE`, but that backstop fires at teardown, not now, so it is deliberately NOT reported as a completed kill here.

Returns: Result<unit, ProcessError>

this.LimitEvidence

Full Usage: this.LimitEvidence

Returns: Result<LimitEvidence, ProcessError>

Post-run, per-axis evidence of whether a resource cap this group ever configured actually **fired** — the question a plain exit code or signal cannot answer, and the other side of `ProcessError.ResourceLimit` (which answers "could the cap be applied at all", never "did it fire"). One `LimitVerdict` (`Tripped`/`NotTripped`/`Unknown`) per axis — `Memory`/`Processes`/`Cpu` — read from the container's own authoritative post-mortem counters, never re-derived from the `ResourceLimits` that requested the cap and never inferred from the run's exit code or signal (a cap-driven kill and a self-inflicted crash can look identical from the outside — see `LimitVerdict`'s own doc comment). **Available only after the group has been torn down** — `ShutdownAsync`/`Dispose`/`DisposeAsync`, or the finalizer — deliberately the OPPOSITE lifetime rule `Stats()` follows. The evidence is captured exactly once, from the still-live container, in the instant immediately before its counters (and, for a Linux cgroup v2 group, the cgroup directory itself) are torn down, and is then cached: any number of later reads — including well after teardown, on any thread — return that same immutable snapshot rather than a re-derived or stale one. Calling this **before** teardown has completed returns a non-transient `ProcessError.Unsupported`: an honest "not yet available", never a fabricated verdict read off counters that are still changing. Per axis, deliberately never folded into one whole-group verdict — see `LimitVerdict` for what each value means, and `LimitEvidence` for why folding would misrepresent "no evidence" as "no" (and for why `IoMax`/`CpuAffinity` have no axis here at all). Only the Linux cgroup v2 mechanism can ever answer `Tripped`/`NotTripped` from real evidence (see `Native.Cgroup.limitEvidence` for exactly which kernel counters back each axis, and `LimitEvidence.Cpu` for the further `CpuTimeMax` refinement that applies on every mechanism). The two mechanisms differ in what they say about an axis this group never capped: a Windows Job Object keeps no post-mortem record that any of these caps fired, so every axis it ever capped reads `Unknown` — not an oversight, a measured conclusion about what it actually preserves after the fact — but an axis it never capped still reads `NotTripped` (nothing was capped, so nothing could fire) without touching native at all. The POSIX process-group fallback and the FreeBSD process reaper have no whole-tree resource-accounting apparatus whatsoever — the same reason `Create`/`UpdateLimits` refuse any whole-tree cap on either of them — so both answer `Unknown` on every axis UNCONDITIONALLY, including one this group never capped: there is no "nothing was capped" case that lets them read `NotTripped` the way a Job Object can, because they never had any evidence apparatus to begin with.

Returns: Result<LimitEvidence, ProcessError>

this.Mechanism

Full Usage: this.Mechanism

Returns: Mechanism

The OS primitive containing this group on the current platform.

Returns: Mechanism

this.MemberStats

Full Usage: this.MemberStats

Returns: Result<IReadOnlyList<MemberStats>, ProcessError>

Return a point-in-time resource snapshot for each current member. CPU time and resident memory are optional per member, and I/O counters are optional on platforms without a per-process counter API. The native backend owns both membership enumeration and metric reads under this lifecycle gate: a member that vanishes during sampling is omitted, and a recycled pid cannot contribute metrics from a foreign process.

Returns: Result<IReadOnlyList<MemberStats>, ProcessError>

this.Members

Full Usage: this.Members

Returns: Result<IReadOnlyList<int>, ProcessError>

The pids of the processes currently in the group — a point-in-time snapshot. On Windows the whole Job tree, on cgroup v2 the whole cgroup, on the FreeBSD process reaper the whole live descendant tree (`setsid` escapees included, exited members excluded), on the POSIX fallback the tracked group leaders plus any process adopted by pid (`AdoptByPid`) whose identity anchor still matches.

Returns: Result<IReadOnlyList<int>, ProcessError>

this.MembersInfo

Full Usage: this.MembersInfo

Returns: Result<IReadOnlyList<MemberInfo>, ProcessError>

An enriched, point-in-time snapshot of the group's members: the same pids `Members` reports, in the same platform matrix (the whole Job tree on Windows, the whole cgroup on cgroup v2, the whole live descendant tree on the FreeBSD process reaper, the tracked group leaders and any anchor-matching `AdoptByPid` member on the POSIX fallback), each carrying its parent pid, executable image name, and OS-reported start time **where the platform can honestly report them** — every enriching field is `option` and is `None` otherwise, never a fabricated value. A member that exits between the enumeration and its metadata read is **omitted** rather than filled with invented fields. The member's command line and environment are **never** included on any platform — argv routinely carries secrets and redaction is the consumer's policy — the same exclusion the logging / tracing / metrics paths enforce. Enumeration errors the same typed way as `Members` once the group is released.

Returns: Result<IReadOnlyList<MemberInfo>, ProcessError>

this.Options

Full Usage: this.Options

Returns: ProcessGroupOptions

The options the group is currently configured with (shutdown grace, resource limits). The resource limits reflect the latest successful `UpdateLimits`, not only the create-time set.

Returns: ProcessGroupOptions

this.Resume

Full Usage: this.Resume

Returns: Result<unit, ProcessError>

Resume a tree suspended by `Suspend`. Failed native delivery or cgroup thaw writes return `ProcessError.Io`, on the same terms as `Suspend`; a member that exited concurrently is a successful no-op.

Returns: Result<unit, ProcessError>

this.SampleStatsAsync

Full Usage: this.SampleStatsAsync

Parameters:
Returns: IAsyncEnumerable<ProcessGroupStats>

A periodic `ProcessGroupStats` series: the first sample immediately, then one per `interval`. **Pull-based** — it samples only as the enumeration is pulled and runs no background task, so it neither keeps the group alive nor leaks if abandoned. The series ends on the first snapshot the group fails to report (notably after it is torn down) or when the enumerator's token fires. A non-positive `interval` (`<= TimeSpan.Zero`) is rejected with `ArgumentOutOfRangeException`, thrown eagerly by this call rather than deferred to enumeration — a sampling cadence must be a positive duration.

interval : TimeSpan
Returns: IAsyncEnumerable<ProcessGroupStats>

this.ShutdownAsync

Full Usage: this.ShutdownAsync

Returns: Task

`ShutdownAsync` using the group's configured `Options.ShutdownTimeout`.

Returns: Task

this.ShutdownAsync

Full Usage: this.ShutdownAsync

Parameters:
Returns: Task

Tear the group down gracefully, then release it. On Unix: the configured `Options.StopSignal`, then SIGKILL if still alive after `gracePeriod`. On Windows: best-effort `WM_CLOSE`, then the atomic Job kill. A negative `gracePeriod` is rejected with `ArgumentOutOfRangeException`; `TimeSpan.Zero` escalates immediately. Idempotent with `Dispose` in the sense that matters — the one-shot teardown (`hardRelease`) still runs exactly once no matter how many callers race `ShutdownAsync`/`Dispose`/`DisposeAsync`, and every `GracefulKillTree` failure still leaves the container released, never leaked. It is NOT idempotent in the sense of "every call observes the same completion": the caller that loses the `claimRelease` race returns immediately without waiting for the winner's (possibly still in-flight, up-to-`gracePeriod`) teardown to finish — unlike `RunningProcess.StopAsync`, which funnels concurrent callers onto one shared conclusion. A loser that needs "the tree is fully torn down" must await the SAME `Task` the winner returned, or otherwise synchronize with it itself.

gracePeriod : TimeSpan
Returns: Task

this.ShutdownReportAsync

Full Usage: this.ShutdownReportAsync

Returns: Task<Result<ShutdownReport, ProcessError>>

`ShutdownReportAsync` using the group's configured `Options.ShutdownTimeout`.

Returns: Task<Result<ShutdownReport, ProcessError>>

this.ShutdownReportAsync

Full Usage: this.ShutdownReportAsync

Parameters:
Returns: Task<Result<ShutdownReport, ProcessError>>

Gracefully tear the group down, additionally reporting what the teardown **actually observed** — the introspective sibling of `ShutdownAsync(gracePeriod)`. Drives the EXACT SAME teardown as `ShutdownAsync(gracePeriod)` — the configured `Options.StopSignal` (a best-effort `WM_CLOSE` on Windows), then up to `gracePeriod` for the tree to drain, then an UNCONDITIONAL hard kill of any survivor, then release — and is purely additive: it changes nothing about `ShutdownAsync`'s own behaviour or signature. What it adds is the `ShutdownReport`: the soft signal's fate, the member counts before/after, whether the tree drained within the grace or needed the hard kill, and how long it actually took — so a caller 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. # Deliberately NOT a "keep the group usable, spare survivors" mode ProcessKit-rs's `ProcessGroup::stop` additionally takes an `escalate: bool` that, when `false`, leaves any survivor running and keeps the group usable — seemingly the Rust source this method otherwise mirrors closely. This port deliberately does not offer that combination — see the "Deliberate divergence" note above `GracefulKillTree` for why: this port's kill-on-drop guarantee is unconditional, and there is no honest way to offer "spare the survivors" on a teardown that still releases the container the way `ShutdownAsync`/this method both document. On Windows in particular, closing the Job handle unconditionally kills whatever the Job still holds (`KILL_ON_JOB_CLOSE`), so a "spared" survivor would be silently killed anyway by the very release this method still performs at the end — reporting `Escalated = false` there would misrepresent a kill this call itself causes as a spare. `Escalated` instead answers an honest, narrower question: did THIS teardown's grace window need the hard kill, or did the tree drain on the soft signal alone. See `ShutdownAsync(gracePeriod)`'s own doc comment for the full teardown/idempotency contract this shares (negative-grace rejection, `Dispose`/`ShutdownAsync` idempotency, the "only the winner awaits the in-flight teardown" rule). A caller that loses the underlying release race — a concurrent `Dispose`/`ShutdownAsync`/`ShutdownReportAsync`/finalizer already claimed it — gets `ProcessError.Unsupported` here rather than a fabricated report, since there is no teardown left for THIS call to have observed.

gracePeriod : TimeSpan
Returns: Task<Result<ShutdownReport, ProcessError>>

this.Signal

Full Usage: this.Signal

Parameters:
Returns: Result<unit, ProcessError>

Broadcast `signal` to every process in the group. A member that has already exited (or an already-empty group) is a best-effort success — that races the target's own exit, not a caller error. `Signal.Other 0` (a liveness *probe* that delivers nothing) and a negative number are not deliverable signals at all: they are refused up front with `ProcessError.Unsupported`, never a false success, even on an empty group. A number that IS a signal but the platform rejects (e.g. an out-of-range `Signal.Other 999`, EINVAL) is a genuine delivery failure and returns `ProcessError.Io` with the errno detail; when the group has several members, the first genuine failure is reported but every member still receives the signal. On **Windows** `Signal.Kill` maps to the atomic Job terminate, and reports a refusal on exactly the terms `KillAll` documents — `ProcessError.Io` while the Job still holds live members, an idempotent success once it is empty. `Signal.Int` and `Signal.Term` are a best-effort soft stop combining two individually-targeted deliveries: a console **CTRL+BREAK** to each child started with `Command.WindowsCtrlSignals()` (spawned in its own console process group) AND a **`WM_CLOSE`** posted to the top-level windows of every member that has one (a windowed Electron/GUI child), targeted strictly by process id so no foreign window is touched. Either mechanism reaching a member is a best-effort `Ok`; the call returns `ProcessError.Unsupported` ONLY when the group has neither a CTRL-capable child nor a windowed member — never a silent downgrade to a kill. Delivery is not compliance even on success — a child may install its own handler or a window may prompt/veto the close. Every other Windows signal returns `ProcessError.Unsupported`. On Linux cgroup v2, `Signal.Kill` uses the same reusable hard-kill path as `KillAll`, including its verified post-kill thaw and the same `ProcessError.Io` failure terms.

signal : Signal
Returns: Result<unit, ProcessError>

this.SoftStopScope

Full Usage: this.SoftStopScope

Returns: Result<SoftStopScope, ProcessError>

How far a soft stop (`Signal.Int`/`Signal.Term`) reaches on this group's CURRENT live membership, right now — see `SoftStopScope` for the full per-mechanism table. A capability report, not an action: side-effect-free, delivering no signal and mutating nothing, so asking never changes the answer a later `Signal`/`ShutdownAsync`/`ShutdownReportAsync` soft stop gets. Errors the same honest way every other control verb does once the group is released (`ProcessError.Unsupported`) — there is no live membership left to read.

Returns: Result<SoftStopScope, ProcessError>

this.StartAsync

Full Usage: this.StartAsync

Parameters:
Returns: Task<Result<RunningProcess, ProcessError>>

Start `command` into this shared group and return a live `RunningProcess`. The **group** owns the child's lifetime: disposing the returned process detaches its I/O but does not kill it — reap the tree with `ShutdownAsync`/`Dispose`, or this one run with its `Kill`. `cancellationToken` is checked once, before the spawn (an already-cancelled token reports `ProcessError.Cancelled` and starts nothing); once the child is running the token is not tracked — this live handle is caller-driven, so kill or reap it yourself. (The capture/completion verbs on a `ProcessGroup` do watch the token — and the command's `CancelOn` — for the whole run.)

command : Command
?cancellationToken : CancellationToken
Returns: Task<Result<RunningProcess, ProcessError>>

this.Stats

Full Usage: this.Stats

Returns: Result<ProcessGroupStats, ProcessError>

A snapshot of the group's resource usage. On Windows this reads the Job Object's accounting (CPU + peak committed memory + I/O + active count); on cgroup v2 it reads the cgroup accounting; on the POSIX fallback only the live count (its tracked groups plus any anchor-matching `AdoptByPid` member) is available, and on the FreeBSD process reaper only the live count as well — the whole contained tree there, since a reaper contains a tree but accounts for nothing in it. Errors once the group is released.

Returns: Result<ProcessGroupStats, ProcessError>

this.Suspend

Full Usage: this.Suspend

Returns: Result<unit, ProcessError>

Suspend (freeze) every process in the group. POSIX: `SIGSTOP` (level-triggered, idempotent). Cgroup v2: `cgroup.freeze`. Windows: suspend every thread of every member; suspend counts stack, so N `Suspend`s need N `Resume`s. A failed native delivery or cgroup write returns `ProcessError.Io` — including a Windows member that was confirmed to be in the job and still could not be suspended, which is reported even when the other members were frozen (a partial freeze is reported, not rolled back). A member that exited concurrently is a successful no-op on every platform, as is a Windows pid that was recycled onto an unrelated process.

Returns: Result<unit, ProcessError>

this.UpdateLimits

Full Usage: this.UpdateLimits

Parameters:
Returns: Result<unit, ProcessError>

Apply a new whole-tree resource-limit set to this **live** group, without recreating it or restarting its children — adaptive resource control (tighten memory on a sagging batch, widen a long-lived worker pool's CPU quota) at runtime. The `limits` are a full REPLACEMENT of the caps in force: a dimension left `None` becomes unbounded again, not left at its previous cap. A limit-capable mechanism re-applies the caps to its live container — **Windows** re-issues `SetInformationJobObject` on the Job (the caps, the CPU-affinity mask, and the UI restrictions), **Linux cgroup v2** rewrites `memory.max`/`memory.oom.group`/`pids.max`/`cpu.max`/`cpuset.cpus` in place — while the **POSIX process-group** mechanism (macOS/BSD, or Linux without cgroup v2) has no whole-tree limit primitive and returns `ProcessError.ResourceLimit`, the same honest, typed refusal `Create` gives for a limited group there — never a silent no-op. The **FreeBSD process reaper** answers that same `ProcessError.ResourceLimit` for its own reason: it contains a whole tree but accounts for nothing in it, so there is no whole-tree cap to update — and never a per-process `RLIMIT_*` surrogate presented as one. On Linux the CPU-affinity pin additionally needs the `cpuset` controller: a hierarchy that does not carry it at all cannot enforce a pin, so an update requesting one is refused with that same typed `ProcessError.ResourceLimit` — and refused before any controller file is written, so the previous caps stay in force — never a dropped pin. Routed through the same lifecycle gate as the other control verbs: the released-flag check AND the native re-apply run in one critical section, so a call racing (or following) teardown either applies fully on the live container or observes the flag and returns a non-transient `ProcessError.Unsupported` BEFORE touching the closed/recycled native handle — never a use-after-teardown. Only on a successful apply is the `Options` snapshot swapped to the new set, under the same lock, so `Options.Limits` a consumer reads back matches what is actually enforced. The caps land through several sequential native writes, so a later write can fail after an earlier one applied. A failed apply is honest about that: a limit-capable mechanism best-effort **restores the previous set** to its live container before returning the error, so a failed `UpdateLimits` leaves BOTH the container and the `Options` snapshot on the previous set (nothing net changed) — the container and `Options.Limits` never silently diverge. In the rare case the restore itself also fails, the returned `ProcessError.ResourceLimit` says so explicitly (its message notes the limits may be partially applied) and `Options.Limits` keeps reporting the previous set: the error is the explicit signal that the container's state is uncertain, so a consumer must not treat the snapshot as authoritative there — never a silent divergence.

limits : ResourceLimits
Returns: Result<unit, ProcessError>

Static members

Static member Description

ProcessGroup.Capabilities(options)

Full Usage: ProcessGroup.Capabilities(options)

Parameters:
Returns: ContainmentCapabilities

What process containment can actually do on **this** host for `options` — the mechanism `Create(options)` would select, and, per axis, either a real availability or a typed `Capability.Unsupported` naming the precondition that is missing. Taken WITHOUT creating a group, spawning a process, or touching any container, so a long-lived orchestrator can choose a portable policy *before* the first spawn instead of creating a group and probing each operation. The mechanism it reports comes from the very decision `Create` dispatches on, and each capability from the very probe the corresponding spawn path consults, so the snapshot cannot drift from what the real calls do. It is a point-in-time report and deliberately not cached — see `ContainmentCapabilities` for what that does and does not promise.

options : ProcessGroupOptions
Returns: ContainmentCapabilities

ProcessGroup.Capabilities()

Full Usage: ProcessGroup.Capabilities()

Returns: ContainmentCapabilities

What process containment can actually do on **this** host, for the default options — a side-effect-free snapshot taken WITHOUT creating a group or spawning anything. See the `ProcessGroupOptions` overload.

Returns: ContainmentCapabilities

ProcessGroup.Create(options)

Full Usage: ProcessGroup.Create(options)

Parameters:
Returns: Result<ProcessGroup, ProcessError>

Create a new kill-on-dispose group with `options` (graceful-shutdown window and whole-tree resource limits). When `options.Limits` is set, the group needs a limit-capable mechanism — a Windows Job Object or a Linux cgroup v2 at the real cgroup root; otherwise creation fails fast with `ProcessError.ResourceLimit` rather than leaving the tree unbounded. Without limits the group uses the platform's default mechanism (Job Object on Windows, the `procctl(2)` process reaper on FreeBSD when reaper status can be acquired, POSIX process group elsewhere). On the Linux cgroup v2 mechanism, a spawned child is migrated into the cgroup right after it starts, and the limits then apply to it and every descendant it forks *afterwards*. A grandchild forked in the brief spawn→migrate window is created in the parent cgroup and stays there — still reaped by kill-on-drop teardown, but outside the resource limits. If the child cannot be migrated at all (e.g. the cgroup was torn down underneath the spawn), it is killed and reaped and the spawn fails with `ProcessError.ResourceLimit` — never left running unconstrained.

options : ProcessGroupOptions
Returns: Result<ProcessGroup, ProcessError>

ProcessGroup.Create()

Full Usage: ProcessGroup.Create()

Returns: Result<ProcessGroup, ProcessError>

Create a new, empty kill-on-dispose group on the current platform (no resource limits).

Returns: Result<ProcessGroup, ProcessError>

Type something to start searching.