1use std::fs::OpenOptions;
5use std::io::{self, Read, Write};
6use std::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command, 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 let (mut command, cleanup_token) = prepare_command(&spec, None)?;
32 let child = command.spawn().map_err(map_spawn_error)?;
33
34 let pid = child.id();
35 let pgid = child_process_group_id(pid);
36 let killer: Arc<dyn ProcessKiller> = Arc::new(RealKiller {
37 pid,
38 cleanup_token: cleanup_token.clone(),
39 });
40
41 Ok(Box::new(RealProcess {
42 pid,
43 pgid,
44 cleanup_token,
45 killer,
46 child: Some(child),
47 stdin: None,
48 stdout: None,
49 stderr: None,
50 stdin_taken: false,
51 stdout_taken: false,
52 stderr_taken: false,
53 }))
54 }
55}
56
57fn prepare_command(
58 spec: &SpawnSpec,
59 cleanup_token: Option<String>,
60) -> Result<(Command, String), ProcessError> {
61 if spec.program.is_empty() {
62 return Err(ProcessError::InvalidArgv(
63 "first element of argv must be a non-empty program name".to_string(),
64 ));
65 }
66
67 let mut command = process_sandbox::std_command_for(&spec.program, &spec.args)
68 .map_err(|e| ProcessError::SandboxSetup(format!("{e:?}")))?;
69
70 if let Some(cwd) = spec.cwd.as_ref() {
71 process_sandbox::enforce_process_cwd(cwd)
72 .map_err(|e| ProcessError::SandboxCwd(format!("{e:?}")))?;
73 command.current_dir(cwd);
74 }
75
76 match spec.env_mode {
77 EnvMode::Replace => {
79 command.env_clear();
80 }
81 EnvMode::InheritClean | EnvMode::Patch => {
88 for (key, _) in std::env::vars_os() {
89 if let Some(name) = key.to_str() {
90 if super::handle::is_sensitive_env_name(name) {
91 command.env_remove(&key);
92 }
93 }
94 }
95 }
96 }
97 for key in &spec.env_remove {
102 command.env_remove(key);
103 }
104 for (key, value) in &spec.env {
105 command.env(key, value);
106 }
107
108 for (key, value) in process_sandbox::active_workspace_tmpdir_env() {
118 if spec.env.contains_key(&key) {
119 continue;
120 }
121 command.env(key, value);
122 }
123
124 if !spec
132 .env
133 .contains_key(process_sandbox::MESSAGE_LOCALE_OVERRIDE_ENV)
134 {
135 command.env_remove(process_sandbox::MESSAGE_LOCALE_OVERRIDE_ENV);
136 }
137 for (key, value) in process_sandbox::deterministic_message_locale_env() {
138 if spec.env.contains_key(&key) {
139 continue;
140 }
141 command.env(key, value);
142 }
143
144 if spec.configure_process_group {
145 configure_background_process_group(&mut command);
146 }
147 let cleanup_token =
148 cleanup_token.unwrap_or_else(harn_vm::op_interrupt::new_process_cleanup_token);
149 command.env(
150 harn_vm::op_interrupt::PROCESS_CLEANUP_TOKEN_ENV,
151 &cleanup_token,
152 );
153
154 match &spec.output_capture {
155 OutputCapture::Inherit => {
156 command.stdout(Stdio::inherit());
157 command.stderr(Stdio::inherit());
158 }
159 OutputCapture::Pipe => {
160 command.stdout(Stdio::piped());
161 command.stderr(Stdio::piped());
162 }
163 OutputCapture::File {
164 stdout_path,
165 stderr_path,
166 } => {
167 let stdout = OpenOptions::new()
168 .write(true)
169 .truncate(true)
170 .open(stdout_path)
171 .map_err(|error| ProcessError::Spawn(format!("open stdout capture: {error}")))?;
172 let stderr = OpenOptions::new()
173 .write(true)
174 .truncate(true)
175 .open(stderr_path)
176 .map_err(|error| ProcessError::Spawn(format!("open stderr capture: {error}")))?;
177 command.stdout(Stdio::from(stdout));
178 command.stderr(Stdio::from(stderr));
179 }
180 }
181 command.stdin(match (&spec.output_capture, spec.use_stdin) {
182 (OutputCapture::Inherit, true) => Stdio::inherit(),
183 (_, true) => Stdio::piped(),
184 (_, false) => Stdio::null(),
185 });
186
187 Ok((command, cleanup_token))
188}
189
190fn map_spawn_error(error: io::Error) -> ProcessError {
191 if let Some(violation) = process_sandbox::process_spawn_error(&error) {
192 return ProcessError::SandboxSpawn(format!("{violation:?}"));
193 }
194 ProcessError::Spawn(error.to_string())
195}
196
197#[cfg(unix)]
200pub fn replace_current_process(spec: SpawnSpec) -> Result<std::convert::Infallible, ProcessError> {
201 use std::os::unix::process::CommandExt;
202
203 super::handle::validate_process_spec(&spec)?;
204 let inherited_cleanup_token = std::env::var(harn_vm::op_interrupt::PROCESS_CLEANUP_TOKEN_ENV)
205 .ok()
206 .filter(|token| !token.is_empty());
207 let (mut command, _cleanup_token) = prepare_command(&spec, inherited_cleanup_token)?;
208 Err(map_spawn_error(command.exec()))
209}
210
211struct RealProcess {
212 pid: u32,
213 pgid: Option<u32>,
214 cleanup_token: String,
215 killer: Arc<dyn ProcessKiller>,
216 child: Option<Child>,
217 stdin: Option<ChildStdin>,
218 stdout: Option<ChildStdout>,
219 stderr: Option<ChildStderr>,
220 stdin_taken: bool,
221 stdout_taken: bool,
222 stderr_taken: bool,
223}
224
225impl RealProcess {
226 fn ensure_pipes_taken(&mut self) {
227 if let Some(child) = self.child.as_mut() {
228 if self.stdin.is_none() && !self.stdin_taken {
229 self.stdin = child.stdin.take();
230 }
231 if self.stdout.is_none() && !self.stdout_taken {
232 self.stdout = child.stdout.take();
233 }
234 if self.stderr.is_none() && !self.stderr_taken {
235 self.stderr = child.stderr.take();
236 }
237 }
238 }
239}
240
241impl ProcessHandle for RealProcess {
242 fn pid(&self) -> Option<u32> {
243 Some(self.pid)
244 }
245
246 fn process_group_id(&self) -> Option<u32> {
247 self.pgid
248 }
249
250 fn killer(&self) -> Arc<dyn ProcessKiller> {
251 Arc::clone(&self.killer)
252 }
253
254 fn take_stdin(&mut self) -> Option<Box<dyn Write + Send>> {
255 self.ensure_pipes_taken();
256 self.stdin_taken = true;
257 self.stdin
258 .take()
259 .map(|s| Box::new(s) as Box<dyn Write + Send>)
260 }
261
262 fn take_stdout(&mut self) -> Option<Box<dyn Read + Send>> {
263 self.ensure_pipes_taken();
264 self.stdout_taken = true;
265 self.stdout
266 .take()
267 .map(|s| Box::new(s) as Box<dyn Read + Send>)
268 }
269
270 fn take_stderr(&mut self) -> Option<Box<dyn Read + Send>> {
271 self.ensure_pipes_taken();
272 self.stderr_taken = true;
273 self.stderr
274 .take()
275 .map(|s| Box::new(s) as Box<dyn Read + Send>)
276 }
277
278 fn wait_with_timeout(
279 &mut self,
280 timeout: Option<Duration>,
281 interrupt: &dyn Fn() -> bool,
282 ) -> io::Result<WaitOutcome> {
283 let killer = Arc::clone(&self.killer);
284 let Some(child) = self.child.as_mut() else {
285 return Err(io::Error::other("child already reaped"));
286 };
287 let deadline = timeout.map(|timeout| Instant::now() + timeout);
288 loop {
289 match child.try_wait()? {
290 Some(status) => return Ok(WaitOutcome::Exited(decode_status(status))),
291 None => {
292 if interrupt() {
293 let (_, report) =
297 harn_vm::op_interrupt::terminate_child_group_with_cleanup_token_report(
298 child,
299 Some(&self.cleanup_token),
300 );
301 return Ok(WaitOutcome::Interrupted(report));
302 }
303 if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
304 let mut report = killer.kill();
311 let _ = child.kill();
312 let _ = child.wait();
313 report.refresh_survivor_status();
314 return Ok(WaitOutcome::TimedOut(report));
315 }
316 let sleep = deadline
317 .map(|deadline| deadline.saturating_duration_since(Instant::now()))
318 .unwrap_or(Duration::MAX)
319 .min(Duration::from_millis(20));
320 thread::sleep(sleep);
321 }
322 }
323 }
324 }
325
326 fn wait(&mut self) -> io::Result<ExitStatus> {
327 let child = self
328 .child
329 .as_mut()
330 .ok_or_else(|| io::Error::other("child already reaped"))?;
331 let status = child.wait()?;
332 Ok(decode_status(status))
333 }
334}
335
336struct RealKiller {
337 pid: u32,
338 cleanup_token: String,
339}
340
341impl ProcessKiller for RealKiller {
342 fn kill(&self) -> ProcessCleanupReport {
343 let report = harn_vm::op_interrupt::signal_pid_tree_group_and_token_with_report(
344 self.pid,
345 Some(&self.cleanup_token),
346 9,
347 );
348 #[cfg(target_os = "windows")]
349 terminate_process(self.pid);
350 report
351 }
352}
353
354#[cfg(target_os = "windows")]
355fn terminate_process(pid: u32) {
356 use windows_sys::Win32::Foundation::CloseHandle;
357 use windows_sys::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
358
359 let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid) };
360 if handle.is_null() {
361 return;
362 }
363 unsafe {
364 TerminateProcess(handle, 1);
365 CloseHandle(handle);
366 }
367}
368
369#[cfg(unix)]
370fn decode_status(status: std::process::ExitStatus) -> ExitStatus {
371 use std::os::unix::process::ExitStatusExt;
372 if let Some(code) = status.code() {
373 ExitStatus::from_code(code)
374 } else if let Some(sig) = status.signal() {
375 ExitStatus::from_signal(sig)
376 } else {
377 ExitStatus {
378 code: None,
379 signal: None,
380 }
381 }
382}
383
384#[cfg(not(unix))]
385fn decode_status(status: std::process::ExitStatus) -> ExitStatus {
386 ExitStatus::from_code(status.code().unwrap_or(-1))
387}
388
389pub(crate) fn child_process_group_id(pid: u32) -> Option<u32> {
390 #[cfg(unix)]
391 {
392 extern "C" {
393 fn getpgid(pid: i32) -> i32;
394 }
395 let pgid = unsafe { getpgid(pid as i32) };
396 if pgid > 0 {
397 Some(pgid as u32)
398 } else {
399 None
400 }
401 }
402 #[cfg(not(unix))]
403 {
404 Some(pid)
405 }
406}
407
408pub(crate) fn configure_background_process_group(command: &mut std::process::Command) {
409 #[cfg(unix)]
410 unsafe {
411 use std::os::unix::process::CommandExt;
412 command.pre_exec(|| {
413 extern "C" {
414 fn setpgid(pid: i32, pgid: i32) -> i32;
415 }
416 if setpgid(0, 0) == -1 {
417 return Err(std::io::Error::last_os_error());
418 }
419 Ok(())
420 });
421 }
422 #[cfg(not(unix))]
423 {
424 let _ = command;
425 }
426}