Skip to content

procstream

stable

Spawn a subprocess with piped stdio and stream its stdout and stderr line-by-line without blocking, while writing to its stdin.

use plugin procstream::{Proc.spawn, read_lines, read_err_lines, …}
7 functions Systems
/ filter jk navigate Esc clear
Functions (7)
  1. Proc.spawn Spawn a subprocess with piped stdio and return a `Proc` handle
  2. read_lines Drain and return the currently-queued stdout lines
  3. read_err_lines Drain and return the currently-queued stderr lines
  4. write Write data to the child's stdin and return the byte count
  5. alive Report whether the process is still running
  6. exit_code Return the process exit code, or -1 if not yet exited
  7. kill Terminate the child process

Overview

procstream fills the one gap in Zolo's blocking std::process: incremental, non-blocking access to a child process's output while it is still running. You spawn a command with Proc.spawn, which returns a Proc handle, then poll stdout and stderr line-by-line as the process produces them — no waiting for the whole program to finish.

The mental model is a stateful handle backed by background reader threads. Each call to read_lines (or read_err_lines) drains whatever output has accumulated so far and returns it as an array of strings, leaving the queue empty for next time. Alongside reading you can push data to the child's stdin with write, check whether it is still running with alive, retrieve its exit_code once it has finished, and stop it early with kill. Reach for procstream whenever you need to drive an interactive or long-running CLI tool and react to its output as it streams.

Common patterns

Spawn a command and drain its stdout until it exits:

use plugin procstream::{Proc}

let p = Proc.spawn("ping", ["-n", "3", "127.0.0.1"])
while p.alive() {
  for line in p.read_lines() {
    print(line)
  }
}
// flush any lines produced just before exit
for line in p.read_lines() {
  print(line)
}
print("exit code: {p.exit_code()}")

Run a process in a working directory and separate stdout from stderr:

use plugin procstream::{Proc}

let p = Proc.spawn("git", ["status"], #{ "cwd": "/tmp/repo" })
while p.alive() {
  for out in p.read_lines() { print("out: {out}") }
  for err in p.read_err_lines() { print("err: {err}") }
}

Feed an interactive tool through its stdin and read the response:

use plugin procstream::{Proc}

let p = Proc.spawn("cat", [])
p.write("hello\n")
p.write("world\n")
for echoed in p.read_lines() {
  print("echo: {echoed}")
}
p.kill()

Spawn a subprocess with piped stdio and return a `Proc` handle

Spawns cmd with the given argument array and returns a Proc handle. Stdout and stderr are piped and read in the background, and stdin is piped so write works. The optional opts table accepts a cwd string (working directory) and a stdin string — set stdin to "null" to connect the child's stdin to the null device so tools that probe stdin don't block (in that mode write becomes a no-op returning 0).

use plugin procstream::{Proc}

let p = Proc.spawn("echo", ["hello from procstream"])
for line in p.read_lines() {
  print(line)
}

Pass an opts table to set the working directory or detach stdin:

use plugin procstream::{Proc}

let p = Proc.spawn("ls", ["-la"], #{ "cwd": "/usr", "stdin": "null" })
while p.alive() {
  for line in p.read_lines() { print(line) }
}

Drain and return the currently-queued stdout lines

Drains and returns every stdout line buffered since the last call, as an array of strings. The queue is emptied, so a subsequent call returns only newly arrived lines. Returns an empty array when no output is pending. Because reading happens on a background thread, poll this in a loop while the process is alive and once more after it exits to catch the final lines.

use plugin procstream::{Proc}

let p = Proc.spawn("printf", ["a\nb\nc\n"])
while p.alive() {
  for line in p.read_lines() { print("got: {line}") }
}
for line in p.read_lines() { print("final: {line}") }

Drain and return the currently-queued stderr lines

Drains and returns every stderr line buffered since the last call, as an array of strings. Identical in behavior to read_lines but reads the error stream, letting you keep diagnostics separate from normal output.

use plugin procstream::{Proc}

let p = Proc.spawn("cmd_that_warns", [])
while p.alive() {
  for out in p.read_lines() { print("out: {out}") }
  for err in p.read_err_lines() { print("warn: {err}") }
}

Write data to the child's stdin and return the byte count

Writes data to the child's stdin exactly as given (no newline is appended) and flushes immediately, returning the number of bytes written. Add a trailing \n yourself when the tool reads line-by-line. If the process was spawned with stdin: "null" or its stdin is otherwise closed, write is a safe no-op and returns 0.

use plugin procstream::{Proc}

let p = Proc.spawn("cat", [])
let n = p.write("a line of input\n")
print("wrote {n} bytes")
for line in p.read_lines() { print(line) }

Send several lines, then read the combined response:

use plugin procstream::{Proc}

let p = Proc.spawn("sort", [])
p.write("banana\n")
p.write("apple\n")
p.write("cherry\n")
p.kill()
for line in p.read_lines() { print(line) }

Report whether the process is still running

Returns true while the child process is still running and false once it has exited (or if no process is attached). Use it as the loop condition while draining output.

use plugin procstream::{Proc}

let p = Proc.spawn("sleep", ["1"])
while p.alive() {
  for line in p.read_lines() { print(line) }
}
print("done")

Return the process exit code, or -1 if not yet exited

Returns the process's exit code once it has finished. While the process is still running (or if the code is unavailable) it returns -1, so check alive first or only read the code after the loop ends.

use plugin procstream::{Proc}

let p = Proc.spawn("false", [])
while p.alive() {
  for line in p.read_lines() { print(line) }
}
print("exited with {p.exit_code()}")

Terminate the child process

Terminates the child process and closes its stdin so it sees EOF. Use it to stop a long-running or interactive tool early; the handle is also cleaned up automatically when it is garbage-collected.

use plugin procstream::{Proc}

let p = Proc.spawn("yes", [])
for line in p.read_lines() { print(line) }
p.kill()
print("stopped, alive: {p.alive()}")
enespt-br