dotfiles/agent/src/system/exec.rs

123 lines
2.9 KiB
Rust
Raw Normal View History

2023-06-15 10:55:24 +00:00
use std::{
ffi::OsStr,
process::{ExitStatus, Stdio},
};
2023-06-13 13:02:49 +00:00
use tokio::{
io::{AsyncBufReadExt, BufReader},
process::{Child, Command},
sync::mpsc,
};
#[derive(Debug)]
pub enum ExecOutput {
2023-06-15 10:55:24 +00:00
Output {
stdout: Option<String>,
stderr: Option<String>,
},
Exit(ExitStatus),
Error(String),
2023-06-13 13:02:49 +00:00
}
pub fn exec<P, A>(program: P, args: &[A]) -> anyhow::Result<mpsc::Receiver<ExecOutput>>
2023-06-13 13:02:49 +00:00
where
2023-06-15 10:55:24 +00:00
P: AsRef<OsStr>,
A: AsRef<OsStr>,
2023-06-13 13:02:49 +00:00
{
let (tx, rx) = mpsc::channel(4);
let command = Command::new(program)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
2023-06-15 10:55:24 +00:00
let fut = run_process(command, tx);
tokio::spawn(fut);
2023-06-13 13:02:49 +00:00
Ok(rx)
}
2023-06-15 10:55:24 +00:00
async fn run_process(mut command: Child, sender: mpsc::Sender<ExecOutput>) {
2023-06-13 13:02:49 +00:00
let mut stdout = {
let stdout = command.stdout.take().expect("bug: no pipe for stdout");
BufReader::new(stdout).lines()
};
let mut stderr = {
let stderr = command.stderr.take().expect("bug: no pipe for stderr");
BufReader::new(stderr).lines()
};
loop {
match (stdout.next_line().await, stderr.next_line().await) {
2023-06-15 10:55:24 +00:00
(Ok(None), Ok(None)) => break,
(Ok(stdout), Ok(stderr)) => sender
.send(ExecOutput::Output { stdout, stderr })
2023-06-13 13:02:49 +00:00
.await
2023-06-15 10:55:24 +00:00
.expect("stream closed"),
(Err(err), _) | (_, Err(err)) => sender
.send(ExecOutput::Error(err.to_string()))
.await
.expect("stream closed"),
2023-06-13 13:02:49 +00:00
}
}
match command.wait().await {
2023-06-15 10:55:24 +00:00
Ok(exit_status) => sender
.send(ExecOutput::Exit(exit_status))
.await
.expect("stream closed"),
Err(err) => sender
.send(ExecOutput::Error(err.to_string()))
.await
.expect("stream closed"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn exec_works() {
let mut rx = exec(&"echo", &["1\n2\n3"]).expect("to spawn command");
let mut outputs = vec![];
while let Some(output) = rx.recv().await {
outputs.push(output);
}
assert_eq!(outputs.len(), 4);
let ExecOutput::Output {
ref stdout,
ref stderr,
} = outputs[0]
else {
panic!()
};
2023-06-15 10:55:24 +00:00
assert_eq!(*stdout, Some("1".to_owned()));
assert_eq!(*stderr, None);
let ExecOutput::Output {
ref stdout,
ref stderr,
} = outputs[1]
else {
panic!()
};
2023-06-15 10:55:24 +00:00
assert_eq!(*stdout, Some("2".to_owned()));
assert_eq!(*stderr, None);
let ExecOutput::Output {
ref stdout,
ref stderr,
} = outputs[2]
else {
panic!()
};
2023-06-15 10:55:24 +00:00
assert_eq!(*stdout, Some("3".to_owned()));
assert_eq!(*stderr, None);
}
2023-06-13 13:02:49 +00:00
}