use super::fs_perms::is_executable;
pub const KILL_GRACE_PERIOD: std::time::Duration = std::time::Duration::from_secs(2);
pub fn command_output_with_timeout(
cmd: &mut std::process::Command,
timeout: std::time::Duration,
) -> std::io::Result<std::process::Output> {
use std::sync::mpsc;
let child = cmd.spawn()?;
let id = child.id();
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
if rx.recv_timeout(timeout).is_err() {
terminate_process(id);
if rx.recv_timeout(KILL_GRACE_PERIOD).is_err() {
force_kill_process(id);
}
}
});
let result = child.wait_with_output();
let _ = tx.send(());
result
}
#[cfg(unix)]
pub fn terminate_process(pid: u32) {
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
let _ = kill(Pid::from_raw(pid as i32), Signal::SIGTERM);
}
#[cfg(windows)]
pub fn terminate_process(pid: u32) {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_TERMINATE, TerminateProcess};
unsafe {
let handle = OpenProcess(PROCESS_TERMINATE, 0, pid);
if !handle.is_null() {
TerminateProcess(handle, 1);
CloseHandle(handle);
}
}
}
#[cfg(unix)]
pub fn force_kill_process(pid: u32) {
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
let _ = kill(Pid::from_raw(pid as i32), Signal::SIGKILL);
}
#[cfg(windows)]
pub fn force_kill_process(pid: u32) {
terminate_process(pid);
}
#[cfg(unix)]
pub fn is_root() -> bool {
use nix::unistd::geteuid;
geteuid().is_root()
}
#[cfg(windows)]
pub fn is_root() -> bool {
use windows_sys::Win32::UI::Shell::IsUserAnAdmin;
unsafe { IsUserAnAdmin() != 0 }
}
pub fn hostname_string() -> String {
hostname::get()
.map(|h| h.to_string_lossy().to_string())
.unwrap_or_else(|_| "unknown".to_string())
}
pub fn stdout_lossy_trimmed(output: &std::process::Output) -> String {
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
pub fn stderr_lossy_trimmed(output: &std::process::Output) -> String {
String::from_utf8_lossy(&output.stderr).trim().to_string()
}
pub fn command_available(cmd: &str) -> bool {
let extensions: &[&str] = if cfg!(windows) {
&["", ".exe", ".cmd", ".bat", ".ps1", ".com"]
} else {
&[""]
};
std::env::var_os("PATH")
.map(|paths| {
std::env::split_paths(&paths).any(|dir| {
extensions.iter().any(|ext| {
let name = format!("{}{}", cmd, ext);
let path = dir.join(&name);
path.is_file()
&& std::fs::metadata(&path)
.map(|m| is_executable(&path, &m))
.unwrap_or(false)
})
})
})
.unwrap_or(false)
}
pub fn tracing_env_filter(default: &str) -> tracing_subscriber::EnvFilter {
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(default))
}
pub fn require_tool(name: &str, install_hint: Option<&str>) -> std::result::Result<(), String> {
if command_available(name) {
return Ok(());
}
Err(match install_hint {
Some(hint) => format!("{name} not found — {hint}"),
None => format!("{name} not found — install it or add it to PATH"),
})
}
pub fn tool_binary_name(env_var: &str, default: &str) -> String {
if env_var.is_empty() {
return default.to_string();
}
std::env::var(env_var).unwrap_or_else(|_| default.to_string())
}
pub fn tool_cmd(env_var: &str, default: &str) -> std::process::Command {
let mut cmd = std::process::Command::new(tool_binary_name(env_var, default));
cmd.stderr(std::process::Stdio::piped());
cmd
}
pub fn require_tool_with_seam(
env_var: &str,
default: &str,
install_hint: Option<&str>,
) -> std::result::Result<(), String> {
if let Ok(custom) = std::env::var(env_var) {
let p = std::path::Path::new(&custom);
if p.is_file() {
return Ok(());
}
return Err(format!("{env_var} points to {custom} which is not a file"));
}
require_tool(default, install_hint)
}
pub fn command_available_with_seam(env_var: &str, default: &str) -> bool {
if let Ok(custom) = std::env::var(env_var) {
return std::path::Path::new(&custom).is_file();
}
command_available(default)
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
fn hostname_string_returns_non_empty() {
let h = hostname_string();
assert!(!h.is_empty());
assert_ne!(h, "unknown");
}
#[test]
fn stdout_lossy_trimmed_trims_whitespace() {
let output = std::process::Output {
status: std::process::ExitStatus::default(),
stdout: b" hello world \n".to_vec(),
stderr: Vec::new(),
};
assert_eq!(stdout_lossy_trimmed(&output), "hello world");
}
#[test]
fn stderr_lossy_trimmed_trims_whitespace() {
let output = std::process::Output {
status: std::process::ExitStatus::default(),
stdout: Vec::new(),
stderr: b"\nerror message\n ".to_vec(),
};
assert_eq!(stderr_lossy_trimmed(&output), "error message");
}
#[test]
fn stdout_lossy_trimmed_handles_invalid_utf8() {
let output = std::process::Output {
status: std::process::ExitStatus::default(),
stdout: vec![0xFF, 0xFE, b'a', b'b'],
stderr: Vec::new(),
};
let result = stdout_lossy_trimmed(&output);
assert!(result.contains("ab"));
}
#[test]
fn command_available_finds_sh() {
assert!(command_available("sh"));
}
#[test]
fn command_available_rejects_nonexistent() {
assert!(!command_available("absolutely-not-a-real-command-xyz"));
}
#[test]
fn require_tool_succeeds_for_sh() {
assert!(require_tool("sh", None).is_ok());
}
#[test]
fn require_tool_fails_for_nonexistent() {
let err = require_tool("not-a-real-tool-xyz", None).unwrap_err();
assert!(err.contains("not-a-real-tool-xyz"));
assert!(err.contains("not found"));
}
#[test]
fn require_tool_includes_custom_hint() {
let err = require_tool("missing-tool", Some("install via cargo")).unwrap_err();
assert!(err.contains("install via cargo"));
}
#[test]
#[serial]
fn tool_binary_name_empty_env_var_returns_default() {
assert_eq!(tool_binary_name("", "cosign"), "cosign");
}
#[test]
#[serial]
fn tool_binary_name_reads_env_var() {
let _guard = crate::test_helpers::EnvVarGuard::set("CFGD_TEST_TOOL_BIN", "/custom/path");
assert_eq!(
tool_binary_name("CFGD_TEST_TOOL_BIN", "default"),
"/custom/path"
);
}
#[test]
#[serial]
fn tool_binary_name_unset_env_returns_default() {
let _guard = crate::test_helpers::EnvVarGuard::unset("CFGD_TEST_TOOL_BIN_UNSET");
assert_eq!(
tool_binary_name("CFGD_TEST_TOOL_BIN_UNSET", "fallback"),
"fallback"
);
}
#[test]
#[serial]
fn require_tool_with_seam_env_pointing_to_file_succeeds() {
let tmp = tempfile::TempDir::new().unwrap();
let bin = tmp.path().join("tool");
std::fs::write(&bin, "").unwrap();
let _guard =
crate::test_helpers::EnvVarGuard::set("CFGD_TEST_SEAM_BIN", bin.to_str().unwrap());
assert!(require_tool_with_seam("CFGD_TEST_SEAM_BIN", "tool", None).is_ok());
}
#[test]
#[serial]
fn require_tool_with_seam_env_pointing_to_missing_file_fails() {
let _guard = crate::test_helpers::EnvVarGuard::set("CFGD_TEST_SEAM_BAD", "/no/such/file");
let err = require_tool_with_seam("CFGD_TEST_SEAM_BAD", "tool", None).unwrap_err();
assert!(err.contains("CFGD_TEST_SEAM_BAD"));
assert!(err.contains("not a file"));
}
#[test]
#[serial]
fn require_tool_with_seam_no_env_falls_through() {
let _guard = crate::test_helpers::EnvVarGuard::unset("CFGD_TEST_SEAM_NONE");
assert!(require_tool_with_seam("CFGD_TEST_SEAM_NONE", "sh", None).is_ok());
}
#[test]
#[serial]
fn command_available_with_seam_env_file_exists() {
let tmp = tempfile::TempDir::new().unwrap();
let bin = tmp.path().join("tool");
std::fs::write(&bin, "").unwrap();
let _guard =
crate::test_helpers::EnvVarGuard::set("CFGD_TEST_AVAIL_SEAM", bin.to_str().unwrap());
assert!(command_available_with_seam(
"CFGD_TEST_AVAIL_SEAM",
"nonexistent"
));
}
#[test]
#[serial]
fn command_available_with_seam_env_file_missing() {
let _guard = crate::test_helpers::EnvVarGuard::set("CFGD_TEST_AVAIL_BAD", "/no/such/file");
assert!(!command_available_with_seam("CFGD_TEST_AVAIL_BAD", "sh"));
}
#[test]
#[serial]
fn command_available_with_seam_no_env_falls_through() {
let _guard = crate::test_helpers::EnvVarGuard::unset("CFGD_TEST_AVAIL_NONE");
assert!(command_available_with_seam("CFGD_TEST_AVAIL_NONE", "sh"));
}
#[test]
fn tool_cmd_creates_command_with_piped_stderr() {
let cmd = tool_cmd("", "echo");
let prog = std::path::Path::new(cmd.get_program())
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("");
assert_eq!(prog, "echo");
}
#[test]
fn command_output_with_timeout_succeeds() {
let mut cmd = std::process::Command::new("echo");
cmd.arg("hello").stdout(std::process::Stdio::piped());
let output =
command_output_with_timeout(&mut cmd, std::time::Duration::from_secs(5)).unwrap();
assert!(output.status.success());
assert!(stdout_lossy_trimmed(&output).contains("hello"));
}
#[test]
fn command_output_with_timeout_kills_on_exceed() {
let mut cmd = std::process::Command::new("sleep");
cmd.arg("60");
let result = command_output_with_timeout(&mut cmd, std::time::Duration::from_millis(100));
assert!(
result.is_ok(),
"process should be killed but still return output"
);
let output = result.unwrap();
assert!(!output.status.success());
}
#[cfg(unix)]
#[test]
fn force_kill_process_signals_sigkill() {
let mut child = std::process::Command::new("sh")
.arg("-c")
.arg("trap '' TERM; sleep 30")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.unwrap();
let pid = child.id();
force_kill_process(pid);
let status = child.wait().unwrap();
use std::os::unix::process::ExitStatusExt;
assert_eq!(
status.signal(),
Some(9),
"expected SIGKILL (9), got status: {status:?}"
);
}
#[test]
fn is_root_returns_bool() {
let _ = is_root();
}
#[test]
fn tracing_env_filter_uses_default_when_no_env() {
let filter = tracing_env_filter("warn");
let s = format!("{filter}");
assert!(s.contains("warn") || !s.is_empty());
}
}