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