nupp.io.process
Starts a child process and drains its streams without deadlocking.
local process = nupp.io.process
local child = assert(process.spawn({args = {"cc", "--version"}}))
local result = assert(child:communicate())
print(result.output)
child:close()spawn returns nil and a reason when the child cannot be started, the way every other acquisition of an operating-system resource does, and raises only for options that could never describe a child.
A child and its streams are owners. See ownership.md for the contract they are handed out under.
Submodules
| Module | Description |
|---|---|
nupp.io.process.spi | Implementation interface for process transports. |
Module contents
Types
| Type | Kind | Description |
|---|---|---|
CommunicateOptions | type | Controls a complete duplex exchange. |
ErrorMode | type | Where standard error goes, which adds the option to join standard output. |
Exit | record | How a child ended. |
Options | type | How a child was asked to be started. |
Process | record | A running child, and the obligation to end it. |
Reader | record | One of a child's readable streams. |
Result | record | A completed duplex exchange. |
StreamMode | type | Where a stream goes: a pipe this process reads or writes, the parent's own stream, or nothing at all. |
Writer | record | A child's writable stream. |
Functions
| Function | Kind | Description |
|---|---|---|
spawn | function | Starts a child process. |
Types#
CommunicateOptionstype#
type CommunicateOptions = {
--- Complete standard input.
---
--- Omitted input sends EOF immediately.
input: (string | Buffer | ByteView)?,
--- Maximum stdout and stderr bytes together.
---
--- Defaults to 256 MiB.
maxOutputBytes: integer?
}Controls a complete duplex exchange.
ErrorModetype#
type ErrorMode = "pipe" | "inherit" | "null" | "stdout"Where standard error goes, which adds the option to join standard output.
Exitrecord#
record Exit ...How a child ended.
Members
| Name | Kind | Description |
|---|---|---|
exitCode | field | The platform status code. |
killed | field | Whether it was terminated rather than exiting on its own. |
timedOut | field | Whether it was terminated because its deadline passed. |
succeeded | method | Exited on its own with status zero. |
exitCodefield#
exitCode: integerThe platform status code. A Unix signal is reported as 128 plus its number. Zero conventionally means success; use succeeded to account for termination.
Optionstype#
type Options = {
--- The program in `args[1]`, then its arguments. A program with no separator in
--- it is resolved through `PATH` by the platform, not by this module.
args: {string},
--- The child's working directory, as anything [](nupp.io.files) accepts, or nil
--- to inherit this one.
cwd: Path?,
--- Variables overlaid on the inherited environment, or the whole environment
--- when `clearEnv` is set. A name must be non-empty and contain no `=`.
env: {[string]: string}?,
--- Whether to start from an empty environment rather than this process's.
clearEnv: boolean?,
--- Where the child's standard input comes from, `"pipe"` by default.
stdin: process.StreamMode?,
--- Where the child's standard output goes, `"pipe"` by default.
stdout: process.StreamMode?,
--- Where the child's standard error goes, `"pipe"` by default.
stderr: process.ErrorMode?,
--- Kill the child after this many milliseconds. The clock starts when it is
--- created, not when it is first waited on.
timeoutMs: integer?
}How a child was asked to be started.
Processrecord#
A running child, and the obligation to end it.
The child is an owner: closing it ends the process, waits for the exit, and releases every stream, and the same runs when its scope ends. Reading a stream or waiting for the exit suspends, so a caller under a scheduler keeps its frame.
Members
| Name | Kind | Description |
|---|---|---|
state | field | |
pid | field | Operating-system process identifier. |
stdin | field | Its standard input, when it was piped. |
stdout | field | Its standard output, when it was piped. |
stderr | field | Its standard error, when it was piped. |
isRunning | method | Whether it is still running, enforcing an elapsed process deadline first. |
wait | method | Waits for it to end and returns how. |
kill | method | Asks the direct child to end; force insists. |
communicate | method | Writes input, closes stdin, drains stdout and stderr, and waits for the exit. |
teardown | method | Closes every stream, ends the child if it is still running, waits for it to actually finish, and releases it. |
close | method | Closes every stream, ends the child if it is still running, waits for it to actually finish, and releases it. |
isRunningmethod#
isRunning: function isRunning(self): booleanWhether it is still running, enforcing an elapsed process deadline first.
Arguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
boolean |
waitmethod#
wait: function wait(self): process.ExitWaits for it to end and returns how.
Suspends while it runs.
Arguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
process.Exit |
killmethod#
kill: function kill(self, force: boolean?): boolean, io.Error?Asks the direct child to end; force insists. Descendants keep their ordinary operating-system lifetime. Returns whether the request was made.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | |
force | boolean? |
Returns
| Type | Description |
|---|---|
boolean | |
io.Error? |
communicatemethod#
communicate: function communicate(self, options: process.CommunicateOptions?): process.Result?, io.Error?Writes input, closes stdin, drains stdout and stderr, and waits for the exit. Returns the complete exchange or a reason it could not be completed.
One combined step per pass: offer some input, take whatever each output has, suspend only when none of the three moved.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | |
options | process.CommunicateOptions? |
Returns
| Type | Description |
|---|---|
process.Result? | |
io.Error? |
teardownmethod#
teardown: function(self: process.Process): nilCloses every stream, ends the child if it is still running, waits for it to actually finish, and releases it. Idempotent, and retryable: a teardown that could not finish leaves each piece's own flag, and the next call skips whatever is already given back.
Arguments
| Name | Type | Description |
|---|---|---|
self | process.Process |
Returns
| Type | Description |
|---|---|
nil |
Raises
| Type | Condition |
|---|---|
string | when closing a stream or releasing the child fails |
closemethod#
close: function close(takes self): nilCloses every stream, ends the child if it is still running, waits for it to actually finish, and releases it. What runs when the owner leaves its scope.
Every step is attempted, and the first failure is raised once all of them have been: a close that stopped at the first would leave the rest held.
Arguments
| Name | Type | Description |
|---|---|---|
takes self | any |
Returns
| Type | Description |
|---|---|
nil |
Raises
| Type | Condition |
|---|---|
string | when closing a stream or releasing the child fails |
Readerrecord#
One of a child's readable streams.
Its waiting read and its nonblocking poll are both here on purpose. A caller draining several streams at once cannot wait on any one of them, because waiting on one is exactly what starves the others, which is the whole reason communicate can drain three pipes without deadlocking.
read is the nupp.io.Reader operation and waits as long as the stream stays open: the child's deadline in Options.timeoutMs, and the enclosing task scope's deadline, are what bound it. poll is the concrete nonblocking operation the combined drain needs; generic readers are not widened with readiness operations.
The tolerance that does exist is narrow and worth naming exactly: closing the same live opaque handle twice is harmless, because the second call finds the stream already released and says so. Destroying a handle while a borrowed reference still exists leaves that reference reading freed memory, which no amount of care at the call site can make safe.
Borrowing the record through asReader keeps a single owner and makes its lifetime a question about Nupp values, which the checker can answer.
Members
| Name | Kind | Description |
|---|---|---|
owner | field | |
handle | field | |
closed | field | |
eof | field | |
takeNow | method | Takes what a stream has without waiting, as poll returns it: the bytes, or nil and whether the stream has ended. |
isEnded | method | Whether the far end has finished and every byte it sent has been taken, or this end has been closed. |
isReleased | method | Whether this end has been closed. |
poll | method | Takes whatever is available: the bytes, or nil when there are none, with whether the stream has ended as the second... |
read | method | Reads up to count bytes, suspending until some arrive. |
readSpan | method | |
readInto | method | |
transferTo | method | |
asReader | method | Borrows this stream through the shared completion-oriented contract. |
close | method | Closes this end. |
takeNowmethod#
takeNow: function(self: process.Reader, wanted: integer): (string?, boolean)Takes what a stream has without waiting, as poll returns it: the bytes, or nil and whether the stream has ended. Shared by every read, so end of stream has one place that decides it.
Arguments
| Name | Type | Description |
|---|---|---|
self | process.Reader | |
wanted | integer |
Returns
| Type | Description |
|---|---|
string? | |
boolean |
Raises
| Type | Condition |
|---|---|
string | when the provider returns an invalid result |
isEndedmethod#
isEnded: function isEnded(self): booleanWhether the far end has finished and every byte it sent has been taken, or this end has been closed. A read returns empty exactly when this is true.
Arguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
boolean |
isReleasedmethod#
isReleased: function isReleased(self): booleanWhether this end has been closed.
Arguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
boolean |
pollmethod#
poll: function poll(self, limit: integer?, timeoutMs: integer?): string?, booleanTakes whatever is available: the bytes, or nil when there are none, with whether the stream has ended as the second result.
Nil with false is "nothing yet" and nil with true is the end, so the two are never the same answer. Without timeoutMs this does not wait, which is the half of reading communicate needs so that one quiet stream does not stop it serving another. With it, this waits up to that many milliseconds for bytes or the end, for a caller that has other work to come back to.
limit caps how many bytes to take, defaulting to a whole pipe's worth. Zero and negative limits read one byte: the platform is where a count stops being a number and becomes a buffer size, and a signed zero or minus one arriving at a native size conversion is either an empty read that looks like end of stream or an enormous one. read passes its caller's count straight through.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this stream |
limit | integer? | the most bytes to take |
timeoutMs | integer? | how long to wait for bytes or the end, or nothing to not wait |
Returns
| Type | Description |
|---|---|
string? | the bytes, or nil when none are available |
boolean | whether the stream has ended |
Raises
| Type | Condition |
|---|---|
string | when the provider returns an invalid result, or timeoutMs is outside 0 through 2147483647 |
readmethod#
read: function read(exclusive self, count: integer): string?, string?Reads up to count bytes, suspending until some arrive. Returns empty at the end of the stream and nil with a reason once this end is closed.
Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | any | |
count | integer |
Returns
| Type | Description |
|---|---|
string? | |
string? |
Raises
| Type | Condition |
|---|---|
string | when count is not a positive integer |
readSpanmethod#
readSpan: function readSpan(exclusive self, exclusive destination: span.Writable<uint8>): integer?, string?Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | any | |
exclusive destination | span.Writable<uint8> |
Returns
| Type | Description |
|---|---|
integer? | |
string? |
Raises
| Type | Condition |
|---|---|
string | when the destination is empty |
readIntomethod#
readInto: function readInto(exclusive self, exclusive destination: Buffer, offset: integer?, count: integer?): integer?, string?Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | any | |
exclusive destination | Buffer | |
offset | integer? | |
count | integer? |
Returns
| Type | Description |
|---|---|
integer? | |
string? |
Raises
| Type | Condition |
|---|---|
string | when offset is negative or count is not a positive integer |
transferTomethod#
transferTo: function transferTo(exclusive self, exclusive destination: Writer2): integer?, string?Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | any | |
exclusive destination | Writer2 |
Returns
| Type | Description |
|---|---|
integer? | |
string? |
asReadermethod#
asReader: function asReader(borrows self): Reader2Borrows this stream through the shared completion-oriented contract.
Arguments
| Name | Type | Description |
|---|---|---|
borrows self | any |
Returns
| Type | Description |
|---|---|
Reader2 |
closemethod#
close: function close(takes self): nilCloses this end.
Idempotent. Closed-ness follows ownership rather than success. A backend that gave the stream up and then complained has still given it up, so this end is shut and the complaint is only reported; one that kept the stream leaves this end open.
Arguments
| Name | Type | Description |
|---|---|---|
takes self | any |
Returns
| Type | Description |
|---|---|
nil |
Raises
| Type | Condition |
|---|---|
string | when the platform reported a problem, released or not |
Resultrecord#
record Result ...A completed duplex exchange.
Members
| Name | Kind | Description |
|---|---|---|
exit | field | How the child ended. |
output | field | Captured standard output. |
errorOutput | field | Captured standard error. |
succeeded | method | Whether the child exited normally with status zero. |
succeededmethod#
succeeded: function succeeded(self): booleanWhether the child exited normally with status zero.
Arguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
boolean |
StreamModetype#
type StreamMode = "pipe" | "inherit" | "null"Where a stream goes: a pipe this process reads or writes, the parent's own stream, or nothing at all.
Writerrecord#
A child's writable stream.
offer is the nonblocking half, for the same reason the reader has poll: the prelude's nupp.io.Writer.write writes the whole value, which a drain loop serving three pipes cannot afford to wait for. The completion-oriented write remains the ordinary nupp.io.Writer operation; offer is concrete and additional.
Members
| Name | Kind | Description |
|---|---|---|
owner | field | |
handle | field | |
closed | field | |
gone | field | Whether the far end has gone, which is not the same as this end being shut. |
isEnded | method | Whether the far end has gone, so that no amount of waiting will deliver another byte, or this end has been closed. |
isReleased | method | Whether this end has been closed. |
offer | method | Writes what the pipe will take without waiting. |
send | method | Writes every byte, suspending as often as the pipe makes it. |
write | method | Writes every byte, suspending until the pipe has taken them all. |
writeSpan | method | |
flush | method | |
asWriter | method | Borrows this stream through the shared completion-oriented contract. |
close | method | Closes this end. |
gonefield#
gone: booleanWhether the far end has gone, which is not the same as this end being shut. closed means the platform has released this stream; gone means nobody is reading the far end any more. A broken pipe sets only gone, because this end is still ours to close.
isEndedmethod#
isEnded: function isEnded(self): booleanWhether the far end has gone, so that no amount of waiting will deliver another byte, or this end has been closed.
Arguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
boolean |
isReleasedmethod#
isReleased: function isReleased(self): booleanWhether this end has been closed.
Arguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
boolean |
offermethod#
offer: function offer(self, data: string): integerWrites what the pipe will take without waiting. Returns how many bytes went, which may be none and may be fewer than offered.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | |
data | string |
Returns
| Type | Description |
|---|---|
integer |
Raises
| Type | Condition |
|---|---|
string | when the provider returns an invalid result |
sendmethod#
Writes every byte, suspending as often as the pipe makes it. Returns how many went, which is all of them unless the far end went away or a bound ran out first.
deadline is a time in the nupp.time.now domain after which no more is sent, however the child is keeping up. stallTimeoutMs bounds a period with no progress instead, so a child that keeps taking bytes keeps the write going until the deadline. Without either, only the child's own deadline and the enclosing task scope's bound the wait.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this stream |
data | string | the bytes to send |
deadline | number? | when to stop, in the monotonic clock's milliseconds |
stallTimeoutMs | integer? | how long the pipe may take nothing before giving up |
Returns
| Type | Description |
|---|---|
integer | how many bytes went |
writemethod#
write: function write(exclusive self, bytes: string): boolean, string?Writes every byte, suspending until the pipe has taken them all.
Bounded the way read is: by the child's deadline and the enclosing task scope's. send takes an explicit deadline or stall bound for a caller that needs one of its own.
Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | any | |
bytes | string |
Returns
| Type | Description |
|---|---|
boolean | |
string? |
writeSpanmethod#
writeSpan: function writeSpan(exclusive self, borrows source: span.ByteSpan): integer?, string?Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | any | |
borrows source | span.ByteSpan |
Returns
| Type | Description |
|---|---|
integer? | |
string? |
flushmethod#
flush: function flush(self): boolean, string?Arguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
boolean | |
string? |
asWritermethod#
asWriter: function asWriter(borrows self): Writer2Borrows this stream through the shared completion-oriented contract.
Arguments
| Name | Type | Description |
|---|---|---|
borrows self | any |
Returns
| Type | Description |
|---|---|
Writer2 |
closemethod#
close: function close(takes self): nilCloses this end. Idempotent, on the same terms as the reader's: closed-ness follows what the platform released, not whether it complained, so a stream already given up is never offered to a retry. Closing a child's stdin is how it is told there is no more input.
Arguments
| Name | Type | Description |
|---|---|---|
takes self | any |
Returns
| Type | Description |
|---|---|
nil |
Raises
| Type | Condition |
|---|---|
string | when the platform reported a problem, released or not |
Functions#
process.spawnfunction#
Starts a child process.
The child is an owner: closing it ends the process, waits for the exit, and releases every stream, and the same runs when its scope ends. A child that cannot be started, such as a program that is not there, returns nil and why rather than raising, as files.open and net.connect do.
local child = assert(process.spawn({
args = {"grep", "-c", "nupp"},
stderr = "stdout",
}))
local result = assert(child:communicate({input = text}))
child:close()Arguments
| Name | Type | Description |
|---|---|---|
options | process.Options | what to run and how to connect it |
Returns
| Type | Description |
|---|---|
affine(process.Process)? | the child, which the caller owns and must close |
io.Error? | why it could not be started, when unsuccessful |
Raises
| Type | Condition |
|---|---|
string | when the options could never describe a child |