# `nupp.io.process` Starts a child process and drains its streams without deadlocking. ```nupp local process = nupp.io.process local child = new process.Process({args = {"cc", "--version"}}) local result = assert(child:communicate()) print(result.output) child:close() ``` A child and its streams are owners. See [ownership.md](../../../../learn/runtime/ownership/borrowing/index.html) for the contract they are handed out under, and [NEP 5: Suspension](../../../../reference/neps/0005-suspension/index.html) for why waiting is a suspension rather than a block. ## Types ### `CommunicateOptions` _type_ ```nupp 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. ### `ErrorMode` _type_ ```nupp type ErrorMode = "pipe" | "inherit" | "null" | "stdout" ``` Where standard error goes, which adds the option to join standard output. ### `Exit` _record_ ```nupp record Exit exitCode: integer killed: boolean timedOut: boolean succeeded: function(Exit): boolean end ``` How a child ended. #### Methods ##### `succeeded` ```nupp succeeded: function(process.Exit): boolean ``` Exited on its own with status zero. A killed child never succeeded, whatever status the platform reported for it. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `?` | `process.Exit` | | ###### Returns | Type | Description | | --- | --- | | `boolean` | | #### Fields ##### `exitCode` ```nupp exitCode: integer ``` The status it exited with. Zero conventionally means success, and `succeeded` is the question worth asking instead. ##### `killed` ```nupp killed: boolean ``` Whether it was terminated rather than exiting on its own. ##### `timedOut` ```nupp timedOut: boolean ``` Whether it was terminated because its deadline passed. ### `Options` _type_ ```nupp 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, or nil to inherit this one. cwd: (string | Path)?, --- Variables overlaid on the inherited environment, or the whole environment --- when `clearEnv` is set. 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. ### `Process` _record_ ```nupp record process.Process drop: nosuspend function(takes self: process.Process): nil pid: integer stdin: process.Writer? stdout: process.Reader? stderr: process.Reader? constructor(self, options: process.Options): affine(process.Process, process.Process.destroy) end function destroy(takes self): nil end function isRunning(self): boolean end function wait(self): process.Exit end function kill(self, force: boolean?): (boolean, string?) end function communicate(self, options: process.CommunicateOptions?): (process.Result?, string?) end function close(self): (boolean, string?) end end ``` A running child. #### Methods ##### `drop` ```nupp drop: nosuspend function(takes self: process.Process): nil ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `takes self` | `process.Process` | | ###### Returns | Type | Description | | --- | --- | | `nil` | | ##### `constructor` ```nupp constructor: function constructor(self, options: process.Options): affine(process.Process, process.Process.destroy) ``` 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. Reading a stream or waiting for the exit suspends, so a caller under a scheduler keeps its frame. ```nupp local child = new process.Process({ args = {"grep", "-c", "nupp"}, stderr = "stdout", }) local result = assert(child:communicate({input = text})) child:close() ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | the process being initialized | | `options` | `process.Options` | what to run and how to connect it | ###### Returns | Type | Description | | --- | --- | | `affine(process.Process, process.Process.destroy)` | | ###### Raises - when the options are invalid or the child cannot be started ##### `destroy` ```nupp destroy: function destroy(takes self): nil ``` Releases the process when its owner leaves scope. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `takes self` | `any` | | ###### Returns | Type | Description | | --- | --- | | `nil` | | ##### `isRunning` ```nupp isRunning: function isRunning(self): boolean ``` Whether it is still running. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | ###### Returns | Type | Description | | --- | --- | | `boolean` | | ##### `wait` ```nupp wait: function wait(self): process.Exit ``` Waits for it to end and answers how. Suspends while it runs. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | ###### Returns | Type | Description | | --- | --- | | `process.Exit` | | ##### `kill` ```nupp kill: function kill(self, force: boolean?): boolean, string? ``` Asks the direct child to end; `force` insists. Descendants keep their ordinary operating-system lifetime. Answers whether the request was made. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | | `force` | `boolean?` | | ###### Returns | Type | Description | | --- | --- | | `boolean` | | | `string?` | | ##### `communicate` ```nupp communicate: function communicate(self, options: process.CommunicateOptions?): process.Result?, string? ``` Writes input, closes stdin, drains stdout and stderr, and waits for the exit. Answers 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?` | | | `string?` | | ##### `close` ```nupp close: function close(self): boolean, string? ``` Closes every stream, ends the child if it is still running, waits for it to actually finish, and releases it. Idempotent, and what `drop` runs when the owner goes out of lexical scope. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | ###### Returns | Type | Description | | --- | --- | | `boolean` | | | `string?` | | ###### Raises - when closing one of the child streams fails #### Fields ##### `pid` ```nupp pid: integer ``` Operating-system process identifier. ##### `stdin` ```nupp stdin: process.Writer? ``` Its standard input, when it was piped. ##### `stdout` ```nupp stdout: process.Reader? ``` Its standard output, when it was piped. ##### `stderr` ```nupp stderr: process.Reader? ``` Its standard error, when it was piped. ### `Reader` _record_ ```nupp record process.Reader is Reader2 timeoutMs: integer function isEOF(self): boolean end function isClosed(self): boolean end function poll(self, limit: integer?): string? end function next(self): string? end function readCompletion(self, limit: integer): (string?, string?) end function setTimeout(self, timeoutMs: integer): nil end function read(self, count: integer): (string?, string?) end function readSpan(self, exclusive destination: span.Writable): (integer?, string?) end function readInto(self, exclusive destination: Buffer, offset: integer?, count: integer?): (integer?, string?) end function transferTo(self, exclusive destination: Writer2): (integer?, string?) end function close(takes self): nil end end ``` One of a child's readable streams. Its blocking `read` and its nonblocking `poll` are both here on purpose. A caller draining several streams at once cannot use the blocking form on any of them, because waiting on one is exactly what starves the others, which is the whole reason `communicate` can drain three pipes without deadlocking. The completion-oriented `nupp.io.Reader` methods are the public tecs-compatible surface. `poll` remains alongside them as 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. #### Methods ##### `isEOF` ```nupp isEOF: function isEOF(self): boolean ``` Whether the far end has finished. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | ###### Returns | Type | Description | | --- | --- | | `boolean` | | ##### `isClosed` ```nupp isClosed: function isClosed(self): boolean ``` Whether this end has been closed. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | ###### Returns | Type | Description | | --- | --- | | `boolean` | | ##### `poll` ```nupp poll: function poll(self, limit: integer?): string? ``` Takes whatever is available without waiting: the bytes, `""` when nothing is ready yet, or nil at end of stream. The non-blocking half of reading, which `communicate` needs so that one quiet stream does not stop it serving another. `limit` caps how many bytes to take, defaulting to a whole pipe's worth. It is here because the shared `nupp.io.Reader` promises "at most `count`". Without the limit its completion-oriented method would have to keep surplus bytes beside this record, giving end of stream and closedness two homes. One place decides both, and the caller says how much it wants. Zero and negative polling limits read one byte. This concrete nonblocking operation keeps that defensive behavior even though the shared `Reader` contract requires a positive count. Settled here rather than at the platform because the platform is where it stops being a number and becomes a buffer size: 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, and neither is worth being able to ask for. `read` passes its caller's count straight through. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | | `limit` | `integer?` | | ###### Returns | Type | Description | | --- | --- | | `string?` | | ##### `next` ```nupp next: function next(self): string? ``` Reads the next available bytes, suspending until there are some. Answers nil at end of stream. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | ###### Returns | Type | Description | | --- | --- | | `string?` | | ##### `readCompletion` ```nupp readCompletion: function readCompletion(self, limit: integer): string?, string? ``` The shared Reader operation uses the concrete bounded poll but still waits through the same state machine as every other completion-oriented call. Keeping the limit here avoids a surplus buffer and its second notion of EOF. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | | `limit` | `integer` | | ###### Returns | Type | Description | | --- | --- | | `string?` | | | `string?` | | ##### `setTimeout` ```nupp setTimeout: function setTimeout(self, timeoutMs: integer): nil ``` Bounds the next completion-oriented read. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | | `timeoutMs` | `integer` | | ###### Returns | Type | Description | | --- | --- | | `nil` | | ###### Raises - when timeoutMs is outside 0 through 2147483647 ##### `read` ```nupp read: function read(self, count: integer): string?, string? ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | | `count` | `integer` | | ###### Returns | Type | Description | | --- | --- | | `string?` | | | `string?` | | ##### `readSpan` ```nupp readSpan: function readSpan(self, exclusive destination: span.Writable): integer?, string? ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | | `exclusive destination` | `span.Writable\` | | ###### Returns | Type | Description | | --- | --- | | `integer?` | | | `string?` | | ##### `readInto` ```nupp readInto: function readInto(self, exclusive destination: Buffer, offset: integer?, count: integer?): integer?, string? ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | | `exclusive destination` | `Buffer` | | | `offset` | `integer?` | | | `count` | `integer?` | | ###### Returns | Type | Description | | --- | --- | | `integer?` | | | `string?` | | ##### `transferTo` ```nupp transferTo: function transferTo(self, exclusive destination: Writer2): integer?, string? ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | | `exclusive destination` | `Writer2` | | ###### Returns | Type | Description | | --- | --- | | `integer?` | | | `string?` | | ##### `close` ```nupp close: function close(takes self): nil ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `takes self` | `any` | | ###### Returns | Type | Description | | --- | --- | | `nil` | | #### Fields ##### `timeoutMs` ```nupp timeoutMs: integer ``` Maximum time one completion-oriented read may wait. ### `Result` _record_ ```nupp record Result exit: process.Exit output: string errorOutput: string function succeeded(self): boolean end end ``` A completed duplex exchange. #### Methods ##### `succeeded` ```nupp succeeded: function succeeded(self): boolean ``` Whether the child exited normally with status zero. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | ###### Returns | Type | Description | | --- | --- | | `boolean` | | #### Fields ##### `exit` ```nupp exit: process.Exit ``` How the child ended. ##### `output` ```nupp output: string ``` Captured standard output. ##### `errorOutput` ```nupp errorOutput: string ``` Captured standard error. ### `StreamMode` _type_ ```nupp 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. ### `Writer` _record_ ```nupp record process.Writer is Writer2 timeoutMs: integer function isGone(self): boolean end function isClosed(self): boolean end function offer(self, data: string): integer end function send(self, data: string, stopAt: number?, stallFor: integer?): integer end function setTimeout(self, timeoutMs: integer): nil end function write(exclusive self, bytes: string): (boolean, string?) end function writeSpan(exclusive self, borrows source: span.ByteSpan): (integer?, string?) end function flush(self): (boolean, string?) end function close(takes self): nil end end ``` A child's writable stream. `offer` and `isGone` are 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. #### Methods ##### `isGone` ```nupp isGone: function isGone(self): boolean ``` Whether the far end has gone: the child is no longer reading this, and no amount of waiting will change that. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | ###### Returns | Type | Description | | --- | --- | | `boolean` | | ##### `isClosed` ```nupp isClosed: function isClosed(self): boolean ``` Whether this end has been closed. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | ###### Returns | Type | Description | | --- | --- | | `boolean` | | ##### `offer` ```nupp offer: function offer(self, data: string): integer ``` Writes what the pipe will take without waiting. Answers 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` | | ##### `send` ```nupp send: function send(self, data: string, stopAt: number?, stallFor: integer?): integer ``` Writes every byte, suspending as often as the pipe makes it. Answers how many went, which is all of them unless the far end went away first. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | | `data` | `string` | | | `stopAt` | `number?` | | | `stallFor` | `integer?` | | ###### Returns | Type | Description | | --- | --- | | `integer` | | ##### `setTimeout` ```nupp setTimeout: function setTimeout(self, timeoutMs: integer): nil ``` Bounds the next completion-oriented write. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | | `timeoutMs` | `integer` | | ###### Returns | Type | Description | | --- | --- | | `nil` | | ###### Raises - when timeoutMs is outside 0 through 2147483647 ##### `write` ```nupp write: function write(exclusive self, bytes: string): boolean, string? ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `exclusive self` | `any` | | | `bytes` | `string` | | ###### Returns | Type | Description | | --- | --- | | `boolean` | | | `string?` | | ##### `writeSpan` ```nupp 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?` | | ##### `flush` ```nupp flush: function flush(self): boolean, string? ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `any` | | ###### Returns | Type | Description | | --- | --- | | `boolean` | | | `string?` | | ##### `close` ```nupp close: function close(takes self): nil ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `takes self` | `any` | | ###### Returns | Type | Description | | --- | --- | | `nil` | | #### Fields ##### `timeoutMs` ```nupp timeoutMs: integer ``` Maximum time one completion-oriented write may wait. ## Functions ### `process.asReader` _function_ ```nupp function process.asReader(borrows source: process.Reader): Reader2 ``` Borrows a process reader through the shared completion-oriented contract. #### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows source` | `process.Reader` | | #### Returns | Type | Description | | --- | --- | | `Reader2` | | ### `process.asWriter` _function_ ```nupp function process.asWriter(borrows source: process.Writer): Writer2 ``` Borrows a process writer through the shared completion-oriented contract. #### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows source` | `process.Writer` | | #### Returns | Type | Description | | --- | --- | | `Writer2` | |