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.
Module contents
Constructors
| Constructor | Description |
|---|---|
new | Starts a child. |
Types
| Type | Kind | Description |
|---|---|---|
CommunicateOptions | type | Re-exported so a caller needs one import. |
Exit | type | Re-exported so a caller needs one import. |
Options | type | Re-exported so a caller needs one import. |
Process | record | A running child. |
Reader | record | One of a child's readable streams. |
Result | type | Re-exported so a caller needs one import. |
Writer | record | A child's writable stream. |
Functions
| Function | Kind | Description |
|---|---|---|
asReader | function | Borrows a process reader through the shared completion-oriented contract. |
asWriter | function | Borrows a process writer through the shared completion-oriented contract. |
exited | function | The exit a backend reports, with succeeded attached. |
spawnOn | function | Starts a child on a given backend. |
useBackend | function | Installs the backend process.new uses. |
Constructors#
process.newconstructor#
Starts a child.
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#
CommunicateOptionstype#
Re-exported so a caller needs one import.
type process.CommunicateOptions = processtypes.CommunicateOptionsExittype#
Re-exported so a caller needs one import.
Optionstype#
Re-exported so a caller needs one import.
Processrecord#
A running child.
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
function close(self): (boolean, string?)
end
endMethods
isRunning
Whether it is still running.
isRunning: function isRunning(self): booleanArguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
boolean |
wait
Waits for it to end and answers how. Suspends while it runs.
wait: function wait(self): processtypes.ExitArguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
processtypes.Exit |
kill
Asks it to end; force insists. Answers whether the request was made.
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.
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
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 |
closing | boolean | Whether a teardown is in progress. Separate from |
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 |
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. |
Readerrecord#
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.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.
record process.Reader is nupp.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.Buffer, offset: integer?, count: integer?): (integer?, string?)
end
function transferTo(self, destination: nupp.Writer): (integer?, string?)
end
function close(self): (boolean, string?)
end
endMethods
isEOF
Whether the far end has finished.
isEOF: function isEOF(self): booleanArguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
boolean |
isClosed
Whether this end has been closed.
isClosed: function isClosed(self): booleanArguments
| 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.
release: function release(self): nilArguments
| 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.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.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.
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.
next: function next(self): string?Arguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
string? |
setTimeout
Bounds the next completion-oriented read.
setTimeout: function setTimeout(self, timeoutMs: integer): nilArguments
| Name | Type | Description |
|---|---|---|
self | any | |
timeoutMs | integer |
Returns
| Type | Description |
|---|---|
nil |
Raises
when timeoutMs is outside 0 through 2147483647
read
read: function read(self, count: integer): string?, string?Arguments
| Name | Type | Description |
|---|---|---|
self | any | |
count | integer |
Returns
| Type | Description |
|---|---|
string? | |
string? |
readInto
readInto: function readInto(self, destination: nupp.Buffer, offset: integer?, count: integer?): integer?, string?Arguments
| Name | Type | Description |
|---|---|---|
self | any | |
destination | nupp.Buffer | |
offset | integer? | |
count | integer? |
Returns
| Type | Description |
|---|---|
integer? | |
string? |
transferTo
Arguments
| Name | Type | Description |
|---|---|---|
self | any | |
destination | nupp.Writer |
Returns
| Type | Description |
|---|---|
integer? | |
string? |
close
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. |
Resulttype#
Re-exported so a caller needs one import.
Writerrecord#
A child's writable stream.
offer and isGone are the nonblocking half, for the same reason the reader has poll: the prelude's nupp.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.Writer operation; offer is concrete and additional.
record process.Writer is nupp.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.Buffer, offset: integer?, count: integer?): (integer?, string?)
end
function writeView(self, source: nupp.ByteView, offset: integer?, count: integer?): (integer?, string?)
end
function flush(self): (boolean, string?)
end
function close(self): (boolean, string?)
end
endMethods
isGone
Whether the far end has gone: the child is no longer reading this, and no amount of waiting will change that.
isGone: function isGone(self): booleanArguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
boolean |
isClosed
Whether this end has been closed.
isClosed: function isClosed(self): booleanArguments
| 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.
release: function release(self): nilArguments
| 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.
offer: function offer(self, data: string): integerArguments
| 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.
send: function send(self, data: string, stopAt: number?, stallFor: integer?): integerArguments
| Name | Type | Description |
|---|---|---|
self | any | |
data | string | |
stopAt | number? | |
stallFor | integer? |
Returns
| Type | Description |
|---|---|
integer |
setTimeout
Bounds the next completion-oriented write.
setTimeout: function setTimeout(self, timeoutMs: integer): nilArguments
| Name | Type | Description |
|---|---|---|
self | any | |
timeoutMs | integer |
Returns
| Type | Description |
|---|---|
nil |
Raises
when timeoutMs is outside 0 through 2147483647
write
write: function write(self, bytes: string): boolean, string?Arguments
| Name | Type | Description |
|---|---|---|
self | any | |
bytes | string |
Returns
| Type | Description |
|---|---|
boolean | |
string? |
writeFrom
writeFrom: function writeFrom(self, source: nupp.Buffer, offset: integer?, count: integer?): integer?, string?Arguments
| Name | Type | Description |
|---|---|---|
self | any | |
source | nupp.Buffer | |
offset | integer? | |
count | integer? |
Returns
| Type | Description |
|---|---|
integer? | |
string? |
writeView
writeView: function writeView(self, source: nupp.ByteView, offset: integer?, count: integer?): integer?, string?Arguments
| Name | Type | Description |
|---|---|---|
self | any | |
source | nupp.ByteView | |
offset | integer? | |
count | integer? |
Returns
| Type | Description |
|---|---|
integer? | |
string? |
flush
flush: function flush(self): boolean, string?Arguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
boolean | |
string? |
close
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. |
timeoutMs | integer | Maximum time one completion-oriented write may wait. |
Functions#
process.asReaderfunction#
Borrows a process reader through the shared completion-oriented contract.
Arguments
| Name | Type | Description |
|---|---|---|
source | process.Reader |
Returns
| Type | Description |
|---|---|
nupp.Reader borrows source |
process.asWriterfunction#
Borrows a process writer through the shared completion-oriented contract.
Arguments
| Name | Type | Description |
|---|---|---|
source | process.Writer |
Returns
| Type | Description |
|---|---|
nupp.Writer borrows source |
process.exitedfunction#
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.
Arguments
| Name | Type | Description |
|---|---|---|
exitCode | integer | |
killed | boolean | |
timedOut | boolean |
Returns
| Type | Description |
|---|---|
processtypes.Exit |
process.spawnOnfunction#
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.
function process.spawnOn(backend: processtypes.Backend, options: processtypes.Options): process.ProcessArguments
| 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.useBackendfunction#
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.
function process.useBackend(backend: processtypes.Backend): nilArguments
| Name | Type | Description |
|---|---|---|
backend | processtypes.Backend |
Returns
| Type | Description |
|---|---|
nil |