use std::io;
use std::ptr;
use std::sync::Arc;
use windows_sys::Win32::Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, HANDLE, WAIT_OBJECT_0};
use windows_sys::Win32::System::Threading::{
CreateEventW, GetExitCodeProcess, OpenProcess, SetEvent, WaitForMultipleObjects,
WaitForSingleObject, INFINITE, PROCESS_QUERY_LIMITED_INFORMATION,
};
use crate::platform::process::{
ProcessExitObservation, ProcessId, ProcessInspectError, ProcessInspectErrorKind,
ProcessSessionExit,
};
pub(crate) const SYNCHRONIZE: u32 = 0x0010_0000;
const WAIT_FAILED: u32 = 0xFFFF_FFFF;
pub struct ProcessExitWatch {
pid: ProcessId,
process: Arc<KernelHandle>,
}
impl std::fmt::Debug for ProcessExitWatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProcessExitWatch")
.field("pid", &self.pid)
.finish_non_exhaustive()
}
}
impl ProcessExitWatch {
pub fn open(pid: ProcessId) -> Result<Self, ProcessInspectError> {
let handle = unsafe {
OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE,
0,
pid.get(),
)
};
if handle.is_null() {
let source = io::Error::last_os_error();
let kind = match source.raw_os_error() {
Some(code) if code == ERROR_INVALID_PARAMETER as i32 => {
ProcessInspectErrorKind::NotFound
}
_ => ProcessInspectErrorKind::Host,
};
return Err(ProcessInspectError { kind, source });
}
let process = KernelHandle(handle);
if has_already_exited(&process) {
return Err(ProcessInspectError::stated(
ProcessInspectErrorKind::NotFound,
"no such process",
));
}
Ok(Self {
pid,
process: Arc::new(process),
})
}
#[must_use]
pub fn pid(&self) -> ProcessId {
self.pid
}
pub async fn exited(&self) -> Result<ProcessExitObservation, ProcessInspectError> {
let cancel = Arc::new(create_cancel_event()?);
let process = Arc::clone(&self.process);
let waiter = Arc::clone(&cancel);
let release = CancelOnDrop(cancel);
let outcome =
crate::async_engine::launch_blocking(move || wait_for_exit(&process, &waiter))
.await
.map_err(|error| ProcessInspectError {
kind: ProcessInspectErrorKind::Host,
source: io::Error::other(error.to_string()),
})?;
drop(release);
outcome
}
}
pub(crate) struct KernelHandle(pub(crate) HANDLE);
unsafe impl Send for KernelHandle {}
unsafe impl Sync for KernelHandle {}
impl Drop for KernelHandle {
fn drop(&mut self) {
unsafe {
CloseHandle(self.0);
}
}
}
struct CancelOnDrop(Arc<KernelHandle>);
impl Drop for CancelOnDrop {
fn drop(&mut self) {
unsafe {
SetEvent(self.0 .0);
}
}
}
pub(crate) fn has_already_exited(process: &KernelHandle) -> bool {
let waited = unsafe { WaitForSingleObject(process.0, 0) };
waited == WAIT_OBJECT_0
}
fn create_cancel_event() -> Result<KernelHandle, ProcessInspectError> {
let handle = unsafe { CreateEventW(ptr::null(), 1, 0, ptr::null()) };
if handle.is_null() {
return Err(ProcessInspectError::last_os_error(
ProcessInspectErrorKind::Host,
));
}
Ok(KernelHandle(handle))
}
fn wait_for_exit(
process: &KernelHandle,
cancel: &KernelHandle,
) -> Result<ProcessExitObservation, ProcessInspectError> {
let handles = [process.0, cancel.0];
let waited = unsafe { WaitForMultipleObjects(2, handles.as_ptr(), 0, INFINITE) };
if waited == WAIT_OBJECT_0 {
return Ok(exit_status(process));
}
if waited == WAIT_FAILED {
return Err(ProcessInspectError::last_os_error(
ProcessInspectErrorKind::Host,
));
}
Err(ProcessInspectError::stated(
ProcessInspectErrorKind::Host,
"the exit wait was cancelled",
))
}
fn exit_status(process: &KernelHandle) -> ProcessExitObservation {
let mut code = 0_u32;
let ok = unsafe { GetExitCodeProcess(process.0, &mut code) };
if ok == 0 {
return ProcessExitObservation::Unreported;
}
ProcessExitObservation::Reported(ProcessSessionExit::from_native(
Some(code as i32),
None,
code,
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_reaped_process_cannot_be_watched() {
let mut child = std::process::Command::new("cmd.exe")
.args(["/C", "exit 0"])
.spawn()
.expect("spawn");
let pid = ProcessId::new(child.id()).expect("child pid is in range");
child.wait().expect("reap");
let retained = ProcessExitWatch::open(pid).expect_err("an exited process has no exit left");
assert_eq!(retained.kind, ProcessInspectErrorKind::NotFound);
drop(child);
let released = ProcessExitWatch::open(pid).expect_err("a reaped pid is gone");
assert_eq!(released.kind, ProcessInspectErrorKind::NotFound);
}
#[test]
fn a_watch_can_be_opened_for_a_process_this_one_did_not_spawn() {
let watch = ProcessExitWatch::open(ProcessId::current()).expect("open self");
assert_eq!(watch.pid(), ProcessId::current());
}
}