pub mod c;
pub mod csharp;
pub mod go;
pub mod java;
pub mod kotlin;
pub mod php;
pub mod python;
pub mod ruby;
pub mod rust;
pub mod swift;
pub mod typescript;
pub use c::ClangtidyTool;
pub use csharp::RoslynTool;
pub use go::StaticcheckTool;
pub use java::PmdTool;
pub use kotlin::DetektTool;
pub use php::PhpstanTool;
pub use python::RuffTool;
pub use ruby::RubocopTool;
pub use rust::ClippyTool;
pub use swift::SwiftlintTool;
pub use typescript::BiomeTool;
use std::io::Read;
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::time::Duration;
const TOOL_TIMEOUT: Duration = Duration::from_secs(30);
const DEFAULT_BUILD_TIMEOUT_SECS: u64 = 300;
#[derive(Debug)]
pub struct CommandOutput {
pub stdout: String,
pub stderr: String,
pub status: Option<i32>,
}
pub fn build_tool_timeout() -> Duration {
let secs = std::env::var("TRUSTY_BUILD_TOOL_TIMEOUT_SECS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.filter(|&s| s > 0)
.unwrap_or(DEFAULT_BUILD_TIMEOUT_SECS);
Duration::from_secs(secs)
}
pub fn run_command_with_timeout(
program: &str,
args: &[&str],
cwd: &Path,
timeout: Duration,
) -> anyhow::Result<CommandOutput> {
let mut cmd = Command::new(program);
cmd.args(args)
.current_dir(cwd)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
unsafe {
cmd.pre_exec(|| {
let _ = libc::setsid();
Ok(())
});
}
}
let mut child = cmd
.spawn()
.map_err(|e| anyhow::anyhow!("failed to spawn {program}: {e}"))?;
let child_pid = child.id();
let mut stdout_pipe = child
.stdout
.take()
.ok_or_else(|| anyhow::anyhow!("no stdout pipe for {program}"))?;
let mut stderr_pipe = child
.stderr
.take()
.ok_or_else(|| anyhow::anyhow!("no stderr pipe for {program}"))?;
let out_handle = std::thread::spawn(move || {
let mut buf = String::new();
let _ = stdout_pipe.read_to_string(&mut buf);
buf
});
let err_handle = std::thread::spawn(move || {
let mut buf = String::new();
let _ = stderr_pipe.read_to_string(&mut buf);
buf
});
let (tx, rx) = mpsc::channel::<std::io::Result<std::process::ExitStatus>>();
let waiter = std::thread::spawn(move || {
let status = child.wait();
let _ = tx.send(status);
});
let status = match rx.recv_timeout(timeout) {
Ok(Ok(status)) => status.code(),
Ok(Err(e)) => {
return Err(anyhow::anyhow!("wait failed for {program}: {e}"));
}
Err(mpsc::RecvTimeoutError::Timeout) => {
kill_process_group(child_pid, program);
let _ = waiter.join();
let _ = out_handle.join();
let _ = err_handle.join();
return Err(anyhow::anyhow!(
"{program} exceeded {}s timeout",
timeout.as_secs()
));
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
return Err(anyhow::anyhow!("waiter thread for {program} disconnected"));
}
};
let _ = waiter.join();
let stdout = out_handle.join().unwrap_or_default();
let stderr = err_handle.join().unwrap_or_default();
Ok(CommandOutput {
stdout,
stderr,
status,
})
}
fn kill_process_group(child_pid: u32, program: &str) {
#[cfg(unix)]
{
let pid = child_pid as libc::pid_t;
let child_pgid = unsafe { libc::getpgid(pid) };
let parent_pgid = unsafe { libc::getpgid(0) };
if child_pgid > 0 && child_pgid != parent_pgid {
let rc = unsafe { libc::kill(-child_pgid, libc::SIGKILL) };
if rc != 0 {
let err = std::io::Error::last_os_error();
tracing::debug!(
"kill(-{child_pgid}, SIGKILL) failed for {program}: {err} \
(process may have already exited)"
);
}
} else {
let rc = unsafe { libc::kill(pid, libc::SIGKILL) };
if rc != 0 {
let err = std::io::Error::last_os_error();
tracing::debug!(
"kill({pid}, SIGKILL) failed for {program}: {err} \
(process may have already exited)"
);
}
}
}
#[cfg(not(unix))]
{
let _ = Command::new("taskkill")
.args(["/F", "/T", "/PID", &child_pid.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
tracing::debug!("taskkill /F /T /PID {child_pid} issued for {program}");
}
}
pub fn run_command(program: &str, args: &[&str], cwd: &Path) -> anyhow::Result<CommandOutput> {
run_command_with_timeout(program, args, cwd, TOOL_TIMEOUT)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn run_command_captures_echo() {
let dir = std::env::temp_dir();
let out = run_command("echo", &["hello"], &dir).expect("echo should run");
assert!(out.stdout.contains("hello"));
assert_eq!(out.status, Some(0));
}
#[test]
fn run_command_reports_missing_binary() {
let dir = std::env::temp_dir();
let res = run_command("trusty-no-such-binary-xyz", &[], &dir);
assert!(res.is_err());
}
#[test]
fn build_tool_timeout_default_is_300s() {
let t = build_tool_timeout();
assert!(
t.as_secs() >= 1,
"build_tool_timeout() should be at least 1 s, got {t:?}"
);
}
#[test]
#[cfg(unix)]
fn timeout_kills_child_process() {
use std::time::Instant;
let dir = std::env::temp_dir();
let pid_file = dir.join(format!(
"trusty_analyze_test_pid_{}.txt",
std::process::id()
));
let pid_file_str = pid_file.to_str().expect("tmpdir must be UTF-8");
let sh_cmd = format!("echo $$ > {pid_file_str}; exec sleep 30");
let result =
run_command_with_timeout("sh", &["-c", &sh_cmd], &dir, Duration::from_millis(100));
assert!(result.is_err(), "expected timeout error, got Ok");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("exceeded") || err_msg.contains("timeout"),
"error message should mention timeout: {err_msg}"
);
let child_pid: libc::pid_t = {
let deadline = Instant::now() + Duration::from_millis(500);
loop {
if let Ok(contents) = std::fs::read_to_string(&pid_file) {
if let Ok(p) = contents.trim().parse::<libc::pid_t>() {
break p;
}
}
if Instant::now() >= deadline {
let _ = std::fs::remove_file(&pid_file);
return;
}
std::thread::sleep(Duration::from_millis(10));
}
};
let _ = std::fs::remove_file(&pid_file);
let grace = Instant::now() + Duration::from_secs(1);
loop {
let rc = unsafe { libc::kill(child_pid, 0) };
if rc < 0 {
let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
if errno == libc::ESRCH {
return;
}
}
assert!(
Instant::now() < grace,
"child pid {child_pid} still alive 1 s after timeout — kill-on-timeout broken"
);
std::thread::sleep(Duration::from_millis(20));
}
}
#[test]
#[cfg(unix)]
fn timeout_returns_quickly_not_after_full_sleep() {
use std::time::Instant;
let dir = std::env::temp_dir();
let start = Instant::now();
let result = run_command_with_timeout("sleep", &["30"], &dir, Duration::from_millis(200));
let elapsed = start.elapsed();
assert!(result.is_err(), "expected timeout error");
assert!(
elapsed < Duration::from_secs(5),
"run_command_with_timeout took {elapsed:?} — kill-on-timeout likely broken"
);
}
}