Skip to main content

harn_hostlib/process/
real.rs

1//! Production [`ProcessSpawner`] implementation backed by
2//! `std::process::Command` + `harn_vm::process_sandbox`.
3
4use std::io::{self, Read, Write};
5use std::process::{Child, ChildStderr, ChildStdin, ChildStdout, Stdio};
6use std::sync::{Arc, LazyLock};
7use std::thread;
8use std::time::{Duration, Instant};
9
10use harn_vm::process_sandbox;
11
12use super::handle::{
13    EnvMode, ExitStatus, ProcessError, ProcessHandle, ProcessKiller, ProcessSpawner, SpawnSpec,
14};
15
16/// Spawner that produces real OS processes via `std::process::Command`.
17pub struct RealSpawner;
18
19static REAL_SPAWNER: LazyLock<Arc<dyn ProcessSpawner>> =
20    LazyLock::new(|| Arc::new(RealSpawner) as Arc<dyn ProcessSpawner>);
21
22/// Returns the singleton real spawner used as the default.
23pub fn default_spawner() -> Arc<dyn ProcessSpawner> {
24    Arc::clone(&REAL_SPAWNER)
25}
26
27impl ProcessSpawner for RealSpawner {
28    fn spawn(&self, spec: SpawnSpec) -> Result<Box<dyn ProcessHandle>, ProcessError> {
29        if spec.program.is_empty() {
30            return Err(ProcessError::InvalidArgv(
31                "first element of argv must be a non-empty program name".to_string(),
32            ));
33        }
34
35        let mut command = process_sandbox::std_command_for(&spec.program, &spec.args)
36            .map_err(|e| ProcessError::SandboxSetup(format!("{e:?}")))?;
37
38        if let Some(cwd) = spec.cwd.as_ref() {
39            process_sandbox::enforce_process_cwd(cwd)
40                .map_err(|e| ProcessError::SandboxCwd(format!("{e:?}")))?;
41            command.current_dir(cwd);
42        }
43
44        if matches!(spec.env_mode, EnvMode::Replace) {
45            command.env_clear();
46        }
47        for (key, value) in &spec.env {
48            command.env(key, value);
49        }
50
51        if spec.configure_process_group {
52            configure_background_process_group(&mut command);
53        }
54
55        command.stdout(Stdio::piped());
56        command.stderr(Stdio::piped());
57        command.stdin(if spec.use_stdin {
58            Stdio::piped()
59        } else {
60            Stdio::null()
61        });
62
63        let child = command.spawn().map_err(|e| {
64            if let Some(violation) = process_sandbox::process_spawn_error(&e) {
65                return ProcessError::SandboxSpawn(format!("{violation:?}"));
66            }
67            ProcessError::Spawn(format!("{e}"))
68        })?;
69
70        let pid = child.id();
71        let pgid = child_process_group_id(pid);
72        let killer: Arc<dyn ProcessKiller> = Arc::new(RealKiller { pid });
73
74        Ok(Box::new(RealProcess {
75            pid,
76            pgid,
77            killer,
78            child: Some(child),
79            stdin: None,
80            stdout: None,
81            stderr: None,
82            stdin_taken: false,
83            stdout_taken: false,
84            stderr_taken: false,
85        }))
86    }
87}
88
89struct RealProcess {
90    pid: u32,
91    pgid: Option<u32>,
92    killer: Arc<dyn ProcessKiller>,
93    child: Option<Child>,
94    stdin: Option<ChildStdin>,
95    stdout: Option<ChildStdout>,
96    stderr: Option<ChildStderr>,
97    stdin_taken: bool,
98    stdout_taken: bool,
99    stderr_taken: bool,
100}
101
102impl RealProcess {
103    fn ensure_pipes_taken(&mut self) {
104        if let Some(child) = self.child.as_mut() {
105            if self.stdin.is_none() && !self.stdin_taken {
106                self.stdin = child.stdin.take();
107            }
108            if self.stdout.is_none() && !self.stdout_taken {
109                self.stdout = child.stdout.take();
110            }
111            if self.stderr.is_none() && !self.stderr_taken {
112                self.stderr = child.stderr.take();
113            }
114        }
115    }
116}
117
118impl ProcessHandle for RealProcess {
119    fn pid(&self) -> Option<u32> {
120        Some(self.pid)
121    }
122
123    fn process_group_id(&self) -> Option<u32> {
124        self.pgid
125    }
126
127    fn killer(&self) -> Arc<dyn ProcessKiller> {
128        Arc::clone(&self.killer)
129    }
130
131    fn take_stdin(&mut self) -> Option<Box<dyn Write + Send>> {
132        self.ensure_pipes_taken();
133        self.stdin_taken = true;
134        self.stdin
135            .take()
136            .map(|s| Box::new(s) as Box<dyn Write + Send>)
137    }
138
139    fn take_stdout(&mut self) -> Option<Box<dyn Read + Send>> {
140        self.ensure_pipes_taken();
141        self.stdout_taken = true;
142        self.stdout
143            .take()
144            .map(|s| Box::new(s) as Box<dyn Read + Send>)
145    }
146
147    fn take_stderr(&mut self) -> Option<Box<dyn Read + Send>> {
148        self.ensure_pipes_taken();
149        self.stderr_taken = true;
150        self.stderr
151            .take()
152            .map(|s| Box::new(s) as Box<dyn Read + Send>)
153    }
154
155    fn wait_with_timeout(
156        &mut self,
157        timeout: Option<Duration>,
158    ) -> io::Result<(Option<ExitStatus>, bool)> {
159        let Some(child) = self.child.as_mut() else {
160            return Ok((None, false));
161        };
162        let Some(timeout) = timeout else {
163            let status = child.wait()?;
164            return Ok((Some(decode_status(status)), false));
165        };
166        let start = Instant::now();
167        loop {
168            match child.try_wait()? {
169                Some(status) => return Ok((Some(decode_status(status)), false)),
170                None => {
171                    let elapsed = start.elapsed();
172                    if elapsed >= timeout {
173                        // `killer.kill()` kills the whole process group on Unix
174                        // (negative pid) to reap grandchildren. That path is a
175                        // no-op on non-Unix targets, where `kill_pid_or_group`
176                        // cannot signal by bare pid — so also kill the child
177                        // handle directly (TerminateProcess on Windows) to
178                        // guarantee the subsequent `child.wait()` cannot block
179                        // forever on a timed-out process.
180                        self.killer.kill();
181                        let _ = child.kill();
182                        let _ = child.wait();
183                        return Ok((None, true));
184                    }
185                    let remaining = timeout.checked_sub(elapsed).unwrap_or_default();
186                    thread::sleep(remaining.min(Duration::from_millis(20)));
187                }
188            }
189        }
190    }
191
192    fn wait(&mut self) -> io::Result<ExitStatus> {
193        let child = self
194            .child
195            .as_mut()
196            .ok_or_else(|| io::Error::other("child already reaped"))?;
197        let status = child.wait()?;
198        Ok(decode_status(status))
199    }
200}
201
202struct RealKiller {
203    pid: u32,
204}
205
206impl ProcessKiller for RealKiller {
207    fn kill(&self) {
208        kill_pid_or_group(self.pid);
209    }
210}
211
212#[cfg(unix)]
213fn decode_status(status: std::process::ExitStatus) -> ExitStatus {
214    use std::os::unix::process::ExitStatusExt;
215    if let Some(code) = status.code() {
216        ExitStatus::from_code(code)
217    } else if let Some(sig) = status.signal() {
218        ExitStatus::from_signal(sig)
219    } else {
220        ExitStatus {
221            code: None,
222            signal: None,
223        }
224    }
225}
226
227#[cfg(not(unix))]
228fn decode_status(status: std::process::ExitStatus) -> ExitStatus {
229    ExitStatus::from_code(status.code().unwrap_or(-1))
230}
231
232pub(crate) fn child_process_group_id(pid: u32) -> Option<u32> {
233    #[cfg(unix)]
234    {
235        extern "C" {
236            fn getpgid(pid: i32) -> i32;
237        }
238        let pgid = unsafe { getpgid(pid as i32) };
239        if pgid > 0 {
240            Some(pgid as u32)
241        } else {
242            None
243        }
244    }
245    #[cfg(not(unix))]
246    {
247        Some(pid)
248    }
249}
250
251pub(crate) fn configure_background_process_group(command: &mut std::process::Command) {
252    #[cfg(unix)]
253    unsafe {
254        use std::os::unix::process::CommandExt;
255        command.pre_exec(|| {
256            extern "C" {
257                fn setpgid(pid: i32, pgid: i32) -> i32;
258            }
259            if setpgid(0, 0) == -1 {
260                return Err(std::io::Error::last_os_error());
261            }
262            Ok(())
263        });
264    }
265    #[cfg(not(unix))]
266    {
267        let _ = command;
268    }
269}
270
271/// Send SIGKILL to a pid (and its process group). Public so existing
272/// non-trait paths (e.g. session-end cleanup) can keep using it during
273/// the transition.
274pub(crate) fn kill_pid_or_group(pid: u32) {
275    #[cfg(unix)]
276    {
277        // SAFETY: kill(2) takes a pid_t (i32 on all Unix targets) and a
278        // signal number. Calling it with SIGKILL (9) is well-defined.
279        extern "C" {
280            fn kill(pid: i32, sig: i32) -> i32;
281        }
282        unsafe {
283            kill(-(pid as i32), 9);
284            kill(pid as i32, 9);
285        }
286    }
287    #[cfg(not(unix))]
288    {
289        let _ = pid;
290    }
291}