Skip to main content

camel_component_exec/
process.rs

1//! process.rs — OS layer: spawn, capped IO drain, process-tree kill.
2
3use bytes::Bytes;
4use std::process::Stdio;
5use tokio::io::AsyncReadExt;
6use tokio::process::Command;
7
8/// Result of a successful (possibly timed-out) execution.
9pub struct RawResult {
10    pub stdout: Bytes,
11    pub stderr: Bytes,
12    pub stdout_truncated: bool,
13    pub stderr_truncated: bool,
14    pub exit_code: Option<i32>,
15    pub timed_out: bool,
16}
17
18/// Drain a reader into Bytes, stopping storage (but continuing to drain) once
19/// `cap` is reached. Sets truncated=true if the stream exceeded the cap.
20pub async fn drain_with_cap<R: tokio::io::AsyncRead + Unpin>(r: R, cap: usize) -> (Bytes, bool) {
21    let mut buf = Vec::with_capacity(cap.min(8192));
22    let mut reader = r;
23    let mut truncated = false;
24    let mut tmp = [0u8; 8192];
25    loop {
26        match reader.read(&mut tmp).await {
27            Ok(0) => break,
28            Ok(n) => {
29                if !truncated {
30                    let space = cap - buf.len();
31                    if n <= space {
32                        buf.extend_from_slice(&tmp[..n]);
33                    } else {
34                        // Store the prefix that fits, then flip truncated.
35                        // Keep draining so the child never blocks on a full pipe.
36                        buf.extend_from_slice(&tmp[..space]);
37                        truncated = true;
38                    }
39                }
40            }
41            Err(e) => {
42                tracing::debug!(error = %e, "drain read interrupted");
43                break;
44            }
45        }
46    }
47    (Bytes::from(buf), truncated)
48}
49
50/// Spawn the child in its own process group (Unix) with given env/cwd/args.
51/// `kill_on_drop(true)` is a belt-and-suspenders guard so an accidentally-dropped
52/// Child never leaks a process (C-1 defense in depth).
53pub fn spawn(
54    exe: &std::path::Path,
55    args: &[String],
56    env: &std::collections::HashMap<String, String>,
57    cwd: &std::path::Path,
58) -> std::io::Result<tokio::process::Child> {
59    let mut cmd = Command::new(exe);
60    cmd.args(args)
61        .current_dir(cwd)
62        .env_clear()
63        .envs(env.iter())
64        .kill_on_drop(true)
65        .stdin(Stdio::piped())
66        .stdout(Stdio::piped())
67        .stderr(Stdio::piped());
68    #[cfg(unix)]
69    {
70        // process_group(0) creates a new process group for the child.
71        // This is a standard POSIX operation. The resulting Child id is
72        // always non-zero after a successful spawn, so kill_tree can safely
73        // use -pgid later.
74        cmd.process_group(0);
75    }
76    cmd.spawn()
77}
78
79/// Kill the whole process group (Unix). Best-effort on Windows (post-v1: job object).
80/// M-5: guard against pid 0/None (would signal the caller's own group if the child
81/// already exited and `id()` returned None).
82pub fn kill_tree(child: &mut tokio::process::Child) {
83    let Some(pid) = child.id() else { return };
84    #[cfg(unix)]
85    {
86        // SAFETY: sending SIGKILL to a negative pid signals the whole process group.
87        // This is a kernel syscall with no in-process memory-safety implications.
88        // pid is guaranteed non-zero (guarded above) so we cannot accidentally kill
89        // our own process group. The return value is intentionally ignored (best-effort
90        // kill — the child may already have exited).
91        unsafe {
92            libc::kill(-(pid as i32), libc::SIGKILL);
93        }
94    }
95    #[cfg(not(unix))]
96    {
97        let _ = child.start_kill();
98    }
99}