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,
pinned_control: bool,
}
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 {
#[cfg(all(test, feature = "independent-spawn"))]
pub(crate) fn test_creation_time(&self) -> io::Result<u64> {
use windows_sys::Win32::{Foundation::FILETIME, System::Threading::GetProcessTimes};
let mut times = [FILETIME {
dwLowDateTime: 0,
dwHighDateTime: 0,
}; 4];
let result = unsafe {
let ptr = times.as_mut_ptr();
GetProcessTimes(self.handle, ptr, ptr.add(1), ptr.add(2), ptr.add(3))
};
if result == 0 {
return Err(io::Error::last_os_error());
}
Ok((u64::from(times[0].dwHighDateTime) << 32) | u64::from(times[0].dwLowDateTime))
}
#[cfg(feature = "independent-spawn")]
pub(crate) fn open_pinned(pid: u32) -> io::Result<Self> {
use windows_sys::Win32::System::Threading::PROCESS_SYNCHRONIZE;
if pid == 0 {
return Err(io::Error::from(io::ErrorKind::InvalidInput));
}
let handle = unsafe {
OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE | PROCESS_SYNCHRONIZE,
0,
pid,
)
};
if handle.is_null() {
return Err(io::Error::last_os_error());
}
Ok(Self {
pid,
handle,
pinned_control: true,
})
}
#[cfg(feature = "independent-spawn")]
pub(crate) fn terminate_pinned(&self) -> io::Result<()> {
if !self.pinned_control {
return Err(io::Error::from(io::ErrorKind::Unsupported));
}
if !self.is_alive() {
return Ok(());
}
if unsafe { TerminateProcess(self.handle, 1) } == 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
#[cfg(feature = "independent-spawn")]
pub(crate) fn outside_current_job(&self, deadline: std::time::Instant) -> io::Result<bool> {
use windows_sys::Win32::Foundation::ERROR_MORE_DATA;
use windows_sys::Win32::System::{
JobObjects::{
IsProcessInJob, JobObjectBasicProcessIdList, QueryInformationJobObject,
JOBOBJECT_BASIC_PROCESS_ID_LIST,
},
Threading::GetCurrentProcess,
};
let mut in_job = 0;
if unsafe { IsProcessInJob(GetCurrentProcess(), std::ptr::null_mut(), &mut in_job) } == 0 {
return Err(io::Error::last_os_error());
}
if !self.is_alive() {
return Err(io::Error::from(io::ErrorKind::NotFound));
}
if in_job == 0 {
return Ok(true);
}
let offset = std::mem::offset_of!(JOBOBJECT_BASIC_PROCESS_ID_LIST, ProcessIdList);
let mut capacity = 128_usize;
loop {
if std::time::Instant::now() >= deadline {
return Err(io::Error::from(io::ErrorKind::TimedOut));
}
if !self.is_alive() {
return Err(io::Error::from(io::ErrorKind::NotFound));
}
let bytes = offset + capacity * std::mem::size_of::<usize>();
let mut storage = vec![0_usize; bytes.div_ceil(std::mem::size_of::<usize>())];
let ok = unsafe {
QueryInformationJobObject(
std::ptr::null_mut(),
JobObjectBasicProcessIdList,
storage.as_mut_ptr().cast(),
bytes as u32,
std::ptr::null_mut(),
)
};
let error = io::Error::last_os_error();
if ok == 0 && error.raw_os_error() != Some(ERROR_MORE_DATA as i32) {
return Err(error);
}
let header = unsafe { &*storage.as_ptr().cast::<JOBOBJECT_BASIC_PROCESS_ID_LIST>() };
let count = header.NumberOfProcessIdsInList as usize;
if ok != 0 && header.NumberOfAssignedProcesses as usize == count && count <= capacity {
let ids = unsafe {
std::slice::from_raw_parts(
storage.as_ptr().cast::<u8>().add(offset).cast::<usize>(),
count,
)
};
if !ids.contains(&(std::process::id() as usize)) {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"caller job changed during placement verification",
));
}
if !self.is_alive() {
return Err(io::Error::from(io::ErrorKind::NotFound));
}
return Ok(!ids.contains(&(self.pid as usize)));
}
if capacity >= 65536 {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"caller job process list exceeds verification bound",
));
}
capacity *= 2;
}
}
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,
pinned_control: false,
})
}
pub fn pid(&self) -> u32 {
self.pid
}
pub fn is_alive(&self) -> bool {
if self.pinned_control {
return unsafe {
windows_sys::Win32::System::Threading::WaitForSingleObject(self.handle, 0)
} == windows_sys::Win32::Foundation::WAIT_TIMEOUT;
}
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"),
));
}
}