use crate::error::{CliTestError, Result};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::Duration;
pub fn validate_binary_path(path: &Path) -> Result<PathBuf> {
if !path.exists() {
return Err(CliTestError::BinaryNotFound(path.to_path_buf()));
}
if !path.is_file() {
return Err(CliTestError::BinaryNotFound(path.to_path_buf()));
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let metadata = path.metadata()?;
let permissions = metadata.permissions();
if permissions.mode() & 0o111 == 0 {
return Err(CliTestError::BinaryNotExecutable(path.to_path_buf()));
}
}
let canonical = path.canonicalize()?;
Ok(canonical)
}
pub fn execute_with_timeout(binary: &Path, args: &[&str], timeout: Duration) -> Result<String> {
execute_with_timeout_and_limits(
binary,
args,
timeout,
Some(&crate::utils::ResourceLimits::default()),
)
}
pub fn execute_with_timeout_and_limits(
binary: &Path,
args: &[&str],
timeout: Duration,
limits: Option<&crate::utils::ResourceLimits>,
) -> Result<String> {
use std::io::Read;
log::debug!(
"Executing: {} {} (timeout: {:?})",
binary.display(),
args.join(" "),
timeout
);
let mut command = Command::new(binary);
command
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(unix)]
if let Some(resource_limits) = limits {
use std::os::unix::process::CommandExt;
let max_memory = resource_limits.max_memory_bytes;
let max_fds = resource_limits.max_file_descriptors;
let max_procs = resource_limits.max_processes;
unsafe {
command.pre_exec(move || {
use libc::{getrlimit, rlimit, setrlimit, RLIMIT_AS, RLIMIT_NOFILE, RLIMIT_NPROC};
let mut current_limit = rlimit {
rlim_cur: 0,
rlim_max: 0,
};
if getrlimit(RLIMIT_AS, &mut current_limit) == 0 {
if current_limit.rlim_max == libc::RLIM_INFINITY
|| current_limit.rlim_max > max_memory
{
let mem_limit = rlimit {
rlim_cur: max_memory,
rlim_max: max_memory,
};
let _ = setrlimit(RLIMIT_AS, &mem_limit);
}
}
if getrlimit(RLIMIT_NOFILE, &mut current_limit) == 0
&& (current_limit.rlim_max == libc::RLIM_INFINITY
|| current_limit.rlim_max > max_fds)
{
let fd_limit = rlimit {
rlim_cur: max_fds,
rlim_max: max_fds,
};
let _ = setrlimit(RLIMIT_NOFILE, &fd_limit);
}
if getrlimit(RLIMIT_NPROC, &mut current_limit) == 0
&& (current_limit.rlim_max == libc::RLIM_INFINITY
|| current_limit.rlim_max > max_procs)
{
let proc_limit = rlimit {
rlim_cur: max_procs,
rlim_max: max_procs,
};
let _ = setrlimit(RLIMIT_NPROC, &proc_limit);
}
Ok(())
});
}
}
let mut child = command.spawn()?;
#[cfg(windows)]
if let Some(resource_limits) = limits {
apply_windows_job_limits(&child, resource_limits)?;
}
let start = std::time::Instant::now();
loop {
match child.try_wait()? {
Some(_status) => {
let mut stdout = String::new();
if let Some(mut pipe) = child.stdout.take() {
pipe.read_to_string(&mut stdout)?;
}
let mut stderr = String::new();
if let Some(mut pipe) = child.stderr.take() {
pipe.read_to_string(&mut stderr)?;
}
let output = if !stdout.is_empty() { stdout } else { stderr };
log::debug!("Execution completed in {:?}", start.elapsed());
return Ok(output);
}
None => {
if start.elapsed() >= timeout {
log::warn!("Execution timeout exceeded, killing process");
child.kill()?;
child.wait()?;
return Err(CliTestError::ExecutionFailed(format!(
"Timeout after {:?}",
timeout
)));
}
std::thread::sleep(Duration::from_millis(50));
}
}
}
}
#[cfg(windows)]
fn apply_windows_job_limits(
child: &std::process::Child,
limits: &crate::utils::ResourceLimits,
) -> Result<()> {
use std::os::windows::process::CommandExt;
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
SetInformationJobObject, JOBOBJECT_BASIC_LIMIT_INFORMATION,
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_ACTIVE_PROCESS,
JOB_OBJECT_LIMIT_JOB_MEMORY, JOB_OBJECT_LIMIT_PROCESS_MEMORY,
};
unsafe {
let job = CreateJobObjectW(None, None).map_err(|e| {
CliTestError::ExecutionFailed(format!("Failed to create job object: {}", e))
})?;
let mut job_limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
BasicLimitInformation: JOBOBJECT_BASIC_LIMIT_INFORMATION {
LimitFlags: JOB_OBJECT_LIMIT_ACTIVE_PROCESS
| JOB_OBJECT_LIMIT_PROCESS_MEMORY
| JOB_OBJECT_LIMIT_JOB_MEMORY,
ActiveProcessLimit: limits.max_processes as u32,
..Default::default()
},
ProcessMemoryLimit: limits.max_memory_bytes as usize,
JobMemoryLimit: limits.max_memory_bytes as usize,
..Default::default()
};
SetInformationJobObject(
job,
JobObjectExtendedLimitInformation,
&mut job_limits as *mut _ as *mut _,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
.map_err(|e| {
CloseHandle(job);
CliTestError::ExecutionFailed(format!("Failed to set job limits: {}", e))
})?;
let child_handle = HANDLE(child.id() as isize);
AssignProcessToJobObject(job, child_handle).map_err(|e| {
CloseHandle(job);
CliTestError::ExecutionFailed(format!("Failed to assign process to job: {}", e))
})?;
log::debug!("Resource limits applied to child process via Job Object");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use tempfile::TempDir;
#[test]
fn test_validate_nonexistent_binary() {
let path = Path::new("/nonexistent/binary");
let result = validate_binary_path(path);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
CliTestError::BinaryNotFound(_)
));
}
#[test]
fn test_validate_directory() {
let temp_dir = TempDir::new().unwrap();
let result = validate_binary_path(temp_dir.path());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
CliTestError::BinaryNotFound(_)
));
}
#[cfg(unix)]
#[test]
fn test_validate_non_executable_file() {
use std::os::unix::fs::PermissionsExt;
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("non_executable");
File::create(&file_path).unwrap();
let mut perms = std::fs::metadata(&file_path).unwrap().permissions();
perms.set_mode(0o644); std::fs::set_permissions(&file_path, perms).unwrap();
let result = validate_binary_path(&file_path);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
CliTestError::BinaryNotExecutable(_)
));
}
#[test]
fn test_execute_with_timeout_echo() {
#[cfg(unix)]
{
let echo_path = Path::new("/bin/echo");
if echo_path.exists() {
let result =
execute_with_timeout(echo_path, &["hello", "world"], Duration::from_secs(5));
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("hello"));
}
}
}
#[test]
fn test_execute_with_timeout_sleep() {
#[cfg(unix)]
{
let sleep_path = Path::new("/bin/sleep");
if sleep_path.exists() {
let result = execute_with_timeout(
sleep_path,
&["10"], Duration::from_millis(500), );
assert!(result.is_err());
if let Err(CliTestError::ExecutionFailed(msg)) = result {
assert!(msg.contains("Timeout"));
}
}
}
}
#[test]
fn test_canonicalization() {
#[cfg(unix)]
{
let ls_path = Path::new("/bin/ls");
if ls_path.exists() {
let result = validate_binary_path(ls_path);
assert!(result.is_ok());
let canonical = result.unwrap();
assert!(canonical.is_absolute());
}
}
}
}