xchecker_runner/
native.rs1use crate::error::RunnerError;
2use std::process::Stdio;
3use std::time::Duration;
4
5use super::{CommandSpec, ProcessOutput, ProcessRunner};
6
7#[derive(Debug, Clone, Copy, Default)]
46pub struct NativeRunner;
47
48impl NativeRunner {
49 #[must_use]
59 pub const fn new() -> Self {
60 Self
61 }
62}
63
64impl ProcessRunner for NativeRunner {
65 fn run(&self, cmd: &CommandSpec, timeout: Duration) -> Result<ProcessOutput, RunnerError> {
89 use std::sync::mpsc;
90 use std::thread;
91
92 let mut command = cmd.to_command();
94 command
95 .stdin(Stdio::null())
96 .stdout(Stdio::piped())
97 .stderr(Stdio::piped());
98
99 let child = command
101 .spawn()
102 .map_err(|e| RunnerError::NativeExecutionFailed {
103 reason: format!(
104 "Failed to spawn process '{}': {}",
105 cmd.program.to_string_lossy(),
106 e
107 ),
108 })?;
109
110 let (tx, rx) = mpsc::channel();
112
113 let child_id = child.id();
115
116 let handle = thread::spawn(move || {
118 let output = child.wait_with_output();
119 let _ = tx.send(output);
120 });
121
122 match rx.recv_timeout(timeout) {
124 Ok(output_result) => {
125 let _ = handle.join();
127
128 let output = output_result.map_err(|e| RunnerError::NativeExecutionFailed {
129 reason: format!("Failed to wait for process: {e}"),
130 })?;
131
132 Ok(ProcessOutput::new(
133 output.stdout,
134 output.stderr,
135 output.status.code(),
136 false,
137 ))
138 }
139 Err(mpsc::RecvTimeoutError::Timeout) => {
140 Self::terminate_process(child_id);
142
143 let _ = handle.join();
145
146 Err(RunnerError::Timeout {
147 timeout_seconds: timeout.as_secs(),
148 })
149 }
150 Err(mpsc::RecvTimeoutError::Disconnected) => {
151 Err(RunnerError::NativeExecutionFailed {
153 reason: "Process monitoring thread terminated unexpectedly".to_string(),
154 })
155 }
156 }
157 }
158}
159
160impl NativeRunner {
161 fn terminate_process(pid: u32) {
166 #[cfg(unix)]
167 {
168 unsafe {
170 libc::kill(pid as i32, libc::SIGKILL);
171 }
172 }
173
174 #[cfg(windows)]
175 {
176 use windows::Win32::Foundation::CloseHandle;
177 use windows::Win32::System::Threading::{
178 OpenProcess, PROCESS_TERMINATE, TerminateProcess,
179 };
180
181 unsafe {
182 if let Ok(handle) = OpenProcess(PROCESS_TERMINATE, false, pid) {
183 let _ = TerminateProcess(handle, 1);
184 let _ = CloseHandle(handle);
185 }
186 }
187 }
188
189 #[cfg(not(any(unix, windows)))]
190 {
191 let _ = pid;
193 }
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
206 fn test_native_runner_new() {
207 let runner = NativeRunner::new();
208 assert!(std::mem::size_of_val(&runner) == 0);
210 }
211
212 #[test]
213 fn test_native_runner_default() {
214 let runner = NativeRunner;
215 assert!(std::mem::size_of_val(&runner) == 0);
216 }
217
218 #[test]
219 fn test_native_runner_clone() {
220 let runner = NativeRunner::new();
221 let cloned = runner;
222 assert!(std::mem::size_of_val(&runner) == 0);
224 assert!(std::mem::size_of_val(&cloned) == 0);
225 }
226
227 #[test]
228 fn test_native_runner_echo_command() {
229 let runner = NativeRunner::new();
232
233 #[cfg(windows)]
234 let cmd = CommandSpec::new("cmd")
235 .arg("/C")
236 .arg("echo")
237 .arg("hello world");
238
239 #[cfg(not(windows))]
240 let cmd = CommandSpec::new("echo").arg("hello world");
241
242 let result = runner.run(&cmd, Duration::from_secs(10));
243
244 assert!(result.is_ok(), "Echo command should succeed: {:?}", result);
245 let output = result.unwrap();
246 assert!(output.success(), "Echo should exit with code 0");
247 assert!(
248 output.stdout_string().contains("hello world"),
249 "Output should contain 'hello world', got: {}",
250 output.stdout_string()
251 );
252 }
253
254 #[test]
255 fn test_native_runner_shell_metacharacters_not_interpreted() {
256 let runner = NativeRunner::new();
259
260 #[cfg(windows)]
262 let cmd = CommandSpec::new("cmd").arg("/C").arg("echo").arg("$PATH");
263
264 #[cfg(not(windows))]
265 let cmd = CommandSpec::new("echo").arg("$PATH");
266
267 let result = runner.run(&cmd, Duration::from_secs(10));
268
269 assert!(result.is_ok(), "Command should succeed");
270 let output = result.unwrap();
271 assert!(
275 output.stdout_string().contains("$PATH") || output.stdout_string().contains("PATH"),
276 "Shell metacharacter should be preserved or echoed, got: {}",
277 output.stdout_string()
278 );
279 }
280
281 #[test]
282 fn test_native_runner_nonexistent_command() {
283 let runner = NativeRunner::new();
285 let cmd = CommandSpec::new("this_command_definitely_does_not_exist_12345");
286
287 let result = runner.run(&cmd, Duration::from_secs(10));
288
289 assert!(result.is_err(), "Nonexistent command should fail");
290 match result {
291 Err(RunnerError::NativeExecutionFailed { reason }) => {
292 assert!(
293 reason.contains("this_command_definitely_does_not_exist_12345"),
294 "Error should mention the command name: {}",
295 reason
296 );
297 }
298 _ => panic!("Expected NativeExecutionFailed error"),
299 }
300 }
301
302 #[test]
303 fn test_native_runner_exit_code_propagation() {
304 let runner = NativeRunner::new();
306
307 #[cfg(windows)]
308 let cmd = CommandSpec::new("cmd").arg("/C").arg("exit").arg("42");
309
310 #[cfg(not(windows))]
311 let cmd = CommandSpec::new("sh").arg("-c").arg("exit 42");
312
313 let result = runner.run(&cmd, Duration::from_secs(10));
314
315 assert!(
316 result.is_ok(),
317 "Command should complete (even with non-zero exit)"
318 );
319 let output = result.unwrap();
320 assert!(!output.success(), "Exit code 42 should not be success");
321 assert_eq!(output.exit_code, Some(42), "Exit code should be 42");
322 }
323
324 #[test]
325 fn test_native_runner_stderr_capture() {
326 let runner = NativeRunner::new();
328
329 #[cfg(windows)]
330 let cmd = CommandSpec::new("cmd")
331 .arg("/C")
332 .arg("echo error message 1>&2");
333
334 #[cfg(not(windows))]
335 let cmd = CommandSpec::new("sh")
336 .arg("-c")
337 .arg("echo 'error message' >&2");
338
339 let result = runner.run(&cmd, Duration::from_secs(10));
340
341 assert!(result.is_ok(), "Command should succeed");
342 let output = result.unwrap();
343 assert!(
344 output.stderr_string().contains("error message"),
345 "Stderr should contain 'error message', got: {}",
346 output.stderr_string()
347 );
348 }
349
350 #[test]
351 fn test_native_runner_implements_process_runner() {
352 fn assert_process_runner<T: ProcessRunner>(_: &T) {}
354
355 let runner = NativeRunner::new();
356 assert_process_runner(&runner);
357 }
358}