harn_hostlib/process/
real.rs1use 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 WaitOutcome,
15};
16
17pub struct RealSpawner;
19
20static REAL_SPAWNER: LazyLock<Arc<dyn ProcessSpawner>> =
21 LazyLock::new(|| Arc::new(RealSpawner) as Arc<dyn ProcessSpawner>);
22
23pub fn default_spawner() -> Arc<dyn ProcessSpawner> {
25 Arc::clone(&REAL_SPAWNER)
26}
27
28impl ProcessSpawner for RealSpawner {
29 fn spawn(&self, spec: SpawnSpec) -> Result<Box<dyn ProcessHandle>, ProcessError> {
30 if spec.program.is_empty() {
31 return Err(ProcessError::InvalidArgv(
32 "first element of argv must be a non-empty program name".to_string(),
33 ));
34 }
35
36 let mut command = process_sandbox::std_command_for(&spec.program, &spec.args)
37 .map_err(|e| ProcessError::SandboxSetup(format!("{e:?}")))?;
38
39 if let Some(cwd) = spec.cwd.as_ref() {
40 process_sandbox::enforce_process_cwd(cwd)
41 .map_err(|e| ProcessError::SandboxCwd(format!("{e:?}")))?;
42 command.current_dir(cwd);
43 }
44
45 match spec.env_mode {
46 EnvMode::Replace => {
48 command.env_clear();
49 }
50 EnvMode::InheritClean | EnvMode::Patch => {
57 for (key, _) in std::env::vars_os() {
58 if let Some(name) = key.to_str() {
59 if super::handle::is_sensitive_env_name(name) {
60 command.env_remove(&key);
61 }
62 }
63 }
64 }
65 }
66 for (key, value) in &spec.env {
67 command.env(key, value);
68 }
69
70 for (key, value) in process_sandbox::active_workspace_tmpdir_env() {
80 if spec.env.contains_key(&key) {
81 continue;
82 }
83 command.env(key, value);
84 }
85
86 if !spec
94 .env
95 .contains_key(process_sandbox::MESSAGE_LOCALE_OVERRIDE_ENV)
96 {
97 command.env_remove(process_sandbox::MESSAGE_LOCALE_OVERRIDE_ENV);
98 }
99 for (key, value) in process_sandbox::deterministic_message_locale_env() {
100 if spec.env.contains_key(&key) {
101 continue;
102 }
103 command.env(key, value);
104 }
105
106 if spec.configure_process_group {
107 configure_background_process_group(&mut command);
108 }
109
110 command.stdout(Stdio::piped());
111 command.stderr(Stdio::piped());
112 command.stdin(if spec.use_stdin {
113 Stdio::piped()
114 } else {
115 Stdio::null()
116 });
117
118 let child = command.spawn().map_err(|e| {
119 if let Some(violation) = process_sandbox::process_spawn_error(&e) {
120 return ProcessError::SandboxSpawn(format!("{violation:?}"));
121 }
122 ProcessError::Spawn(format!("{e}"))
123 })?;
124
125 let pid = child.id();
126 let pgid = child_process_group_id(pid);
127 let killer: Arc<dyn ProcessKiller> = Arc::new(RealKiller { pid });
128
129 Ok(Box::new(RealProcess {
130 pid,
131 pgid,
132 killer,
133 child: Some(child),
134 stdin: None,
135 stdout: None,
136 stderr: None,
137 stdin_taken: false,
138 stdout_taken: false,
139 stderr_taken: false,
140 }))
141 }
142}
143
144struct RealProcess {
145 pid: u32,
146 pgid: Option<u32>,
147 killer: Arc<dyn ProcessKiller>,
148 child: Option<Child>,
149 stdin: Option<ChildStdin>,
150 stdout: Option<ChildStdout>,
151 stderr: Option<ChildStderr>,
152 stdin_taken: bool,
153 stdout_taken: bool,
154 stderr_taken: bool,
155}
156
157impl RealProcess {
158 fn ensure_pipes_taken(&mut self) {
159 if let Some(child) = self.child.as_mut() {
160 if self.stdin.is_none() && !self.stdin_taken {
161 self.stdin = child.stdin.take();
162 }
163 if self.stdout.is_none() && !self.stdout_taken {
164 self.stdout = child.stdout.take();
165 }
166 if self.stderr.is_none() && !self.stderr_taken {
167 self.stderr = child.stderr.take();
168 }
169 }
170 }
171}
172
173impl ProcessHandle for RealProcess {
174 fn pid(&self) -> Option<u32> {
175 Some(self.pid)
176 }
177
178 fn process_group_id(&self) -> Option<u32> {
179 self.pgid
180 }
181
182 fn killer(&self) -> Arc<dyn ProcessKiller> {
183 Arc::clone(&self.killer)
184 }
185
186 fn take_stdin(&mut self) -> Option<Box<dyn Write + Send>> {
187 self.ensure_pipes_taken();
188 self.stdin_taken = true;
189 self.stdin
190 .take()
191 .map(|s| Box::new(s) as Box<dyn Write + Send>)
192 }
193
194 fn take_stdout(&mut self) -> Option<Box<dyn Read + Send>> {
195 self.ensure_pipes_taken();
196 self.stdout_taken = true;
197 self.stdout
198 .take()
199 .map(|s| Box::new(s) as Box<dyn Read + Send>)
200 }
201
202 fn take_stderr(&mut self) -> Option<Box<dyn Read + Send>> {
203 self.ensure_pipes_taken();
204 self.stderr_taken = true;
205 self.stderr
206 .take()
207 .map(|s| Box::new(s) as Box<dyn Read + Send>)
208 }
209
210 fn wait_with_timeout(
211 &mut self,
212 timeout: Option<Duration>,
213 interrupt: &dyn Fn() -> bool,
214 ) -> io::Result<WaitOutcome> {
215 let killer = Arc::clone(&self.killer);
216 let Some(child) = self.child.as_mut() else {
217 return Err(io::Error::other("child already reaped"));
218 };
219 let deadline = timeout.map(|timeout| Instant::now() + timeout);
220 loop {
221 match child.try_wait()? {
222 Some(status) => return Ok(WaitOutcome::Exited(decode_status(status))),
223 None => {
224 if interrupt() {
225 harn_vm::op_interrupt::terminate_child_group(child);
229 return Ok(WaitOutcome::Interrupted);
230 }
231 if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
232 killer.kill();
240 let _ = child.kill();
241 let _ = child.wait();
242 return Ok(WaitOutcome::TimedOut);
243 }
244 let sleep = deadline
245 .map(|deadline| deadline.saturating_duration_since(Instant::now()))
246 .unwrap_or(Duration::MAX)
247 .min(Duration::from_millis(20));
248 thread::sleep(sleep);
249 }
250 }
251 }
252 }
253
254 fn wait(&mut self) -> io::Result<ExitStatus> {
255 let child = self
256 .child
257 .as_mut()
258 .ok_or_else(|| io::Error::other("child already reaped"))?;
259 let status = child.wait()?;
260 Ok(decode_status(status))
261 }
262}
263
264struct RealKiller {
265 pid: u32,
266}
267
268impl ProcessKiller for RealKiller {
269 fn kill(&self) {
270 kill_pid_or_group(self.pid);
271 }
272}
273
274#[cfg(unix)]
275fn decode_status(status: std::process::ExitStatus) -> ExitStatus {
276 use std::os::unix::process::ExitStatusExt;
277 if let Some(code) = status.code() {
278 ExitStatus::from_code(code)
279 } else if let Some(sig) = status.signal() {
280 ExitStatus::from_signal(sig)
281 } else {
282 ExitStatus {
283 code: None,
284 signal: None,
285 }
286 }
287}
288
289#[cfg(not(unix))]
290fn decode_status(status: std::process::ExitStatus) -> ExitStatus {
291 ExitStatus::from_code(status.code().unwrap_or(-1))
292}
293
294pub(crate) fn child_process_group_id(pid: u32) -> Option<u32> {
295 #[cfg(unix)]
296 {
297 extern "C" {
298 fn getpgid(pid: i32) -> i32;
299 }
300 let pgid = unsafe { getpgid(pid as i32) };
301 if pgid > 0 {
302 Some(pgid as u32)
303 } else {
304 None
305 }
306 }
307 #[cfg(not(unix))]
308 {
309 Some(pid)
310 }
311}
312
313pub(crate) fn configure_background_process_group(command: &mut std::process::Command) {
314 #[cfg(unix)]
315 unsafe {
316 use std::os::unix::process::CommandExt;
317 command.pre_exec(|| {
318 extern "C" {
319 fn setpgid(pid: i32, pgid: i32) -> i32;
320 }
321 if setpgid(0, 0) == -1 {
322 return Err(std::io::Error::last_os_error());
323 }
324 Ok(())
325 });
326 }
327 #[cfg(not(unix))]
328 {
329 let _ = command;
330 }
331}
332
333pub(crate) fn kill_pid_or_group(pid: u32) {
337 #[cfg(unix)]
338 {
339 extern "C" {
342 fn kill(pid: i32, sig: i32) -> i32;
343 }
344 unsafe {
345 kill(-(pid as i32), 9);
346 kill(pid as i32, 9);
347 }
348 }
349 #[cfg(not(unix))]
350 {
351 let _ = pid;
352 }
353}