use std::{
borrow::Cow,
io::{BufRead, BufReader},
process::{Command, Output, Stdio},
sync::{Arc, Mutex},
};
pub trait CommandExt {
fn output_ok(&mut self) -> std::io::Result<Output>;
fn output_ok_info(&mut self) -> std::io::Result<Output>;
fn output_ok_inner(&mut self, level: tracing::Level) -> std::io::Result<Output>;
}
impl CommandExt for Command {
fn output_ok(&mut self) -> std::io::Result<Output> {
self.output_ok_inner(tracing::Level::DEBUG)
}
fn output_ok_info(&mut self) -> std::io::Result<Output> {
self.output_ok_inner(tracing::Level::INFO)
}
fn output_ok_inner(&mut self, level: tracing::Level) -> std::io::Result<Output> {
tracing::debug!("Running Command `{self:?}`");
self.stdout(Stdio::piped());
self.stderr(Stdio::piped());
let mut child = self.spawn()?;
let mut stdout = child.stdout.take().map(BufReader::new).unwrap();
let stdout_lines = Arc::new(Mutex::new(Vec::new()));
let stdout_lines_ = stdout_lines.clone();
std::thread::spawn(move || {
let mut buf = Vec::new();
let mut lines = stdout_lines_.lock().unwrap();
loop {
buf.clear();
if let Ok(0) = stdout.read_until(b'\n', &mut buf) {
break;
}
log(
level,
"stdout",
String::from_utf8_lossy(&buf[..buf.len() - 1]),
);
lines.extend(&buf);
}
});
let mut stderr = child.stderr.take().map(BufReader::new).unwrap();
let stderr_lines = Arc::new(Mutex::new(Vec::new()));
let stderr_lines_ = stderr_lines.clone();
std::thread::spawn(move || {
let mut buf = Vec::new();
let mut lines = stderr_lines_.lock().unwrap();
loop {
buf.clear();
if let Ok(0) = stderr.read_until(b'\n', &mut buf) {
break;
}
log(
level,
"stderr",
String::from_utf8_lossy(&buf[..buf.len() - 1]),
);
lines.extend(&buf);
}
});
let status = child.wait()?;
let output = Output {
status,
stdout: std::mem::take(&mut *stdout_lines.lock().unwrap()),
stderr: std::mem::take(&mut *stderr_lines.lock().unwrap()),
};
if output.status.success() {
Ok(output)
} else {
Err(std::io::Error::other(format!(
"failed to run command: {self:?}\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)))
}
}
}
#[inline]
fn log(level: tracing::Level, shell: &str, msg: Cow<'_, str>) {
match level {
tracing::Level::INFO => tracing::info!(shell = shell, "{msg}"),
_ => tracing::debug!(shell = shell, "{msg}"),
}
}