1mod scan;
36
37pub use scan::{
38 find_processes, sweep_matching, MatchSweepOptions, MatchSweepReport, MatchedProcess,
39 ProcessQuery,
40};
41
42use std::{
43 io,
44 num::NonZeroU32,
45 path::PathBuf,
46 thread,
47 time::{Duration, Instant, SystemTime},
48};
49
50#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
54pub struct StartToken {
55 pub major: u64,
56 pub minor: u64,
57}
58
59#[derive(Clone, Debug, Eq, PartialEq)]
62pub struct AdoptedIdentity {
63 pid: NonZeroU32,
64 start: StartToken,
65 exe: Option<PathBuf>,
66}
67
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub enum IdentityCheck {
71 Same,
73 Gone,
75 Replaced,
77}
78
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
82pub enum AdoptedSignal {
83 Graceful,
85 Force,
87}
88
89#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92enum Delivery {
93 Graceful,
94 Force,
95 Stop,
96 Continue,
97}
98
99impl From<AdoptedSignal> for Delivery {
100 fn from(signal: AdoptedSignal) -> Self {
101 match signal {
102 AdoptedSignal::Graceful => Self::Graceful,
103 AdoptedSignal::Force => Self::Force,
104 }
105 }
106}
107
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110pub enum SignalResult {
111 Delivered,
113 Gone,
115 IdentityMismatch,
117}
118
119#[derive(Clone, Debug, Default, Eq, PartialEq)]
121pub struct SweepReport {
122 pub terminated: Vec<u32>,
124 pub gone: Vec<u32>,
126 pub refused: Vec<u32>,
128 pub survivors: Vec<u32>,
130 pub failed: Vec<u32>,
132}
133
134impl AdoptedIdentity {
135 pub fn capture(pid: NonZeroU32) -> io::Result<Option<Self>> {
139 Ok(sys::read(pid.get())?.map(|seen| Self {
140 pid,
141 start: seen.start,
142 exe: seen.exe,
143 }))
144 }
145
146 pub fn from_recorded(pid: NonZeroU32, start: StartToken, exe: Option<PathBuf>) -> Self {
149 Self { pid, start, exe }
150 }
151
152 pub fn pid(&self) -> u32 {
153 self.pid.get()
154 }
155
156 pub fn start_token(&self) -> StartToken {
157 self.start
158 }
159
160 pub fn exe(&self) -> Option<&std::path::Path> {
161 self.exe.as_deref()
162 }
163
164 fn matches(&self, seen: &sys::Seen) -> bool {
165 seen.start == self.start
170 }
171
172 pub fn from_recorded_unix_ms(
179 pid: NonZeroU32,
180 recorded_unix_ms: u64,
181 tolerance: Duration,
182 ) -> io::Result<Option<Self>> {
183 let Some(current) = Self::capture(pid)? else {
184 return Ok(None);
185 };
186 let Some(started) = current.start_time() else {
187 return Ok(None);
188 };
189 let Some(recorded) =
190 SystemTime::UNIX_EPOCH.checked_add(Duration::from_millis(recorded_unix_ms))
191 else {
192 return Ok(None);
193 };
194 let gap = match started.duration_since(recorded) {
195 Ok(gap) => gap,
196 Err(early) => early.duration(),
197 };
198 Ok((gap <= tolerance).then_some(current))
199 }
200
201 pub fn start_time(&self) -> Option<SystemTime> {
205 sys::start_time(self.start)
206 }
207
208 pub fn verify(&self) -> io::Result<IdentityCheck> {
210 Ok(match sys::read(self.pid())? {
211 None => IdentityCheck::Gone,
212 Some(seen) if self.matches(&seen) => IdentityCheck::Same,
213 Some(_) => IdentityCheck::Replaced,
214 })
215 }
216
217 pub fn is_running(&self) -> io::Result<bool> {
222 Ok(self.verify()? == IdentityCheck::Same && !process_is_zombie(self.pid()))
223 }
224
225 pub fn confirmed_gone(&self) -> io::Result<bool> {
230 Ok(!self.is_running()?)
231 }
232
233 pub fn wait_confirmed_gone(&self, timeout: Duration) -> io::Result<bool> {
236 let deadline = Instant::now().checked_add(timeout);
237 loop {
238 if self.confirmed_gone()? {
239 return Ok(true);
240 }
241 let Some(deadline) = deadline else {
242 return Ok(false);
243 };
244 let remaining = deadline.saturating_duration_since(Instant::now());
245 if remaining.is_zero() {
246 return Ok(false);
247 }
248 thread::sleep(SWEEP_POLL.min(remaining));
249 }
250 }
251
252 pub fn quit_gracefully(&self, timeout: Duration) -> io::Result<bool> {
256 match self.signal(AdoptedSignal::Graceful)? {
257 SignalResult::Gone | SignalResult::IdentityMismatch => self.confirmed_gone(),
258 SignalResult::Delivered => self.wait_confirmed_gone(timeout),
259 }
260 }
261
262 pub fn signal(&self, signal: AdoptedSignal) -> io::Result<SignalResult> {
265 if self.pid() == std::process::id() {
266 return Err(io::Error::new(
267 io::ErrorKind::InvalidInput,
268 "refusing to signal this process",
269 ));
270 }
271 sys::signal(self, signal.into())
272 }
273
274 pub fn suspend(&self) -> io::Result<SignalResult> {
277 self.deliver(Delivery::Stop)
278 }
279
280 pub fn resume(&self) -> io::Result<SignalResult> {
283 self.deliver(Delivery::Continue)
284 }
285
286 fn deliver(&self, delivery: Delivery) -> io::Result<SignalResult> {
287 if self.pid() == std::process::id() {
288 return Err(io::Error::new(
289 io::ErrorKind::InvalidInput,
290 "refusing to signal this process",
291 ));
292 }
293 sys::signal(self, delivery)
294 }
295
296 pub fn descendants(&self) -> io::Result<Vec<AdoptedIdentity>> {
301 if self.verify()? != IdentityCheck::Same {
302 return Ok(Vec::new());
303 }
304 let pairs = sys::process_pairs()?;
305 let mut found: Vec<AdoptedIdentity> = Vec::new();
306 let mut frontier = vec![(self.pid(), self.start)];
308 let mut next = 0;
309 while next < frontier.len() {
310 let (parent, parent_start) = frontier[next];
311 next += 1;
312 for &(child, claimed_parent) in &pairs {
313 if claimed_parent != parent
314 || child == parent
315 || child == self.pid()
316 || frontier.iter().any(|(pid, _)| *pid == child)
317 {
318 continue;
319 }
320 let (Some(pid), Ok(Some(seen))) = (NonZeroU32::new(child), sys::read(child)) else {
321 continue;
322 };
323 if seen.parent.is_some_and(|actual| actual != parent) || seen.start < parent_start {
324 continue; }
326 frontier.push((child, seen.start));
327 found.push(AdoptedIdentity {
328 pid,
329 start: seen.start,
330 exe: seen.exe,
331 });
332 }
333 }
334 if self.verify()? != IdentityCheck::Same {
336 return Ok(Vec::new());
337 }
338 Ok(found)
339 }
340}
341
342const SWEEP_POLL: Duration = Duration::from_millis(20);
343const FORCE_WAIT: Duration = Duration::from_millis(500);
344
345pub fn sweep(targets: &[AdoptedIdentity], grace: Duration) -> io::Result<SweepReport> {
351 let mut report = SweepReport::default();
352 let mut pending: Vec<&AdoptedIdentity> = Vec::new();
353 for target in targets {
354 match target.signal(AdoptedSignal::Graceful) {
355 Ok(SignalResult::Delivered) => pending.push(target),
356 Ok(SignalResult::Gone) => report.gone.push(target.pid()),
357 Ok(SignalResult::IdentityMismatch) => report.refused.push(target.pid()),
358 Err(_) => report.failed.push(target.pid()),
359 }
360 }
361 drain(&mut pending, grace, &mut report)?;
362 let mut forced: Vec<&AdoptedIdentity> = Vec::new();
363 for target in pending {
364 match target.signal(AdoptedSignal::Force) {
365 Ok(SignalResult::Delivered) => forced.push(target),
366 Ok(SignalResult::Gone) => report.terminated.push(target.pid()),
367 Ok(SignalResult::IdentityMismatch) => report.refused.push(target.pid()),
368 Err(_) => report.failed.push(target.pid()),
369 }
370 }
371 drain(&mut forced, FORCE_WAIT, &mut report)?;
372 report
373 .survivors
374 .extend(forced.iter().map(|target| target.pid()));
375 Ok(report)
376}
377
378pub fn sweep_descendants(root: &AdoptedIdentity, grace: Duration) -> io::Result<SweepReport> {
380 let mut targets = root.descendants()?;
381 targets.reverse();
382 sweep(&targets, grace)
383}
384
385#[cfg(any(target_os = "linux", target_os = "android"))]
390pub(crate) fn process_is_zombie(pid: u32) -> bool {
391 let Ok(text) = std::fs::read_to_string(format!("/proc/{pid}/stat")) else {
392 return false;
393 };
394 text.rsplit_once(')')
396 .is_some_and(|(_, rest)| matches!(rest.trim_start().chars().next(), Some('Z' | 'X' | 'x')))
397}
398
399#[cfg(target_os = "macos")]
400pub(crate) fn process_is_zombie(pid: u32) -> bool {
401 const SZOMB: u32 = 5;
403 let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() };
405 let size = std::mem::size_of::<libc::proc_bsdinfo>() as libc::c_int;
406 let written = unsafe {
408 libc::proc_pidinfo(
409 pid as libc::pid_t,
410 libc::PROC_PIDTBSDINFO,
411 1,
412 (&mut info as *mut libc::proc_bsdinfo).cast(),
413 size,
414 )
415 };
416 written == size && info.pbi_pid == pid && info.pbi_status == SZOMB
417}
418
419#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
420pub(crate) fn process_is_zombie(_pid: u32) -> bool {
421 false
422}
423
424#[cfg(unix)]
432pub fn process_group_exists(pgid: NonZeroU32) -> io::Result<bool> {
433 let group = check_group_id(pgid)?;
434 loop {
435 if unsafe { libc::killpg(group, 0) } == 0 {
437 return Ok(true);
438 }
439 let error = io::Error::last_os_error();
440 match error.raw_os_error() {
441 Some(libc::EINTR) => continue,
442 Some(libc::EPERM) => return Ok(true),
443 Some(libc::ESRCH) => return Ok(false),
444 _ => return Err(error),
445 }
446 }
447}
448
449#[cfg(not(unix))]
451pub fn process_group_exists(_pgid: NonZeroU32) -> io::Result<bool> {
452 Err(io::Error::from(io::ErrorKind::Unsupported))
453}
454
455pub fn wait_process_group_empty(pgid: NonZeroU32, timeout: Duration) -> io::Result<bool> {
458 #[cfg(not(unix))]
459 {
460 let _ = timeout;
461 process_group_exists(pgid).map(|exists| !exists)
463 }
464 #[cfg(unix)]
465 {
466 let deadline = Instant::now().checked_add(timeout);
467 loop {
468 if !process_group_exists(pgid)? {
469 return Ok(true);
470 }
471 let Some(deadline) = deadline else {
472 return Ok(false);
473 };
474 let remaining = deadline.saturating_duration_since(Instant::now());
475 if remaining.is_zero() {
476 return Ok(false);
477 }
478 thread::sleep(SWEEP_POLL.min(remaining));
479 }
480 }
481}
482
483#[cfg(unix)]
486pub(crate) fn check_group_id(pgid: NonZeroU32) -> io::Result<libc::pid_t> {
487 if pgid.get() == 1 {
488 return Err(io::Error::new(
489 io::ErrorKind::InvalidInput,
490 "refusing to observe process group 1",
491 ));
492 }
493 libc::pid_t::try_from(pgid.get()).map_err(io::Error::other)
494}
495
496#[cfg(unix)]
500pub fn suspend_current_process() -> io::Result<()> {
501 if unsafe { libc::raise(libc::SIGTSTP) } == 0 {
503 Ok(())
504 } else {
505 Err(io::Error::last_os_error())
506 }
507}
508
509#[cfg(not(unix))]
511pub fn suspend_current_process() -> io::Result<()> {
512 Err(io::Error::from(io::ErrorKind::Unsupported))
513}
514
515#[cfg(unix)]
517fn unix_signal_number(delivery: Delivery) -> libc::c_int {
518 match delivery {
519 Delivery::Graceful => libc::SIGTERM,
520 Delivery::Force => libc::SIGKILL,
521 Delivery::Stop => libc::SIGSTOP,
522 Delivery::Continue => libc::SIGCONT,
523 }
524}
525
526#[cfg(unix)]
530pub(crate) fn session_groups(sid: NonZeroU32) -> io::Result<Vec<libc::pid_t>> {
531 let wanted = sid.get() as libc::pid_t;
532 let own_group = unsafe { libc::getpgrp() };
534 let mut groups: Vec<libc::pid_t> = Vec::new();
535 for pid in sys::list_pids()? {
536 let pid = pid as libc::pid_t;
537 if unsafe { libc::getsid(pid) } != wanted {
539 continue;
540 }
541 let group = unsafe { libc::getpgid(pid) };
542 if group > 0 && group != own_group && !groups.contains(&group) {
543 groups.push(group);
544 }
545 }
546 Ok(groups)
547}
548
549#[cfg(unix)]
555pub fn signal_session(sid: NonZeroU32, signal: AdoptedSignal) -> io::Result<bool> {
556 signal_session_with(sid, signal.into())
557}
558
559#[cfg(unix)]
562pub fn suspend_session(sid: NonZeroU32) -> io::Result<bool> {
563 signal_session_with(sid, Delivery::Stop)
564}
565
566#[cfg(unix)]
569pub fn resume_session(sid: NonZeroU32) -> io::Result<bool> {
570 signal_session_with(sid, Delivery::Continue)
571}
572
573#[cfg(unix)]
574fn signal_session_with(sid: NonZeroU32, delivery: Delivery) -> io::Result<bool> {
575 let number = unix_signal_number(delivery);
576 let mut reached = false;
577 for group in session_groups(sid)? {
578 if unsafe { libc::kill(-group, number) } == 0 {
580 reached = true;
581 continue;
582 }
583 let error = io::Error::last_os_error();
584 if error.raw_os_error() != Some(libc::ESRCH) {
585 return Err(error);
586 }
587 }
588 Ok(reached)
589}
590
591fn drain(
594 pending: &mut Vec<&AdoptedIdentity>,
595 wait: Duration,
596 report: &mut SweepReport,
597) -> io::Result<()> {
598 let deadline = Instant::now().checked_add(wait);
599 loop {
600 let mut still = Vec::new();
601 for target in pending.drain(..) {
602 if target.is_running()? {
606 still.push(target);
607 } else {
608 report.terminated.push(target.pid());
609 }
610 }
611 *pending = still;
612 let expired = deadline.is_none_or(|limit| Instant::now() >= limit);
613 if pending.is_empty() || expired {
614 return Ok(());
615 }
616 thread::sleep(SWEEP_POLL);
617 }
618}
619
620mod sys {
621 use super::StartToken;
622 #[cfg(not(windows))]
623 use super::{AdoptedIdentity, SignalResult};
624 #[cfg(not(windows))]
625 use std::io;
626 use std::path::PathBuf;
627
628 pub(super) struct Seen {
630 pub(super) start: StartToken,
631 pub(super) exe: Option<PathBuf>,
632 pub(super) parent: Option<u32>,
635 }
636
637 #[cfg(target_os = "macos")]
639 pub(super) fn start_time(start: StartToken) -> Option<std::time::SystemTime> {
640 let micros = u32::try_from(start.minor).ok()?;
642 std::time::SystemTime::UNIX_EPOCH.checked_add(std::time::Duration::new(
643 start.major,
644 micros.checked_mul(1000)?,
645 ))
646 }
647
648 #[cfg(any(target_os = "linux", target_os = "android"))]
650 pub(super) fn start_time(start: StartToken) -> Option<std::time::SystemTime> {
651 let hertz = u64::try_from(unsafe { libc::sysconf(libc::_SC_CLK_TCK) }).ok()?;
653 if hertz == 0 {
654 return None;
655 }
656 let boot = std::fs::read_to_string("/proc/stat")
657 .ok()?
658 .lines()
659 .find_map(|line| line.strip_prefix("btime "))
660 .and_then(|value| value.trim().parse::<u64>().ok())?;
661 let since_boot = std::time::Duration::new(
662 start.major / hertz,
663 u32::try_from((start.major % hertz) * 1_000_000_000 / hertz).ok()?,
664 );
665 std::time::SystemTime::UNIX_EPOCH
666 .checked_add(std::time::Duration::from_secs(boot))?
667 .checked_add(since_boot)
668 }
669
670 #[cfg(all(
671 unix,
672 not(any(target_os = "macos", target_os = "linux", target_os = "android"))
673 ))]
674 pub(super) fn start_time(_start: StartToken) -> Option<std::time::SystemTime> {
675 None
676 }
677
678 #[cfg(windows)]
680 pub(super) fn start_time(start: StartToken) -> Option<std::time::SystemTime> {
681 const EPOCH_OFFSET_TICKS: u64 = 116_444_736_000_000_000;
683 let since_unix = start.major.checked_sub(EPOCH_OFFSET_TICKS)?;
684 std::time::SystemTime::UNIX_EPOCH.checked_add(std::time::Duration::new(
685 since_unix / 10_000_000,
686 u32::try_from((since_unix % 10_000_000) * 100).ok()?,
687 ))
688 }
689
690 #[cfg(not(any(unix, windows)))]
691 pub(super) fn start_time(_start: StartToken) -> Option<std::time::SystemTime> {
692 None
693 }
694
695 #[cfg(unix)]
696 pub(super) fn read(pid: u32) -> io::Result<Option<Seen>> {
697 Ok(crate::identity::describe(pid)?.map(|identity| Seen {
698 start: StartToken {
699 major: identity.started_sec,
700 minor: identity.started_usec,
701 },
702 exe: exe_path(pid),
703 parent: Some(identity.parent),
704 }))
705 }
706
707 #[cfg(target_os = "macos")]
708 fn exe_path(pid: u32) -> Option<PathBuf> {
709 use std::os::unix::ffi::OsStringExt;
710 let mut buffer = vec![0u8; 4096];
712 let written = unsafe {
714 libc::proc_pidpath(
715 pid as libc::c_int,
716 buffer.as_mut_ptr().cast(),
717 buffer.len() as u32,
718 )
719 };
720 if written <= 0 {
721 return None;
722 }
723 buffer.truncate(written as usize);
724 Some(PathBuf::from(std::ffi::OsString::from_vec(buffer)))
725 }
726
727 #[cfg(any(target_os = "linux", target_os = "android"))]
728 fn exe_path(pid: u32) -> Option<PathBuf> {
729 std::fs::read_link(format!("/proc/{pid}/exe")).ok()
730 }
731
732 #[cfg(all(
733 unix,
734 not(any(target_os = "macos", target_os = "linux", target_os = "android"))
735 ))]
736 fn exe_path(_pid: u32) -> Option<PathBuf> {
737 None
738 }
739
740 #[cfg(unix)]
741 pub(super) fn signal(
742 identity: &AdoptedIdentity,
743 signal: super::Delivery,
744 ) -> io::Result<SignalResult> {
745 use super::IdentityCheck;
746 match identity.verify()? {
747 IdentityCheck::Gone => return Ok(SignalResult::Gone),
748 IdentityCheck::Replaced => return Ok(SignalResult::IdentityMismatch),
749 IdentityCheck::Same => {}
750 }
751 if super::process_is_zombie(identity.pid()) {
752 return Ok(SignalResult::Gone);
753 }
754 let pid = i32::try_from(identity.pid()).map_err(io::Error::other)?;
755 let number = super::unix_signal_number(signal);
756 if unsafe { libc::kill(pid, number) } == 0 {
759 return Ok(SignalResult::Delivered);
760 }
761 let error = io::Error::last_os_error();
762 match error.raw_os_error() {
763 Some(libc::ESRCH) => Ok(SignalResult::Gone),
764 _ => Err(error),
765 }
766 }
767
768 #[cfg(unix)]
770 pub(super) fn process_pairs() -> io::Result<Vec<(u32, u32)>> {
771 let mut pairs = Vec::new();
772 for pid in list_pids()? {
773 if let Ok(Some(identity)) = crate::identity::describe(pid) {
774 pairs.push((pid, identity.parent));
775 }
776 }
777 Ok(pairs)
778 }
779
780 #[cfg(target_os = "macos")]
781 pub(super) fn list_pids() -> io::Result<Vec<u32>> {
782 const PROC_ALL_PIDS: u32 = 1;
784 let size = std::mem::size_of::<libc::pid_t>();
785 let needed = unsafe { libc::proc_listpids(PROC_ALL_PIDS, 0, std::ptr::null_mut(), 0) };
787 if needed <= 0 {
788 return Err(io::Error::last_os_error());
789 }
790 let mut pids = vec![0 as libc::pid_t; needed as usize / size + 64];
792 let written = unsafe {
794 libc::proc_listpids(
795 PROC_ALL_PIDS,
796 0,
797 pids.as_mut_ptr().cast(),
798 (pids.len() * size) as libc::c_int,
799 )
800 };
801 if written <= 0 {
802 return Err(io::Error::last_os_error());
803 }
804 pids.truncate(written as usize / size);
805 Ok(pids
806 .into_iter()
807 .filter(|pid| *pid > 0)
808 .map(|pid| pid as u32)
809 .collect())
810 }
811
812 #[cfg(any(target_os = "linux", target_os = "android"))]
813 pub(super) fn list_pids() -> io::Result<Vec<u32>> {
814 Ok(std::fs::read_dir("/proc")?
815 .flatten()
816 .filter_map(|entry| entry.file_name().to_str()?.parse::<u32>().ok())
817 .collect())
818 }
819
820 #[cfg(all(
821 unix,
822 not(any(target_os = "macos", target_os = "linux", target_os = "android"))
823 ))]
824 pub(super) fn list_pids() -> io::Result<Vec<u32>> {
825 Err(io::Error::from(io::ErrorKind::Unsupported))
826 }
827
828 #[cfg(windows)]
829 mod windows_impl {
830 use super::super::{AdoptedIdentity, SignalResult, StartToken};
831 use super::Seen;
832 use crate::process_windows::os_error;
833 use std::{io, path::PathBuf};
834 use windows::{
835 core::PWSTR,
836 Win32::{
837 Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, FILETIME, HANDLE},
838 System::{
839 Diagnostics::ToolHelp::{
840 CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
841 TH32CS_SNAPPROCESS,
842 },
843 Threading::{
844 GetProcessTimes, OpenProcess, QueryFullProcessImageNameW, TerminateProcess,
845 WaitForSingleObject, PROCESS_ACCESS_RIGHTS, PROCESS_NAME_WIN32,
846 PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SYNCHRONIZE, PROCESS_TERMINATE,
847 },
848 },
849 },
850 };
851
852 struct Owned(HANDLE);
854 impl Drop for Owned {
855 fn drop(&mut self) {
856 let _ = unsafe { CloseHandle(self.0) };
858 }
859 }
860
861 fn open(pid: u32, access: PROCESS_ACCESS_RIGHTS) -> io::Result<Option<Owned>> {
862 match unsafe { OpenProcess(access, false, pid) } {
864 Ok(handle) => Ok(Some(Owned(handle))),
865 Err(error) if error.code() == ERROR_INVALID_PARAMETER.to_hresult() => Ok(None),
866 Err(error) => Err(os_error(error)),
867 }
868 }
869
870 fn observe(process: &Owned) -> io::Result<Option<Seen>> {
872 if unsafe { WaitForSingleObject(process.0, 0) }.0 == 0 {
874 return Ok(None);
875 }
876 let (mut created, mut exited, mut kernel, mut user) = (
877 FILETIME::default(),
878 FILETIME::default(),
879 FILETIME::default(),
880 FILETIME::default(),
881 );
882 unsafe {
884 GetProcessTimes(process.0, &mut created, &mut exited, &mut kernel, &mut user)
885 }
886 .map_err(os_error)?;
887 let ticks =
888 (u64::from(created.dwHighDateTime) << 32) | u64::from(created.dwLowDateTime);
889 let mut buffer = vec![0u16; 32768];
890 let mut length = buffer.len() as u32;
891 let exe = unsafe {
893 QueryFullProcessImageNameW(
894 process.0,
895 PROCESS_NAME_WIN32,
896 PWSTR(buffer.as_mut_ptr()),
897 &mut length,
898 )
899 }
900 .ok()
901 .map(|()| PathBuf::from(String::from_utf16_lossy(&buffer[..length as usize])));
902 Ok(Some(Seen {
903 start: StartToken {
904 major: ticks,
905 minor: 0,
906 },
907 exe,
908 parent: None,
909 }))
910 }
911
912 pub(in super::super) fn read(pid: u32) -> io::Result<Option<Seen>> {
913 let access = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE;
914 match open(pid, access)? {
915 Some(process) => observe(&process),
916 None => Ok(None),
917 }
918 }
919
920 pub(in super::super) fn signal(
921 identity: &AdoptedIdentity,
922 signal: super::super::Delivery,
923 ) -> io::Result<SignalResult> {
924 if matches!(
925 signal,
926 super::super::Delivery::Stop | super::super::Delivery::Continue
927 ) {
928 return Err(io::Error::from(io::ErrorKind::Unsupported));
930 }
931 let access =
932 PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE | PROCESS_TERMINATE;
933 let Some(process) = open(identity.pid(), access)? else {
934 return Ok(SignalResult::Gone);
935 };
936 let Some(seen) = observe(&process)? else {
938 return Ok(SignalResult::Gone);
939 };
940 if !identity.matches(&seen) {
941 return Ok(SignalResult::IdentityMismatch);
942 }
943 unsafe { TerminateProcess(process.0, 1) }.map_err(os_error)?;
945 Ok(SignalResult::Delivered)
946 }
947
948 pub(in super::super) fn process_pairs() -> io::Result<Vec<(u32, u32)>> {
949 let snapshot = Owned(
951 unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.map_err(os_error)?,
952 );
953 let mut entry: PROCESSENTRY32W = unsafe { std::mem::zeroed() };
955 entry.dwSize = std::mem::size_of::<PROCESSENTRY32W>() as u32;
956 unsafe { Process32FirstW(snapshot.0, &mut entry) }.map_err(os_error)?;
958 let mut pairs = Vec::new();
959 loop {
960 pairs.push((entry.th32ProcessID, entry.th32ParentProcessID));
961 if unsafe { Process32NextW(snapshot.0, &mut entry) }.is_err() {
963 break;
964 }
965 }
966 Ok(pairs)
967 }
968 }
969
970 #[cfg(windows)]
971 pub(super) use windows_impl::{process_pairs, read, signal};
972
973 #[cfg(not(any(unix, windows)))]
974 pub(super) fn read(_pid: u32) -> io::Result<Option<Seen>> {
975 Err(io::Error::from(io::ErrorKind::Unsupported))
976 }
977 #[cfg(not(any(unix, windows)))]
978 pub(super) fn signal(_: &AdoptedIdentity, _: super::Delivery) -> io::Result<SignalResult> {
979 Err(io::Error::from(io::ErrorKind::Unsupported))
980 }
981 #[cfg(not(any(unix, windows)))]
982 pub(super) fn process_pairs() -> io::Result<Vec<(u32, u32)>> {
983 Err(io::Error::from(io::ErrorKind::Unsupported))
984 }
985}
986
987#[cfg(all(test, unix))]
988mod suspend_resume_tests {
989 use super::{AdoptedIdentity, SignalResult};
990 use std::{
991 num::NonZeroU32,
992 process::{Command, Stdio},
993 thread,
994 time::Duration,
995 };
996
997 fn state(pid: u32) -> String {
998 let out = Command::new("ps")
999 .args(["-o", "stat=", "-p", &pid.to_string()])
1000 .output()
1001 .expect("ps");
1002 String::from_utf8_lossy(&out.stdout).trim().to_string()
1003 }
1004
1005 fn wait_for(pid: u32, stopped: bool) -> bool {
1006 for _ in 0..100 {
1007 if state(pid).starts_with('T') == stopped {
1008 return true;
1009 }
1010 thread::sleep(Duration::from_millis(20));
1011 }
1012 false
1013 }
1014
1015 #[test]
1016 fn suspend_then_resume_stops_and_continues_the_identified_process() {
1017 let mut child = Command::new("sleep")
1018 .arg("30")
1019 .stdin(Stdio::null())
1020 .spawn()
1021 .expect("spawn sleep");
1022 let pid = NonZeroU32::new(child.id()).unwrap();
1023 let identity = AdoptedIdentity::capture(pid)
1024 .expect("capture")
1025 .expect("identity");
1026 assert_eq!(identity.suspend().unwrap(), SignalResult::Delivered);
1027 assert!(wait_for(pid.get(), true), "process did not stop");
1028 assert_eq!(identity.resume().unwrap(), SignalResult::Delivered);
1029 assert!(wait_for(pid.get(), false), "process did not continue");
1030 child.kill().expect("kill");
1031 let _ = child.wait();
1032 assert_eq!(identity.suspend().unwrap(), SignalResult::Gone);
1033 }
1034
1035 #[test]
1036 fn suspend_and_resume_refuse_this_process() {
1037 let pid = NonZeroU32::new(std::process::id()).unwrap();
1038 let identity = AdoptedIdentity::capture(pid)
1039 .expect("capture")
1040 .expect("identity");
1041 assert_eq!(
1042 identity.suspend().unwrap_err().kind(),
1043 std::io::ErrorKind::InvalidInput
1044 );
1045 assert_eq!(
1046 identity.resume().unwrap_err().kind(),
1047 std::io::ErrorKind::InvalidInput
1048 );
1049 }
1050}