acyclic_native_runtime/
process_tree.rs1#![allow(unsafe_code)]
2
3use std::io;
4use std::process::{Child, Command, ExitStatus, Output};
5
6pub struct ProcessTree {
11 child: Option<Child>,
12 guard: platform::Guard,
13}
14
15impl ProcessTree {
16 pub(crate) fn spawn(command: &mut Command) -> io::Result<Self> {
17 let (child, guard) = platform::spawn(command)?;
18 Ok(Self {
19 child: Some(child),
20 guard,
21 })
22 }
23
24 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
26 self.child
27 .as_mut()
28 .ok_or_else(|| io::Error::other("process output was already collected"))?
29 .try_wait()
30 }
31
32 pub fn wait_with_output(&mut self) -> io::Result<Output> {
34 self.child
35 .take()
36 .ok_or_else(|| io::Error::other("process output was already collected"))?
37 .wait_with_output()
38 }
39
40 pub fn terminate(&mut self) -> io::Result<()> {
42 self.terminate_descendants()?;
43 if let Some(child) = self.child.as_mut() {
44 child.wait()?;
45 self.child.take();
46 }
47 Ok(())
48 }
49
50 pub fn terminate_descendants(&mut self) -> io::Result<()> {
53 self.guard.terminate()
54 }
55}
56
57impl Drop for ProcessTree {
58 fn drop(&mut self) {
59 let _ = self.terminate();
60 }
61}
62
63#[cfg(unix)]
64mod platform {
65 use std::io;
66 use std::os::unix::process::CommandExt as _;
67 use std::process::{Child, Command};
68
69 pub(super) struct Guard {
70 process_group: libc::pid_t,
71 active: bool,
72 }
73
74 pub(super) fn spawn(command: &mut Command) -> io::Result<(Child, Guard)> {
75 command.process_group(0);
76 let child = command.spawn()?;
77 let process_group = libc::pid_t::try_from(child.id())
78 .map_err(|_| io::Error::other("child process id does not fit pid_t"))?;
79 Ok((
80 child,
81 Guard {
82 process_group,
83 active: true,
84 },
85 ))
86 }
87
88 impl Guard {
89 pub(super) fn terminate(&mut self) -> io::Result<()> {
90 if !self.active {
91 return Ok(());
92 }
93 if unsafe { libc::kill(-self.process_group, libc::SIGKILL) } == 0 {
96 self.active = false;
97 return Ok(());
98 }
99 let error = io::Error::last_os_error();
100 if error.raw_os_error() == Some(libc::ESRCH) {
101 self.active = false;
102 Ok(())
103 } else {
104 Err(error)
105 }
106 }
107 }
108}
109
110#[cfg(test)]
111#[allow(clippy::expect_used)]
112mod tests {
113 use super::ProcessTree;
114 use std::fs;
115 use std::process::{Command, Stdio};
116 use std::thread;
117 use std::time::{Duration, Instant};
118
119 const MODE: &str = "ACYCLIC_PROCESS_TREE_TEST_MODE";
120 const ROOT: &str = "ACYCLIC_PROCESS_TREE_TEST_ROOT";
121
122 #[test]
123 fn process_tree_helper() {
124 let Ok(mode) = std::env::var(MODE) else {
125 return;
126 };
127 let root = std::path::PathBuf::from(std::env::var_os(ROOT).expect("helper root"));
128 if mode == "grandchild" {
129 fs::write(root.join("grandchild-ready"), b"ready").expect("grandchild ready");
130 thread::sleep(Duration::from_millis(750));
131 fs::write(root.join("escaped"), b"descendant survived").expect("escaped marker");
132 return;
133 }
134 assert_eq!(mode, "child");
135 let mut grandchild = Command::new(std::env::current_exe().expect("test executable"));
136 grandchild
137 .args(["--exact", "process_tree::tests::process_tree_helper"])
138 .env(MODE, "grandchild")
139 .env(ROOT, &root)
140 .stdin(Stdio::null())
141 .stdout(Stdio::null())
142 .stderr(Stdio::null());
143 let mut grandchild = grandchild.spawn().expect("spawn grandchild");
144 let deadline = Instant::now() + Duration::from_secs(5);
145 while !root.join("grandchild-ready").exists() && Instant::now() < deadline {
146 thread::sleep(Duration::from_millis(10));
147 }
148 assert!(root.join("grandchild-ready").exists());
149 fs::write(root.join("tree-ready"), b"ready").expect("tree ready");
150 let _ = grandchild.wait();
151 }
152
153 #[test]
154 fn termination_contains_descendants() {
155 let temporary = tempfile::tempdir().expect("temporary process-tree directory");
156 let mut command = Command::new(std::env::current_exe().expect("test executable"));
157 command
158 .args(["--exact", "process_tree::tests::process_tree_helper"])
159 .env(MODE, "child")
160 .env(ROOT, temporary.path())
161 .stdin(Stdio::null())
162 .stdout(Stdio::null())
163 .stderr(Stdio::null());
164 let mut tree = ProcessTree::spawn(&mut command).expect("spawn process tree");
165 let deadline = Instant::now() + Duration::from_secs(5);
166 while !temporary.path().join("tree-ready").exists() && Instant::now() < deadline {
167 thread::sleep(Duration::from_millis(10));
168 }
169 assert!(temporary.path().join("tree-ready").exists());
170 tree.terminate().expect("terminate process tree");
171 thread::sleep(Duration::from_secs(1));
172 assert!(!temporary.path().join("escaped").exists());
173 }
174}
175
176#[cfg(windows)]
177mod platform {
178 use std::io;
179 use std::mem::{size_of, zeroed};
180 use std::os::windows::io::AsRawHandle as _;
181 use std::os::windows::process::CommandExt as _;
182 use std::process::{Child, Command};
183 use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
184 use windows_sys::Win32::System::Diagnostics::ToolHelp::{
185 CreateToolhelp32Snapshot, TH32CS_SNAPTHREAD, THREADENTRY32, Thread32First, Thread32Next,
186 };
187 use windows_sys::Win32::System::JobObjects::{
188 AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
189 JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation,
190 SetInformationJobObject, TerminateJobObject,
191 };
192 use windows_sys::Win32::System::Threading::{
193 CREATE_NEW_PROCESS_GROUP, CREATE_SUSPENDED, OpenThread, ResumeThread, THREAD_SUSPEND_RESUME,
194 };
195
196 pub(super) struct Guard {
197 job: HANDLE,
198 active: bool,
199 }
200
201 pub(super) fn spawn(command: &mut Command) -> io::Result<(Child, Guard)> {
202 let mut guard = Guard::new()?;
203 command.creation_flags(CREATE_SUSPENDED | CREATE_NEW_PROCESS_GROUP);
204 let mut child = command.spawn()?;
205 if unsafe { AssignProcessToJobObject(guard.job, child.as_raw_handle().cast()) } == 0 {
209 let error = io::Error::last_os_error();
210 let _ = guard.terminate();
211 let _ = child.kill();
212 let _ = child.wait();
213 return Err(error);
214 }
215 if let Err(error) = resume_process(child.id()) {
216 let _ = guard.terminate();
217 let _ = child.wait();
218 return Err(error);
219 }
220 Ok((child, guard))
221 }
222
223 impl Guard {
224 fn new() -> io::Result<Self> {
225 let job = unsafe { CreateJobObjectW(std::ptr::null(), std::ptr::null()) };
228 if job.is_null() {
229 return Err(io::Error::last_os_error());
230 }
231 let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { zeroed() };
234 limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
235 let configured = unsafe {
237 SetInformationJobObject(
238 job,
239 JobObjectExtendedLimitInformation,
240 (&raw const limits).cast(),
241 u32::try_from(size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>())
242 .map_err(|_| io::Error::other("Job limit structure is too large"))?,
243 )
244 };
245 if configured == 0 {
246 let error = io::Error::last_os_error();
247 unsafe { CloseHandle(job) };
249 return Err(error);
250 }
251 Ok(Self { job, active: true })
252 }
253
254 pub(super) fn terminate(&mut self) -> io::Result<()> {
255 if !self.active {
256 return Ok(());
257 }
258 if unsafe { TerminateJobObject(self.job, 1) } == 0 {
260 Err(io::Error::last_os_error())
261 } else {
262 self.active = false;
263 Ok(())
264 }
265 }
266 }
267
268 impl Drop for Guard {
269 fn drop(&mut self) {
270 let _ = self.terminate();
271 unsafe { CloseHandle(self.job) };
273 }
274 }
275
276 fn resume_process(process_id: u32) -> io::Result<()> {
277 let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) };
279 if snapshot == INVALID_HANDLE_VALUE {
280 return Err(io::Error::last_os_error());
281 }
282 let mut entry: THREADENTRY32 = unsafe { zeroed() };
284 entry.dwSize = u32::try_from(size_of::<THREADENTRY32>())
285 .map_err(|_| io::Error::other("thread entry structure is too large"))?;
286 let mut found = unsafe { Thread32First(snapshot, &raw mut entry) } != 0;
288 let mut result = Err(io::Error::new(
289 io::ErrorKind::NotFound,
290 "suspended child thread was not found",
291 ));
292 while found {
293 if entry.th32OwnerProcessID == process_id {
294 let thread = unsafe { OpenThread(THREAD_SUSPEND_RESUME, 0, entry.th32ThreadID) };
296 if thread.is_null() {
297 result = Err(io::Error::last_os_error());
298 } else {
299 result = if unsafe { ResumeThread(thread) } == u32::MAX {
301 Err(io::Error::last_os_error())
302 } else {
303 Ok(())
304 };
305 unsafe { CloseHandle(thread) };
307 }
308 break;
309 }
310 found = unsafe { Thread32Next(snapshot, &raw mut entry) } != 0;
312 }
313 unsafe { CloseHandle(snapshot) };
315 result
316 }
317}