use std::ffi::OsString;
use std::path::Path;
use std::process::Stdio;
use std::time::Duration;
use tokio::process::Command;
pub(crate) const TOOL_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug)]
pub(crate) enum ToolError {
NotFound {
tool: &'static str,
install_hint: &'static str,
},
ExitNonZero {
tool: &'static str,
code: Option<i32>,
stderr: String,
},
TimedOut {
tool: &'static str,
timeout: Duration,
},
NoOutput { tool: &'static str },
Io(std::io::Error),
}
impl std::fmt::Display for ToolError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ToolError::NotFound { tool, install_hint } => {
write!(f, "{tool} not found on PATH ({install_hint})")
}
ToolError::ExitNonZero { tool, code, stderr } => {
write!(f, "{tool} exited with code {code:?}: {stderr}")
}
ToolError::TimedOut { tool, timeout } => {
write!(f, "{tool} timed out after {timeout:?}")
}
ToolError::NoOutput { tool } => {
write!(f, "{tool} exited successfully but produced no output")
}
ToolError::Io(e) => write!(f, "failed to run external tool: {e}"),
}
}
}
impl std::error::Error for ToolError {}
pub(crate) async fn execute(
tool: &'static str,
install_hint: &'static str,
program: &str,
args: &[OsString],
output_path: &Path,
timeout: Duration,
) -> Result<(), ToolError> {
let spawned = Command::new(program)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn();
let child = match spawned {
Ok(child) => child,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(ToolError::NotFound { tool, install_hint });
}
Err(e) => return Err(ToolError::Io(e)),
};
let output = match tokio::time::timeout(timeout, child.wait_with_output()).await {
Ok(Ok(output)) => output,
Ok(Err(e)) => return Err(ToolError::Io(e)),
Err(_) => return Err(ToolError::TimedOut { tool, timeout }),
};
if !output.status.success() {
return Err(ToolError::ExitNonZero {
tool,
code: output.status.code(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
});
}
match tokio::fs::metadata(output_path).await {
Ok(meta) if meta.len() > 0 => Ok(()),
_ => Err(ToolError::NoOutput { tool }),
}
}
pub(crate) fn locate_on_path(binary_name: &str) -> bool {
let Some(path_var) = std::env::var_os("PATH") else {
return false;
};
std::env::split_paths(&path_var).any(|dir| is_executable_file(&dir.join(binary_name)))
}
#[cfg(unix)]
fn is_executable_file(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path)
.map(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(not(unix))]
fn is_executable_file(path: &Path) -> bool {
path.is_file()
}
#[cfg(test)]
#[path = "../tests/unit/tool.rs"]
mod tests;