silk.system.subprocess.
Start a child process whose standard streams stay open while it runs, send it signals, and learn when it ends through a descriptor that an event loop can watch.
When to use#
Use Subprocess.spawn when your program talks to the child while it runs, as a JavaScript
runtime or a shell does: you get the process ID and the parent end of each pipe as raw
descriptors, and you read and write them in your own loop. To run a program to the end and get
all of its output, use the ChildProcess effect from silk.system.child_process instead.
Details#
A ProcessRequest from silk.system.child_process gives the program, the arguments, the
environment, and the working directory. SpawnOptions gives the rest: what the child gets in
each descriptor slot (Stdio), the first argument, a new session, and the user and group.
Every call returns at once. Nothing in this module waits for the child:
The parent end of each
Stdio.pipeslot is a nonblocking, close-on-exec UNIX stream socket.ExitWatch.opengives a descriptor that becomes readable when the child ends: a pidfd on Linux, and a kqueue with anEVFILT_PROCfilter on Darwin. Register it with a reactor, such assilk.io.reactor, next to the pipes.ExitWatch.collectcollects the exit status once the exit descriptor is readable.Subprocess.tryWaitcollects it if the child has ended, or returnsnoneif it still runs.
The child starts with every signal unblocked, and with the default action for every signal that can be caught, so a signal that the parent blocks or ignores reaches the child normally.
Errors#
Each operation fails with SubprocessError: the SubprocessOperation and the native error
code, such as 2 (ENOENT) when the program does not exist.
Gotchas#
Collect the status after the child ends, or the child stays a zombie process.
Collect the status of a child in one place only. A second
waitpidfor the same child, from other code, takes the status away.A program is a path. This module does not search
PATH.On GNU/Linux, a child with a working directory, a user, or a group starts with
fork, which is slower for a large parent. Darwin usesforkonly for a user or a group.On GNU/Linux, a child started without
forkalso gets each descriptor that the parent itself inherited without close-on-exec.The module is empty on targets other than Darwin on x86_64 or aarch64, and Linux with GNU libc on x86_64 or aarch64.
Examples#
Read the output of a child and wait for its exit#
import silk.mem.allocator { Allocator, OutOfMemoryError }
import silk.mem.pointer { Pointer }
import silk.core.effect { Effect }
import silk.core.option { Option }
import silk.core.result { Result }
import silk.system.child_process { ChildProcess }
import silk.system.filesystem { FileError, Path }
import silk.system.subprocess { ExitStatus, ExitWatch, SpawnOptions, Stdio, Subprocess, SubprocessError }
extern "C" struct PollFd { fd: i32, events: i16, revents: i16 }
static if Intrinsic.targetOperatingSystem() == "darwin" {
@foreign(noCapture: .("fds"))
unsafe extern "C" fn poll(fds: ?[*]mut PollFd, count: u32, timeout: i32) -> i32
} else {
@foreign(noCapture: .("fds"))
unsafe extern "C" fn poll(fds: ?[*]mut PollFd, count: usize, timeout: i32) -> i32
}
@foreign(noCapture: .("buffer"))
unsafe extern "C" fn read(fd: i32, buffer: ?[*]mut u8, count: usize) -> isize
effect fn program() -> i32 ! FileError | SubprocessError | OutOfMemoryError ? &mut Allocator {
let path = run Path.make("/bin/echo")
let mut request = run ChildProcess.request(&path)
run ChildProcess.addArgument(&mut request, "hi")
let mut options = SpawnOptions.make()
run options.setStdio(1, Stdio.pipe())
let mut child = run Subprocess.spawn(&request, &options)
let output = match child.takePipe(1) {
Option<i32>.Some { value } => value
Option<i32>.None => { return 1 }
}
let watch = run ExitWatch.open(child.pid())
// Wait for the exit descriptor; a real program registers it with a reactor instead.
let mut watched = [PollFd { fd: watch.descriptor(), events: 1, revents: 0 }]
let _ready = unsafe poll(Pointer.fromMutSlice<PollFd>(&mut watched), 1, -1)
let status = run watch.collect()
// echo has ended, so its output is in the socket.
let mut bytes: [u8; 8] = [0; 8]
let count = unsafe read(output, Pointer.fromMutSlice<u8>(&mut bytes), 8)
Subprocess.closeDescriptor(output)
if status.signaled || status.code != 0 || count != 3 || bytes[0] != 104 {
return 1
}
return 0
}
pub effect fn main() -> i32 {
let mut allocator = Allocator.systemAllocatorProvider()
let result = run Effect.result(program()) |> Effect.provideMut<Allocator>(&mut allocator)
return match move result {
Result<i32, FileError | SubprocessError | OutOfMemoryError>.Success { value } => value
Result<i32, FileError | SubprocessError | OutOfMemoryError>.Failure { .. } => 1
}
}Import as Subprocess with import silk.system.subprocess { Subprocess }.
Public declarations: 7.
SubprocessOperation#
pub enum SubprocessOperationThe operation of this module that failed.
Spawn#
SpawnStarting the child, in Subprocess.spawn.
Wait#
WaitCollecting an exit status, in Subprocess.tryWait.
Kill#
KillSending a signal, in Subprocess.kill.
Watch#
WatchOpening the exit descriptor, in ExitWatch.open.
SubprocessError#
pub struct SubprocessErrorA failed operation of this module: which one, and the native error code.
Details#
code is the errno value of the failed system call, such as 2 (ENOENT) when the program
does not exist, 13 (EACCES) when it cannot be run, 3 (ESRCH) when no process has the ID, or
22 (EINVAL) for a request with a NUL byte or an empty program.
Field operation#
pub operation: SubprocessOperationThe operation that failed.
Field code#
pub code: i32The native error code.
Stdio#
pub struct StdioWhat a child gets in one descriptor slot.
Details#
Make one with Stdio.ignore, Stdio.inherit, or Stdio.pipe.
Implementation Copy for Stdio#
impl Copy for StdioImplementation Stdio#
impl StdioAssociated function ignore#
pub fn ignore() -> StdioReturns a slot that the child does not get from the parent.
Details#
Slots 0, 1, and 2 are opened on /dev/null in the child, so a read sees the end at once and a
write is discarded. A higher slot is not opened by this module.
Associated function inherit#
pub fn inherit(fd: i32) -> StdioReturns a slot that the child gets as a copy of the parent's descriptor fd.
Details#
The parent keeps fd open and owns it. fd can be any open descriptor, such as the
parent's own standard output, a file, or a socket. Subprocess.spawn fails with code 9
(EBADF) if fd is not open.
Associated function pipe#
pub fn pipe() -> StdioReturns a slot connected to a new pipe between the parent and the child.
Details#
The pipe is a UNIX stream socket pair, so both sides can read and write, and it can carry
descriptors with SCM_RIGHTS. The parent end is nonblocking and close-on-exec. Take it with
Subprocess.takePipe. The child end is blocking, as programs expect.
SpawnOptions#
pub struct SpawnOptionsHow Subprocess.spawn starts a child, apart from the ProcessRequest.
Details#
SpawnOptions.make gives slots 0, 1, and 2 as Stdio.ignore, the program path as the
first argument, the session, user, and group of the parent.
Implementation SpawnOptions#
impl SpawnOptionsAssociated function make#
pub fn make() -> SpawnOptionsReturns the options with no slot set, the program path as the first argument, and the session, user, and group of the parent. It does not allocate.
Method setStdio#
pub effect fn setStdio(self: &mut Self, slot: usize, stdio: Stdio) -> () ! OutOfMemoryError ? &mut AllocatorSets what the child gets in descriptor slot, such as 0 for standard input.
Details#
Each slot below slot that is not set is Stdio.ignore. The child gets slots 0, 1, and 2,
and each higher slot that you set. Each other descriptor of the parent is closed in the child,
except as the module gotchas say for GNU/Linux.
Errors#
OutOfMemoryError if the Allocator cannot supply the slot list.
Method setArgumentZero#
pub effect fn setArgumentZero(self: &mut Self, value: &[u8]) -> () ! OutOfMemoryError ? &mut AllocatorSets the first argument of the child, which programs read as their own name. By default it is the program path.
Errors#
OutOfMemoryError if the Allocator cannot supply the copy.
Gotchas#
value must not contain NUL.
Method setDetached#
pub fn setDetached(self: &mut Self, detached: bool) -> ()Starts the child in a new session and process group, with no controlling terminal, when
detached is true. A signal for the terminal of the parent, such as an interrupt from
Control-C, then does not reach the child.
Method setUser#
pub fn setUser(self: &mut Self, user: u32) -> ()Runs the child as the user with ID user. The parent needs the privilege to do so.
Method setGroup#
pub fn setGroup(self: &mut Self, group: u32) -> ()Runs the child with the group ID group and no supplementary groups. The parent needs the
privilege to do so.
ExitStatus#
pub struct ExitStatusHow an ended child ended, from Subprocess.tryWait.
Details#
When signaled is false, the child exited and code is its exit code from 0 to 255, and
signal is 0. When signaled is true, a signal ended the child: signal is its number, such
as 9 for SIGKILL, and code is 0.
Field signaled#
pub signaled: boolA signal ended the child.
Field code#
pub code: i32The exit code, when signaled is false.
Field signal#
pub signal: i32The signal number, when signaled is true.
Implementation Copy for ExitStatus#
impl Copy for ExitStatusSubprocess#
pub struct SubprocessA started child: its process ID and the parent ends of its pipes.
Details#
The value owns each pipe end until you take it with Subprocess.takePipe; dropping the value
closes the pipe ends that were not taken. Dropping it does not stop the child and does not
collect its status.
Gotchas#
Call Subprocess.tryWait after the child ends, or the child stays a zombie process.
Implementation Drop for Subprocess#
impl Drop for SubprocessOperation drop#
fn drop(self: &mut Subprocess) -> ()Implementation Subprocess#
impl SubprocessAssociated function spawn#
pub effect fn spawn(request: &ProcessRequest, options: &SpawnOptions) -> Subprocess ! SubprocessError | OutOfMemoryError ? &mut AllocatorStarts the program of request with options, and returns at once.
Details#
The child gets the arguments and the environment of request, and its working directory, or
the parent's when it has none. Each descriptor slot gets what SpawnOptions.setStdio set.
The child starts with every signal unblocked and with the default action for each signal
that can be caught.
A failure to start, such as a program that does not exist, is an error of this call; no child is left.
Errors#
SubprocessErrorwith operationSpawnand the native code, such as 2 (ENOENT) for a missing program or working directory, 13 (EACCES), 1 (EPERM) for a user or group change that is not allowed, 9 (EBADF) for aStdio.inheritdescriptor that is not open, or 22 (EINVAL) for an empty program or a NUL byte in the request.OutOfMemoryErrorif theAllocatorcannot supply the copies of the request.
Gotchas#
This function does not search PATH. Give the full path of the program.
Method pid#
pub fn pid(self: &Self) -> i32Returns the process ID of the child.
Method takePipe#
pub fn takePipe(self: &mut Self, slot: usize) -> Option<i32>Takes the parent end of the pipe in slot, or returns none if slot is not a
Stdio.pipe slot or its end was taken before.
Details#
The end is a nonblocking, close-on-exec UNIX stream socket. You own it: close it with
Subprocess.closeDescriptor or other code.
Associated function closeDescriptor#
pub fn closeDescriptor(fd: i32) -> ()Closes the descriptor fd that you took with Subprocess.takePipe. A descriptor that is
not open is ignored.
Associated function kill#
pub effect fn kill(pid: i32, signal: i32) -> () ! SubprocessErrorSends signal to the process pid, such as 15 for SIGTERM.
Details#
A negative pid sends the signal to each process of the process group -pid, such as a
group that a SpawnOptions.setDetached child leads. Signal 0 sends nothing and checks
that the process exists.
Errors#
SubprocessError with operation Kill, for example with code 3 (ESRCH) if no process has
the ID, 1 (EPERM) if the caller may not signal it, or 22 (EINVAL) for a signal that does
not exist.
Associated function tryWait#
pub effect fn tryWait(pid: i32) -> Option<ExitStatus> ! SubprocessErrorCollects the exit status of the child pid if it has ended, and returns none if it still
runs. It never blocks.
Details#
After a status is returned, the process ID is free and can belong to a new process, so do not
signal or wait for pid again. A stopped child counts as running.
Errors#
SubprocessError with operation Wait, for example with code 10 (ECHILD) if pid is not
a child of this process or its status was collected before.
ExitWatch#
pub struct ExitWatchA descriptor that becomes readable when a child process ends.
When to use#
Open one for each child that you start with Subprocess.spawn, and register
ExitWatch.descriptor with a reactor for reads. When it is readable, call
ExitWatch.collect and drop the watch.
Details#
The descriptor is a pidfd on Linux and a kqueue with an EVFILT_PROC exit filter on Darwin. It
is close-on-exec. It stays readable after the child ends; do not read from it. Dropping the
watch closes it.
Gotchas#
Open the watch before you collect the status of the child. After that, the process ID can belong to another process.
On Darwin the descriptor can become readable while the kernel still finishes the exit, so
Subprocess.tryWaitcan still returnnonethen.ExitWatch.collectwaits for that short step.On Linux, pidfds need kernel 5.3 or later.
Implementation Drop for ExitWatch#
impl Drop for ExitWatchOperation drop#
fn drop(self: &mut ExitWatch) -> ()Implementation ExitWatch#
impl ExitWatchAssociated function open#
pub effect fn open(pid: i32) -> ExitWatch ! SubprocessErrorOpens a descriptor that becomes readable when the process pid ends. A child that has
already ended, but whose status was not collected, makes it readable at once.
Errors#
SubprocessError with operation Watch, for example with code 3 (ESRCH) if no process
has the ID, or 38 (ENOSYS) on a Linux kernel before 5.3.
Method descriptor#
pub fn descriptor(self: &Self) -> i32Returns the descriptor to register for reads. The watch keeps owning it.
Method collect#
pub effect fn collect(self: &Self) -> ExitStatus ! SubprocessErrorCollects the exit status of the watched child after ExitWatch.descriptor is readable.
Details#
The child has ended when the descriptor is readable. On Linux the status is ready then, and
the call returns at once. On Darwin the kernel reports the exit a moment before the status is
ready, and the call waits for the kernel to finish the exit. After it returns, the process ID
is free, as after Subprocess.tryWait.
Errors#
SubprocessError with operation Wait, for example with code 10 (ECHILD) if the status
was collected before.
Gotchas#
Before the descriptor is readable, the call blocks until the child ends.
Category: System.
Profiles: aarch64-apple-darwin, aarch64-unknown-linux-gnu, x86_64-unknown-linux-gnu.