//! Small Windows process bridge. The rest of the detection code stays safe Rust. #![allow(unsafe_code)] use std::{ io, path::Path, sync::{ Arc, atomic::{AtomicBool, Ordering}, mpsc::{self, Receiver}, }, thread::{self, JoinHandle}, }; use windows_sys::Win32::{ Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE, WAIT_OBJECT_0, WAIT_TIMEOUT}, System::{ Diagnostics::ToolHelp::{ CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW, TH32CS_SNAPPROCESS, }, Threading::{ OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SYNCHRONIZE, QueryFullProcessImageNameW, WaitForSingleObject, }, }, }; // The numeric handle can be transferred to the waiting thread; Windows process // handles are valid across threads. Ownership still has exactly one Drop. struct Handle(usize); impl Handle { fn raw(&self) -> HANDLE { self.0 as HANDLE } fn process(pid: u32) -> io::Result { // SAFETY: valid access flags and PID; ownership of the result is taken here. let raw = unsafe { OpenProcess( PROCESS_SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, 0, pid, ) }; if raw.is_null() { Err(io::Error::last_os_error()) } else { Ok(Self(raw as usize)) } } fn path(&self) -> io::Result { let mut buffer = vec![0u16; 32768]; let mut length = buffer.len() as u32; // SAFETY: the handle is owned, and the writable buffer has the stated capacity. if unsafe { QueryFullProcessImageNameW(self.raw(), 0, buffer.as_mut_ptr(), &mut length) } == 0 { return Err(io::Error::last_os_error()); } Ok(String::from_utf16_lossy(&buffer[..length as usize])) } } impl Drop for Handle { fn drop(&mut self) { // SAFETY: Handle owns a successful API result and closes it exactly once. unsafe { CloseHandle(self.raw()); } } } pub fn processes_in(directory: &Path) -> io::Result> { // SAFETY: flags request process enumeration; no caller pointers are involved. let raw = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }; if raw == INVALID_HANDLE_VALUE { return Err(io::Error::last_os_error()); } let snapshot = Handle(raw as usize); let mut entry = PROCESSENTRY32W { dwSize: std::mem::size_of::() as u32, ..Default::default() }; let prefix = format!( "{}\\", directory.to_string_lossy().trim_end_matches(['\\', '/']) ) .to_lowercase(); let mut result = vec![]; // SAFETY: snapshot remains live and entry is correctly sized and writable. let mut found = unsafe { Process32FirstW(snapshot.raw(), &mut entry) } != 0; while found { // Access-denied system processes and processes exiting during enumeration // are expected and cannot belong to the selected installation. if let Ok(process) = Handle::process(entry.th32ProcessID) && let Ok(path) = process.path() && path.to_lowercase().starts_with(&prefix) && let Some(executable) = Path::new(&path).file_name().and_then(|s| s.to_str()) { result.push((entry.th32ProcessID, executable.into())); } // SAFETY: as for Process32FirstW; snapshot and entry are still live. found = unsafe { Process32NextW(snapshot.raw(), &mut entry) } != 0; } result.sort_by_key(|(pid, _)| *pid); Ok(result) } pub struct ExitWatch { events: Receiver>, cancel: Arc, thread: Option>, } impl ExitWatch { pub fn open(pid: u32, executable: &str) -> io::Result { let handle = Handle::process(pid)?; if !Path::new(&handle.path()?) .file_name() .is_some_and(|name| name.to_string_lossy().eq_ignore_ascii_case(executable)) { return Err(io::Error::other("Game exited during discovery")); } let (sender, events) = mpsc::channel(); let cancel = Arc::new(AtomicBool::new(false)); let cancelled = cancel.clone(); let thread = thread::Builder::new() .name("clipbin-game-exit".into()) .spawn(move || { while !cancelled.load(Ordering::Relaxed) { // SAFETY: the thread owns a live process handle with SYNCHRONIZE // access. A finite wait lets Drop cancel without leaking a waiter. let result = unsafe { WaitForSingleObject(handle.raw(), 250) }; match result { WAIT_OBJECT_0 => { let _ = sender.send(Ok(())); break; } WAIT_TIMEOUT => {} _ => { let _ = sender.send(Err(io::Error::last_os_error())); break; } } } })?; Ok(Self { events, cancel, thread: Some(thread), }) } pub fn exited(&self) -> io::Result { match self.events.try_recv() { Ok(result) => result.map(|()| true), Err(mpsc::TryRecvError::Empty) => Ok(false), Err(mpsc::TryRecvError::Disconnected) => Ok(true), } } } impl Drop for ExitWatch { fn drop(&mut self) { self.cancel.store(true, Ordering::Relaxed); if let Some(thread) = self.thread.take() { let _ = thread.join(); } } }