Skip to content

Subprocesses

Spawning and controlling external programs. Import with (import (kaappi process)).

Note

(kaappi process) is absent in the WebAssembly build (including the browser playground), which has no process model at all, and under --sandbox, which excludes it deliberately. Portable code branches with (cond-expand ((library (kaappi process)) ...) (else ...)) — see Running External Programs for the full pattern.

Availability

(kaappi process) arrived after v0.25.0. On an older binary the (library (kaappi process)) gate below is simply false, and kaappi features reports what your build has.

The command is always a list of strings, never a shell command line. Nothing is word-split, glob-expanded, or interpreted; a filename containing a space or a semicolon is just an argument. Use sh -c explicitly if you want a shell, and then you own the quoting.


One-shot Capture

run-process

Syntax: (run-process argv option ...)

Spawns argv, feeds it optional input, collects everything it writes to stdout and stderr, waits for it to exit, and returns three values: the exit status, the captured stdout, and the captured stderr.

All three streams move at the same time, driven by fibers inside the call. A child that fills its stdin, stdout and stderr buffers simultaneously completes normally instead of deadlocking.

Options are quoted keyword symbols followed by a value:

Option Value Meaning
'input: string or bytevector Written to the child's stdin, which is then closed. Without it the child's stdin is empty (/dev/null), never the terminal
'timeout: seconds (real) Kill the child and raise a process-timeout condition if it has not exited in time
'output: 'string (default) or 'bytevector How stdout and stderr come back. Use 'bytevector for a program whose output is not UTF-8 text
'directory: string Working directory for the child
'env: alist of (name . value) strings Replaces the child's environment wholesale — see process-environment
'new-group: boolean Put the child in its own process group. Implied by 'timeout:, which refuses an explicit #f — only a group kill reaches a grandchild holding the pipes, so a child-only kill could not bound the call
kaappi> (call-with-values
          (lambda () (run-process '("echo" "hello")))
          list)
;=> (0 "hello\n" "")
kaappi> (call-with-values
          (lambda () (run-process '("tr" "a-z" "A-Z") 'input: "shout"))
          list)
;=> (0 "SHOUT" "")

The status is an exit code, or the pair (signaled . n) on POSIX when the child died from signal n.

Failure to start the program — it does not exist, or is not executable — raises a file error, not a timeout condition, and carries the underlying errno so "not found" and "not allowed" stay distinguishable.

See also: spawn-process, process-timeout?


process-timeout?

Syntax: (process-timeout? obj)

Returns #t if obj is the condition run-process raises when its 'timeout: expires. By then the child (and its process group) has been killed and reaped, so this condition is the only route to what the child managed to produce.

kaappi> (guard (e ((process-timeout? e) (process-timeout-stdout e)))
          (run-process '("sh" "-c" "printf started; sleep 30") 'timeout: 0.5))
;=> "started"

It is an ordinary error object as well: error-object? is true, error-object-message is "run-process: timed out", and error-object-irritants is (argv seconds). The captured output is deliberately not in the irritants — an uncaught condition prints those, and a child that wrote megabytes before stalling should not print them.

See also: process-timeout-stdout, run-process


process-timeout-stdout

Syntax: (process-timeout-stdout condition)

Everything the child wrote to stdout before its 'timeout: expired, in whatever form 'output: asked for. Raises a type error if condition is not a process-timeout condition.

See also: process-timeout-stderr, process-timeout?


process-timeout-stderr

Syntax: (process-timeout-stderr condition)

The stderr counterpart of process-timeout-stdout.

See also: process-timeout-stdout, process-timeout?


Spawning

spawn-process

Syntax: (spawn-process argv option ...)

Starts argv and returns a process object immediately, without waiting. Use this when the child is long-lived, when you want to interleave reads and writes yourself, or when you need its pid.

Option Value Meaning
'stdin: 'stdout: 'stderr: a redirection spec See below; the default is 'inherit
'directory: string Working directory for the child
'env: alist of (name . value) strings Replaces the child's environment wholesale
'new-group: boolean Put the child in its own process group, so process-kill can signal the whole tree

Redirection specs:

Spec Meaning
'inherit The child shares Kaappi's own stream (the default)
'pipe Create a pipe; the Kaappi end is a port on the process object
'null /dev/null (NUL on Windows)
'stdout stderr only — merge stderr into the same pipe as stdout
a port An open, file-descriptor-backed port
kaappi> (define p (spawn-process '("sort") 'stdin: 'pipe 'stdout: 'pipe))
kaappi> (write-string "pear\napple\n" (process-stdin p))
kaappi> (close-port (process-stdin p))
kaappi> (read-line (process-stdout p))
;=> "apple"
kaappi> (process-wait p)
;=> 0

The child inherits exactly three descriptors — 0, 1 and 2. Every other file, socket and pipe Kaappi holds is closed before the program starts.

See also: run-process, process-wait


process?

Syntax: (process? obj)

Returns #t if obj is a process object.

kaappi> (process? (spawn-process '("true")))
;=> #t
kaappi> (process? 'nope)
;=> #f

process-pid

Syntax: (process-pid p)

The child's process id, as the operating system reports it.

Do not use it to signal the child — use process-kill, which refuses to signal a process that has already been reaped. A reaped pid may have been reused by an unrelated process.

See also: process-group, process-kill


process-group

Syntax: (process-group p)

The child's process-group id when it was spawned with 'new-group: #t, and #f otherwise. On Windows the group is a Job Object, and this reports the same value a POSIX group leader would.

See also: spawn-process, process-kill


Pipe Ports

process-stdin

Syntax: (process-stdin p)

The port that writes to the child's stdin, if 'stdin: was 'pipe, and #f for every other spec.

It is an ordinary binary output port: every port procedure works on it, and a write that would block parks only the calling fiber. Closing it is what signals end-of-input to the child.

See also: process-stdout, spawn-process


process-stdout

Syntax: (process-stdout p)

The port that reads the child's stdout, if 'stdout: was 'pipe, and #f otherwise. A read that would block parks only the calling fiber; sibling fibers keep running.

See also: process-stderr, process-stdin


process-stderr

Syntax: (process-stderr p)

The stderr counterpart of process-stdout. Also #f when 'stderr: was 'stdout, since the merged stream arrives on the stdout port.

Note

Reading one pipe to end-of-file before touching the other deadlocks the moment the child fills the pipe you are not reading. Read them in separate fibers, or use run-process, which does it for you.

See also: process-stdout


Waiting and Signaling

process-status

Syntax: (process-status p)

Returns #f while the child is still running, and its status once it has exited: an integer exit code, or the pair (signaled . n) on POSIX for a child killed by signal n. Never blocks.

kaappi> (define p (spawn-process '("sleep" "5")))
kaappi> (process-status p)
;=> #f

See also: process-wait


process-wait

Syntax: (process-wait p) | (process-wait p 'timeout: seconds)

Waits for the child to exit and returns its status. The calling fiber parks; every other fiber on the thread keeps running.

With 'timeout:, returns #f if the child is still running when the deadline passes — the child is left alive, and a later process-wait still reaps it.

kaappi> (define p (spawn-process '("sleep" "30")))
kaappi> (process-wait p 'timeout: 0.1)
;=> #f
kaappi> (process-kill p 'signal: 9)
kaappi> (process-wait p)
;=> (signaled . 9)

Waiting again on an already-reaped process returns the stored status immediately.

See also: process-status, process-kill


process-kill

Syntax: (process-kill p) | (process-kill p 'signal: n) | (process-kill p 'group: #t)

Sends a signal to the child — SIGTERM (15) by default. With 'group: #t the signal goes to the whole process group, which reaches grandchildren; that requires the child to have been spawned with 'new-group: #t, since otherwise the group is Kaappi's own.

Killing an already-reaped process is a quiet no-op, never an error: its pid may belong to somebody else by now.

Note

Windows has no signal delivery. process-kill terminates the process (or the Job Object, with 'group: #t) and folds 'signal: into the exit code it stamps as 128 + n — so 'signal: 9 reports 137 rather than (signaled . 9).

See also: process-wait, process-group


Environment

process-environment

Syntax: (process-environment)

Returns the current process environment as a list of (name . value) string pairs — the exact shape 'env: accepts.

'env: replaces the environment rather than extending it, so this is what you build on to add a variable without dropping everything else:

kaappi> (call-with-values
          (lambda ()
            (run-process '("printenv" "GREETING")
                         'env: (cons (cons "GREETING" "hello")
                                     (process-environment))))
          list)
;=> (0 "hello\n" "")

A wholesale replacement that dropped the platform's own variables would leave the child unable to start at all on Windows.

See also: run-process, spawn-process