harn_hostlib/process/
real.rs1use std::fs::OpenOptions;
5use std::io::{self, Read, Write};
6use std::process::{Child, ChildStderr, ChildStdin, ChildStdout, Stdio};
7use std::sync::{Arc, LazyLock};
8use std::thread;
9use std::time::{Duration, Instant};
10
11use harn_vm::process_sandbox;
12
13use super::handle::{
14 EnvMode, ExitStatus, OutputCapture, ProcessCleanupReport, ProcessError, ProcessHandle,
15 ProcessKiller, ProcessSpawner, SpawnSpec, WaitOutcome,
16};
17
18pub struct RealSpawner;
20
21static REAL_SPAWNER: LazyLock<Arc<dyn ProcessSpawner>> =
22 LazyLock::new(|| Arc::new(RealSpawner) as Arc<dyn ProcessSpawner>);
23
24pub fn default_spawner() -> Arc<dyn ProcessSpawner> {
26 Arc::clone(&REAL_SPAWNER)
27}
28
29impl ProcessSpawner for RealSpawner {
30 fn spawn(&self, spec: SpawnSpec) -> Result<Box<dyn ProcessHandle>, ProcessError> {
31 if spec.program.is_empty() {
32 return Err(ProcessError::InvalidArgv(
33 "first element of argv must be a non-empty program name".to_string(),
34 ));
35 }
36
37 let mut command = process_sandbox::std_command_for(&spec.program, &spec.args)
38 .map_err(|e| ProcessError::SandboxSetup(format!("{e:?}")))?;
39
40 if let Some(cwd) = spec.cwd.as_ref() {
41 process_sandbox::enforce_process_cwd(cwd)
42 .map_err(|e| ProcessError::SandboxCwd(format!("{e:?}")))?;
43 command.current_dir(cwd);
44 }
45
46 match spec.env_mode {
47 EnvMode::Replace => {
49 command.env_clear();
50 }
51 EnvMode::InheritClean | EnvMode::Patch => {
58 for (key, _) in std::env::vars_os() {
59 if let Some(name) = key.to_str() {
60 if super::handle::is_sensitive_env_name(name) {
61 command.env_remove(&key);
62 }
63 }
64 }
65 }
66 }
67 for key in &spec.env_remove {
72 command.env_remove(key);
73 }
74 for (key, value) in &spec.env {
75 command.env(key, value);
76 }
77
78 for (key, value) in process_sandbox::active_workspace_tmpdir_env() {
88 if spec.env.contains_key(&key) {
89 continue;
90 }
91 command.env(key, value);
92 }
93
94 if !spec
102 .env
103 .contains_key(process_sandbox::MESSAGE_LOCALE_OVERRIDE_ENV)
104 {
105 command.env_remove(process_sandbox::MESSAGE_LOCALE_OVERRIDE_ENV);
106 }
107 for (key, value) in process_sandbox::deterministic_message_locale_env() {
108 if spec.env.contains_key(&key) {
109 continue;
110 }
111 command.env(key, value);
112 }
113
114 if spec.configure_process_group {
115 configure_background_process_group(&mut command);
116 }
117 let cleanup_token = harn_vm::op_interrupt::new_process_cleanup_token();
118 command.env(
119 harn_vm::op_interrupt::PROCESS_CLEANUP_TOKEN_ENV,
120 &cleanup_token,
121 );
122
123 match &spec.output_capture {
124 OutputCapture::Pipe => {
125 command.stdout(Stdio::piped());
126 command.stderr(Stdio::piped());
127 }
128 OutputCapture::File {
129 stdout_path,
130 stderr_path,
131 } => {
132 let stdout = OpenOptions::new()
133 .write(true)
134 .truncate(true)
135 .open(stdout_path)
136 .map_err(|error| {
137 ProcessError::Spawn(format!("open stdout capture: {error}"))
138 })?;
139 let stderr = OpenOptions::new()
140 .write(true)
141 .truncate(true)
142 .open(stderr_path)
143 .map_err(|error| {
144 ProcessError::Spawn(format!("open stderr capture: {error}"))
145 })?;
146 command.stdout(Stdio::from(stdout));
147 command.stderr(Stdio::from(stderr));
148 }
149 }
150 command.stdin(if spec.use_stdin {
151 Stdio::piped()
152 } else {
153 Stdio::null()
154 });
155
156 let child = command.spawn().map_err(|e| {
157 if let Some(violation) = process_sandbox::process_spawn_error(&e) {
158 return ProcessError::SandboxSpawn(format!("{violation:?}"));
159 }
160 ProcessError::Spawn(format!("{e}"))
161 })?;
162
163 let pid = child.id();
164 let pgid = child_process_group_id(pid);
165 let killer: Arc<dyn ProcessKiller> = Arc::new(RealKiller {
166 pid,
167 cleanup_token: cleanup_token.clone(),
168 });
169
170 Ok(Box::new(RealProcess {
171 pid,
172 pgid,
173 cleanup_token,
174 killer,
175 child: Some(child),
176 stdin: None,
177 stdout: None,
178 stderr: None,
179 stdin_taken: false,
180 stdout_taken: false,
181 stderr_taken: false,
182 }))
183 }
184}
185
186struct RealProcess {
187 pid: u32,
188 pgid: Option<u32>,
189 cleanup_token: String,
190 killer: Arc<dyn ProcessKiller>,
191 child: Option<Child>,
192 stdin: Option<ChildStdin>,
193 stdout: Option<ChildStdout>,
194 stderr: Option<ChildStderr>,
195 stdin_taken: bool,
196 stdout_taken: bool,
197 stderr_taken: bool,
198}
199
200impl RealProcess {
201 fn ensure_pipes_taken(&mut self) {
202 if let Some(child) = self.child.as_mut() {
203 if self.stdin.is_none() && !self.stdin_taken {
204 self.stdin = child.stdin.take();
205 }
206 if self.stdout.is_none() && !self.stdout_taken {
207 self.stdout = child.stdout.take();
208 }
209 if self.stderr.is_none() && !self.stderr_taken {
210 self.stderr = child.stderr.take();
211 }
212 }
213 }
214}
215
216impl ProcessHandle for RealProcess {
217 fn pid(&self) -> Option<u32> {
218 Some(self.pid)
219 }
220
221 fn process_group_id(&self) -> Option<u32> {
222 self.pgid
223 }
224
225 fn killer(&self) -> Arc<dyn ProcessKiller> {
226 Arc::clone(&self.killer)
227 }
228
229 fn take_stdin(&mut self) -> Option<Box<dyn Write + Send>> {
230 self.ensure_pipes_taken();
231 self.stdin_taken = true;
232 self.stdin
233 .take()
234 .map(|s| Box::new(s) as Box<dyn Write + Send>)
235 }
236
237 fn take_stdout(&mut self) -> Option<Box<dyn Read + Send>> {
238 self.ensure_pipes_taken();
239 self.stdout_taken = true;
240 self.stdout
241 .take()
242 .map(|s| Box::new(s) as Box<dyn Read + Send>)
243 }
244
245 fn take_stderr(&mut self) -> Option<Box<dyn Read + Send>> {
246 self.ensure_pipes_taken();
247 self.stderr_taken = true;
248 self.stderr
249 .take()
250 .map(|s| Box::new(s) as Box<dyn Read + Send>)
251 }
252
253 fn wait_with_timeout(
254 &mut self,
255 timeout: Option<Duration>,
256 interrupt: &dyn Fn() -> bool,
257 ) -> io::Result<WaitOutcome> {
258 let killer = Arc::clone(&self.killer);
259 let Some(child) = self.child.as_mut() else {
260 return Err(io::Error::other("child already reaped"));
261 };
262 let deadline = timeout.map(|timeout| Instant::now() + timeout);
263 loop {
264 match child.try_wait()? {
265 Some(status) => return Ok(WaitOutcome::Exited(decode_status(status))),
266 None => {
267 if interrupt() {
268 let (_, report) =
272 harn_vm::op_interrupt::terminate_child_group_with_cleanup_token_report(
273 child,
274 Some(&self.cleanup_token),
275 );
276 return Ok(WaitOutcome::Interrupted(report));
277 }
278 if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
279 let mut report = killer.kill();
286 let _ = child.kill();
287 let _ = child.wait();
288 report.refresh_survivor_status();
289 return Ok(WaitOutcome::TimedOut(report));
290 }
291 let sleep = deadline
292 .map(|deadline| deadline.saturating_duration_since(Instant::now()))
293 .unwrap_or(Duration::MAX)
294 .min(Duration::from_millis(20));
295 thread::sleep(sleep);
296 }
297 }
298 }
299 }
300
301 fn wait(&mut self) -> io::Result<ExitStatus> {
302 let child = self
303 .child
304 .as_mut()
305 .ok_or_else(|| io::Error::other("child already reaped"))?;
306 let status = child.wait()?;
307 Ok(decode_status(status))
308 }
309}
310
311struct RealKiller {
312 pid: u32,
313 cleanup_token: String,
314}
315
316impl ProcessKiller for RealKiller {
317 fn kill(&self) -> ProcessCleanupReport {
318 harn_vm::op_interrupt::signal_pid_tree_group_and_token_with_report(
319 self.pid,
320 Some(&self.cleanup_token),
321 9,
322 )
323 }
324}
325
326#[cfg(unix)]
327fn decode_status(status: std::process::ExitStatus) -> ExitStatus {
328 use std::os::unix::process::ExitStatusExt;
329 if let Some(code) = status.code() {
330 ExitStatus::from_code(code)
331 } else if let Some(sig) = status.signal() {
332 ExitStatus::from_signal(sig)
333 } else {
334 ExitStatus {
335 code: None,
336 signal: None,
337 }
338 }
339}
340
341#[cfg(not(unix))]
342fn decode_status(status: std::process::ExitStatus) -> ExitStatus {
343 ExitStatus::from_code(status.code().unwrap_or(-1))
344}
345
346pub(crate) fn child_process_group_id(pid: u32) -> Option<u32> {
347 #[cfg(unix)]
348 {
349 extern "C" {
350 fn getpgid(pid: i32) -> i32;
351 }
352 let pgid = unsafe { getpgid(pid as i32) };
353 if pgid > 0 {
354 Some(pgid as u32)
355 } else {
356 None
357 }
358 }
359 #[cfg(not(unix))]
360 {
361 Some(pid)
362 }
363}
364
365pub(crate) fn configure_background_process_group(command: &mut std::process::Command) {
366 #[cfg(unix)]
367 unsafe {
368 use std::os::unix::process::CommandExt;
369 command.pre_exec(|| {
370 extern "C" {
371 fn setpgid(pid: i32, pgid: i32) -> i32;
372 }
373 if setpgid(0, 0) == -1 {
374 return Err(std::io::Error::last_os_error());
375 }
376 Ok(())
377 });
378 }
379 #[cfg(not(unix))]
380 {
381 let _ = command;
382 }
383}