Dependency injection
The ProcessKit.Extensions.DependencyInjection package wires ProcessKit into
Microsoft.Extensions.DependencyInjection. It stays dependency-light — only the DI, Logging,
Options, and Configuration extension packages that a DI-integration package inevitably needs, and no
hosting dependency — and every registration uses TryAdd, so a pre-existing registration of yours
always wins.
The runner
AddProcessKit() registers IProcessRunner as a singleton JobRunner. When the container also has an
ILoggerFactory, the runner is wrapped so every run it drives emits ProcessKit's lifecycle events under
the ProcessKit category (argv/env never logged — see Observability).
services.AddProcessKit();
// Injected anywhere:
public class Deployer(IProcessRunner runner)
{
public Task<FSharpResult<string, ProcessError>> Deploy() =>
runner.RunAsync(new Command("deploy"), CancellationToken.None);
}
Default settings (ProcessKitOptions)
Configure defaults applied to every DI-resolved run — from code or from configuration. Each default is applied only when the command does not set it itself, so a per-command value always wins.
// From code:
services.AddProcessKit(o =>
{
o.DefaultTimeout = TimeSpan.FromSeconds(30);
o.DefaultWorkingDirectory = "/app";
});
// …or bound from an IConfiguration section (appsettings.json "ProcessKit"):
services.AddProcessKit(configuration.GetSection("ProcessKit"));
ProcessKitOptions covers what a primitive runner can apply on the spawn path — timeout and working
directory. Retry is a verb-layer policy (the retry loop reads the command before this runner sees it),
so a retry default can't ride on the bare runner; set it — and richer per-tool defaults like encoding,
ok-codes, and environment — on a named client instead, whose template precedes the verb:
services.AddProcessKitClient("git", "git",
c => c.WithDefaults(cmd => cmd.Retry(3, TimeSpan.FromSeconds(1), e => e.IsTransient)));
Named / keyed tool clients
Register a keyed CliClient per external tool, so an app injects "the git client" or "the ffmpeg client"
by role. Each client runs through the container's registered IProcessRunner (so it is logger-aware and
honours a shared group or a test runner), and configure applies shared defaults via the CliClient
builder.
services.AddProcessKit();
services.AddProcessKitClient("git", "git", c => c.WithDefaults(cmd => cmd.CurrentDir("/repo")));
services.AddProcessKitClient("ffmpeg", "ffmpeg");
public class Repo([FromKeyedServices("git")] CliClient git)
{
public Task<FSharpResult<string, ProcessError>> Status() => git.RunAsync(["status"]);
}
A shared, container-managed process group
AddProcessKitGroup() backs IProcessRunner with a single shared ProcessGroup whose lifetime is the
container's — every run goes into one kill-on-dispose container, and disposing the provider reaps the
whole tree. Ideal for a hosted service that should leave no orphaned children when it stops. The
ProcessGroup is also registered directly, so you can inject it for tree control (Signal / Suspend /
Members / …). Call it instead of AddProcessKit() when you want a shared group.
services.AddProcessKitGroup();
// IProcessRunner now runs every command into the shared group;
// await using the provider (or host shutdown) reaps all children.
Both AddProcessKitGroup() and AddProcessKit() register IProcessRunner with TryAdd, so call one
or the other — whichever runs first wins. If AddProcessKit() runs first, IProcessRunner stays the
per-run JobRunner, and a later AddProcessKitGroup() still registers the ProcessGroup but no runs
go into it — an easy-to-miss mis-wire. AddProcessKitGroup(configure) / AddProcessKitGroup(configuration)
apply the same ProcessKitOptions defaults as the AddProcessKit overloads.
Hosting a supervised child
Use the ProcessKit.Extensions.Hosting package when a supervised child should live for the host's
lifetime. It depends only on Microsoft.Extensions.Hosting.Abstractions, discovers an existing
DI-registered IProcessRunner when one is present, starts Supervisor.RunAsync in the background, and
calls RunningProcess.StopAsync during host shutdown.
services.AddProcessKitGroup();
services.AddProcessKitHostedProcess(
"worker",
new Command("worker").Arg("--serve"),
supervisor => supervisor
.Restart(RestartPolicy.OnCrash)
.OnRestart(e => metrics.Restarts.Add(1)));
services.ConfigureProcessKitHostedProcess("worker", o =>
{
o.ShutdownGracePeriod = TimeSpan.FromSeconds(10);
});
Resolve HostedProcessService by the same key when you need the last SupervisionOutcome or stop
outcome for health reporting. It also exposes live supervision telemetry — IsSupervisionActive,
RestartCount, IsStormPaused — for anything that wants to observe the child without waiting for
supervision to end (e.g. metrics, or the health check below).
Health-checking a hosted process
AddProcessKitHostedProcessHealthCheck(name) registers a keyed IHealthCheck
(HostedProcessHealthCheck, same key as AddProcessKitHostedProcess) that maps the named hosted
process's supervision state: Healthy while it is running (including restarting within policy),
Degraded while the failure-storm guard (Supervisor.StormPause) is throttling restarts, and
Unhealthy once supervision is not active (not started yet, or ended — an error, an exhausted
restart budget, a permanent-failure give-up, or a stop-predicate match).
This is opt-in and stays in ProcessKit.Extensions.Hosting (not a separate package): its only extra
dependency, Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions, is Abstractions-only —
IHealthCheck / HealthCheckResult / HealthCheckRegistration, never the full
Microsoft.Extensions.Diagnostics.HealthChecks package that supplies AddHealthChecks() /
IHealthChecksBuilder / the concrete polling HealthCheckService. That package stays out of this
one's dependency graph, so a consumer who never calls AddProcessKitHostedProcessHealthCheck never
pulls it in either — but it also means this method cannot call AddHealthChecks() on your behalf.
Wire the registered keyed check into your own health-checks pipeline (already referenced
transitively via the ASP.NET Core shared framework in a web host; add
Microsoft.Extensions.Diagnostics.HealthChecks explicitly in a Worker Service) with
HealthCheckRegistration's factory overload:
services.AddProcessKitHostedProcess("worker", new Command("worker").Arg("--serve"));
services.AddProcessKitHostedProcessHealthCheck("worker");
services.AddHealthChecks().Add(
new HealthCheckRegistration(
"worker",
sp => sp.GetRequiredKeyedService<HostedProcessHealthCheck>("worker"),
failureStatus: null,
tags: null));