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