Skip to main content

perl_subprocess_runtime/os_runtime/
process.rs

1use super::resolve_command_invocation;
2use super::validation::validate_command_input;
3use crate::{SubprocessError, SubprocessOutput};
4use std::io::Write;
5use std::process::{Child, Command, Stdio};
6use std::time::{Duration, Instant};
7
8pub(super) fn run_os_command(
9    program: &str,
10    args: &[&str],
11    stdin: Option<&[u8]>,
12    timeout_secs: Option<u64>,
13) -> Result<SubprocessOutput, SubprocessError> {
14    validate_command_input(program, args)?;
15    let mut child = spawn_child(program, args, stdin)?;
16    write_stdin(program, &mut child, stdin)?;
17    wait_for_child(program, child, timeout_secs)
18}
19
20fn spawn_child(
21    program: &str,
22    args: &[&str],
23    stdin: Option<&[u8]>,
24) -> Result<Child, SubprocessError> {
25    let (resolved_program, resolved_args) = resolve_command_invocation(program, args);
26    let mut cmd = Command::new(&resolved_program);
27    cmd.args(resolved_args.iter().map(String::as_str));
28    if stdin.is_some() {
29        cmd.stdin(Stdio::piped());
30    }
31    cmd.stdout(Stdio::piped());
32    cmd.stderr(Stdio::piped());
33    cmd.spawn().map_err(|e| SubprocessError::new(format!("Failed to start {}: {}", program, e)))
34}
35
36fn write_stdin(
37    program: &str,
38    child: &mut Child,
39    stdin: Option<&[u8]>,
40) -> Result<(), SubprocessError> {
41    if let Some(input) = stdin
42        && let Some(mut child_stdin) = child.stdin.take()
43    {
44        child_stdin.write_all(input).map_err(|e| {
45            SubprocessError::new(format!("Failed to write to {} stdin: {}", program, e))
46        })?;
47    }
48    Ok(())
49}
50
51fn wait_for_child(
52    program: &str,
53    child: Child,
54    timeout_secs: Option<u64>,
55) -> Result<SubprocessOutput, SubprocessError> {
56    match timeout_secs {
57        None => wait_without_timeout(program, child),
58        Some(secs) => wait_with_timeout(program, child, secs),
59    }
60}
61
62fn wait_without_timeout(program: &str, child: Child) -> Result<SubprocessOutput, SubprocessError> {
63    let output = child
64        .wait_with_output()
65        .map_err(|e| SubprocessError::new(format!("Failed to wait for {}: {}", program, e)))?;
66    Ok(output.into())
67}
68
69fn wait_with_timeout(
70    program: &str,
71    mut child: Child,
72    timeout_secs: u64,
73) -> Result<SubprocessOutput, SubprocessError> {
74    let deadline = Instant::now() + Duration::from_secs(timeout_secs);
75    loop {
76        if child
77            .try_wait()
78            .map_err(|e| SubprocessError::new(format!("Failed to poll {}: {}", program, e)))?
79            .is_some()
80        {
81            let output = child.wait_with_output().map_err(|e| {
82                SubprocessError::new(format!("Failed to wait for {}: {}", program, e))
83            })?;
84            return Ok(output.into());
85        }
86        if Instant::now() >= deadline {
87            terminate_timed_out_child(program, &mut child, timeout_secs)?;
88            return Err(SubprocessError::new(format!(
89                "subprocess timed out after {} seconds",
90                timeout_secs
91            )));
92        }
93        std::thread::sleep(Duration::from_millis(50));
94    }
95}
96
97fn terminate_timed_out_child(
98    program: &str,
99    child: &mut Child,
100    timeout_secs: u64,
101) -> Result<(), SubprocessError> {
102    if let Err(kill_err) = child.kill() {
103        // Best effort: process may have already exited between `try_wait` and `kill`.
104        let already_exited = child
105            .try_wait()
106            .map_err(|e| SubprocessError::new(format!("Failed to poll {}: {}", program, e)))?
107            .is_some();
108        if !already_exited {
109            return Err(SubprocessError::new(format!(
110                "subprocess timed out after {} seconds and failed to terminate {}: {}",
111                timeout_secs, program, kill_err
112            )));
113        }
114    }
115    let _ = child.wait();
116    Ok(())
117}
118
119impl From<std::process::Output> for SubprocessOutput {
120    fn from(output: std::process::Output) -> Self {
121        Self {
122            stdout: output.stdout,
123            stderr: output.stderr,
124            status_code: output.status.code().unwrap_or(-1),
125        }
126    }
127}