use crate::error::RunnerError;
use std::process::Stdio;
use std::time::Duration;
use super::{CommandSpec, ProcessOutput, ProcessRunner};
#[derive(Debug, Clone, Copy, Default)]
pub struct NativeRunner;
impl NativeRunner {
#[must_use]
pub const fn new() -> Self {
Self
}
}
impl ProcessRunner for NativeRunner {
fn run(&self, cmd: &CommandSpec, timeout: Duration) -> Result<ProcessOutput, RunnerError> {
use std::sync::mpsc;
use std::thread;
let mut command = cmd.to_command();
command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let child = command
.spawn()
.map_err(|e| RunnerError::NativeExecutionFailed {
reason: format!(
"Failed to spawn process '{}': {}",
cmd.program.to_string_lossy(),
e
),
})?;
let (tx, rx) = mpsc::channel();
let child_id = child.id();
let handle = thread::spawn(move || {
let output = child.wait_with_output();
let _ = tx.send(output);
});
match rx.recv_timeout(timeout) {
Ok(output_result) => {
let _ = handle.join();
let output = output_result.map_err(|e| RunnerError::NativeExecutionFailed {
reason: format!("Failed to wait for process: {e}"),
})?;
Ok(ProcessOutput::new(
output.stdout,
output.stderr,
output.status.code(),
false,
))
}
Err(mpsc::RecvTimeoutError::Timeout) => {
Self::terminate_process(child_id);
let _ = handle.join();
Err(RunnerError::Timeout {
timeout_seconds: timeout.as_secs(),
})
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
Err(RunnerError::NativeExecutionFailed {
reason: "Process monitoring thread terminated unexpectedly".to_string(),
})
}
}
}
}
impl NativeRunner {
fn terminate_process(pid: u32) {
#[cfg(unix)]
{
unsafe {
libc::kill(pid as i32, libc::SIGKILL);
}
}
#[cfg(windows)]
{
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Threading::{
OpenProcess, PROCESS_TERMINATE, TerminateProcess,
};
unsafe {
if let Ok(handle) = OpenProcess(PROCESS_TERMINATE, false, pid) {
let _ = TerminateProcess(handle, 1);
let _ = CloseHandle(handle);
}
}
}
#[cfg(not(any(unix, windows)))]
{
let _ = pid;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_native_runner_new() {
let runner = NativeRunner::new();
assert!(std::mem::size_of_val(&runner) == 0);
}
#[test]
fn test_native_runner_default() {
let runner = NativeRunner;
assert!(std::mem::size_of_val(&runner) == 0);
}
#[test]
fn test_native_runner_clone() {
let runner = NativeRunner::new();
let cloned = runner;
assert!(std::mem::size_of_val(&runner) == 0);
assert!(std::mem::size_of_val(&cloned) == 0);
}
#[test]
fn test_native_runner_echo_command() {
let runner = NativeRunner::new();
#[cfg(windows)]
let cmd = CommandSpec::new("cmd")
.arg("/C")
.arg("echo")
.arg("hello world");
#[cfg(not(windows))]
let cmd = CommandSpec::new("echo").arg("hello world");
let result = runner.run(&cmd, Duration::from_secs(10));
assert!(result.is_ok(), "Echo command should succeed: {:?}", result);
let output = result.unwrap();
assert!(output.success(), "Echo should exit with code 0");
assert!(
output.stdout_string().contains("hello world"),
"Output should contain 'hello world', got: {}",
output.stdout_string()
);
}
#[test]
fn test_native_runner_shell_metacharacters_not_interpreted() {
let runner = NativeRunner::new();
#[cfg(windows)]
let cmd = CommandSpec::new("cmd").arg("/C").arg("echo").arg("$PATH");
#[cfg(not(windows))]
let cmd = CommandSpec::new("echo").arg("$PATH");
let result = runner.run(&cmd, Duration::from_secs(10));
assert!(result.is_ok(), "Command should succeed");
let output = result.unwrap();
assert!(
output.stdout_string().contains("$PATH") || output.stdout_string().contains("PATH"),
"Shell metacharacter should be preserved or echoed, got: {}",
output.stdout_string()
);
}
#[test]
fn test_native_runner_nonexistent_command() {
let runner = NativeRunner::new();
let cmd = CommandSpec::new("this_command_definitely_does_not_exist_12345");
let result = runner.run(&cmd, Duration::from_secs(10));
assert!(result.is_err(), "Nonexistent command should fail");
match result {
Err(RunnerError::NativeExecutionFailed { reason }) => {
assert!(
reason.contains("this_command_definitely_does_not_exist_12345"),
"Error should mention the command name: {}",
reason
);
}
_ => panic!("Expected NativeExecutionFailed error"),
}
}
#[test]
fn test_native_runner_exit_code_propagation() {
let runner = NativeRunner::new();
#[cfg(windows)]
let cmd = CommandSpec::new("cmd").arg("/C").arg("exit").arg("42");
#[cfg(not(windows))]
let cmd = CommandSpec::new("sh").arg("-c").arg("exit 42");
let result = runner.run(&cmd, Duration::from_secs(10));
assert!(
result.is_ok(),
"Command should complete (even with non-zero exit)"
);
let output = result.unwrap();
assert!(!output.success(), "Exit code 42 should not be success");
assert_eq!(output.exit_code, Some(42), "Exit code should be 42");
}
#[test]
fn test_native_runner_stderr_capture() {
let runner = NativeRunner::new();
#[cfg(windows)]
let cmd = CommandSpec::new("cmd")
.arg("/C")
.arg("echo error message 1>&2");
#[cfg(not(windows))]
let cmd = CommandSpec::new("sh")
.arg("-c")
.arg("echo 'error message' >&2");
let result = runner.run(&cmd, Duration::from_secs(10));
assert!(result.is_ok(), "Command should succeed");
let output = result.unwrap();
assert!(
output.stderr_string().contains("error message"),
"Stderr should contain 'error message', got: {}",
output.stderr_string()
);
}
#[test]
fn test_native_runner_implements_process_runner() {
fn assert_process_runner<T: ProcessRunner>(_: &T) {}
let runner = NativeRunner::new();
assert_process_runner(&runner);
}
}