(sigil process)

Process Management Library

Process management operations including spawning subprocesses, environment variable access, and command execution. Native functions are implemented in C with higher-level utilities defined here.

Running Commands

(import (sigil process))

;; Run and wait for completion
(process-run "ls" "-l")

;; Capture output as string
(process-output->string "date")  ; => "Thu Jan 15 14:30:00 2025"

;; Capture output as lines
(process-lines "ls")  ; => ("file1" "file2" ...)

Spawning Processes

(let ((p (process-spawn "cat")))
  (display "Hello\n" (process-stdin p))
  (close-output-port (process-stdin p))
  (display (read-line (process-stdout p)))
  (process-wait p))

Environment

(getenv "HOME")      ; => "/home/user"
(setenv! "MY_VAR" "value")

Exports

process-runprocedure

Run a command and wait for it to complete.

Returns the exit status (0 for success; 128+N when the child died from signal N). Raises an io-error when the process cannot be started or waited on (for example, fork failing under process pressure) -- it never returns a non-integer.

(process-run "ls" "-l")  ; => 0
process-spawnprocedure

Spawn a subprocess without waiting.

Returns a process object, or #f if the process cannot be started. Use process-wait to wait for completion and retrieve the exit status.

With die-with-parent: #t, the child is killed automatically when this (parent) process dies, even on an uncatchable SIGKILL. On Linux this uses PR_SET_PDEATHSIG; on other platforms it is a no-op.

cwd: changes only the child's working directory. environment: is a list of KEY=VALUE overrides merged into the child's inherited environment without mutating the parent. Both options use the native spawn path on Unix, macOS, and Windows.

With new-process-group: #t, the child calls setsid before exec, so it leads its own session and its own process group ((process-pgid p) equals (process-pid p)). Anything the child spawns inherits that group, so process-signal! then reaches the child's own children too; without it, a signal reaches only the child and grandchildren are left running. Use it when you need to tear down a whole process tree. Unix only: on Windows it is a no-op (process-pgid and process-signal! both raise there, so nothing can act on a group that was not created).

This is the group-and-session half of what process-spawn-pty does, without the terminal half and without merging the streams: stdout and stderr stay on separate pipes. The pty spawn goes on to claim the pty as a controlling terminal, which this does NOT — so the two are not equivalent arrangements.

Two consequences of setsid worth knowing: the child detaches from this process's controlling terminal, so a Ctrl-C typed at the terminal no longer reaches it, and it acquires no terminal of its own, so it cannot open /dev/tty to prompt.

(let ((p (process-spawn "sleep" "10")))
  (process-kill! p))

;; Child dies on its own if this process is killed:
(process-spawn "my-worker" "--serve" die-with-parent: #t)

;; Signal the child AND everything it spawned, stdout/stderr
;; still separate:
(let ((p (process-spawn "build.sh" new-process-group: #t)))
  (process-signal! p 'term))

Concurrently feed input and drain both output streams (Unix). Returns (wait-result stdout stderr timed-out?). Owns and closes raw pipes; call before accessing process ports. TIMEOUT bounds communication as well as execution, including descendants retaining a pipe after parent exit.

Spawn a subprocess on a pseudo-terminal (Unix only).

Allocates a pty, forks, and runs the command as a session leader with the pty slave as its controlling terminal wired to stdin/stdout/stderr. All fork discipline is handled in C; do not attempt fork or forkpty from Scheme.

Returns a process object whose I/O rides the pty MASTER, not pipes: use process-pty-read / process-pty-write (guarded by await-readable-fd / await-writable-fd on process-pty-fd in async code), NOT process-stdout / process-stdin.

The master fd is non-blocking. process-pty-read returns an empty bytevector when no data is ready and an eof-object once the child exited and the stream drained. cwd: sets the child's working directory; term: sets its TERM (default "xterm-256color"; pass #f to inherit).

(let ((p (process-spawn-pty "sh" cols: 120 rows: 40)))
  (process-pty-write p "ls\n")
  ...
  (process-pty-close! p)
  (process-wait p))

Spawn a subprocess on non-blocking pipes for a stream session (Unix only).

The pipe analog of process-spawn-pty, for a child that speaks a line/byte protocol over stdin/stdout rather than a terminal — e.g. tmux -C, which hard-fails on a tty. stdin and stdout are NON-BLOCKING pipes; the child's stderr is inherited (not captured), keeping the stdout stream clean for a strict control protocol.

Returns a process object whose I/O rides raw pipe fds: use process-pipe-read / process-pipe-write (guarded by await-readable-fd / await-writable-fd on process-stdout-fd / process-stdin-fd), NOT process-stdout / process-stdin. process-pipe-read returns an empty bytevector when no data is ready and an eof-object once the child closes stdout. cwd: sets the child's working directory.

(let ((p (process-spawn-pipe "tmux" "-C" "new-session")))
  (process-pipe-write p "list-sessions\n")
  ...
  (process-pipe-close-stdin! p)
  (process-wait p))

Send a signal to a spawned process.

Accepts a signal name symbol ('hup 'int 'quit 'kill 'usr1 'usr2 'term 'cont 'stop 'tstp 'winch) or a raw signal number. Returns #t if the signal was delivered.

The signal goes to the child's whole PROCESS GROUP — reaching the child's own children as well, matching terminal semantics — exactly when SIGIL created that group for it: every process-spawn-pty child, and a process-spawn child asked for with new-process-group: #t. Otherwise only the child itself is signalled, and anything it spawned is left running; a default process-spawn child shares THIS process's group, so signalling that group would signal the caller too.

The test is what was requested AT SPAWN, not what the child did afterwards. A child that runs setsid itself ((process-spawn "setsid" ...), nohup, a daemonising program) will report (= (process-pgid p) (process-pid p)) and still be signalled individually — process-pgid describes the child, this describes what we will do to it, and they can disagree.

Returns #f for a process already reaped by process-wait, group or not: after reaping, the pid may be reissued to a stranger, and refusing is the only safe answer. So when tearing down a tree, SIGNAL FIRST AND WAIT AFTER. A child that has exited but not yet been reaped is still signallable, and its group id stays valid as long as the group has members — which is what lets you sweep up grandchildren that outlived their parent.

(process-signal! p 'int)   ; like Ctrl-C
(process-signal! p 'term)

;; Tearing down a tree on a timeout:
(process-signal! p 'term)  ; reaches the group
(process-wait p)           ; only then reap
process-waitprocedure

Wait for a process to complete.

Returns the exit status.

(process-wait p)  ; => 0
process-kill!procedure

Kill a running process.

(process-kill! p)
process?procedure

Check if a value is a process object.

process-pty?procedure

Check if a process was spawned with a pty (via process-spawn-pty).

Get the raw pty master file descriptor of a pty-spawned process.

Feed this to await-readable-fd / await-writable-fd from (sigil async) before calling process-pty-read / process-pty-write so the single-threaded VM never blocks.

Read available bytes from the pty master.

Returns a bytevector (EMPTY when no data is ready — the fd is non-blocking) or an eof-object once the child exited and the stream is drained. Optional second argument caps the read size (default 4096).

Write a string or bytevector to the pty master (child input).

Returns the number of bytes actually written; may be 0 (kernel buffer full) or partial. Await writability and retry with the remainder.

Set the pty window size. The kernel delivers SIGWINCH to the child's foreground process group automatically.

Close the pty master fd (idempotent). A still-running child sees terminal hangup.

Get the raw stdout (read) pipe fd of a pipe-spawned process.

Feed this to await-readable-fd from (sigil async) before calling process-pipe-read so the single-threaded VM never blocks. The fd is non-blocking.

Get the raw stdin (write) pipe fd of a pipe-spawned process.

Feed this to await-writable-fd from (sigil async) before calling process-pipe-write. The fd is non-blocking.

Read available bytes from the child's stdout pipe.

Returns a bytevector (EMPTY when no data is ready — the fd is non-blocking) or an eof-object once the child closes its stdout. Optional second argument caps the read size (default 4096).

Write a string or bytevector to the child's stdin pipe.

Returns the number of bytes actually written; may be 0 (kernel buffer full) or partial. Await writability and retry with the remainder. Raises an io-error if the child has closed its stdin.

Internal non-raising EPIPE variant used by (sigil shell)'s pump. Returns #f when the child has closed its stdin; otherwise has the same partial/non-blocking integer result as process-pipe-write.

Close the child's stdin pipe (idempotent), delivering EOF to the child.

process-pidprocedure

Get the OS process id of a spawned process.

process-pgidprocedure

Get the process group id of a spawned process (Unix only).

Read from the kernel, so it reports what is actually true even if the child changed its own group after exec. Returns #f once the process has been reaped by process-wait — deliberately, and the refusal comes BEFORE the lookup: a reaped pid can be reissued, and asking the kernel about it would answer about a stranger. Read it (and signal) BEFORE you reap.

For a child spawned with new-process-group: #t, or on a pty, this equals (process-pid p) — the child leads its own group. For a default spawn it is THIS process's own group id; do not signal it.

(= (process-pgid p) (process-pid p)) does NOT imply that process-signal! will signal the group: a child that ran setsid itself also reports that, and process-signal! keys off what was requested at spawn. See its docstring.

(let ((p (process-spawn "build.sh" new-process-group: #t)))
  (= (process-pgid p) (process-pid p)))  ; => #t

Check if a process is still running.

(process-alive? p)  ; => #t or #f
process-stdinprocedure

Get the stdin port of a spawned process.

Returns an output port for writing to the process.

Get the stdout port of a spawned process.

Returns an input port for reading the process output.

Get the stderr port of a spawned process.

Returns an input port for reading error output.

getenvprocedure

Get an environment variable.

Returns the value as a string, or #f if not set.

(getenv "HOME")    ; => "/home/user"
(getenv "MISSING") ; => #f
setenv!procedure

Set an environment variable.

(setenv! "MY_VAR" "my-value")
command-lineprocedure

Get the command-line arguments.

Returns a list where the first element is the program name.

(command-line)  ; => ("./my-program" "arg1" "arg2")

Replace the list command-line returns (a list of strings).

sigil run uses this to give an entry point it runs in-process the argv a built binary of the same program would see.

exitprocedure

Exit the process with a status code.

(exit 0)  ; success
(exit 1)  ; failure

Install a procedure that exit runs immediately before the process terminates, or #f to clear it. The hook receives the pending exit status and may call exit again with a different status to OVERRIDE it; re-entry is guarded so the hook runs at most once.

This exists for one job: letting a command that loads and runs untrusted code survive that code calling exit out from under it. sigil test uses it so a test file ending in (exit 0) can no longer abandon the run and report success. It is deliberately exception-free — a guard built on raise is worthless in exactly the release binaries where propagation out of native frames is broken.

Prefer ordinary control flow. This is a last-ditch guard, not a general interception mechanism.

(%set-exit-hook! (lambda (status)
                   (display "about to exit\n" (current-error-port))))
process-idprocedure

Get the current process ID.

(process-id)  ; => 12345

Check if a command exists in PATH.

(command-exists? "ls")   ; => #t
(command-exists? "foo")  ; => #f

Call a procedure with a spawned process.

The process is automatically waited for when proc returns.

(call-with-process "ls" '("-l")
  (lambda (p)
    (read-line (process-stdout p))))

Run a command and capture its stdout as a string.

Returns #f if the process cannot be started.

(process-output->string "echo" "hello")  ; => "hello"
(process-output->string "date")          ; => "Thu Jan 15..."

Read all content from a port as a string. Helper for process-output->string.

process-linesprocedure

Run a command and return its stdout as a list of lines.

Returns #f if the process cannot be started.

(process-lines "ls")           ; => ("file1" "file2" ...)
(process-lines "ls" "-la")     ; => ("total 42" "drwxr-x..." ...)

Read all lines from a port. Helper for process-lines.

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)