Separate the command from the process owner.
A Command is a copyable launch description. Build it with To_Command. It contains the executable, ordered arguments, environment policy, working directory, and path-search policy.
A Process is the limited owner created by Spawn. It owns the root process, its process group, the parent ends of three pipes, and the reaper that prevents a zombie.
The child receives blocking descriptors 0, 1, and 2. The parent ends are nonblocking and close-on-exec. A lightweight caller suspends only its task on pipe or exit readiness. A native caller can block only its own pthread.
Choose capture or explicit pipe ownership.
Flyology.Subprocesses.Capture- Use one finite input value and bounded retained output. The helper owns all pipe progress, exit waiting, timeout cleanup, and cancellation cleanup.
- Owned
Process - Use a streaming or interactive protocol, incremental parsing, custom signal policy, or output destinations that should not accumulate in memory.
- Native-task boundary
- Use this boundary when synchronous spawn latency is not acceptable on an event-loop pthread. Pipe and exit waits remain task-aware after spawn.
Capture is the normal choice for short control commands. Use the low-level owner only when the application must control the stream lifecycle itself.
Build argv and launch policy explicitly.
- Arguments
- Call
Append_Argumentonce for each exactargvvalue. Empty arguments remain present. NUL bytes are rejected. - Environment
- The default inherits the complete parent environment.
Set_Environment_Variableimmediately switches to an explicit environment containing only variables set on that command. CallClear_Environmentfirst when an intentionally empty starting point makes that choice clearer.Inherit_Environmentrestores inheritance. - Directory
- The default inherits the parent working directory.
Set_Working_Directoryapplies a spawn file action before the executable starts. - Path search
- The default requires an executable path.
Set_Path_Searchselectsposix_spawnp. Lookup uses the parent process'sPATH, even when the child receives an explicit environment.
A value such as "*.txt", "$HOME", or "a | b" remains one literal argument. When the application needs a pipeline, start separate processes and pump bytes through their parent pipe operations, or use a separate pipeline adapter.
Run a finite command with bounded output.
Capture.Run progresses stdin, stdout, stderr, process exit, and cancellation in one readiness loop. It returns a bounded Result only after the root is reaped and all three pipes are closed.
Test Successful, then read Standard_Output. On command failure, inspect Standard_Error and the captured Status. That Exit_Status has an Exit_Kind and the Kind, Code, and Signal fields used below. Treat Output_Truncated or Error_Truncated as an explicit data-loss condition.
with Ada.Text_IO;
with Flyology.Subprocesses;
with Flyology.Subprocesses.Capture;
procedure Uppercase is
package Processes renames Flyology.Subprocesses;
package Capture renames Flyology.Subprocesses.Capture;
use type Processes.Exit_Kind;
Command : Processes.Command := Processes.To_Command ("/usr/bin/tr");
begin
Processes.Append_Argument (Command, "[:lower:]");
Processes.Append_Argument (Command, "[:upper:]");
declare
Result : constant Capture.Result := Capture.Run
(Command,
Standard_Input => "flyology" & ASCII.LF,
Maximum_Output => 4 * 1_024,
Maximum_Error => 4 * 1_024,
Timeout => 5.0);
Status : constant Processes.Exit_Status := Capture.Status (Result);
begin
if Capture.Output_Truncated (Result)
or else Capture.Error_Truncated (Result)
then
raise Program_Error with "tr output exceeded its contract";
elsif Processes.Successful (Status) then
Ada.Text_IO.Put (Capture.Standard_Output (Result));
elsif Status.Kind = Processes.Exited then
raise Program_Error with
"tr exited" & Natural'Image (Status.Code) & ": "
& Capture.Standard_Error (Result);
else
raise Program_Error with
"tr received signal" & Natural'Image (Status.Signal);
end if;
end;
end Uppercase;
The program prints FLYOLOGY. It passes each argument separately, writes the complete input, publishes stdin EOF, and verifies both retention bounds.
Each bound limits retained bytes, not drained bytes. After a bound fills, capture discards later bytes until EOF. The defaults retain 64 KiB from each output stream and use a 30-second timeout.
Continued draining prevents a full pipe from blocking a verbose child. Input writes are interleaved with both output streams, so output-before-input protocols do not create a two-pipe deadlock.
Captured strings preserve raw eight-bit values and perform no character decoding. Stdout and stderr remain separate, so their relative byte ordering is not available in the result.
Classify launch separately from completion.
An Exit_Status describes a root process that was successfully launched and later reaped. Its Exit_Kind is Exited or Signaled. A nonzero exit is data, not a spawn exception.
- Exited, code 0
Successfulreturns true. The command completed with the conventional success code.- Exited, nonzero code
KindisExited, andCodecontains 1 through 255. Apply command-specific meaning.- Signaled
KindisSignaled.Signalcontains the host signal number, andCore_Dumpedreports the host wait status.
Identifier returns the root's launch-time process identifier for diagnostics. Once the root is reaped, the host can reuse that value for an unrelated process even while the owner remains open. Never use it for signaling or as a durable application identity.
Loop over partial pipe operations.
The low-level owner exposes synchronous Write_Standard_Input, Read_Standard_Output, and Read_Standard_Error operations. Call Close_Standard_Input to publish EOF after the final input byte.
One call transfers one available chunk. A write can stop before the end of its source slice. A read reports EOF when Last precedes the destination's first index. Loop until every input byte is accepted and each output stream reaches EOF.
procedure Write_All
(Child : in out Processes.Process;
Data : Ada.Streams.Stream_Element_Array)
is
Next : Ada.Streams.Stream_Element_Offset := Data'First;
Last : Ada.Streams.Stream_Element_Offset;
begin
while Next <= Data'Last loop
Processes.Write_Standard_Input
(Child, Data (Next .. Data'Last), Last, Timeout => 5.0);
Next := Last + 1;
end loop;
Processes.Close_Standard_Input (Child);
end Write_All;
procedure Drain_Stdout (Child : in out Processes.Process) is
Buffer : Ada.Streams.Stream_Element_Array (1 .. 16 * 1_024);
Last : Ada.Streams.Stream_Element_Offset;
begin
loop
Processes.Read_Standard_Output
(Child, Buffer, Last, Timeout => 5.0);
exit when Last < Buffer'First;
Consume (Buffer (Buffer'First .. Last));
end loop;
end Drain_Stdout;
Run the stdin writer, stdout reader, and stderr reader concurrently. Give each standard stream at most one active operation. A child can otherwise block while writing one full output pipe before another stream is drained.
- stdin owner
- Loop over partial writes. Close stdin exactly once after the last byte so the child observes EOF.
- stdout owner
- Loop until stdout EOF. Parse or forward every returned slice before reusing the buffer.
- stderr owner
- Run an independent read loop until stderr EOF. Do not wait for stdout to finish first.
- Spawn
- Write and drain concurrently
- Close stdin
- Read both EOFs
- Wait
- Join operations
- Close
Join every stream operation before calling Close or finalizing the owner. On exceptional exit, cancel active stream waits and apply a terminal policy with Stop or Kill, then join the operations before closing the owner. A catchable signal alone may be ignored and does not guarantee that a blocked stream operation can finish.
Close_Standard_Output and Close_Standard_Error abandon unread bytes. Serialize either close after the affected reader returns. A later child write can then fail with EPIPE or terminate under its own SIGPIPE disposition.
Pipe readiness uses the existing Flyology.IO descriptor machinery. Each parent write suppresses SIGPIPE for that write on the calling pthread; a closed child read end raises Pipe_Error instead.
Distinguish waiting from cleanup policy.
Pipe operations and Wait accept a relative monotonic timeout and an optional Flyology.Cancellation.Token. A low-level timeout or cancellation ends only that operation. It does not signal or close the process.
Each low-level call starts its own deadline. Repeating a five-second read can wait five seconds on every iteration. Compute a remaining duration from one application deadline when the complete protocol needs a shared bound, or use Capture.Run.
A negative timeout waits without a deadline. A zero timeout permits only an immediate transfer or readiness observation.
Capture.Run starts one command-progress deadline before spawn. Synchronous spawn cannot be interrupted by that deadline. After spawn returns, capture checks the deadline and cancellation between readiness operations, even while output remains continuously ready.
Any task can call Token.Request. Pass the token with Token => Stop'Access to a pipe operation, Wait, or capture.
Structured cleanup runs outside the capture deadline. Signal delivery, root reaping, reaper joining, and kernel task states can therefore delay exception delivery.
Choose graceful or hard termination explicitly.
Send_Signal- Send interrupt, graceful termination, or hard kill to the original process group. This operation does not wait or close pipes.
Kill- Send the uncatchable hard signal without waiting. Follow it with
WaitorCloseto reap the root. Stop- Send graceful termination, wait for the root, and escalate when the grace interval expires. Output pipes remain open for explicit draining or close.
Close- End ownership. Close stdin, hard-terminate a running group, reap the root, close both output pipes, join the reaper, then release the exit wake source.
A nonpositive Stop interval permits only an immediate root observation before hard termination. The grace interval bounds waiting for the root, not graceful survival of every descendant.
The root process starts in a new process group. Before publishing exit readiness, the reaper hard-terminates members that remain after root exit. This cleanup can occur before the Stop grace interval ends because the root already terminated.
Finalization performs the same ownership cleanup as Close but does not propagate an exception. Use explicit close when the application must observe cleanup failure.
Handle launch, progress, and command failure separately.
Spawn_Error- The executable, pipes, spawn attributes, working-directory action, exec step, exit-readiness setup, or reaper setup failed. No command result exists.
- Nonzero exit
- Spawn succeeded. Inspect
Exit_Statusand command output. Do not catch this as a launch exception. Flyology.IO.Timeout_Error- The applicable monotonic deadline expired. Low-level ownership remains open. Capture performs structured cleanup before propagation.
Operation_Cancelled- The supplied token was requested. Low-level ownership remains open. Capture performs structured cleanup before propagation.
Pipe_Error- A standard-stream descriptor or transfer failed. Decide whether retained protocol state is still usable, then stop or close the owner.
Process_Error- Exit observation, signaling, reaping, or explicit cleanup failed. The exception marks an ownership failure, not a child exit code.
Flyology.IO.Device_Error- The capture readiness multiplexer failed. Capture closes the structured child scope before propagation.
- Constraint, storage, or program error
- The command description is invalid, allocation failed, or the caller violated owner state. These are caller or runtime failures rather than child results.
Put the process inside one task generation.
A supervised generation should declare its process owner inside the task body. Its callback receives a Generation_Control. Call Mark_Ready only after spawn and application validation succeed.
Pass Stopping as the wait token. On cancellation, stop the child and close it before the task returns.
procedure Execute
(Context : in out Service_Context;
Control : not null access Generation_Control)
is
Command : Processes.Command := Build_Command (Context);
Child : Processes.Process;
Status : Processes.Exit_Status;
begin
Processes.Spawn (Command, Child);
Validate_Child (Child, Context);
Mark_Ready (Control.all);
begin
Processes.Wait
(Child, Status, Token => Stopping (Control.all));
exception
when Flyology.Cancellation.Operation_Cancelled =>
Processes.Stop (Child, Grace => 2.0, Status => Status);
end;
Processes.Close (Child);
end Execute;
- Generation N owns child
- Stop requested
- Root reaped
- Pipes closed
- Reaper joined
- Task joined
- Generation N+1 validates
- Generation N+1 may publish
The example has the callback profile of Flyology.Supervision.Children. Its Run call returns only after the generation's Ada master has joined the task and completed task-body finalization. Applications with a custom task type can use Flyology.Supervision.Task_Generations as the lower-level alternative. A controller must cross the chosen join boundary before it publishes replacement readiness.
If spawn or validation raises before Mark_Ready, controlled finalization still closes the local process owner. The failed generation never publishes readiness.
Process termination remains application policy. Flyology supervision does not automatically kill arbitrary descendants of every supervised Ada task. Only an explicitly declared Process owner applies subprocess cleanup.
Reconcile externally durable effects before restart.
Process ownership controls one launched operating-system process group. It does not reverse external effects created by that process. A command can modify files, network services, remote state, namespaces, or persistent registries before it exits.
Set supervision Restart_Safe only when a replacement can inspect and reconcile those effects. Use stable application identities and idempotent operations. Do not infer external state only from the old process identifier or its exit status.
For example, a command can rename a completed report into its final path and exit before the generation publishes readiness. The replacement must inspect the stable path and validate the report. It can adopt a complete result, remove an incomplete temporary file, or report a conflicting result. Starting the command again without that check is not restart-safe.
- Identity
- Record an application-selected job, resource, or output identity that survives process replacement.
- Inspection
- Classify the external state as absent, incomplete, complete, or conflicting before publication.
- Action
- Adopt, finish, replace, or reject that state through an idempotent operation with explicit authority.
If a child deliberately leaves the original process group with setpgid or setsid, Flyology can no longer contain it through that group. Treat that behavior as an external durable resource. Record its identity, reconcile it before publication, and assign its cleanup to an explicit authority.
Use the Darwin and Linux POSIX boundary.
- Spawn
- Darwin and Linux use synchronous
posix_spawnorposix_spawnp. Flyology does not callforkand then enter Ada or GNARL state in the child. Spawn resets the child signal mask and catchable dispositions before exec. - Directory
- Darwin uses its compatible
addchdirspawn action. Supported glibc Linux builds provideposix_spawn_file_actions_addchdir_np. Older glibc versions without that extension are outside the current build matrix; other hosts reject a configured directory. - Pipe waits
- Darwin uses
kqueue. Linux usesepoll. Native tasks usepollon both hosts. - Exit waits
- Each live process owns one native reaper task. The task waits for root exit and signals an ordinary Flyology wake descriptor.
- Signals
- Signals target the negative process-group identifier. Hard cleanup uses the host
SIGKILLvalue.
The native reaper gives exit readiness the same cancellation and deadline composition as other descriptors. It also consumes one native task and pthread per live subprocess. Keep large subprocess populations outside this initial slice or add a separately designed shared reaper backend.
Budget the ownership scope before scaling it.
- Supported hosts
- Darwin and Linux. The API starts native operating-system processes and never runs an Ada callback in the child.
- Parent descriptors
- A live owner with all streams open holds five Flyology descriptors: stdin, stdout, stderr, and both ends of the exit wake source. Closing streams releases their descriptors before owner close.
- Cancellation descriptors
- An unrequested token can lazily add a separate two-descriptor wake source when capture or a low-level wait borrows it. The caller owns that token, can share it across operations, and must keep it alive through every call.
- Native resources
- Each live subprocess owns one native reaper task and pthread. This initial design is not intended for large subprocess populations.
- Capture payload
- Retained payload is bounded by
Maximum_Output + Maximum_Error. Total storage also includes the complete caller-supplied input and command plus implementation-dependent unbounded-string capacity and allocator overhead. Capture uses one 4,096-byte transfer buffer and a temporary retained slice of at most 4,096 bytes. - Stream ordering
- Bytes stay ordered within each stream. Capture does not preserve a total order between stdout and stderr.
- Descriptor inheritance
- Flyology-created descriptors are close-on-exec. Application descriptors without
FD_CLOEXECfollow their own inheritance policy. - Spawn latency
- Spawn can occupy a lightweight caller's event-loop pthread. Its synchronous work cannot be interrupted by a capture timeout or token.
- Containment
- Cleanup reaches the root and descendants that remain in its original process group. It cannot reach a deliberate
setpgidorsetsidescape. - Kernel completion
- Finalization may wait indefinitely when the kernel cannot complete hard termination, such as an uninterruptible task state.