use anyhow::{Context, Result};
use std::fs::File;
use std::io::{self, Read, Write};
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
use std::thread;
pub trait Runner {
fn run(&self, cmd: &str, logfile: &Path, verbose: bool) -> Result<(i32, String)>;
}
pub struct ShellRunner;
impl Runner for ShellRunner {
fn run(&self, cmd: &str, logfile: &Path, verbose: bool) -> Result<(i32, String)> {
run_command(cmd, logfile, verbose)
}
}
pub fn run_command(cmd: &str, logfile: &Path, verbose: bool) -> Result<(i32, String)> {
let file = File::create(logfile).with_context(|| format!("create logfile {:?}", logfile))?;
if verbose {
writeln!(&file, "Running: {}", cmd)?;
}
let mut child = Command::new("sh")
.arg("-c")
.arg(cmd)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.with_context(|| format!("spawn command: {}", cmd))?;
let shared_file = Arc::new(Mutex::new(file));
let mut handles = Vec::new();
let suppress_output = std::env::var("DEVTOOL_SUPPRESS_OUTPUT")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false);
if let Some(stdout_rd) = child.stdout.take() {
let f = Arc::clone(&shared_file);
let verbose_flag = verbose;
let suppress_flag = suppress_output;
let h = thread::spawn(move || {
let mut rd = stdout_rd;
let mut buf = [0u8; 4096];
loop {
match rd.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
if let Ok(mut fh) = f.lock() {
let _ = fh.write_all(&buf[..n]);
let _ = fh.flush();
}
if verbose_flag && !suppress_flag {
let _ = io::stdout().write_all(&buf[..n]);
let _ = io::stdout().flush();
}
}
Err(_) => break,
}
}
});
handles.push(h);
}
if let Some(stderr_rd) = child.stderr.take() {
let f = Arc::clone(&shared_file);
let verbose_flag = verbose;
let suppress_flag = suppress_output;
let h = thread::spawn(move || {
let mut rd = stderr_rd;
let mut buf = [0u8; 4096];
loop {
match rd.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
if let Ok(mut fh) = f.lock() {
let _ = fh.write_all(&buf[..n]);
let _ = fh.flush();
}
if verbose_flag && !suppress_flag {
let _ = io::stdout().write_all(&buf[..n]);
let _ = io::stdout().flush();
}
}
Err(_) => break,
}
}
});
handles.push(h);
}
let status = child.wait()?;
for h in handles {
let _ = h.join();
}
let rc = status.code().unwrap_or(1);
let mut short = String::new();
if let Ok(mut f2) = File::open(logfile) {
let mut s = String::new();
f2.read_to_string(&mut s).ok();
let lines: Vec<&str> = s.lines().rev().take(40).collect();
short = lines.into_iter().rev().collect::<Vec<&str>>().join("\n");
}
Ok((rc, short))
}
pub fn enable_output_suppression() {
std::env::set_var("DEVTOOL_SUPPRESS_OUTPUT", "1");
}
pub fn disable_output_suppression() {
std::env::remove_var("DEVTOOL_SUPPRESS_OUTPUT");
}
#[allow(dead_code)]
pub fn is_output_suppressed() -> bool {
std::env::var("DEVTOOL_SUPPRESS_OUTPUT")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_run_command_success() {
let tmp = tempdir().unwrap();
let logfile = tmp.path().join("test.log");
let result = run_command("echo 'test'", &logfile, false);
assert!(result.is_ok());
let (rc, output) = result.unwrap();
assert_eq!(rc, 0);
assert!(output.contains("test"));
}
#[test]
fn test_run_command_failure() {
let tmp = tempdir().unwrap();
let logfile = tmp.path().join("test.log");
let result = run_command("exit 1", &logfile, false);
assert!(result.is_ok());
let (rc, _) = result.unwrap();
assert_eq!(rc, 1);
}
#[test]
fn test_shell_runner() {
let runner = ShellRunner;
let tmp = tempdir().unwrap();
let logfile = tmp.path().join("test.log");
let result = runner.run("echo 'runner test'", &logfile, false);
assert!(result.is_ok());
}
}