pset 0.1.0

Orchestrate a set of child processes: one event stream for their output and their exits
docs.rs failed to build pset-0.1.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

pset

Orchestrate a set of child processes: one event stream for their output and their exits. It gives you the same functionality (almost) that async gives you, but without bringing async runtimes in your program.

A ProcessSet owns any number of children, each labelled with a tag of the caller's choosing, and hands out what they do — output on stdout, output on stderr, exiting — one PsetEvent at a time. One thread waits on all the pipes and all the exits together; there is no signal handler and no thread per pipe.

A ProcessSet is using std::process::Command for actual spawning, so every feature supported by std is also supported by pset.

use pset::{ProcessSet, PsetEvent};
use std::process::{Command, Stdio};

#[derive(Debug, Clone, Copy, PartialEq)]
enum Tag {
    Date,
    Uname,
}

fn main() -> std::io::Result<()> {
    let mut pset = pset::create::<Tag>()?;

    // Piping a stream is what turns it into events — see below.
    let mut cmd1 = Command::new("sh");
    cmd1.arg("-c").arg("date").stdout(Stdio::piped()).stderr(Stdio::piped());

    let mut cmd2 = Command::new("sh");
    cmd2.arg("-c").arg("uname").stdout(Stdio::piped()).stderr(Stdio::piped());

    pset.spawn(Tag::Date, cmd1)?;
    pset.spawn(Tag::Uname, cmd2)?;

    while let Some((tag, event)) = pset.wait_next()? {
        match event {
            PsetEvent::Stdout(data) => println!("{tag:?} said {} bytes", data.len()),
            PsetEvent::Stderr(data) => eprintln!("{tag:?} complained: {} bytes", data.len()),
            PsetEvent::ProcessExited(status) => println!("{tag:?} exited: {status}"),
        }
    }
    Ok(())
}

wait_next returns None once the set is empty, so the loop ends when the last child is gone — no bookkeeping of who is still running. wait_next_timeout is the same wait with a deadline.

Guarantees

  • ProcessExited is the last event for a process. It is emitted only after the child has been reaped and every pipe the set holds for it has reached end of file, so no output can appear after it and nothing written before the exit is lost.
  • No output is lost to a full pipe. Every pipe is drained by the same loop that waits for the exits, so a child writing megabytes cannot deadlock against a parent waiting for it to finish.
  • Signals cannot go astray. kill never signals through a pid that could have been recycled: on Linux a pidfd carries the signal, and on macOS it only ever goes to a child the set has not reaped yet, whose pid therefore still belongs to it.
  • No orphans, no zombies. Dropping the set kills (SIGKILL) and reaps every child still running. Wait for the children, or kill_all them on your own terms, if that is not what you want.

Platform support

Linux and macOS, each on its native way of making a process exit an ordinary event in a kernel queue:

  • Linuxpidfd_open(2) + epoll(7)
  • macOSkqueue(2) + EVFILT_PROC

The platform-specific plumbing lives in the sys module behind a small common surface; everything above it is shared.

Documentation

The crate-level docs cover the API and its contract in detail:

cargo doc --open

Testing

cargo test