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