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    // Fail closed: if the program cannot be resolved to a safe absolute path
26    // (Windows CWD-planting RCE defense), error out rather than spawning a bare
27    // name that CreateProcess would resolve against the current directory.
28    let (resolved_program, resolved_args) = resolve_command_invocation(program, args)?;
29    let mut cmd = Command::new(&resolved_program);
30    cmd.args(resolved_args.iter().map(String::as_str));
31    if stdin.is_some() {
32        cmd.stdin(Stdio::piped());
33    }
34    cmd.stdout(Stdio::piped());
35    cmd.stderr(Stdio::piped());
36    cmd.spawn().map_err(|e| SubprocessError::new(format!("Failed to start {}: {}", program, e)))
37}
38
39fn write_stdin(
40    program: &str,
41    child: &mut Child,
42    stdin: Option<&[u8]>,
43) -> Result<(), SubprocessError> {
44    if let Some(input) = stdin
45        && let Some(mut child_stdin) = child.stdin.take()
46    {
47        child_stdin.write_all(input).map_err(|e| {
48            SubprocessError::new(format!("Failed to write to {} stdin: {}", program, e))
49        })?;
50    }
51    Ok(())
52}
53
54fn wait_for_child(
55    program: &str,
56    child: Child,
57    timeout_secs: Option<u64>,
58) -> Result<SubprocessOutput, SubprocessError> {
59    match timeout_secs {
60        None => wait_without_timeout(program, child),
61        Some(secs) => wait_with_timeout(program, child, secs),
62    }
63}
64
65fn wait_without_timeout(program: &str, child: Child) -> Result<SubprocessOutput, SubprocessError> {
66    let output = child
67        .wait_with_output()
68        .map_err(|e| SubprocessError::new(format!("Failed to wait for {}: {}", program, e)))?;
69    Ok(output.into())
70}
71
72fn wait_with_timeout(
73    program: &str,
74    mut child: Child,
75    timeout_secs: u64,
76) -> Result<SubprocessOutput, SubprocessError> {
77    let deadline = Instant::now() + Duration::from_secs(timeout_secs);
78    loop {
79        if child
80            .try_wait()
81            .map_err(|e| SubprocessError::new(format!("Failed to poll {}: {}", program, e)))?
82            .is_some()
83        {
84            let output = child.wait_with_output().map_err(|e| {
85                SubprocessError::new(format!("Failed to wait for {}: {}", program, e))
86            })?;
87            return Ok(output.into());
88        }
89        if Instant::now() >= deadline {
90            terminate_timed_out_child(program, &mut child, timeout_secs)?;
91            return Err(SubprocessError::new(format!(
92                "subprocess timed out after {} seconds",
93                timeout_secs
94            )));
95        }
96        std::thread::sleep(Duration::from_millis(50));
97    }
98}
99
100fn terminate_timed_out_child(
101    program: &str,
102    child: &mut Child,
103    timeout_secs: u64,
104) -> Result<(), SubprocessError> {
105    if let Err(kill_err) = child.kill() {
106        // Best effort: process may have already exited between `try_wait` and `kill`.
107        let already_exited = child
108            .try_wait()
109            .map_err(|e| SubprocessError::new(format!("Failed to poll {}: {}", program, e)))?
110            .is_some();
111        if !already_exited {
112            return Err(SubprocessError::new(format!(
113                "subprocess timed out after {} seconds and failed to terminate {}: {}",
114                timeout_secs, program, kill_err
115            )));
116        }
117    }
118    let _ = child.wait();
119    Ok(())
120}
121
122impl From<std::process::Output> for SubprocessOutput {
123    fn from(output: std::process::Output) -> Self {
124        Self {
125            stdout: output.stdout,
126            stderr: output.stderr,
127            status_code: output.status.code().unwrap_or(-1),
128        }
129    }
130}