Chapter 13 of 17

Processes

Run programs with async pipes, and take signals as streams.

Running a program

nuclei::process::Command is shaped like std's: arguments, environment, working directory and standard streams, then output (collect what it printed), status (only the exit) or spawn (a Child to talk to). The examples run Unix programs.

src/main.rs
use nuclei::process::Command;

#[nuclei::main]
async fn main() -> std::io::Result<()> {
    // Run a program to its end and collect what it printed.
    let output = Command::new("echo").arg("hello").output().await?;
    assert!(output.status.success());
    assert_eq!(output.stdout, b"hello\n");

    // Or just its exit status, its streams left as they are.
    let status = Command::new("true").status().await?;
    assert_eq!(status.code(), Some(0));
    Ok(())
}

A shard's driver watches each child's exit (a pidfd on Linux, a process event on kqueue, a wait on the process handle on Windows) and reaps it, so a wait holds no thread, and a child whose Child was dropped leaves no zombie behind.

Talking to a child

Piped streams are async: write to stdin, read stdout and stderr with the IO helpers. Dropping stdin ends the child's input; wait closes it first too, so a child that reads to the end can exit.

src/main.rs
use nuclei::io::{AsyncReadExt, AsyncWriteExt};
use nuclei::process::{Command, Stdio};

#[nuclei::main]
async fn main() -> std::io::Result<()> {
    let mut child = Command::new("sort")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()?;

    // The pipes are async streams.
    let mut stdin = child.stdin.take().expect("piped");
    stdin.write_all(b"pear\napple\nfig\n").await?;
    // Dropping stdin ends the child's input, so sort can finish.
    drop(stdin);

    let mut stdout = child.stdout.take().expect("piped");
    let mut sorted = String::new();
    stdout.read_to_string(&mut sorted).await?;
    assert_eq!(sorted, "apple\nfig\npear\n");
    assert!(child.wait().await?.success());
    Ok(())
}

Ending a child

src/main.rs
use nuclei::process::Command;

#[nuclei::main]
async fn main() -> std::io::Result<()> {
    // kill ends the child and waits for it to go.
    let mut child = Command::new("sleep").arg("60").spawn()?;
    child.kill().await?;
    assert!(!child.wait().await?.success());

    // start_kill only starts it; wait to see it go.
    let mut child = Command::new("sleep").arg("60").spawn()?;
    child.start_kill()?;
    child.wait().await?;

    // kill_on_drop: dropping the Child ends the process too.
    let child = Command::new("sleep").arg("60").kill_on_drop(true).spawn()?;
    drop(child);
    Ok(())
}
  • kill().await ends the child (SIGKILL on Unix, TerminateProcess on Windows) and waits for its exit; start_kill() only starts it.
  • kill_on_drop(true) ends the child when its Child is dropped before it exits. By default a dropped child runs on.
  • try_wait() gives the status if the child exited, without waiting.
  • For what Command does not mirror, as_std_mut reaches std's command underneath, spawn_with starts the child through your own code, and into_std takes std's command back.

Ctrl-C

nuclei::signal::ctrl_c() completes when the process gets a Ctrl-C: SIGINT on Unix, the console's CTRL_C_EVENT on Windows. Listening starts with its first poll, and replaces the default action, which would end the process.

src/main.rs
use std::time::Duration;

use nuclei::time::sleep;

#[nuclei::main]
async fn main() {
    nuclei::select! {
        _ = nuclei::signal::ctrl_c() => println!("stopping on Ctrl-C"),
        _ = sleep(Duration::from_millis(100)) => println!("the work finished first"),
    }
}
output
the work finished first

Unix signals

signal::unix::signal(kind) listens to one signal from then on, as a stream: recv().await ends once it was delivered, once or more, since the last recv. SignalKind names the usual ones (hangup, terminate, user_defined1, ...) and from_raw any other.

src/main.rs
use nuclei::process::Command;
use nuclei::signal::unix::{SignalKind, signal};

#[nuclei::main]
async fn main() -> std::io::Result<()> {
    // Listening starts here; the signal's default action is replaced.
    let mut reload = signal(SignalKind::user_defined1())?;
    let mut hangup = signal(SignalKind::hangup())?;

    // Send this process SIGUSR1, as an operator would with `kill -USR1`.
    let pid = std::process::id().to_string();
    Command::new("kill").args(["-USR1", &pid]).status().await?;

    nuclei::select! {
        _ = reload.recv() => println!("reloading"),
        _ = hangup.recv() => println!("hung up"),
    }
    Ok(())
}
output
reloading

Windows console events

signal::windows has ctrl_c, ctrl_break, ctrl_close, ctrl_logoff and ctrl_shutdown, each a CtrlEvent whose recv().await ends at the next such event.