use anyhow::Result;
use std::{
io::{BufRead, BufReader},
process::{ChildStderr, ChildStdout, ExitStatus, Stdio},
sync::mpsc::Sender,
thread,
time::Duration,
};
use tracing::trace;
use crate::{error::AuditCheckError, utils::handle_join_error};
pub(crate) fn audit(
deny: &str,
tx_stdout: Sender<String>,
tx_stderr: Sender<String>,
tx_code: Sender<i32>,
) -> Result<()> {
let command = format!("cargo audit -D{deny}");
trace!("Running '{command}'");
let mut cmd = std::process::Command::new("sh");
let _ = cmd.arg("-c");
let _ = cmd.arg(command);
let _ = cmd.stdout(Stdio::piped());
let _ = cmd.stderr(Stdio::piped());
let mut child = cmd.spawn()?;
let stdout = child.stdout.take().ok_or(AuditCheckError::Stdout)?;
let stdout_handle = thread::spawn(move || handle_stdout(stdout, &tx_stdout));
let stderr = child.stderr.take().ok_or(AuditCheckError::Stderr)?;
let stderr_handle = thread::spawn(move || handle_stderr(stderr, &tx_stderr));
loop {
match child.try_wait() {
Ok(Some(exit_status)) => {
handle_status(exit_status, tx_code)?;
break;
}
Ok(None) => thread::sleep(Duration::from_millis(500)),
Err(e) => return Err(e.into()),
}
}
stdout_handle.join().map_err(handle_join_error)??;
stderr_handle.join().map_err(handle_join_error)??;
Ok(())
}
fn handle_stdout(stdout: ChildStdout, tx: &Sender<String>) -> Result<()> {
let stdout_reader = BufReader::new(stdout);
for line in stdout_reader.lines().map_while(Result::ok) {
tx.send(line)?;
}
Ok(())
}
fn handle_stderr(stderr: ChildStderr, tx: &Sender<String>) -> Result<()> {
let stderr_reader = BufReader::new(stderr);
for line in stderr_reader.lines().map_while(Result::ok) {
tx.send(line)?;
}
Ok(())
}
fn handle_status(exit_status: ExitStatus, tx: Sender<i32>) -> Result<()> {
let code = exit_status.code().ok_or(AuditCheckError::Code)?;
tx.send(code)?;
drop(tx);
Ok(())
}
#[cfg(test)]
mod test {
use super::{handle_status, handle_stderr, handle_stdout};
use std::{
process::{Command, Stdio},
sync::mpsc::channel,
};
#[test]
fn handle_stdout_works() -> anyhow::Result<()> {
let mut child = Command::new("echo")
.arg("hello")
.stdout(Stdio::piped())
.spawn()?;
let stdout = child.stdout.take().unwrap();
let (tx, rx) = channel();
handle_stdout(stdout, &tx)?;
drop(tx);
drop(child.wait());
assert_eq!(rx.recv()?, "hello");
Ok(())
}
#[test]
fn handle_stderr_works() -> anyhow::Result<()> {
let mut child = Command::new("sh")
.args(["-c", "echo err >&2"])
.stderr(Stdio::piped())
.spawn()?;
let stderr = child.stderr.take().unwrap();
let (tx, rx) = channel();
handle_stderr(stderr, &tx)?;
drop(tx);
drop(child.wait());
assert_eq!(rx.recv()?, "err");
Ok(())
}
#[test]
fn handle_status_success() -> anyhow::Result<()> {
let status = Command::new("true").status()?;
let (tx, rx) = channel();
handle_status(status, tx)?;
assert_eq!(rx.recv()?, 0);
Ok(())
}
#[test]
fn handle_status_failure() -> anyhow::Result<()> {
let status = Command::new("false").status()?;
let (tx, rx) = channel();
handle_status(status, tx)?;
assert_eq!(rx.recv()?, 1);
Ok(())
}
}