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 && buf.len() + n <= cap {
30                    buf.extend_from_slice(&tmp[..n]);
31                } else {
32                    // stop storing, keep draining so the child never blocks on a full pipe
33                    truncated = true;
34                }
35            }
36            Err(_) => break,
37        }
38    }
39    (Bytes::from(buf), truncated)
40}
41
42/// Spawn the child in its own process group (Unix) with given env/cwd/args.
43/// `kill_on_drop(true)` is a belt-and-suspenders guard so an accidentally-dropped
44/// Child never leaks a process (C-1 defense in depth).
45pub fn spawn(
46    exe: &std::path::Path,
47    args: &[String],
48    env: &std::collections::HashMap<String, String>,
49    cwd: &std::path::Path,
50) -> std::io::Result<tokio::process::Child> {
51    let mut cmd = Command::new(exe);
52    cmd.args(args)
53        .current_dir(cwd)
54        .env_clear()
55        .envs(env.iter())
56        .kill_on_drop(true)
57        .stdin(Stdio::piped())
58        .stdout(Stdio::piped())
59        .stderr(Stdio::piped());
60    #[cfg(unix)]
61    {
62        // process_group(0) creates a new process group for the child.
63        // This is a standard POSIX operation. The resulting Child id is
64        // always non-zero after a successful spawn, so kill_tree can safely
65        // use -pgid later.
66        cmd.process_group(0);
67    }
68    cmd.spawn()
69}
70
71/// Kill the whole process group (Unix). Best-effort on Windows (post-v1: job object).
72/// M-5: guard against pid 0/None (would signal the caller's own group if the child
73/// already exited and `id()` returned None).
74pub fn kill_tree(child: &tokio::process::Child) {
75    let Some(pid) = child.id() else { return };
76    #[cfg(unix)]
77    {
78        // SAFETY: sending SIGKILL to a negative pid signals the whole process group.
79        // This is a kernel syscall with no in-process memory-safety implications.
80        // pid is guaranteed non-zero (guarded above) so we cannot accidentally kill
81        // our own process group. The return value is intentionally ignored (best-effort
82        // kill — the child may already have exited).
83        unsafe {
84            libc::kill(-(pid as i32), libc::SIGKILL);
85        }
86    }
87    #[cfg(not(unix))]
88    {
89        let _ = child.start_kill();
90    }
91}