Asynchronous Execution¶
Two layers sit below the synchronous run/run-command family: the raw
spawn/spawn-command primitive for callers who want to drive a process
themselves, and the event-driven communicate-async/process-task API for
callers who want structured async completion without managing streams by
hand.
Spawning a handle¶
spawn (command arguments &key search input output error environment
directory external-format status-hook preserve-fds fd-limit) -> process-handle
spawn-command (command-spec &key stdin stdout stderr) -> process-handle
fd-limit, if given, temporarily lowers this process's own RLIMIT_NOFILE
soft limit around the spawn (restored immediately afterward) — the spawned
child inherits the lower limit through exec. See Synchronous
Execution for why this
matters.
Streams exposed by spawn or spawn-command are the caller's
responsibility until ownership is handed to communicate. communicate,
run-command, and run-pipeline drain and close the process streams they
manage.
process-handle operations¶
process-id,process-status,process-stdin,process-output, andprocess-stderrprocess-alive-p,process-try-wait, andprocess-wait.process-waitacceptstimeout,poll-interval, andclock(acl-boundary-kitclock boundary — see Synchronous Execution), and returnsnilif its timeout expires.process-exit-codeandprocess-signalprocess-send-leader-signal, which signals the live process-group leader onlyprocess-send-group-signal, which signals the live owned process groupprocess-terminate(SIGTERM) andprocess-kill(SIGKILL)close-process-streams, andclose-processwith optionalterminateandtimeout. Useclose-processin cleanup paths to terminate and reap a live child before closing its streams.call-with-process (process continuation &key terminate timeout), which callscontinuationwith the process and applies that cleanup withunwind-protecton the way out, andwith-process, the macro sugar over it.
(process-kit:with-process
(process (process-kit:spawn-command
(process-kit:make-command
"/usr/bin/printf" (list "hello\n")
:stdout :pipe)))
(read-line (process-kit:process-output process)))
Blocking communication¶
communicate (process &key input timeout grace-period timeout-signal
kill-signal on-timeout max-output-characters drain-timeout-seconds
result-type external-format decoding-error-policy clock sleeper
poll-interval cancellation-token on-cancel) -> process-result
communicate sends optional input, drains stdout and stderr concurrently,
waits for the process, and supports the same timeout, bounded-capture,
result-type, and decoding-error-policy
behavior as run. It owns communication for that handle: a
second concurrent call is rejected. After completion, the same normalized
options return the identical cached result object; different options signal
communicate-options-mismatch (readers: communicate-options-mismatch-process,
communicate-options-mismatch-expected-options,
communicate-options-mismatch-actual-options). Input strings and octet
vectors are snapshotted when communication is reserved.
Event-driven communication¶
communicate-async (process &rest options) -> process-task
await-process (task &key timeout) -> process-result, completed-p
cancel-process (task) -> task
communicate-async accepts the communicate options except
cancellation-token, because the task owns its cancellation token. Its
additional options are event-callback, event-queue-capacity (a positive
integer, default 64), and event-overflow-policy (:drop-newest by
default, or :block). event-history-capacity is a non-negative integer
that limits retained events and callback errors; it defaults to
event-queue-capacity, and zero disables history retention. It reserves the
process synchronously, so reservation and option errors are reported before
the task is returned.
await-process returns (values nil nil) if its own non-negative timeout
expires; otherwise it returns (values result t) or re-signals the task's
terminal condition. cancel-process requests cancellation and returns the
same task — see Cancellation for on-cancel semantics.
task-state is one of :reserved, :running, :completed, :cancelled,
or :failed. task-result, task-condition, process-events,
callback-errors, and dropped-event-count expose the task outcome and
event history. process-events and callback-errors return chronological
list copies containing at most the newest event-history-capacity entries.
Queue overflow and retained-history eviction are independent; a terminal
event can evict the oldest retained event.
process-task-first-event-sequence and process-task-last-event-sequence
report the sequence range currently retained (nil before the first
event); process-task-history-evicted-count counts events evicted from
that retained window.
run-command-async¶
The command-spec-driven counterpart of communicate-async: spawns
command-spec, forwards its result-type/external-format/
decoding-error-policy into communicate-async, and closes the process's
streams once the task's terminal event is delivered. Accepts the same
options as communicate-async and, like it, owns its cancellation token —
passing cancellation-token signals an error.
Streaming events with a cursor¶
Independent consumers that cannot poll process-events — for example, a
second thread watching the same task — can instead stream events with a
caller-owned cursor:
cursor is nil (start at the oldest retained event) or a positive
sequence number; status is:
| Status | Meaning |
|---|---|
:event |
An event was returned; use next-cursor for the following call. |
:gap |
The cursor fell behind the retained history — resume from next-cursor; gap-count is the exact number of events skipped. |
:terminal |
The task has finished and no further events remain. |
:timeout |
The call's own non-negative timeout expired first. |
Because cursors are plain integers owned by the caller, multiple independent consumers can each stream the same task's events at their own pace without mutating shared task state.
Event structure¶
Events have monotonically increasing process-event-sequence values and
are inspected with process-event-kind, process-event-octets,
process-event-result, process-event-condition, and
process-event-dropped-count. process-event-octets returns a fresh copy.
:stdout and :stderr events always contain raw octets. When the bounded
event queue is full, :drop-newest drops new output events; :block
applies backpressure to the output producer. Dropped output is summarized
by an :overflow event when space becomes available, and its cumulative
count is available through dropped-event-count.
The callback is invoked serially in event order. A condition signalled by
the callback is captured in callback-errors and does not fail the task or
stop later events. Exactly one :terminal event is delivered after all
queued output and overflow events. It is outside the bounded queue, cannot
be dropped, and contains either the final result or terminal condition.
Task completion is published only after this terminal callback returns, so
a completed await-process also means terminal-event dispatch has
finished.