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.pipe slot is a nonblocking, close-on-exec UNIX stream socket.

  • ExitWatch.open gives a descriptor that becomes readable when the child ends: a pidfd on Linux, and a kqueue with an EVFILT_PROC filter on Darwin. Register it with a reactor, such as silk.io.reactor, next to the pipes.

  • ExitWatch.collect collects the exit status once the exit descriptor is readable. Subprocess.tryWait collects it if the child has ended, or returns none if 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 waitpid for 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 uses fork only for a user or a group.

  • On GNU/Linux, a child started without fork also 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#

silk
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#

silk
pub enum SubprocessOperation

The operation of this module that failed.

Spawn#

silk
Spawn

Starting the child, in Subprocess.spawn.

Wait#

silk
Wait

Collecting an exit status, in Subprocess.tryWait.

Kill#

silk
Kill

Sending a signal, in Subprocess.kill.

Watch#

silk
Watch

Opening the exit descriptor, in ExitWatch.open.

SubprocessError#

silk
pub struct SubprocessError

A 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#

silk
pub operation: SubprocessOperation

The operation that failed.

Field code#

silk
pub code: i32

The native error code.

Stdio#

silk
pub struct Stdio

What a child gets in one descriptor slot.

Details#

Make one with Stdio.ignore, Stdio.inherit, or Stdio.pipe.

Implementation Copy for Stdio#

silk
impl Copy for Stdio

Implementation Stdio#

silk
impl Stdio

Associated function ignore#

silk
pub fn ignore() -> Stdio

Returns 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#

silk
pub fn inherit(fd: i32) -> Stdio

Returns 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#

silk
pub fn pipe() -> Stdio

Returns 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#

silk
pub struct SpawnOptions

How 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#

silk
impl SpawnOptions

Associated function make#

silk
pub fn make() -> SpawnOptions

Returns 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#

silk
pub effect fn setStdio(self: &mut Self, slot: usize, stdio: Stdio) -> () ! OutOfMemoryError ? &mut Allocator

Sets 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#

silk
pub effect fn setArgumentZero(self: &mut Self, value: &[u8]) -> () ! OutOfMemoryError ? &mut Allocator

Sets 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#

silk
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#

silk
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#

silk
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#

silk
pub struct ExitStatus

How 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#

silk
pub signaled: bool

A signal ended the child.

Field code#

silk
pub code: i32

The exit code, when signaled is false.

Field signal#

silk
pub signal: i32

The signal number, when signaled is true.

Implementation Copy for ExitStatus#

silk
impl Copy for ExitStatus

Subprocess#

silk
pub struct Subprocess

A 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#

silk
impl Drop for Subprocess

Operation drop#

silk
fn drop(self: &mut Subprocess) -> ()

Implementation Subprocess#

silk
impl Subprocess

Associated function spawn#

silk
pub effect fn spawn(request: &ProcessRequest, options: &SpawnOptions) -> Subprocess ! SubprocessError | OutOfMemoryError ? &mut Allocator

Starts 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#

  • SubprocessError with operation Spawn and 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 a Stdio.inherit descriptor that is not open, or 22 (EINVAL) for an empty program or a NUL byte in the request.

  • OutOfMemoryError if the Allocator cannot supply the copies of the request.

Gotchas#

This function does not search PATH. Give the full path of the program.

Method pid#

silk
pub fn pid(self: &Self) -> i32

Returns the process ID of the child.

Method takePipe#

silk
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#

silk
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#

silk
pub effect fn kill(pid: i32, signal: i32) -> () ! SubprocessError

Sends 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#

silk
pub effect fn tryWait(pid: i32) -> Option<ExitStatus> ! SubprocessError

Collects 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#

silk
pub struct ExitWatch

A 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.tryWait can still return none then. ExitWatch.collect waits for that short step.

  • On Linux, pidfds need kernel 5.3 or later.

Implementation Drop for ExitWatch#

silk
impl Drop for ExitWatch

Operation drop#

silk
fn drop(self: &mut ExitWatch) -> ()

Implementation ExitWatch#

silk
impl ExitWatch

Associated function open#

silk
pub effect fn open(pid: i32) -> ExitWatch ! SubprocessError

Opens 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#

silk
pub fn descriptor(self: &Self) -> i32

Returns the descriptor to register for reads. The watch keeps owning it.

Method collect#

silk
pub effect fn collect(self: &Self) -> ExitStatus ! SubprocessError

Collects 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.

View the source of this page