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, ProcessCleanupReport, ProcessError, ProcessHandle, ProcessKiller,
14 ProcessSpawner, SpawnSpec, 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 in &spec.env_remove {
71 command.env_remove(key);
72 }
73 for (key, value) in &spec.env {
74 command.env(key, value);
75 }
76
77 for (key, value) in process_sandbox::active_workspace_tmpdir_env() {
87 if spec.env.contains_key(&key) {
88 continue;
89 }
90 command.env(key, value);
91 }
92
93 if !spec
101 .env
102 .contains_key(process_sandbox::MESSAGE_LOCALE_OVERRIDE_ENV)
103 {
104 command.env_remove(process_sandbox::MESSAGE_LOCALE_OVERRIDE_ENV);
105 }
106 for (key, value) in process_sandbox::deterministic_message_locale_env() {
107 if spec.env.contains_key(&key) {
108 continue;
109 }
110 command.env(key, value);
111 }
112
113 if spec.configure_process_group {
114 configure_background_process_group(&mut command);
115 }
116 let cleanup_token = harn_vm::op_interrupt::new_process_cleanup_token();
117 command.env(
118 harn_vm::op_interrupt::PROCESS_CLEANUP_TOKEN_ENV,
119 &cleanup_token,
120 );
121
122 command.stdout(Stdio::piped());
123 command.stderr(Stdio::piped());
124 command.stdin(if spec.use_stdin {
125 Stdio::piped()
126 } else {
127 Stdio::null()
128 });
129
130 let child = command.spawn().map_err(|e| {
131 if let Some(violation) = process_sandbox::process_spawn_error(&e) {
132 return ProcessError::SandboxSpawn(format!("{violation:?}"));
133 }
134 ProcessError::Spawn(format!("{e}"))
135 })?;
136
137 let pid = child.id();
138 let pgid = child_process_group_id(pid);
139 let killer: Arc<dyn ProcessKiller> = Arc::new(RealKiller {
140 pid,
141 cleanup_token: cleanup_token.clone(),
142 });
143
144 Ok(Box::new(RealProcess {
145 pid,
146 pgid,
147 cleanup_token,
148 killer,
149 child: Some(child),
150 stdin: None,
151 stdout: None,
152 stderr: None,
153 stdin_taken: false,
154 stdout_taken: false,
155 stderr_taken: false,
156 }))
157 }
158}
159
160struct RealProcess {
161 pid: u32,
162 pgid: Option<u32>,
163 cleanup_token: String,
164 killer: Arc<dyn ProcessKiller>,
165 child: Option<Child>,
166 stdin: Option<ChildStdin>,
167 stdout: Option<ChildStdout>,
168 stderr: Option<ChildStderr>,
169 stdin_taken: bool,
170 stdout_taken: bool,
171 stderr_taken: bool,
172}
173
174impl RealProcess {
175 fn ensure_pipes_taken(&mut self) {
176 if let Some(child) = self.child.as_mut() {
177 if self.stdin.is_none() && !self.stdin_taken {
178 self.stdin = child.stdin.take();
179 }
180 if self.stdout.is_none() && !self.stdout_taken {
181 self.stdout = child.stdout.take();
182 }
183 if self.stderr.is_none() && !self.stderr_taken {
184 self.stderr = child.stderr.take();
185 }
186 }
187 }
188}
189
190impl ProcessHandle for RealProcess {
191 fn pid(&self) -> Option<u32> {
192 Some(self.pid)
193 }
194
195 fn process_group_id(&self) -> Option<u32> {
196 self.pgid
197 }
198
199 fn killer(&self) -> Arc<dyn ProcessKiller> {
200 Arc::clone(&self.killer)
201 }
202
203 fn take_stdin(&mut self) -> Option<Box<dyn Write + Send>> {
204 self.ensure_pipes_taken();
205 self.stdin_taken = true;
206 self.stdin
207 .take()
208 .map(|s| Box::new(s) as Box<dyn Write + Send>)
209 }
210
211 fn take_stdout(&mut self) -> Option<Box<dyn Read + Send>> {
212 self.ensure_pipes_taken();
213 self.stdout_taken = true;
214 self.stdout
215 .take()
216 .map(|s| Box::new(s) as Box<dyn Read + Send>)
217 }
218
219 fn take_stderr(&mut self) -> Option<Box<dyn Read + Send>> {
220 self.ensure_pipes_taken();
221 self.stderr_taken = true;
222 self.stderr
223 .take()
224 .map(|s| Box::new(s) as Box<dyn Read + Send>)
225 }
226
227 fn wait_with_timeout(
228 &mut self,
229 timeout: Option<Duration>,
230 interrupt: &dyn Fn() -> bool,
231 ) -> io::Result<WaitOutcome> {
232 let killer = Arc::clone(&self.killer);
233 let Some(child) = self.child.as_mut() else {
234 return Err(io::Error::other("child already reaped"));
235 };
236 let deadline = timeout.map(|timeout| Instant::now() + timeout);
237 loop {
238 match child.try_wait()? {
239 Some(status) => return Ok(WaitOutcome::Exited(decode_status(status))),
240 None => {
241 if interrupt() {
242 let (_, report) =
246 harn_vm::op_interrupt::terminate_child_group_with_cleanup_token_report(
247 child,
248 Some(&self.cleanup_token),
249 );
250 return Ok(WaitOutcome::Interrupted(report));
251 }
252 if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
253 let mut report = killer.kill();
260 let _ = child.kill();
261 let _ = child.wait();
262 report.refresh_survivor_status();
263 return Ok(WaitOutcome::TimedOut(report));
264 }
265 let sleep = deadline
266 .map(|deadline| deadline.saturating_duration_since(Instant::now()))
267 .unwrap_or(Duration::MAX)
268 .min(Duration::from_millis(20));
269 thread::sleep(sleep);
270 }
271 }
272 }
273 }
274
275 fn wait(&mut self) -> io::Result<ExitStatus> {
276 let child = self
277 .child
278 .as_mut()
279 .ok_or_else(|| io::Error::other("child already reaped"))?;
280 let status = child.wait()?;
281 Ok(decode_status(status))
282 }
283}
284
285struct RealKiller {
286 pid: u32,
287 cleanup_token: String,
288}
289
290impl ProcessKiller for RealKiller {
291 fn kill(&self) -> ProcessCleanupReport {
292 harn_vm::op_interrupt::signal_pid_tree_group_and_token_with_report(
293 self.pid,
294 Some(&self.cleanup_token),
295 9,
296 )
297 }
298}
299
300#[cfg(unix)]
301fn decode_status(status: std::process::ExitStatus) -> ExitStatus {
302 use std::os::unix::process::ExitStatusExt;
303 if let Some(code) = status.code() {
304 ExitStatus::from_code(code)
305 } else if let Some(sig) = status.signal() {
306 ExitStatus::from_signal(sig)
307 } else {
308 ExitStatus {
309 code: None,
310 signal: None,
311 }
312 }
313}
314
315#[cfg(not(unix))]
316fn decode_status(status: std::process::ExitStatus) -> ExitStatus {
317 ExitStatus::from_code(status.code().unwrap_or(-1))
318}
319
320pub(crate) fn child_process_group_id(pid: u32) -> Option<u32> {
321 #[cfg(unix)]
322 {
323 extern "C" {
324 fn getpgid(pid: i32) -> i32;
325 }
326 let pgid = unsafe { getpgid(pid as i32) };
327 if pgid > 0 {
328 Some(pgid as u32)
329 } else {
330 None
331 }
332 }
333 #[cfg(not(unix))]
334 {
335 Some(pid)
336 }
337}
338
339pub(crate) fn configure_background_process_group(command: &mut std::process::Command) {
340 #[cfg(unix)]
341 unsafe {
342 use std::os::unix::process::CommandExt;
343 command.pre_exec(|| {
344 extern "C" {
345 fn setpgid(pid: i32, pgid: i32) -> i32;
346 }
347 if setpgid(0, 0) == -1 {
348 return Err(std::io::Error::last_os_error());
349 }
350 Ok(())
351 });
352 }
353 #[cfg(not(unix))]
354 {
355 let _ = command;
356 }
357}