Skip to main content

oxdock_process/
child.rs

1use anyhow::Result;
2#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
3use std::process::{Child, ExitStatus};
4use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
5use std::sync::{Arc, Mutex};
6
7use crate::contract::BackgroundHandle;
8
9/// Shared inner state for `ChildHandle`, enabling safe cloning.
10/// The OS PID is stored separately so `kill()` can signal
11/// the process without needing `&mut Child`, avoiding undefined behavior.
12#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
13struct ChildInner {
14    child: Option<Child>,
15    io_threads: Vec<std::thread::JoinHandle<()>>,
16    reaped: bool,
17    exit_status: Option<ExitStatus>,
18    killed: AtomicBool,
19}
20
21#[derive(Clone)]
22#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
23pub struct ChildHandle {
24    inner: Arc<Mutex<ChildInner>>,
25    /// OS process ID for signal-based kill on Unix and `OpenProcess`-based
26    /// terminate on Windows.
27    pid: Arc<AtomicU32>,
28}
29
30impl ChildHandle {
31    #[allow(clippy::disallowed_types, clippy::disallowed_methods)]
32    pub(crate) fn new(child: Child, io_threads: Vec<std::thread::JoinHandle<()>>) -> Self {
33        #[cfg(unix)]
34        let pid = child.id();
35        #[cfg(windows)]
36        let pid = child.id();
37        Self {
38            inner: Arc::new(Mutex::new(ChildInner {
39                child: Some(child),
40                io_threads,
41                reaped: false,
42                exit_status: None,
43                killed: AtomicBool::new(false),
44            })),
45            pid: Arc::new(AtomicU32::new(pid)),
46        }
47    }
48}
49
50impl BackgroundHandle for ChildHandle {
51    fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
52        let mut guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
53        if let Some(ref mut child) = guard.child {
54            match child.try_wait()? {
55                Some(status) => {
56                    guard.reaped = true;
57                    guard.exit_status = Some(status);
58                    // Zero PID to prevent killing recycled PIDs
59                    self.pid.store(0, Ordering::SeqCst);
60                    for thread in guard.io_threads.drain(..) {
61                        let _ = thread.join();
62                    }
63                    guard.child = None;
64                    Ok(Some(status))
65                }
66                None => Ok(None),
67            }
68        } else if guard.reaped {
69            // Process already reaped — return cached exit status
70            Ok(Some(
71                guard
72                    .exit_status
73                    .unwrap_or_else(|| exit_status_from_code(0)),
74            ))
75        } else {
76            // wait() is executing on another thread — process is still running
77            Ok(None)
78        }
79    }
80
81    fn wait(&mut self) -> Result<ExitStatus> {
82        // Take the child out of the mutex. This ensures only one thread
83        // performs the blocking OS wait, preventing ECHILD from dual waitpid.
84        let child_opt = {
85            let mut guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
86            guard.child.take()
87        };
88
89        if let Some(mut child) = child_opt {
90            let status = child.wait()?;
91            // Zero PID to prevent killing recycled PIDs
92            self.pid.store(0, Ordering::SeqCst);
93            // Re-acquire lock to store result and join IO threads
94            let mut guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
95            for thread in guard.io_threads.drain(..) {
96                let _ = thread.join();
97            }
98            guard.reaped = true;
99            guard.exit_status = Some(status);
100            Ok(status)
101        } else {
102            // Already reaped — return cached exit status
103            let guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
104            Ok(guard
105                .exit_status
106                .unwrap_or_else(|| exit_status_from_code(0)))
107        }
108    }
109
110    fn kill(&mut self) -> Result<()> {
111        let pid = self.pid.load(Ordering::SeqCst);
112        if pid == 0 {
113            return Ok(());
114        }
115        // Signal the process directly via OS PID. This does NOT need
116        // &mut Child — no aliasing, no UB.
117        #[cfg(unix)]
118        {
119            unsafe {
120                libc::kill(pid as i32, libc::SIGKILL);
121            }
122        }
123        #[cfg(windows)]
124        {
125            // Open the process by PID and terminate it. This works even when
126            // wait() has taken the `Child` out of `ChildInner`, and avoids
127            // storing a raw HANDLE (which is neither Send nor Sync and would
128            // break the `BackgroundHandle: Send` bound).
129            // `pid` is zeroed after successful wait, so recycled PIDs are safe.
130            use windows_sys::Win32::Foundation::CloseHandle;
131            use windows_sys::Win32::System::Threading::{
132                OpenProcess, PROCESS_TERMINATE, TerminateProcess,
133            };
134            unsafe {
135                let handle = OpenProcess(PROCESS_TERMINATE, 0, pid);
136                if !handle.is_null() {
137                    TerminateProcess(handle, 1);
138                    CloseHandle(handle);
139                }
140            }
141        }
142        self.inner
143            .lock()
144            .unwrap_or_else(|e| e.into_inner())
145            .killed
146            .store(true, Ordering::SeqCst);
147        Ok(())
148    }
149}
150
151impl Drop for ChildHandle {
152    fn drop(&mut self) {
153        // Only the last clone (when Arc refcount is 1) runs the actual cleanup.
154        if Arc::strong_count(&self.inner) > 1 {
155            return;
156        }
157        let mut guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
158        if guard.reaped {
159            return;
160        }
161        if let Some(ref mut child) = guard.child
162            && matches!(child.try_wait(), Ok(None))
163        {
164            let _ = child.kill();
165            let _ = child.wait();
166        }
167        guard.reaped = true;
168        // `io_threads` are deliberately NOT joined: a grandchild inheriting
169        // the pipe can keep pump threads alive indefinitely. They terminate
170        // on pipe EOF after the kill and only ever write into Arc'd buffers.
171    }
172}
173
174/// Helper to create an exit status from a raw code. Used for synthetic statuses.
175fn exit_status_from_code(code: i32) -> ExitStatus {
176    #[cfg(unix)]
177    {
178        use std::os::unix::process::ExitStatusExt;
179        ExitStatus::from_raw(code << 8)
180    }
181    #[cfg(windows)]
182    {
183        use std::os::windows::process::ExitStatusExt;
184        ExitStatus::from_raw(code as u32)
185    }
186}