Ports
A port is a child operating-system process the program starts, talks to over its stdio, and takes down.
Spawn a child
It is how a Scarlet program runs native code without running it inside the runtime: the child is a separate OS process, so it can neither stall a scheduler nor corrupt the program, and when it dies the program finds out the way it finds out about a closed connection.
A port reads and writes exactly like a socket, with the same Read and
NetError types: read, read_exact,
read_within, read_until, write, and
write_parts. The child's stdout is what read reads, and its
stdin is what write writes. Framing is the program's concern, as on a
socket. The child's stderr is inherited, so its diagnostics appear alongside the
program's own.
match port.spawn('cat', []) {
Ok(p) -> {
_ = port.write(p, <<'hello ports\n'>>)
match port.read_exact(p, 12) {
Ok(b) -> println('cat echoed ${binary.byte_size(b)} bytes')
Err(e) -> println('read failed: ${string.inspect(e)}')
}
println('close: ${string.inspect(port.close(p))}')
}
Err(e) -> println('spawn failed: ${string.inspect(e)}')
}
port.spawn(program, args) resolves program the way a shell
does: a name is looked up on the PATH, a path is used as written. It parks the calling
process until the child is running. A program that cannot be started is reported as the
IoError the operating system gave, NotFound,
PermissionDenied, or an Errno, and nothing is left running.
port.spawn_env adds variables to the inherited environment.
Close it
close closes both pipes, collects the child, and returns how it ended:
Exited(code) or Signaled(signal). A well-behaved child sees the
closed pipes as end of input and exits. One still running 1 second later is sent SIGTERM,
and one still running 5 seconds after that is killed. Ok(Closed) from a
read means the child closed its stdout, almost always because it exited.
fn drain(p Port, acc Binary) Binary {
match port.read(p, 4096) {
Ok(Data(b)) -> drain(p, binary.append(acc, b))
Ok(Closed) -> acc
Err(_) -> acc
}
}
fn run_and_report() Nil {
match port.spawn('/bin/sh', ['-c', 'echo out; exit 3']) {
Ok(p) -> {
output = drain(p, <<>>)
match port.close(p) {
Ok(Exited(code)) -> println('${binary.byte_size(output)} bytes, exit ${code}')
Ok(Signaled(signal)) -> println('ended by signal ${signal}')
Err(e) -> println('close failed: ${string.inspect(e)}')
}
}
Err(e) -> println('spawn failed: ${string.inspect(e)}')
}
}
A port belongs to the process that spawned it and closes when that process ends, exactly like a connection. Nothing a child does leaves it running once its port is gone.