mod scan;
pub use scan::{
find_processes, sweep_matching, MatchSweepOptions, MatchSweepReport, MatchedProcess,
ProcessQuery,
};
use std::{
io,
num::NonZeroU32,
path::PathBuf,
thread,
time::{Duration, Instant, SystemTime},
};
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct StartToken {
pub major: u64,
pub minor: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AdoptedIdentity {
pid: NonZeroU32,
start: StartToken,
exe: Option<PathBuf>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IdentityCheck {
Same,
Gone,
Replaced,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AdoptedSignal {
Graceful,
Force,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Delivery {
Graceful,
Force,
Stop,
Continue,
}
impl From<AdoptedSignal> for Delivery {
fn from(signal: AdoptedSignal) -> Self {
match signal {
AdoptedSignal::Graceful => Self::Graceful,
AdoptedSignal::Force => Self::Force,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SignalResult {
Delivered,
Gone,
IdentityMismatch,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct SweepReport {
pub terminated: Vec<u32>,
pub gone: Vec<u32>,
pub refused: Vec<u32>,
pub survivors: Vec<u32>,
pub failed: Vec<u32>,
}
impl AdoptedIdentity {
pub fn capture(pid: NonZeroU32) -> io::Result<Option<Self>> {
Ok(sys::read(pid.get())?.map(|seen| Self {
pid,
start: seen.start,
exe: seen.exe,
}))
}
pub fn from_recorded(pid: NonZeroU32, start: StartToken, exe: Option<PathBuf>) -> Self {
Self { pid, start, exe }
}
pub fn pid(&self) -> u32 {
self.pid.get()
}
pub fn start_token(&self) -> StartToken {
self.start
}
pub fn exe(&self) -> Option<&std::path::Path> {
self.exe.as_deref()
}
fn matches(&self, seen: &sys::Seen) -> bool {
seen.start == self.start
}
pub fn from_recorded_unix_ms(
pid: NonZeroU32,
recorded_unix_ms: u64,
tolerance: Duration,
) -> io::Result<Option<Self>> {
let Some(current) = Self::capture(pid)? else {
return Ok(None);
};
let Some(started) = current.start_time() else {
return Ok(None);
};
let Some(recorded) =
SystemTime::UNIX_EPOCH.checked_add(Duration::from_millis(recorded_unix_ms))
else {
return Ok(None);
};
let gap = match started.duration_since(recorded) {
Ok(gap) => gap,
Err(early) => early.duration(),
};
Ok((gap <= tolerance).then_some(current))
}
pub fn start_time(&self) -> Option<SystemTime> {
sys::start_time(self.start)
}
pub fn verify(&self) -> io::Result<IdentityCheck> {
Ok(match sys::read(self.pid())? {
None => IdentityCheck::Gone,
Some(seen) if self.matches(&seen) => IdentityCheck::Same,
Some(_) => IdentityCheck::Replaced,
})
}
pub fn signal(&self, signal: AdoptedSignal) -> io::Result<SignalResult> {
if self.pid() == std::process::id() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"refusing to signal this process",
));
}
sys::signal(self, signal.into())
}
pub fn suspend(&self) -> io::Result<SignalResult> {
self.deliver(Delivery::Stop)
}
pub fn resume(&self) -> io::Result<SignalResult> {
self.deliver(Delivery::Continue)
}
fn deliver(&self, delivery: Delivery) -> io::Result<SignalResult> {
if self.pid() == std::process::id() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"refusing to signal this process",
));
}
sys::signal(self, delivery)
}
pub fn descendants(&self) -> io::Result<Vec<AdoptedIdentity>> {
if self.verify()? != IdentityCheck::Same {
return Ok(Vec::new());
}
let pairs = sys::process_pairs()?;
let mut found: Vec<AdoptedIdentity> = Vec::new();
let mut frontier = vec![(self.pid(), self.start)];
let mut next = 0;
while next < frontier.len() {
let (parent, parent_start) = frontier[next];
next += 1;
for &(child, claimed_parent) in &pairs {
if claimed_parent != parent
|| child == parent
|| child == self.pid()
|| frontier.iter().any(|(pid, _)| *pid == child)
{
continue;
}
let (Some(pid), Ok(Some(seen))) = (NonZeroU32::new(child), sys::read(child)) else {
continue;
};
if seen.parent.is_some_and(|actual| actual != parent) || seen.start < parent_start {
continue; }
frontier.push((child, seen.start));
found.push(AdoptedIdentity {
pid,
start: seen.start,
exe: seen.exe,
});
}
}
if self.verify()? != IdentityCheck::Same {
return Ok(Vec::new());
}
Ok(found)
}
}
const SWEEP_POLL: Duration = Duration::from_millis(20);
const FORCE_WAIT: Duration = Duration::from_millis(500);
pub fn sweep(targets: &[AdoptedIdentity], grace: Duration) -> io::Result<SweepReport> {
let mut report = SweepReport::default();
let mut pending: Vec<&AdoptedIdentity> = Vec::new();
for target in targets {
match target.signal(AdoptedSignal::Graceful) {
Ok(SignalResult::Delivered) => pending.push(target),
Ok(SignalResult::Gone) => report.gone.push(target.pid()),
Ok(SignalResult::IdentityMismatch) => report.refused.push(target.pid()),
Err(_) => report.failed.push(target.pid()),
}
}
drain(&mut pending, grace, &mut report)?;
let mut forced: Vec<&AdoptedIdentity> = Vec::new();
for target in pending {
match target.signal(AdoptedSignal::Force) {
Ok(SignalResult::Delivered) => forced.push(target),
Ok(SignalResult::Gone) => report.terminated.push(target.pid()),
Ok(SignalResult::IdentityMismatch) => report.refused.push(target.pid()),
Err(_) => report.failed.push(target.pid()),
}
}
drain(&mut forced, FORCE_WAIT, &mut report)?;
report
.survivors
.extend(forced.iter().map(|target| target.pid()));
Ok(report)
}
pub fn sweep_descendants(root: &AdoptedIdentity, grace: Duration) -> io::Result<SweepReport> {
let mut targets = root.descendants()?;
targets.reverse();
sweep(&targets, grace)
}
#[cfg(unix)]
pub fn suspend_current_process() -> io::Result<()> {
if unsafe { libc::raise(libc::SIGTSTP) } == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
#[cfg(not(unix))]
pub fn suspend_current_process() -> io::Result<()> {
Err(io::Error::from(io::ErrorKind::Unsupported))
}
#[cfg(unix)]
fn unix_signal_number(delivery: Delivery) -> libc::c_int {
match delivery {
Delivery::Graceful => libc::SIGTERM,
Delivery::Force => libc::SIGKILL,
Delivery::Stop => libc::SIGSTOP,
Delivery::Continue => libc::SIGCONT,
}
}
#[cfg(unix)]
pub(crate) fn session_groups(sid: NonZeroU32) -> io::Result<Vec<libc::pid_t>> {
let wanted = sid.get() as libc::pid_t;
let own_group = unsafe { libc::getpgrp() };
let mut groups: Vec<libc::pid_t> = Vec::new();
for pid in sys::list_pids()? {
let pid = pid as libc::pid_t;
if unsafe { libc::getsid(pid) } != wanted {
continue;
}
let group = unsafe { libc::getpgid(pid) };
if group > 0 && group != own_group && !groups.contains(&group) {
groups.push(group);
}
}
Ok(groups)
}
#[cfg(unix)]
pub fn signal_session(sid: NonZeroU32, signal: AdoptedSignal) -> io::Result<bool> {
signal_session_with(sid, signal.into())
}
#[cfg(unix)]
pub fn suspend_session(sid: NonZeroU32) -> io::Result<bool> {
signal_session_with(sid, Delivery::Stop)
}
#[cfg(unix)]
pub fn resume_session(sid: NonZeroU32) -> io::Result<bool> {
signal_session_with(sid, Delivery::Continue)
}
#[cfg(unix)]
fn signal_session_with(sid: NonZeroU32, delivery: Delivery) -> io::Result<bool> {
let number = unix_signal_number(delivery);
let mut reached = false;
for group in session_groups(sid)? {
if unsafe { libc::kill(-group, number) } == 0 {
reached = true;
continue;
}
let error = io::Error::last_os_error();
if error.raw_os_error() != Some(libc::ESRCH) {
return Err(error);
}
}
Ok(reached)
}
fn drain(
pending: &mut Vec<&AdoptedIdentity>,
wait: Duration,
report: &mut SweepReport,
) -> io::Result<()> {
let deadline = Instant::now().checked_add(wait);
loop {
let mut still = Vec::new();
for target in pending.drain(..) {
if target.verify()? == IdentityCheck::Same {
still.push(target);
} else {
report.terminated.push(target.pid());
}
}
*pending = still;
let expired = deadline.is_none_or(|limit| Instant::now() >= limit);
if pending.is_empty() || expired {
return Ok(());
}
thread::sleep(SWEEP_POLL);
}
}
mod sys {
use super::StartToken;
#[cfg(not(windows))]
use super::{AdoptedIdentity, SignalResult};
#[cfg(not(windows))]
use std::io;
use std::path::PathBuf;
pub(super) struct Seen {
pub(super) start: StartToken,
pub(super) exe: Option<PathBuf>,
pub(super) parent: Option<u32>,
}
#[cfg(target_os = "macos")]
pub(super) fn start_time(start: StartToken) -> Option<std::time::SystemTime> {
let micros = u32::try_from(start.minor).ok()?;
std::time::SystemTime::UNIX_EPOCH.checked_add(std::time::Duration::new(
start.major,
micros.checked_mul(1000)?,
))
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub(super) fn start_time(start: StartToken) -> Option<std::time::SystemTime> {
let hertz = u64::try_from(unsafe { libc::sysconf(libc::_SC_CLK_TCK) }).ok()?;
if hertz == 0 {
return None;
}
let boot = std::fs::read_to_string("/proc/stat")
.ok()?
.lines()
.find_map(|line| line.strip_prefix("btime "))
.and_then(|value| value.trim().parse::<u64>().ok())?;
let since_boot = std::time::Duration::new(
start.major / hertz,
u32::try_from((start.major % hertz) * 1_000_000_000 / hertz).ok()?,
);
std::time::SystemTime::UNIX_EPOCH
.checked_add(std::time::Duration::from_secs(boot))?
.checked_add(since_boot)
}
#[cfg(all(
unix,
not(any(target_os = "macos", target_os = "linux", target_os = "android"))
))]
pub(super) fn start_time(_start: StartToken) -> Option<std::time::SystemTime> {
None
}
#[cfg(windows)]
pub(super) fn start_time(start: StartToken) -> Option<std::time::SystemTime> {
const EPOCH_OFFSET_TICKS: u64 = 116_444_736_000_000_000;
let since_unix = start.major.checked_sub(EPOCH_OFFSET_TICKS)?;
std::time::SystemTime::UNIX_EPOCH.checked_add(std::time::Duration::new(
since_unix / 10_000_000,
u32::try_from((since_unix % 10_000_000) * 100).ok()?,
))
}
#[cfg(not(any(unix, windows)))]
pub(super) fn start_time(_start: StartToken) -> Option<std::time::SystemTime> {
None
}
#[cfg(unix)]
pub(super) fn read(pid: u32) -> io::Result<Option<Seen>> {
Ok(crate::identity::describe(pid)?.map(|identity| Seen {
start: StartToken {
major: identity.started_sec,
minor: identity.started_usec,
},
exe: exe_path(pid),
parent: Some(identity.parent),
}))
}
#[cfg(target_os = "macos")]
fn exe_path(pid: u32) -> Option<PathBuf> {
use std::os::unix::ffi::OsStringExt;
let mut buffer = vec![0u8; 4096];
let written = unsafe {
libc::proc_pidpath(
pid as libc::c_int,
buffer.as_mut_ptr().cast(),
buffer.len() as u32,
)
};
if written <= 0 {
return None;
}
buffer.truncate(written as usize);
Some(PathBuf::from(std::ffi::OsString::from_vec(buffer)))
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn exe_path(pid: u32) -> Option<PathBuf> {
std::fs::read_link(format!("/proc/{pid}/exe")).ok()
}
#[cfg(all(
unix,
not(any(target_os = "macos", target_os = "linux", target_os = "android"))
))]
fn exe_path(_pid: u32) -> Option<PathBuf> {
None
}
#[cfg(unix)]
pub(super) fn signal(
identity: &AdoptedIdentity,
signal: super::Delivery,
) -> io::Result<SignalResult> {
use super::IdentityCheck;
match identity.verify()? {
IdentityCheck::Gone => return Ok(SignalResult::Gone),
IdentityCheck::Replaced => return Ok(SignalResult::IdentityMismatch),
IdentityCheck::Same => {}
}
let pid = i32::try_from(identity.pid()).map_err(io::Error::other)?;
let number = super::unix_signal_number(signal);
if unsafe { libc::kill(pid, number) } == 0 {
return Ok(SignalResult::Delivered);
}
let error = io::Error::last_os_error();
match error.raw_os_error() {
Some(libc::ESRCH) => Ok(SignalResult::Gone),
_ => Err(error),
}
}
#[cfg(unix)]
pub(super) fn process_pairs() -> io::Result<Vec<(u32, u32)>> {
let mut pairs = Vec::new();
for pid in list_pids()? {
if let Ok(Some(identity)) = crate::identity::describe(pid) {
pairs.push((pid, identity.parent));
}
}
Ok(pairs)
}
#[cfg(target_os = "macos")]
pub(super) fn list_pids() -> io::Result<Vec<u32>> {
const PROC_ALL_PIDS: u32 = 1;
let size = std::mem::size_of::<libc::pid_t>();
let needed = unsafe { libc::proc_listpids(PROC_ALL_PIDS, 0, std::ptr::null_mut(), 0) };
if needed <= 0 {
return Err(io::Error::last_os_error());
}
let mut pids = vec![0 as libc::pid_t; needed as usize / size + 64];
let written = unsafe {
libc::proc_listpids(
PROC_ALL_PIDS,
0,
pids.as_mut_ptr().cast(),
(pids.len() * size) as libc::c_int,
)
};
if written <= 0 {
return Err(io::Error::last_os_error());
}
pids.truncate(written as usize / size);
Ok(pids
.into_iter()
.filter(|pid| *pid > 0)
.map(|pid| pid as u32)
.collect())
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub(super) fn list_pids() -> io::Result<Vec<u32>> {
Ok(std::fs::read_dir("/proc")?
.flatten()
.filter_map(|entry| entry.file_name().to_str()?.parse::<u32>().ok())
.collect())
}
#[cfg(all(
unix,
not(any(target_os = "macos", target_os = "linux", target_os = "android"))
))]
pub(super) fn list_pids() -> io::Result<Vec<u32>> {
Err(io::Error::from(io::ErrorKind::Unsupported))
}
#[cfg(windows)]
mod windows_impl {
use super::super::{AdoptedIdentity, SignalResult, StartToken};
use super::Seen;
use crate::process_windows::os_error;
use std::{io, path::PathBuf};
use windows::{
core::PWSTR,
Win32::{
Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, FILETIME, HANDLE},
System::{
Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
TH32CS_SNAPPROCESS,
},
Threading::{
GetProcessTimes, OpenProcess, QueryFullProcessImageNameW, TerminateProcess,
WaitForSingleObject, PROCESS_ACCESS_RIGHTS, PROCESS_NAME_WIN32,
PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SYNCHRONIZE, PROCESS_TERMINATE,
},
},
},
};
struct Owned(HANDLE);
impl Drop for Owned {
fn drop(&mut self) {
let _ = unsafe { CloseHandle(self.0) };
}
}
fn open(pid: u32, access: PROCESS_ACCESS_RIGHTS) -> io::Result<Option<Owned>> {
match unsafe { OpenProcess(access, false, pid) } {
Ok(handle) => Ok(Some(Owned(handle))),
Err(error) if error.code() == ERROR_INVALID_PARAMETER.to_hresult() => Ok(None),
Err(error) => Err(os_error(error)),
}
}
fn observe(process: &Owned) -> io::Result<Option<Seen>> {
if unsafe { WaitForSingleObject(process.0, 0) }.0 == 0 {
return Ok(None);
}
let (mut created, mut exited, mut kernel, mut user) = (
FILETIME::default(),
FILETIME::default(),
FILETIME::default(),
FILETIME::default(),
);
unsafe {
GetProcessTimes(process.0, &mut created, &mut exited, &mut kernel, &mut user)
}
.map_err(os_error)?;
let ticks =
(u64::from(created.dwHighDateTime) << 32) | u64::from(created.dwLowDateTime);
let mut buffer = vec![0u16; 32768];
let mut length = buffer.len() as u32;
let exe = unsafe {
QueryFullProcessImageNameW(
process.0,
PROCESS_NAME_WIN32,
PWSTR(buffer.as_mut_ptr()),
&mut length,
)
}
.ok()
.map(|()| PathBuf::from(String::from_utf16_lossy(&buffer[..length as usize])));
Ok(Some(Seen {
start: StartToken {
major: ticks,
minor: 0,
},
exe,
parent: None,
}))
}
pub(in super::super) fn read(pid: u32) -> io::Result<Option<Seen>> {
let access = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE;
match open(pid, access)? {
Some(process) => observe(&process),
None => Ok(None),
}
}
pub(in super::super) fn signal(
identity: &AdoptedIdentity,
signal: super::super::Delivery,
) -> io::Result<SignalResult> {
if matches!(
signal,
super::super::Delivery::Stop | super::super::Delivery::Continue
) {
return Err(io::Error::from(io::ErrorKind::Unsupported));
}
let access =
PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE | PROCESS_TERMINATE;
let Some(process) = open(identity.pid(), access)? else {
return Ok(SignalResult::Gone);
};
let Some(seen) = observe(&process)? else {
return Ok(SignalResult::Gone);
};
if !identity.matches(&seen) {
return Ok(SignalResult::IdentityMismatch);
}
unsafe { TerminateProcess(process.0, 1) }.map_err(os_error)?;
Ok(SignalResult::Delivered)
}
pub(in super::super) fn process_pairs() -> io::Result<Vec<(u32, u32)>> {
let snapshot = Owned(
unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.map_err(os_error)?,
);
let mut entry: PROCESSENTRY32W = unsafe { std::mem::zeroed() };
entry.dwSize = std::mem::size_of::<PROCESSENTRY32W>() as u32;
unsafe { Process32FirstW(snapshot.0, &mut entry) }.map_err(os_error)?;
let mut pairs = Vec::new();
loop {
pairs.push((entry.th32ProcessID, entry.th32ParentProcessID));
if unsafe { Process32NextW(snapshot.0, &mut entry) }.is_err() {
break;
}
}
Ok(pairs)
}
}
#[cfg(windows)]
pub(super) use windows_impl::{process_pairs, read, signal};
#[cfg(not(any(unix, windows)))]
pub(super) fn read(_pid: u32) -> io::Result<Option<Seen>> {
Err(io::Error::from(io::ErrorKind::Unsupported))
}
#[cfg(not(any(unix, windows)))]
pub(super) fn signal(_: &AdoptedIdentity, _: super::Delivery) -> io::Result<SignalResult> {
Err(io::Error::from(io::ErrorKind::Unsupported))
}
#[cfg(not(any(unix, windows)))]
pub(super) fn process_pairs() -> io::Result<Vec<(u32, u32)>> {
Err(io::Error::from(io::ErrorKind::Unsupported))
}
}
#[cfg(all(test, unix))]
mod suspend_resume_tests {
use super::{AdoptedIdentity, SignalResult};
use std::{
num::NonZeroU32,
process::{Command, Stdio},
thread,
time::Duration,
};
fn state(pid: u32) -> String {
let out = Command::new("ps")
.args(["-o", "stat=", "-p", &pid.to_string()])
.output()
.expect("ps");
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
fn wait_for(pid: u32, stopped: bool) -> bool {
for _ in 0..100 {
if state(pid).starts_with('T') == stopped {
return true;
}
thread::sleep(Duration::from_millis(20));
}
false
}
#[test]
fn suspend_then_resume_stops_and_continues_the_identified_process() {
let mut child = Command::new("sleep")
.arg("30")
.stdin(Stdio::null())
.spawn()
.expect("spawn sleep");
let pid = NonZeroU32::new(child.id()).unwrap();
let identity = AdoptedIdentity::capture(pid)
.expect("capture")
.expect("identity");
assert_eq!(identity.suspend().unwrap(), SignalResult::Delivered);
assert!(wait_for(pid.get(), true), "process did not stop");
assert_eq!(identity.resume().unwrap(), SignalResult::Delivered);
assert!(wait_for(pid.get(), false), "process did not continue");
child.kill().expect("kill");
let _ = child.wait();
assert_eq!(identity.suspend().unwrap(), SignalResult::Gone);
}
#[test]
fn suspend_and_resume_refuse_this_process() {
let pid = NonZeroU32::new(std::process::id()).unwrap();
let identity = AdoptedIdentity::capture(pid)
.expect("capture")
.expect("identity");
assert_eq!(
identity.suspend().unwrap_err().kind(),
std::io::ErrorKind::InvalidInput
);
assert_eq!(
identity.resume().unwrap_err().kind(),
std::io::ErrorKind::InvalidInput
);
}
}