Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 49 additions & 15 deletions docs/v2/specs/std-process.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,71 @@
# std/process Spec

Spawn a subprocess, run it to completion, and read its exit code plus
captured stdout and stderr. Optionally feed data to the child's stdin. The
primitives bind the raven-runtime C ABI (backed by `std::process::Command`);
the wrappers add the Result/Error model and the `Output` value type in pure
Spawn a subprocess and either run it to completion (`run`) or keep it
running while its output streams in (`start`). The primitives bind the
raven-runtime C ABI (backed by `std::process::Command`); the wrappers add
the Result/Error model and the `Output` and `Child` value types in pure
Raven.

## Import

```rust
import std/process { run, run_with_input }
import std/process { run, run_with_input, start, Child }
```

The `success` method on `Output` comes in with the type and needs no
separate selector.
The methods on `Output` and `Child` come in with the types and need no
separate selectors.

## Surface

```rust
struct Output { code: Int, stdout: String, stderr: String }
struct Child { id: Int }

fun run(program: String, args: List<String>) -> Result<Output, Error>
fun run_with_input(program: String, args: List<String>, input: String) -> Result<Output, Error>
fun start(program: String, args: List<String>) -> Result<Child, Error>

impl Output {
fun success(self) -> Bool // code == 0
}

impl Child {
fun read_stdout(self) -> String // drained since the last read, "" when nothing new
fun read_stderr(self) -> String
fun poll(self) -> Option<Int> // None while running, Some(code) once exited
fun running(self) -> Bool
fun wait(self) -> Int // block (polling) until exit
fun kill(self) -> Bool
fun free(self) // drop the runtime entry; does not kill
}
```

`run` spawns `program` with `args`, feeds it no stdin, waits for it to
finish, and returns its captured output. `run_with_input` is the same but
writes `input` to the child's stdin (which is then closed).

## Run-to-completion model

There is no streaming in v2.0. A run executes the child to completion in a
single runtime call that captures the whole of stdout and stderr into an
owned result before returning. A caller that needs incremental output is
not served by this surface.
writes `input` to the child's stdin (which is then closed). `start` spawns
the child and returns immediately with a handle. (`spawn` is the goroutine
keyword, hence `start`.)

## Two capture models

A `run` executes the child to completion in a single runtime call that
captures the whole of stdout and stderr into an owned result before
returning.

A `start` leaves the child running: runtime reader threads pump its stdout
and stderr into buffers as they arrive, and each `read_stdout` /
`read_stderr` call drains what accumulated since the previous read. Each
buffer is capped at 8 MB: a child that outruns its reader keeps only the
most recent bytes, so an undrained pipe cannot grow memory without bound
(keep draining to see everything). `poll` try-waits without blocking
(`running` is its Bool shorthand); `wait` loops poll with a short
scheduler-friendly sleep so other goroutines proceed. `kill` terminates the
child (killing an already-exited child reports success). `free` drops the
registry entry without killing, so a caller that wants the child stopped
must `kill` first; reaping is still guaranteed, since a freed child whose
exit was not yet observed is handed to a background waiter that collects it
when it exits, never leaving a zombie. The child's stdin is null: it sees
end of input immediately and does not inherit the parent's terminal.

## Argument encoding (NUL-joined)

Expand All @@ -57,6 +85,12 @@ wrappers read the three fields by id through the extractors, then free the
entry, so a caller never sees the id. Ids start at 1; an id of 0 is the
spawn-failure sentinel paired with a set last-error.

Started children live in a second registry sharing the same id sequence
(so an id is unique across both): the entry holds the live process handle,
the two output buffers the reader threads fill, and the exit code once
observed, cached so polling after exit stays cheap. `poll` reports `-2`
over the FFI while the child runs; the wrapper maps that to `None`.

## Non-zero exit is not an error

A child that spawns and runs but exits with a non-zero code is a normal,
Expand Down
27 changes: 27 additions & 0 deletions examples/v2/spawn_child.rv
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// golden:skip - the child half of the std/process spawn smoke tests. Prints
// "early" immediately, idles long enough for the parent to observe it
// running (and to drain the first line while it still runs), then prints
// "late" and exits 3. The idle defaults to 600ms; the parent's kill test
// passes a large one as the first argument to make the child effectively
// hang until killed.
import std/io { println }
import std/string
import std/sync { sleep_millis }
import std/env { args, exit }

fun main() {
println("early")
let idle = 600
let a = args()
if a.len() > 1 {
match a[1].parse_int() {
Some(ms) -> {
idle = ms
},
None -> {},
}
}
sleep_millis(idle)
println("late")
exit(3)
}
85 changes: 85 additions & 0 deletions examples/v2/spawn_parent.rv
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// golden:skip - std/process spawn parent, checked in codegen_smoke.rs
// (process_spawn_streams_and_kills). Drives the whole streaming surface
// against the child in RAVEN_PROC_CHILD (spawn_child.rv):
//
// 1. Streaming: start the child, drain stdout until "early" arrives, and
// prove the child is still running at that moment ("streamed-early").
// Then wait for exit and print the code (3) and whether the full output
// also carried "late" ("got-late").
// 2. Kill: start the child with a 20s idle, kill it while it runs, and
// print "killed" when the exit code comes back non-zero.
// 3. Spawn failure: a nonexistent program takes the Err path.
//
// Expected stdout:
// streamed-early
// 3
// got-late
// killed
// spawn failed
import std/io { println }
import std/process { Child, start }
import std/sync { sleep_millis }
import std/env { get_env }

fun main() {
let child_path = get_env("RAVEN_PROC_CHILD")

// 1. Streaming while the child runs.
let none: List<String> = []
match start(child_path, none) {
Ok(c) -> {
let seen = ""
let live = false
let tries = 0
while !seen.contains("early") && tries < 400 {
seen = seen.concat(c.read_stdout())
if seen.contains("early") && c.running() {
live = true
}
sleep_millis(10)
tries = tries + 1
}
if live {
println("streamed-early")
}
let code = c.wait()
println("${code}")
seen = seen.concat(c.read_stdout())
if seen.contains("late") {
println("got-late")
}
c.free()
},
Err(e) -> {
println("spawn failed early: ${e.message()}")
},
}

// 2. Kill a long-running child (a 20s idle passed as its argument).
match start(child_path, ["20000"]) {
Ok(c) -> {
// Let it start, then stop it.
sleep_millis(100)
c.kill()
let code = c.wait()
if code != 0 {
println("killed")
}
c.free()
},
Err(e) -> {
println("spawn failed kill: ${e.message()}")
},
}

// 3. The spawn-failure Err path.
match start("definitely-not-a-real-program-12345", none) {
Ok(c) -> {
println("unexpectedly spawned")
c.free()
},
Err(e) -> {
println("spawn failed")
},
}
}
Loading
Loading