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