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