1use std::{
2 ffi::OsStr,
3 io,
4 num::NonZeroU32,
5 process::{Child, ChildStderr, ChildStdin, ChildStdout, Command, ExitStatus},
6 sync::{
7 atomic::{AtomicU32, Ordering},
8 Arc,
9 },
10 thread,
11 time::{Duration, Instant},
12};
13
14use process_wrap::std::{StdChildWrapper, StdCommandWrap, StdCommandWrapper};
15
16#[derive(Debug)]
17pub struct OwnedCommand {
18 command: Command,
19 windows_hidden: bool,
20}
21
22impl OwnedCommand {
23 pub fn new(program: impl AsRef<OsStr>) -> Self {
24 Self::from_command(Command::new(program))
25 }
26
27 pub fn from_command(command: Command) -> Self {
28 Self {
29 command,
30 windows_hidden: false,
31 }
32 }
33
34 pub fn command_mut(&mut self) -> &mut Command {
35 &mut self.command
36 }
37
38 pub fn windows_hide(&mut self) -> &mut Self {
39 self.windows_hidden = true;
40 self
41 }
42
43 pub fn spawn(self) -> io::Result<OwnedChild> {
44 spawn_owned(self.command, self.windows_hidden, false, None)
45 }
46}
47
48#[derive(Debug)]
49pub struct OwnedChild {
50 child: Option<Box<dyn StdChildWrapper>>,
51}
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54pub enum WaitOutcome {
55 Exited(ExitStatus),
56 Terminated(ExitStatus),
57}
58
59impl OwnedChild {
60 pub fn id(&self) -> u32 {
61 self.child().id()
62 }
63
64 pub fn take_stdin(&mut self) -> Option<ChildStdin> {
65 self.child_mut().stdin().take()
66 }
67
68 pub fn take_stdout(&mut self) -> Option<ChildStdout> {
69 self.child_mut().stdout().take()
70 }
71
72 pub fn take_stderr(&mut self) -> Option<ChildStderr> {
73 self.child_mut().stderr().take()
74 }
75
76 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
77 self.child_mut().try_wait()
78 }
79
80 pub fn wait(&mut self) -> io::Result<ExitStatus> {
81 self.child_mut().wait()
82 }
83
84 pub fn wait_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
85 let started = Instant::now();
86 loop {
87 if let Some(status) = self.try_wait()? {
88 return Ok(Some(status));
89 }
90 if started.elapsed() >= timeout {
91 return Ok(None);
92 }
93 thread::sleep(Duration::from_millis(5).min(timeout.saturating_sub(started.elapsed())));
94 }
95 }
96
97 pub fn wait_or_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
98 #[cfg(unix)]
99 self.child().signal(nix::libc::SIGTERM)?;
100
101 if let Some(status) = self.wait_timeout(grace)? {
102 return Ok(WaitOutcome::Exited(status));
103 }
104 self.terminate_tree().map(WaitOutcome::Terminated)
105 }
106
107 pub fn terminate_tree(&mut self) -> io::Result<ExitStatus> {
108 if let Some(status) = self.try_wait()? {
109 return Ok(status);
110 }
111 self.child_mut().start_kill()?;
112 self.child_mut().wait()
113 }
114
115 fn child(&self) -> &dyn StdChildWrapper {
116 self.child
117 .as_deref()
118 .expect("owned child is always present")
119 }
120
121 fn child_mut(&mut self) -> &mut dyn StdChildWrapper {
122 self.child
123 .as_deref_mut()
124 .expect("owned child is always present")
125 }
126}
127
128impl Drop for OwnedChild {
129 fn drop(&mut self) {
130 let _ = self.terminate_tree();
131 }
132}
133
134#[derive(Clone, Copy, Debug, Eq, PartialEq)]
135pub struct AdoptedProcess {
136 pid: NonZeroU32,
137}
138
139impl AdoptedProcess {
140 pub fn new(pid: NonZeroU32) -> Self {
141 Self { pid }
142 }
143
144 pub fn id(&self) -> u32 {
145 self.pid.get()
146 }
147
148 pub fn is_running(&self) -> io::Result<bool> {
149 process_is_running(self.pid)
150 }
151}
152
153#[derive(Debug)]
154struct CleanupWrapper {
155 captured_pid: Option<Arc<AtomicU32>>,
156}
157
158impl StdCommandWrapper for CleanupWrapper {
159 fn post_spawn(&mut self, child: &mut Child, _core: &StdCommandWrap) -> io::Result<()> {
160 if let Some(pid) = &self.captured_pid {
161 pid.store(child.id(), Ordering::SeqCst);
162 }
163 Ok(())
164 }
165
166 fn wrap_child(
167 &mut self,
168 child: Box<dyn StdChildWrapper>,
169 _core: &StdCommandWrap,
170 ) -> io::Result<Box<dyn StdChildWrapper>> {
171 Ok(Box::new(CleanupChild { child: Some(child) }))
172 }
173}
174
175#[derive(Debug)]
176struct CleanupChild {
177 child: Option<Box<dyn StdChildWrapper>>,
178}
179
180impl CleanupChild {
181 fn child(&self) -> &dyn StdChildWrapper {
182 self.child.as_deref().expect("cleanup child is present")
183 }
184
185 fn child_mut(&mut self) -> &mut dyn StdChildWrapper {
186 self.child.as_deref_mut().expect("cleanup child is present")
187 }
188}
189
190impl StdChildWrapper for CleanupChild {
191 fn inner(&self) -> &Child {
192 self.child().inner()
193 }
194
195 fn inner_mut(&mut self) -> &mut Child {
196 self.child_mut().inner_mut()
197 }
198
199 fn into_inner(mut self: Box<Self>) -> Child {
200 self.child
201 .take()
202 .expect("cleanup child is present")
203 .into_inner()
204 }
205
206 fn stdin(&mut self) -> &mut Option<ChildStdin> {
207 self.child_mut().stdin()
208 }
209
210 fn stdout(&mut self) -> &mut Option<ChildStdout> {
211 self.child_mut().stdout()
212 }
213
214 fn stderr(&mut self) -> &mut Option<ChildStderr> {
215 self.child_mut().stderr()
216 }
217
218 fn id(&self) -> u32 {
219 self.child().id()
220 }
221
222 fn start_kill(&mut self) -> io::Result<()> {
223 self.child_mut().start_kill()
224 }
225
226 fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
227 self.child_mut().try_wait()
228 }
229
230 fn wait(&mut self) -> io::Result<ExitStatus> {
231 self.child_mut().wait()
232 }
233
234 #[cfg(unix)]
235 fn signal(&self, signal: i32) -> io::Result<()> {
236 self.child().signal(signal)
237 }
238}
239
240impl Drop for CleanupChild {
241 fn drop(&mut self) {
242 let Some(child) = self.child.as_deref_mut() else {
243 return;
244 };
245 if !matches!(child.try_wait(), Ok(Some(_))) {
246 let _ = child.start_kill();
247 let _ = child.wait();
248 }
249 }
250}
251
252#[derive(Debug)]
253struct FailAfterSpawn;
254
255impl StdCommandWrapper for FailAfterSpawn {
256 fn wrap_child(
257 &mut self,
258 _child: Box<dyn StdChildWrapper>,
259 _core: &StdCommandWrap,
260 ) -> io::Result<Box<dyn StdChildWrapper>> {
261 Err(io::Error::other("injected post-spawn wrapping failure"))
262 }
263}
264
265fn spawn_owned(
266 command: Command,
267 windows_hidden: bool,
268 fail_after_spawn: bool,
269 captured_pid: Option<Arc<AtomicU32>>,
270) -> io::Result<OwnedChild> {
271 let mut command = StdCommandWrap::from(command);
272 command.wrap(CleanupWrapper { captured_pid });
273
274 #[cfg(windows)]
275 {
276 use process_wrap::std::{CreationFlags, JobObject};
277 use windows::Win32::System::Threading::CREATE_NO_WINDOW;
278
279 if windows_hidden {
280 command.wrap(CreationFlags(CREATE_NO_WINDOW));
281 }
282 command.wrap(JobObject);
283 }
284
285 #[cfg(unix)]
286 {
287 use process_wrap::std::ProcessGroup;
288 let _ = windows_hidden;
289 command.wrap(ProcessGroup::leader());
290 }
291
292 if fail_after_spawn {
293 command.wrap(FailAfterSpawn);
294 }
295
296 command
297 .spawn()
298 .map(|child| OwnedChild { child: Some(child) })
299}
300
301#[cfg(test)]
302fn spawn_for_test(command: Command, captured_pid: Arc<AtomicU32>) -> io::Result<OwnedChild> {
303 spawn_owned(command, true, true, Some(captured_pid))
304}
305
306#[cfg(windows)]
307fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
308 use windows::Win32::{
309 Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, WAIT_TIMEOUT},
310 System::Threading::{
311 OpenProcess, WaitForSingleObject, PROCESS_QUERY_LIMITED_INFORMATION,
312 PROCESS_SYNCHRONIZE,
313 },
314 };
315
316 let handle = unsafe {
317 OpenProcess(
318 PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE,
319 false,
320 pid.get(),
321 )
322 };
323 let handle = match handle {
324 Ok(handle) => handle,
325 Err(error) => {
326 if error.code() == ERROR_INVALID_PARAMETER.to_hresult() {
327 return Ok(false);
328 }
329 return Err(io::Error::other(error.to_string()));
330 }
331 };
332 let wait = unsafe { WaitForSingleObject(handle, 0) };
333 let close = unsafe { CloseHandle(handle) };
334 close.map_err(|error| io::Error::other(error.to_string()))?;
335 match wait.0 {
336 0 => Ok(false),
337 value if value == WAIT_TIMEOUT.0 => Ok(true),
338 _ => Err(io::Error::last_os_error()),
339 }
340}
341
342#[cfg(unix)]
343fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
344 use nix::{errno::Errno, sys::signal::kill, unistd::Pid};
345
346 let pid = i32::try_from(pid.get())
347 .map(Pid::from_raw)
348 .map_err(io::Error::other)?;
349 match kill(pid, None) {
350 Ok(()) | Err(Errno::EPERM) => Ok(true),
351 Err(Errno::ESRCH) => Ok(false),
352 Err(error) => Err(io::Error::from(error)),
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 #[test]
359 fn a_failure_after_spawn_cleans_up_the_partially_wrapped_child() {
360 use std::{
361 process::Command,
362 sync::{
363 atomic::{AtomicU32, Ordering},
364 Arc,
365 },
366 thread,
367 time::{Duration, Instant},
368 };
369
370 let command = if cfg!(windows) {
372 let mut command = Command::new("powershell.exe");
373 command.args([
374 "-NoLogo",
375 "-NoProfile",
376 "-NonInteractive",
377 "-Command",
378 "Start-Sleep -Seconds 60",
379 ]);
380 command
381 } else {
382 let mut command = Command::new("sleep");
383 command.arg("60");
384 command
385 };
386 let spawned_pid = Arc::new(AtomicU32::new(0));
387
388 assert!(super::spawn_for_test(command, Arc::clone(&spawned_pid)).is_err());
389 let pid = spawned_pid.load(Ordering::SeqCst);
390 assert_ne!(pid, 0, "test must fail after the OS process was spawned");
391 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
392 let deadline = Instant::now() + Duration::from_secs(5);
393 while process.is_running().unwrap_or(false) {
394 assert!(
395 Instant::now() < deadline,
396 "partially spawned process leaked"
397 );
398 thread::sleep(Duration::from_millis(20));
399 }
400 }
401}