use std::io;
use std::path::PathBuf;
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE};
use windows_sys::Win32::System::Threading::{
GetExitCodeProcess, OpenProcess, QueryFullProcessImageNameW, TerminateProcess,
PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_TERMINATE,
};
use crate::platform::process::{ProcessInspectError, ProcessInspectErrorKind};
const STILL_ACTIVE: u32 = 259;
pub struct ProcessLiveness {
pid: u32,
handle: HANDLE,
}
unsafe impl Send for ProcessLiveness {}
unsafe impl Sync for ProcessLiveness {}
impl std::fmt::Debug for ProcessLiveness {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProcessLiveness")
.field("pid", &self.pid)
.finish_non_exhaustive()
}
}
impl ProcessLiveness {
pub fn open(pid: u32) -> Result<Self, ProcessInspectError> {
let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
if handle.is_null() {
return Err(ProcessInspectError::stated(
ProcessInspectErrorKind::NotFound,
"no such process",
));
}
Ok(Self { pid, handle })
}
pub fn pid(&self) -> u32 {
self.pid
}
pub fn is_alive(&self) -> bool {
let mut exit_code = 0_u32;
let ok = unsafe { GetExitCodeProcess(self.handle, &mut exit_code) };
ok != 0 && exit_code == STILL_ACTIVE
}
}
impl Drop for ProcessLiveness {
fn drop(&mut self) {
unsafe {
CloseHandle(self.handle);
}
}
}
pub fn process_executable_path(pid: u32) -> Result<PathBuf, io::Error> {
let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
if handle.is_null() {
return Err(io::Error::last_os_error());
}
let mut path = vec![0_u16; 32768];
let mut len = path.len() as u32;
let ok = unsafe { QueryFullProcessImageNameW(handle, 0, path.as_mut_ptr(), &mut len) };
let source = io::Error::last_os_error();
unsafe {
CloseHandle(handle);
}
if ok == 0 {
return Err(source);
}
path.truncate(len as usize);
Ok(PathBuf::from(String::from_utf16_lossy(&path)))
}
pub fn process_signal_terminate(_pid: u32) -> Result<(), ProcessInspectError> {
Err(ProcessInspectError::stated(
ProcessInspectErrorKind::Unsupported,
"this host has no graceful terminate signal",
))
}
pub fn process_force_kill(pid: u32) -> Result<(), ProcessInspectError> {
let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid) };
if handle.is_null() {
return Err(ProcessInspectError::stated(
ProcessInspectErrorKind::NotFound,
"no such process",
));
}
let ok = unsafe { TerminateProcess(handle, 1) };
let source = io::Error::last_os_error();
unsafe {
CloseHandle(handle);
}
if ok == 0 {
Err(ProcessInspectError {
kind: ProcessInspectErrorKind::Host,
source,
})
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pid_zero_is_never_valid() {
let error = ProcessLiveness::open(0).expect_err("pid 0");
assert_eq!(error.kind, ProcessInspectErrorKind::NotFound);
}
#[test]
fn this_process_is_alive_and_locatable() {
let me = std::process::id();
let handle = ProcessLiveness::open(me).expect("open self");
assert_eq!(handle.pid(), me);
assert!(handle.is_alive());
assert_eq!(
process_executable_path(me).expect("exe"),
std::env::current_exe().expect("current_exe")
);
}
#[test]
fn a_dead_process_reports_dead() {
let mut child = std::process::Command::new("cmd.exe")
.args(["/C", "exit 0"])
.spawn()
.expect("spawn");
let handle = ProcessLiveness::open(child.id()).expect("open child");
child.wait().expect("wait");
assert!(!handle.is_alive(), "an exited child must report dead");
}
#[test]
fn graceful_terminate_is_reported_unsupported() {
let error = process_signal_terminate(std::process::id()).expect_err("unsupported");
assert_eq!(error.kind, ProcessInspectErrorKind::Unsupported);
}
}
pub fn process_same_executable_path(actual: &std::path::Path, expected: &std::path::Path) -> bool {
comparable(actual) == comparable(expected)
}
fn comparable(path: &std::path::Path) -> String {
let path = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
let path = path.to_string_lossy().replace('\\', "/");
let path = path.strip_prefix("//?/").unwrap_or(&path);
path.to_ascii_lowercase()
}
#[cfg(test)]
mod path_tests {
use super::*;
use std::path::Path;
#[test]
fn spelling_differences_do_not_make_two_images() {
assert!(process_same_executable_path(
Path::new(r"C:\Windows\System32\cmd.exe"),
Path::new(r"c:\windows\system32\CMD.EXE"),
));
assert!(process_same_executable_path(
Path::new(r"\\?\C:\tmp\daemon.exe"),
Path::new(r"C:\tmp\daemon.exe"),
));
}
#[test]
fn different_images_are_still_different() {
assert!(!process_same_executable_path(
Path::new(r"C:\tmp\daemon.exe"),
Path::new(r"C:\tmp\other.exe"),
));
}
}