# Module: `nupp.io.process`
Running a child process.
The lifecycle, the streams, and the draining, written once against a backend
that answers in bytes and opaque handles. Platform providers differ in how they
spawn and how they read; they do not differ in what a process is, so nothing
about what a process is lives in any one of them.
Waiting is a suspension. Under a handler -- a scheduler, a game frame -- reading
a pipe parks the caller and the frame keeps running; with none installed the
built-in handler drives the pumps and blocks. The same call either way, which is
the whole reason `nupp.suspension` exists.
Three details are worth stating, because they are the ones a first
implementation gets wrong and this one got two of them wrong before they were
pointed out.
**`communicate` makes one combined step.** Not "write all the input, then read
stdout to the end, then read stderr". A child that reads its input in pieces
while filling stderr deadlocks against every sequential order: the parent blocks
writing input the child is not reading, because the child is blocked writing
output the parent is not reading. The loop attempts all three every pass and
suspends only when none of them moved.
**A deadline has to be checked while suspended.** Checking it before parking and
never again means a quiet child -- one producing nothing at all -- waits past its
deadline forever, which is precisely the case a deadline is for.
**A killed child is not immediately reapable.** Terminating takes as long as it
takes, so closing waits for the exit rather than reaping something still dying.
See `plans/suspension.md`, S5.
## Constructors
### `process.new` _constructor_
Starts a child.
```nupp
function process.new(options: processtypes.Options): process.Process?, string?
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `options` | `processtypes.Options` | what to run and how to connect it |
#### Returns
| Type | Description |
| --- | --- |
| `process.Process?` | the child, which must be closed |
| `string?` | |
## Types
### `CommunicateOptions` _type_
Re-exported so a caller needs one import.
```nupp
type process.CommunicateOptions = processtypes.CommunicateOptions
```
### `Exit` _type_
Re-exported so a caller needs one import.
```nupp
type process.Exit = processtypes.Exit
```
### `Options` _type_
Re-exported so a caller needs one import.
```nupp
type process.Options = processtypes.Options
```
### `Process` _record_
A running child.
```nupp
record process.Process
backend: processtypes.Backend
handle: any
deadline: number?
exit: processtypes.Exit?
reaped: boolean
childReleased: boolean
closing: boolean
closingBy: any
pump: any
timedOut: boolean
pid: integer
stdin: process.Writer?
stdout: process.Reader?
stderr: process.Reader?
function isRunning(self): boolean
end
function wait(self): processtypes.Exit
end
function kill(self, force: boolean?): (boolean, string?)
end
function communicate(self, options: processtypes.CommunicateOptions?): (processtypes.Result?, string?)
end
@drop
function close(self): (boolean, string?)
end
end
```
#### Methods
##### `isRunning`
Whether it is still running.
```nupp
isRunning: function isRunning(self): boolean
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
##### `wait`
Waits for it to end and answers how. Suspends while it runs.
```nupp
wait: function wait(self): processtypes.Exit
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
###### Returns
| Type | Description |
| --- | --- |
| `processtypes.Exit` | |
##### `kill`
Asks it to end; `force` insists. Answers whether the request was made.
```nupp
kill: function kill(self, force: boolean?): boolean, string?
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
| `force` | `boolean?` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
| `string?` | |
##### `communicate`
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.
```nupp
communicate: function communicate(self, options: processtypes.CommunicateOptions?): processtypes.Result?, string?
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
| `options` | `processtypes.CommunicateOptions?` | |
###### Returns
| Type | Description |
| --- | --- |
| `processtypes.Result?` | |
| `string?` | |
##### `close`
```nupp
close: function close(self): boolean, string?
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
| `string?` | |
#### Fields
| Name | Type | Description |
| --- | --- | --- |
| `backend` | `processtypes.Backend` | |
| `handle` | `any` | |
| `deadline` | `number?` | |
| `exit` | `processtypes.Exit?` | |
| `reaped` | `boolean` | Whether the whole teardown finished: every stream released, the pump released, and the child handle released. The aggregate, and what makes a repeat call a no-op. |
| `childReleased` | `boolean` | Whether the child *handle* has been given back, which is a smaller fact than `reaped` and has to be tracked apart from it. A teardown where a stream close complained still has a child to release, and a retry that had already released it must not ask again -- a pid is reused as readily as a descriptor. |
| `closing` | `boolean` | Whether a teardown is in progress. Separate from `reaped`, which means it finished: a close that failed part way must be retryable rather than silently successful. |
| `closingBy` | `any` | Which coroutine is running that teardown, so a second call can tell whether it is the same frame coming back around or a different one arriving. Nil on the main coroutine, which is why `closing` is the flag and this is only the owner. |
| `pump` | `any` | |
| `timedOut` | `boolean` | Whether the deadline is what ended it, remembered so the exit can say so. |
| `pid` | `integer` | Operating-system process identifier. |
| `stdin` | `process.Writer?` | Its standard input, when it was piped. |
| `stdout` | `process.Reader?` | Its standard output, when it was piped. |
| `stderr` | `process.Reader?` | Its standard error, when it was piped. |
### `Reader` _record_
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 descriptor
already gone and says so. Retrying the raw descriptor underneath is the opposite --
that number belongs to whatever the platform has since handed it to. And 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.
```nupp
record process.Reader is nupp.io.Reader
owner: any
handle: any
closed: boolean
eof: boolean
timeoutMs: integer
function isEOF(self): boolean
end
function isClosed(self): boolean
end
function release(self): nil
end
function poll(self, limit: integer?): string?
end
function next(self): string?
end
function setTimeout(self, timeoutMs: integer): nil
end
function read(self, count: integer): (string?, string?)
end
function readInto(self, destination: nupp.io.Buffer, offset: integer?, count: integer?): (integer?, string?)
end
function transferTo(self, destination: nupp.io.Writer): (integer?, string?)
end
function close(self): (boolean, string?)
end
end
```
#### Methods
##### `isEOF`
Whether the far end has finished.
```nupp
isEOF: function isEOF(self): boolean
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
##### `isClosed`
Whether this end has been closed.
```nupp
isClosed: function isClosed(self): boolean
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
##### `release`
Closes this end. Idempotent.
`closed` follows ownership rather than success. A platform that gave the
descriptor up and then complained has still given it up, so this end is shut and
the complaint is only reported; a platform that failed with the descriptor still
ours leaves this end open, and another attempt is right. Marking a released
descriptor open again is how a retry comes to close whichever unrelated thing
has since been given that number.
```nupp
release: function release(self): nil
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
###### Raises
- when the platform reported a problem, released or not
##### `poll`
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 limits read one byte, which is what `nupp.io.Reader` says a
non-positive count does. 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.
```nupp
poll: function poll(self, limit: integer?): string?
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
| `limit` | `integer?` | |
###### Returns
| Type | Description |
| --- | --- |
| `string?` | |
##### `next`
Reads the next available bytes, suspending until there are some. Answers nil at
end of stream.
```nupp
next: function next(self): string?
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
###### Returns
| Type | Description |
| --- | --- |
| `string?` | |
##### `setTimeout`
Bounds the next completion-oriented read.
```nupp
setTimeout: function setTimeout(self, timeoutMs: integer): nil
```
###### 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?` | |
##### `readInto`
```nupp
readInto: function readInto(self, destination: nupp.io.Buffer, offset: integer?, count: integer?): integer?, string?
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
| `destination` | `nupp.io.Buffer` | |
| `offset` | `integer?` | |
| `count` | `integer?` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer?` | |
| `string?` | |
##### `transferTo`
```nupp
transferTo: function transferTo(self, destination: nupp.io.Writer): integer?, string?
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
| `destination` | `nupp.io.Writer` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer?` | |
| `string?` | |
##### `close`
```nupp
close: function close(self): boolean, string?
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
| `string?` | |
#### Fields
| Name | Type | Description |
| --- | --- | --- |
| `owner` | `any` | |
| `handle` | `any` | |
| `closed` | `boolean` | |
| `eof` | `boolean` | |
| `timeoutMs` | `integer` | Maximum time one completion-oriented read may wait. |
### `Result` _type_
Re-exported so a caller needs one import.
```nupp
type process.Result = processtypes.Result
```
### `Writer` _record_
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.
```nupp
record process.Writer is nupp.io.Writer
owner: any
handle: any
closed: boolean
gone: boolean
timeoutMs: integer
function isGone(self): boolean
end
function isClosed(self): boolean
end
function release(self): nil
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(self, bytes: string): (boolean, string?)
end
function writeFrom(self, source: nupp.io.Buffer, offset: integer?, count: integer?): (integer?, string?)
end
function writeView(self, source: nupp.io.ByteView, offset: integer?, count: integer?): (integer?, string?)
end
function flush(self): (boolean, string?)
end
function close(self): (boolean, string?)
end
end
```
#### Methods
##### `isGone`
Whether the far end has gone: the child is no longer reading this, and no
amount of waiting will change that.
```nupp
isGone: function isGone(self): boolean
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
##### `isClosed`
Whether this end has been closed.
```nupp
isClosed: function isClosed(self): boolean
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
##### `release`
Closes this end. Idempotent, on the same terms as the reader's: `closed` follows
what the platform released, not whether it complained, so a descriptor already
given up is never offered to a retry. Closing a child's stdin is how it is told
there is no more input.
```nupp
release: function release(self): nil
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
###### Raises
- when the platform reported a problem, released or not
##### `offer`
Writes what the pipe will take without waiting. Answers how many bytes went,
which may be none and may be fewer than offered.
```nupp
offer: function offer(self, data: string): integer
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
| `data` | `string` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer` | |
##### `send`
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.
```nupp
send: function send(self, data: string, stopAt: number?, stallFor: integer?): integer
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
| `data` | `string` | |
| `stopAt` | `number?` | |
| `stallFor` | `integer?` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer` | |
##### `setTimeout`
Bounds the next completion-oriented write.
```nupp
setTimeout: function setTimeout(self, timeoutMs: integer): nil
```
###### 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(self, bytes: string): boolean, string?
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
| `bytes` | `string` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
| `string?` | |
##### `writeFrom`
```nupp
writeFrom: function writeFrom(self, source: nupp.io.Buffer, offset: integer?, count: integer?): integer?, string?
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
| `source` | `nupp.io.Buffer` | |
| `offset` | `integer?` | |
| `count` | `integer?` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer?` | |
| `string?` | |
##### `writeView`
```nupp
writeView: function writeView(self, source: nupp.io.ByteView, offset: integer?, count: integer?): integer?, string?
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
| `source` | `nupp.io.ByteView` | |
| `offset` | `integer?` | |
| `count` | `integer?` | |
###### 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(self): boolean, string?
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
| `string?` | |
#### Fields
| Name | Type | Description |
| --- | --- | --- |
| `owner` | `any` | |
| `handle` | `any` | |
| `closed` | `boolean` | |
| `gone` | `boolean` | Whether the far end has gone, which is not the same as this end being shut. `closed` means the platform has released the descriptor; this means nobody is reading it any more. A broken pipe sets only this, because the descriptor is still ours to close and skipping that would leak it. |
| `timeoutMs` | `integer` | Maximum time one completion-oriented write may wait. |
## Functions
### `process.asReader` _function_
Borrows a process reader through the shared completion-oriented contract.
```nupp
function process.asReader(borrows source: process.Reader): nupp.io.Reader borrows source
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `source` | `process.Reader` | |
#### Returns
| Type | Description |
| --- | --- |
| `nupp.io.Reader borrows source` | |
### `process.asWriter` _function_
Borrows a process writer through the shared completion-oriented contract.
```nupp
function process.asWriter(borrows source: process.Writer): nupp.io.Writer borrows source
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `source` | `process.Writer` | |
#### Returns
| Type | Description |
| --- | --- |
| `nupp.io.Writer borrows source` | |
### `process.exited` _function_
The exit a backend reports, with `succeeded` attached.
Backends build these, so the one judgement every caller makes -- did this work --
has one definition rather than one per platform.
```nupp
function process.exited(exitCode: integer, killed: boolean, timedOut: boolean): processtypes.Exit
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exitCode` | `integer` | |
| `killed` | `boolean` | |
| `timedOut` | `boolean` | |
#### Returns
| Type | Description |
| --- | --- |
| `processtypes.Exit` | |
### `process.spawnOn` _function_
Starts a child on a given backend.
Internal, and what the tests use so the state machine can be driven without a
platform. Ordinary callers want `process.new`.
```nupp
function process.spawnOn(backend: processtypes.Backend, options: processtypes.Options): process.Process
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `backend` | `processtypes.Backend` | the platform that runs it |
| `options` | `processtypes.Options` | what to run and how to connect it |
#### Returns
| Type | Description |
| --- | --- |
| `process.Process` | the child, which must be closed |
### `process.useBackend` _function_
Installs the backend `process.new` uses.
Called by a platform module, not by a caller: the point of the seam is that nothing
above it names a platform.
```nupp
function process.useBackend(backend: processtypes.Backend): nil
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `backend` | `processtypes.Backend` | |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |