use std::io;
use std::path::PathBuf;
use windows_sys::Win32::Foundation::{
CloseHandle, ERROR_INVALID_PARAMETER, WAIT_FAILED, WAIT_OBJECT_0, WAIT_TIMEOUT,
};
use windows_sys::Win32::System::Threading::{
GetExitCodeProcess, OpenProcess, QueryFullProcessImageNameW, TerminateProcess,
WaitForSingleObject, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_TERMINATE,
};
use super::process_exit_watch::{has_already_exited, KernelHandle, SYNCHRONIZE};
use crate::platform::process::{ProcessId, ProcessInspectError, ProcessInspectErrorKind};
const STILL_ACTIVE: u32 = 259;
pub struct ProcessLiveness {
pid: u32,
process: KernelHandle,
waitable: bool,
}
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 pid = ProcessId::new(pid)?.get();
let wide =
unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, 0, pid) };
if !wide.is_null() {
return Ok(Self {
pid,
process: KernelHandle(wide),
waitable: true,
});
}
let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
if handle.is_null() {
let source = io::Error::last_os_error();
if source.raw_os_error() == Some(ERROR_INVALID_PARAMETER as i32) {
return Err(ProcessInspectError::stated(
ProcessInspectErrorKind::NotFound,
"no such process",
));
}
return Err(ProcessInspectError {
kind: ProcessInspectErrorKind::Host,
source,
});
}
Ok(Self {
pid,
process: KernelHandle(handle),
waitable: false,
})
}
pub fn pid(&self) -> u32 {
self.pid
}
pub fn is_alive(&self) -> bool {
if self.waitable {
return !has_already_exited(&self.process);
}
let mut exit_code = 0_u32;
let ok = unsafe { GetExitCodeProcess(self.process.0, &mut exit_code) };
ok != 0 && exit_code == STILL_ACTIVE
}
pub fn has_exited(&self) -> io::Result<bool> {
if self.waitable {
let waited = unsafe { WaitForSingleObject(self.process.0, 0) };
return match waited {
WAIT_OBJECT_0 => Ok(true),
WAIT_TIMEOUT => Ok(false),
WAIT_FAILED => Err(io::Error::last_os_error()),
other => Err(io::Error::other(format!(
"unexpected process wait result {other:#x}"
))),
};
}
let mut exit_code = 0_u32;
if unsafe { GetExitCodeProcess(self.process.0, &mut exit_code) } == 0 {
return Err(io::Error::last_os_error());
}
Ok(exit_code != STILL_ACTIVE)
}
}
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)))
}
#[allow(dead_code)] pub fn process_signal_terminate(_pid: u32) -> Result<(), ProcessInspectError> {
Err(ProcessInspectError::stated(
ProcessInspectErrorKind::Unsupported,
"this host has no graceful terminate signal",
))
}
#[allow(dead_code)] 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::InvalidPid);
}
#[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");
assert!(handle.has_exited().expect("observe exit"));
}
#[test]
fn has_exited_answers_through_both_handle_kinds() {
let me = ProcessLiveness::open(std::process::id()).expect("open self");
assert!(!me.has_exited().expect("observe self"));
let mut child = std::process::Command::new("cmd.exe")
.args(["/C", "exit 3"])
.spawn()
.expect("spawn");
let waitable = ProcessLiveness::open(child.id()).expect("open child");
let mut query_only = ProcessLiveness::open(child.id()).expect("open child");
query_only.waitable = false;
child.wait().expect("wait");
assert!(waitable.has_exited().expect("wait-based observation"));
assert!(query_only.has_exited().expect("exit-code observation"));
}
#[test]
fn a_child_that_exits_with_the_still_active_code_reports_dead() {
let mut child = std::process::Command::new("cmd.exe")
.args(["/C", "exit 259"])
.spawn()
.expect("spawn");
let handle = ProcessLiveness::open(child.id()).expect("open child");
let status = child.wait().expect("wait");
assert_eq!(
status.code(),
Some(STILL_ACTIVE as i32),
"the child must actually have exited with the ambiguous code"
);
assert!(
!handle.is_alive(),
"an exit code of 259 is an exit, not a running process"
);
}
#[test]
fn without_synchronize_the_still_active_code_is_ambiguous() {
let mut child = std::process::Command::new("cmd.exe")
.args(["/C", "exit 259"])
.spawn()
.expect("spawn");
let mut handle = ProcessLiveness::open(child.id()).expect("open child");
handle.waitable = false;
child.wait().expect("wait");
assert!(
handle.is_alive(),
"the fallback cannot tell 259 from STILL_ACTIVE, and says so"
);
assert!(!handle.has_exited().expect("exit-code observation"));
}
#[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"),
));
}
}