1use std::{
33 io,
34 num::NonZeroU32,
35 path::PathBuf,
36 thread,
37 time::{Duration, Instant},
38};
39
40#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
44pub struct StartToken {
45 pub major: u64,
46 pub minor: u64,
47}
48
49#[derive(Clone, Debug, Eq, PartialEq)]
52pub struct AdoptedIdentity {
53 pid: NonZeroU32,
54 start: StartToken,
55 exe: Option<PathBuf>,
56}
57
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
60pub enum IdentityCheck {
61 Same,
63 Gone,
65 Replaced,
67}
68
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum AdoptedSignal {
73 Graceful,
75 Force,
77}
78
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub enum SignalResult {
82 Delivered,
84 Gone,
86 IdentityMismatch,
88}
89
90#[derive(Clone, Debug, Default, Eq, PartialEq)]
92pub struct SweepReport {
93 pub terminated: Vec<u32>,
95 pub gone: Vec<u32>,
97 pub refused: Vec<u32>,
99 pub survivors: Vec<u32>,
101 pub failed: Vec<u32>,
103}
104
105impl AdoptedIdentity {
106 pub fn capture(pid: NonZeroU32) -> io::Result<Option<Self>> {
110 Ok(sys::read(pid.get())?.map(|seen| Self {
111 pid,
112 start: seen.start,
113 exe: seen.exe,
114 }))
115 }
116
117 pub fn from_recorded(pid: NonZeroU32, start: StartToken, exe: Option<PathBuf>) -> Self {
120 Self { pid, start, exe }
121 }
122
123 pub fn pid(&self) -> u32 {
124 self.pid.get()
125 }
126
127 pub fn start_token(&self) -> StartToken {
128 self.start
129 }
130
131 pub fn exe(&self) -> Option<&std::path::Path> {
132 self.exe.as_deref()
133 }
134
135 fn matches(&self, seen: &sys::Seen) -> bool {
136 seen.start == self.start
141 }
142
143 pub fn verify(&self) -> io::Result<IdentityCheck> {
145 Ok(match sys::read(self.pid())? {
146 None => IdentityCheck::Gone,
147 Some(seen) if self.matches(&seen) => IdentityCheck::Same,
148 Some(_) => IdentityCheck::Replaced,
149 })
150 }
151
152 pub fn signal(&self, signal: AdoptedSignal) -> io::Result<SignalResult> {
155 if self.pid() == std::process::id() {
156 return Err(io::Error::new(
157 io::ErrorKind::InvalidInput,
158 "refusing to signal this process",
159 ));
160 }
161 sys::signal(self, signal)
162 }
163
164 pub fn descendants(&self) -> io::Result<Vec<AdoptedIdentity>> {
169 if self.verify()? != IdentityCheck::Same {
170 return Ok(Vec::new());
171 }
172 let pairs = sys::process_pairs()?;
173 let mut found: Vec<AdoptedIdentity> = Vec::new();
174 let mut frontier = vec![(self.pid(), self.start)];
176 let mut next = 0;
177 while next < frontier.len() {
178 let (parent, parent_start) = frontier[next];
179 next += 1;
180 for &(child, claimed_parent) in &pairs {
181 if claimed_parent != parent
182 || child == parent
183 || child == self.pid()
184 || frontier.iter().any(|(pid, _)| *pid == child)
185 {
186 continue;
187 }
188 let (Some(pid), Ok(Some(seen))) = (NonZeroU32::new(child), sys::read(child)) else {
189 continue;
190 };
191 if seen.parent.is_some_and(|actual| actual != parent) || seen.start < parent_start {
192 continue; }
194 frontier.push((child, seen.start));
195 found.push(AdoptedIdentity {
196 pid,
197 start: seen.start,
198 exe: seen.exe,
199 });
200 }
201 }
202 if self.verify()? != IdentityCheck::Same {
204 return Ok(Vec::new());
205 }
206 Ok(found)
207 }
208}
209
210const SWEEP_POLL: Duration = Duration::from_millis(20);
211const FORCE_WAIT: Duration = Duration::from_millis(500);
212
213pub fn sweep(targets: &[AdoptedIdentity], grace: Duration) -> io::Result<SweepReport> {
219 let mut report = SweepReport::default();
220 let mut pending: Vec<&AdoptedIdentity> = Vec::new();
221 for target in targets {
222 match target.signal(AdoptedSignal::Graceful) {
223 Ok(SignalResult::Delivered) => pending.push(target),
224 Ok(SignalResult::Gone) => report.gone.push(target.pid()),
225 Ok(SignalResult::IdentityMismatch) => report.refused.push(target.pid()),
226 Err(_) => report.failed.push(target.pid()),
227 }
228 }
229 drain(&mut pending, grace, &mut report)?;
230 let mut forced: Vec<&AdoptedIdentity> = Vec::new();
231 for target in pending {
232 match target.signal(AdoptedSignal::Force) {
233 Ok(SignalResult::Delivered) => forced.push(target),
234 Ok(SignalResult::Gone) => report.terminated.push(target.pid()),
235 Ok(SignalResult::IdentityMismatch) => report.refused.push(target.pid()),
236 Err(_) => report.failed.push(target.pid()),
237 }
238 }
239 drain(&mut forced, FORCE_WAIT, &mut report)?;
240 report
241 .survivors
242 .extend(forced.iter().map(|target| target.pid()));
243 Ok(report)
244}
245
246pub fn sweep_descendants(root: &AdoptedIdentity, grace: Duration) -> io::Result<SweepReport> {
248 let mut targets = root.descendants()?;
249 targets.reverse();
250 sweep(&targets, grace)
251}
252
253fn drain(
256 pending: &mut Vec<&AdoptedIdentity>,
257 wait: Duration,
258 report: &mut SweepReport,
259) -> io::Result<()> {
260 let deadline = Instant::now().checked_add(wait);
261 loop {
262 let mut still = Vec::new();
263 for target in pending.drain(..) {
264 if target.verify()? == IdentityCheck::Same {
265 still.push(target);
266 } else {
267 report.terminated.push(target.pid());
268 }
269 }
270 *pending = still;
271 let expired = deadline.is_none_or(|limit| Instant::now() >= limit);
272 if pending.is_empty() || expired {
273 return Ok(());
274 }
275 thread::sleep(SWEEP_POLL);
276 }
277}
278
279mod sys {
280 use super::StartToken;
281 #[cfg(not(windows))]
282 use super::{AdoptedIdentity, AdoptedSignal, SignalResult};
283 #[cfg(not(windows))]
284 use std::io;
285 use std::path::PathBuf;
286
287 pub(super) struct Seen {
289 pub(super) start: StartToken,
290 pub(super) exe: Option<PathBuf>,
291 pub(super) parent: Option<u32>,
294 }
295
296 #[cfg(unix)]
297 pub(super) fn read(pid: u32) -> io::Result<Option<Seen>> {
298 Ok(crate::identity::describe(pid)?.map(|identity| Seen {
299 start: StartToken {
300 major: identity.started_sec,
301 minor: identity.started_usec,
302 },
303 exe: exe_path(pid),
304 parent: Some(identity.parent),
305 }))
306 }
307
308 #[cfg(target_os = "macos")]
309 fn exe_path(pid: u32) -> Option<PathBuf> {
310 use std::os::unix::ffi::OsStringExt;
311 let mut buffer = vec![0u8; 4096];
313 let written = unsafe {
315 libc::proc_pidpath(
316 pid as libc::c_int,
317 buffer.as_mut_ptr().cast(),
318 buffer.len() as u32,
319 )
320 };
321 if written <= 0 {
322 return None;
323 }
324 buffer.truncate(written as usize);
325 Some(PathBuf::from(std::ffi::OsString::from_vec(buffer)))
326 }
327
328 #[cfg(any(target_os = "linux", target_os = "android"))]
329 fn exe_path(pid: u32) -> Option<PathBuf> {
330 std::fs::read_link(format!("/proc/{pid}/exe")).ok()
331 }
332
333 #[cfg(all(
334 unix,
335 not(any(target_os = "macos", target_os = "linux", target_os = "android"))
336 ))]
337 fn exe_path(_pid: u32) -> Option<PathBuf> {
338 None
339 }
340
341 #[cfg(unix)]
342 pub(super) fn signal(
343 identity: &AdoptedIdentity,
344 signal: AdoptedSignal,
345 ) -> io::Result<SignalResult> {
346 use super::IdentityCheck;
347 match identity.verify()? {
348 IdentityCheck::Gone => return Ok(SignalResult::Gone),
349 IdentityCheck::Replaced => return Ok(SignalResult::IdentityMismatch),
350 IdentityCheck::Same => {}
351 }
352 let pid = i32::try_from(identity.pid()).map_err(io::Error::other)?;
353 let number = match signal {
354 AdoptedSignal::Graceful => libc::SIGTERM,
355 AdoptedSignal::Force => libc::SIGKILL,
356 };
357 if unsafe { libc::kill(pid, number) } == 0 {
360 return Ok(SignalResult::Delivered);
361 }
362 let error = io::Error::last_os_error();
363 match error.raw_os_error() {
364 Some(libc::ESRCH) => Ok(SignalResult::Gone),
365 _ => Err(error),
366 }
367 }
368
369 #[cfg(unix)]
371 pub(super) fn process_pairs() -> io::Result<Vec<(u32, u32)>> {
372 let mut pairs = Vec::new();
373 for pid in list_pids()? {
374 if let Ok(Some(identity)) = crate::identity::describe(pid) {
375 pairs.push((pid, identity.parent));
376 }
377 }
378 Ok(pairs)
379 }
380
381 #[cfg(target_os = "macos")]
382 fn list_pids() -> io::Result<Vec<u32>> {
383 const PROC_ALL_PIDS: u32 = 1;
385 let size = std::mem::size_of::<libc::pid_t>();
386 let needed = unsafe { libc::proc_listpids(PROC_ALL_PIDS, 0, std::ptr::null_mut(), 0) };
388 if needed <= 0 {
389 return Err(io::Error::last_os_error());
390 }
391 let mut pids = vec![0 as libc::pid_t; needed as usize / size + 64];
393 let written = unsafe {
395 libc::proc_listpids(
396 PROC_ALL_PIDS,
397 0,
398 pids.as_mut_ptr().cast(),
399 (pids.len() * size) as libc::c_int,
400 )
401 };
402 if written <= 0 {
403 return Err(io::Error::last_os_error());
404 }
405 pids.truncate(written as usize / size);
406 Ok(pids
407 .into_iter()
408 .filter(|pid| *pid > 0)
409 .map(|pid| pid as u32)
410 .collect())
411 }
412
413 #[cfg(any(target_os = "linux", target_os = "android"))]
414 fn list_pids() -> io::Result<Vec<u32>> {
415 Ok(std::fs::read_dir("/proc")?
416 .flatten()
417 .filter_map(|entry| entry.file_name().to_str()?.parse::<u32>().ok())
418 .collect())
419 }
420
421 #[cfg(all(
422 unix,
423 not(any(target_os = "macos", target_os = "linux", target_os = "android"))
424 ))]
425 fn list_pids() -> io::Result<Vec<u32>> {
426 Err(io::Error::from(io::ErrorKind::Unsupported))
427 }
428
429 #[cfg(windows)]
430 mod windows_impl {
431 use super::super::{AdoptedIdentity, AdoptedSignal, SignalResult, StartToken};
432 use super::Seen;
433 use crate::process_windows::os_error;
434 use std::{io, path::PathBuf};
435 use windows::{
436 core::PWSTR,
437 Win32::{
438 Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, FILETIME, HANDLE},
439 System::{
440 Diagnostics::ToolHelp::{
441 CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
442 TH32CS_SNAPPROCESS,
443 },
444 Threading::{
445 GetProcessTimes, OpenProcess, QueryFullProcessImageNameW, TerminateProcess,
446 WaitForSingleObject, PROCESS_ACCESS_RIGHTS, PROCESS_NAME_WIN32,
447 PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SYNCHRONIZE, PROCESS_TERMINATE,
448 },
449 },
450 },
451 };
452
453 struct Owned(HANDLE);
455 impl Drop for Owned {
456 fn drop(&mut self) {
457 let _ = unsafe { CloseHandle(self.0) };
459 }
460 }
461
462 fn open(pid: u32, access: PROCESS_ACCESS_RIGHTS) -> io::Result<Option<Owned>> {
463 match unsafe { OpenProcess(access, false, pid) } {
465 Ok(handle) => Ok(Some(Owned(handle))),
466 Err(error) if error.code() == ERROR_INVALID_PARAMETER.to_hresult() => Ok(None),
467 Err(error) => Err(os_error(error)),
468 }
469 }
470
471 fn observe(process: &Owned) -> io::Result<Option<Seen>> {
473 if unsafe { WaitForSingleObject(process.0, 0) }.0 == 0 {
475 return Ok(None);
476 }
477 let (mut created, mut exited, mut kernel, mut user) = (
478 FILETIME::default(),
479 FILETIME::default(),
480 FILETIME::default(),
481 FILETIME::default(),
482 );
483 unsafe {
485 GetProcessTimes(process.0, &mut created, &mut exited, &mut kernel, &mut user)
486 }
487 .map_err(os_error)?;
488 let ticks =
489 (u64::from(created.dwHighDateTime) << 32) | u64::from(created.dwLowDateTime);
490 let mut buffer = vec![0u16; 32768];
491 let mut length = buffer.len() as u32;
492 let exe = unsafe {
494 QueryFullProcessImageNameW(
495 process.0,
496 PROCESS_NAME_WIN32,
497 PWSTR(buffer.as_mut_ptr()),
498 &mut length,
499 )
500 }
501 .ok()
502 .map(|()| PathBuf::from(String::from_utf16_lossy(&buffer[..length as usize])));
503 Ok(Some(Seen {
504 start: StartToken {
505 major: ticks,
506 minor: 0,
507 },
508 exe,
509 parent: None,
510 }))
511 }
512
513 pub(in super::super) fn read(pid: u32) -> io::Result<Option<Seen>> {
514 let access = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE;
515 match open(pid, access)? {
516 Some(process) => observe(&process),
517 None => Ok(None),
518 }
519 }
520
521 pub(in super::super) fn signal(
522 identity: &AdoptedIdentity,
523 _signal: AdoptedSignal,
524 ) -> io::Result<SignalResult> {
525 let access =
526 PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE | PROCESS_TERMINATE;
527 let Some(process) = open(identity.pid(), access)? else {
528 return Ok(SignalResult::Gone);
529 };
530 let Some(seen) = observe(&process)? else {
532 return Ok(SignalResult::Gone);
533 };
534 if !identity.matches(&seen) {
535 return Ok(SignalResult::IdentityMismatch);
536 }
537 unsafe { TerminateProcess(process.0, 1) }.map_err(os_error)?;
539 Ok(SignalResult::Delivered)
540 }
541
542 pub(in super::super) fn process_pairs() -> io::Result<Vec<(u32, u32)>> {
543 let snapshot = Owned(
545 unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.map_err(os_error)?,
546 );
547 let mut entry: PROCESSENTRY32W = unsafe { std::mem::zeroed() };
549 entry.dwSize = std::mem::size_of::<PROCESSENTRY32W>() as u32;
550 unsafe { Process32FirstW(snapshot.0, &mut entry) }.map_err(os_error)?;
552 let mut pairs = Vec::new();
553 loop {
554 pairs.push((entry.th32ProcessID, entry.th32ParentProcessID));
555 if unsafe { Process32NextW(snapshot.0, &mut entry) }.is_err() {
557 break;
558 }
559 }
560 Ok(pairs)
561 }
562 }
563
564 #[cfg(windows)]
565 pub(super) use windows_impl::{process_pairs, read, signal};
566
567 #[cfg(not(any(unix, windows)))]
568 pub(super) fn read(_pid: u32) -> io::Result<Option<Seen>> {
569 Err(io::Error::from(io::ErrorKind::Unsupported))
570 }
571 #[cfg(not(any(unix, windows)))]
572 pub(super) fn signal(_: &AdoptedIdentity, _: AdoptedSignal) -> io::Result<SignalResult> {
573 Err(io::Error::from(io::ErrorKind::Unsupported))
574 }
575 #[cfg(not(any(unix, windows)))]
576 pub(super) fn process_pairs() -> io::Result<Vec<(u32, u32)>> {
577 Err(io::Error::from(io::ErrorKind::Unsupported))
578 }
579}