Scarlet Industries

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.