# 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` | |