pset 0.1.0

Orchestrate a set of child processes: one event stream for their output and their exits
# 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.

```rust
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:

- **Linux**`pidfd_open(2)` + `epoll(7)`
- **macOS**`kqueue(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:

```sh
cargo doc --open
```

## Testing

```sh
cargo test
```