LittleForker 4.0.0
dotnet add package LittleForker --version 4.0.0
NuGet\Install-Package LittleForker -Version 4.0.0
<PackageReference Include="LittleForker" Version="4.0.0" />
<PackageVersion Include="LittleForker" Version="4.0.0" />
<PackageReference Include="LittleForker" />
paket add LittleForker --version 4.0.0
#r "nuget: LittleForker, 4.0.0"
#:package LittleForker@4.0.0
#addin nuget:?package=LittleForker&version=4.0.0
#tool nuget:?package=LittleForker&version=4.0.0
Little Forker
A utility to aid in the launching and supervision of processes. The original use case is installing a single service who then spawns other processes as part of a multi-process application.
Features
ProcessExitedHelper: a helper around
Process.Exitedwith some additional logging and event raising if the process has already exited or not found.ProcessSupervisor: allows a parent process to launch a child process and its lifecycle is represented as a state machine. Supervisors can participate in cooperative shutdown if supported by the child process.
CooperativeShutdown: allows a process to listen for a shutdown signal over a named pipe for a parent process to instruct a process to shut down.
DI-friendly hosted services for ASP.NET Core / Generic Host applications:
CooperativeShutdownHostedService— listens for cooperative shutdown signals and stops the application when received.WatchParentProcessHostedService— monitors a parent process and stops the application when the parent exits.
Installation
dotnet add package LittleForker
CI packages are published as artifacts on GitHub Actions.
Using
All components use Microsoft.Extensions.Logging.ILoggerFactory for structured
logging.
1. ProcessExitedHelper
This helper is typically used by "child" processes to monitor a "parent" process so that it exits itself when the parent exits. It's also a safeguard in cooperative shutdown if the parent failed to signal correctly (i.e. it crashed).
It wraps Process.Exited with some additional behaviour:
- Raises the event if the process is not found.
- Raises the event if the process has already exited which would otherwise
result in an
InvalidOperationException. - Logging.
This is something simple to implement in your own code so you may
consider copying it if you don't want a dependency on LittleForker.
Typically you will tell a process to monitor another process by passing in the other process's Id as a command line argument. Something like:
.\MyApp --ParentProcessID=12345
Here we extract the CLI arg using Microsoft.Extensions.Configuration, watch
for a parent to exit and exit ourselves when that happens.
var loggerFactory = LoggerFactory.Create(b => b.AddConsole());
var configRoot = new ConfigurationBuilder()
.AddCommandLine(args)
.Build();
var parentPid = configRoot.GetValue<int>("ParentProcessId");
using (new ProcessExitedHelper(parentPid, _ => Environment.Exit(0), loggerFactory))
{
// Rest of application
}
Environment.Exit(0) is quite an abrupt way to shut down; you may want to
handle things more gracefully such as flush data, cancel requests in flight etc.
For an example, see
NonTerminatingProcess that uses
a CancellationTokenSource.
2. ProcessSupervisor
Process supervisor launches a process and tracks its lifecycle represented as a state machine. Typical use case is a "parent" process launching one or more "child" processes.
A process's state is represented by ProcessSupervisor.State enum:
- NotStarted
- Running
- StartFailed
- Stopping
- ExitedSuccessfully
- ExitedWithError
- ExitedKilled
... with the transitions between them described with this state machine:
All terminal states (StartFailed, ExitedSuccessfully, ExitedWithError,
ExitedKilled) permit restarting by calling Start() again.
Typically, you will want to launch a process and wait until it is in a specific state before continuing (or handle errors).
var loggerFactory = LoggerFactory.Create(b => b.AddConsole());
var settings = new ProcessSupervisorSettings(
workingDirectory: Environment.CurrentDirectory,
processPath: "dotnet")
{
Arguments = "./LongRunningProcess/LongRunningProcess.dll"
};
var supervisor = new ProcessSupervisor(settings, loggerFactory);
// attach to events
supervisor.StateChanged += state => { /* handle state changes */ };
supervisor.OutputDataReceived += s => { /* console output */ };
// start the supervisor which will launch the process
await supervisor.Start();
// ... some time later
// attempts a cooperative shutdown with a timeout of 3
// seconds otherwise kills the process
await supervisor.Stop(TimeSpan.FromSeconds(3));
With an async extension, it is possible to await a supervisor state:
var exitedSuccessfully = supervisor.WhenStateIs(ProcessSupervisor.State.ExitedSuccessfully);
await supervisor.Start();
await exitedSuccessfully;
WhenStateIs completes immediately if the supervisor is already in the
requested state, so it is safe to call at any point.
You can also leverage tasks to combine waiting for various expected states:
var startFailed = supervisor.WhenStateIs(ProcessSupervisor.State.StartFailed);
var exitedSuccessfully = supervisor.WhenStateIs(ProcessSupervisor.State.ExitedSuccessfully);
var exitedWithError = supervisor.WhenStateIs(ProcessSupervisor.State.ExitedWithError);
await supervisor.Start();
var result = await Task.WhenAny(startFailed, exitedSuccessfully, exitedWithError);
if (result == startFailed)
{
Log.Error(supervisor.OnStartException, "Process start failed.");
}
// etc.
3. CooperativeShutdown
Cooperative shutdown allows a "parent" process to instruct a "child" process to
shut down. Different to SIGTERM and Process.Kill() in that it allows a child
to acknowledge receipt of the request and shut down cleanly (and fast!). Combined with
Supervisor.Stop() a parent can send the signal and then wait for ExitedSuccessfully.
The inter-process communication is done via named pipes where the pipe name is
of the format LittleForker-{processId}. On Windows, pipe access is restricted
to the current user via PipeSecurity ACLs.
For a "child" process to be able to receive cooperative shutdown requests it uses
CooperativeShutdown.Listen() to listen on a named pipe. Handling signals should
be fast operations and are typically implemented by signalling to another mechanism
to start cleanly shutting down:
var loggerFactory = LoggerFactory.Create(b => b.AddConsole());
var shutdown = new CancellationTokenSource();
using (await CooperativeShutdown.Listen(() => shutdown.Cancel(), loggerFactory))
{
// rest of application checks shutdown token for cooperative
// cancellation. See MSDN for details.
}
For a "parent" process to signal:
await CooperativeShutdown.SignalExit(childProcessId, loggerFactory);
This is used internally by ProcessSupervisor so if your parent process is using that,
you typically won't need to call this directly.
Security nonce
To prevent other local processes from sending unsolicited EXIT signals, you can use a shared nonce known to both parent and child. The nonce is validated over the wire protocol (not embedded in the pipe name), so it cannot be discovered by enumerating named pipes.
// Child process — read nonce from environment variable set by parent
var nonce = Environment.GetEnvironmentVariable("LITTLEFORKER_NONCE");
using (await CooperativeShutdown.Listen(() => shutdown.Cancel(), loggerFactory, nonce))
{
// ...
}
// Parent process — signal with the same nonce
await CooperativeShutdown.SignalExit(childProcessId, loggerFactory, nonce);
4. DI / Hosted Service Integration
For applications using the .NET Generic Host (Microsoft.Extensions.Hosting),
LittleForker provides hosted services that can be registered via dependency
injection. This is the recommended approach for ASP.NET Core and worker service
applications.
CooperativeShutdownHostedService
Listens for a cooperative shutdown signal and calls
IHostApplicationLifetime.StopApplication() when received:
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
// Default: listens on pipe "LittleForker-{pid}"
services.AddCooperativeShutdownHostedService();
// Or with a security nonce:
services.AddCooperativeShutdownHostedService(o =>
o.Nonce = Environment.GetEnvironmentVariable("LITTLEFORKER_NONCE"));
// Or with an explicit pipe name:
services.AddCooperativeShutdownHostedService(o =>
o.PipeName = "my-custom-pipe");
})
.Build();
await host.RunAsync();
WatchParentProcessHostedService
Monitors a parent process and stops the application when the parent exits. This is a safeguard in case cooperative shutdown fails (e.g. the parent crashed):
var parentPid = config.GetValue<int?>("ParentProcessId");
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
if (parentPid.HasValue)
{
services.AddWatchParentProcessHostedService(o =>
o.ParentProcessId = parentPid);
}
})
.Build();
await host.RunAsync();
Building
Requires .NET 10.0 SDK.
dotnet build -c Release
dotnet test --project src/LittleForker.Tests -c Release
Credits & Feedback
@randompunter for feedback.
Hat tip to @markrendle for the project name.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.3)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.3)
- Stateless (>= 5.20.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.