use std::sync::{
atomic::{AtomicU32, Ordering},
RwLock,
};
use windows::{
Win32::Foundation::{HANDLE, HWND},
Win32::Graphics::Gdi::HDC,
};
use crate::{
get_window_client_dc, get_window_dc,
handle::{close_handle, open_process_rw_handle},
hdc::{get_desktop_dc, release_dc, release_desktop_dc},
snapshot::get_process_pid,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DCMode {
Standard = 1,
WindowClient = 2,
Desktop = 3,
}
impl DCMode {
pub fn as_u8(&self) -> u8 {
*self as u8
}
pub fn from_u8(value: u8) -> Option<Self> {
match value {
1 => Some(DCMode::Standard),
2 => Some(DCMode::WindowClient),
3 => Some(DCMode::Desktop),
_ => None,
}
}
}
impl Default for DCMode {
fn default() -> Self {
DCMode::Standard
}
}
#[derive(Debug, Clone)]
pub enum ProcessError {
ProcessNotFound(String),
HandleOpenFailed(u32),
WindowNotFound(u32),
DCNotFound(HWND),
InvalidDCMode(u8),
}
impl std::fmt::Display for ProcessError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProcessError::ProcessNotFound(name) => write!(f, "Process '{}' not found", name),
ProcessError::HandleOpenFailed(pid) => {
write!(f, "Failed to open handle for PID {}", pid)
}
ProcessError::WindowNotFound(pid) => write!(f, "No window found for PID {}", pid),
ProcessError::DCNotFound(hwnd) => write!(f, "Failed to get DC for window {:?}", hwnd),
ProcessError::InvalidDCMode(mode) => {
write!(f, "Invalid DC mode: {} (must be 1, 2, or 3)", mode)
}
}
}
}
impl std::error::Error for ProcessError {}
pub type ProcessResult<T> = Result<T, ProcessError>;
pub type HwndFilter = Vec<(String, u8)>;
#[derive(Debug)]
pub struct ProcessBuilder {
name: String,
dc_mode: DCMode,
hwnd_filter: Option<HwndFilter>,
}
impl ProcessBuilder {
pub fn new(name: &str) -> Self {
Self {
name: name.to_owned(),
dc_mode: DCMode::default(),
hwnd_filter: None,
}
}
pub fn set_dc_mode(mut self, mode: DCMode) -> Self {
self.dc_mode = mode;
self
}
pub fn set_dc_mode_num(mut self, mode_value: u8) -> Self {
self.dc_mode = DCMode::from_u8(mode_value)
.unwrap_or_else(|| panic!("Invalid DC mode: {}. Must be 1, 2, or 3", mode_value));
self
}
pub fn try_set_dc_mode_num(mut self, mode_value: u8) -> Result<Self, Self> {
match DCMode::from_u8(mode_value) {
Some(mode) => {
self.dc_mode = mode;
Ok(self)
}
None => Err(self),
}
}
pub fn desktop_mode(self) -> Self {
self.set_dc_mode(DCMode::Desktop)
}
pub fn window_client_mode(self) -> Self {
self.set_dc_mode(DCMode::WindowClient)
}
pub fn standard_dc_mode(self) -> Self {
self.set_dc_mode(DCMode::Standard)
}
pub fn set_hwnd_filter(mut self, filter: HwndFilter) -> Self {
self.hwnd_filter = Some(filter);
self
}
pub fn clear_hwnd_filter(mut self) -> Self {
self.hwnd_filter = None;
self
}
pub fn build(self) -> Process {
Process {
name: self.name,
cached_pid: AtomicU32::new(0),
dc_mode: self.dc_mode,
hwnd_filter: self.hwnd_filter,
info: RwLock::new(None),
}
}
}
#[derive(Debug, Clone, Copy)]
struct ProcessInfo {
pid: u32,
hwnd: HWND,
handle: HANDLE,
hdc: HDC,
}
impl ProcessInfo {
fn is_valid(&self) -> bool {
self.pid != 0 && !self.handle.is_invalid()
}
fn cleanup(&mut self) {
if !self.handle.is_invalid() {
close_handle(self.handle);
self.handle = HANDLE::default();
}
if !self.hdc.0.is_null() && !self.hwnd.0.is_null() {
release_dc(self.hwnd, self.hdc);
self.hdc = HDC::default();
}
}
}
#[derive(Debug)]
pub struct Process {
name: String,
cached_pid: AtomicU32,
dc_mode: DCMode,
hwnd_filter: Option<HwndFilter>,
info: RwLock<Option<ProcessInfo>>,
}
unsafe impl Send for Process {}
unsafe impl Sync for Process {}
impl Process {
pub fn builder(name: &str) -> ProcessBuilder {
ProcessBuilder::new(name)
}
pub fn new(name: &str) -> Self {
Self::builder(name).build()
}
pub fn get_name(&self) -> &str {
&self.name
}
pub fn get_dc_mode(&self) -> DCMode {
self.dc_mode
}
pub fn get_dc_mode_value(&self) -> u8 {
self.dc_mode.as_u8()
}
pub fn get_hwnd_filter(&self) -> Option<&HwndFilter> {
self.hwnd_filter.as_ref()
}
pub fn init(&self) -> ProcessResult<()> {
let pid =
get_process_pid(&self.name).ok_or_else(|| ProcessError::ProcessNotFound(self.name.clone()))?;
let hwnd = match self.dc_mode {
DCMode::Desktop => {
HWND::default()
}
_ => {
if let Some(filter) = &self.hwnd_filter {
crate::hwnd::get_hwnd_by_pid_and_filter(pid, filter)
.ok_or_else(|| ProcessError::WindowNotFound(pid))?
} else {
crate::hwnd::get_hwnd_by_pid(pid).ok_or_else(|| ProcessError::WindowNotFound(pid))?
}
}
};
let handle =
open_process_rw_handle(pid).ok_or_else(|| ProcessError::HandleOpenFailed(pid))?;
let hdc = match self.dc_mode {
DCMode::Desktop => {
get_desktop_dc().ok_or_else(|| ProcessError::DCNotFound(HWND::default()))?
}
DCMode::Standard => {
get_window_dc(hwnd).ok_or_else(|| ProcessError::DCNotFound(hwnd))?
}
DCMode::WindowClient => {
get_window_client_dc(hwnd).ok_or_else(|| ProcessError::DCNotFound(hwnd))?
}
};
let new_info = ProcessInfo {
pid,
hwnd,
handle,
hdc,
};
{
let mut info_lock = self.info.write().map_err(|_| {
close_handle(handle);
match self.dc_mode {
DCMode::Desktop => {
release_desktop_dc(hdc);
}
_ => {
release_dc(hwnd, hdc);
}
}
ProcessError::HandleOpenFailed(pid)
})?;
if let Some(old_info) = info_lock.as_mut() {
old_info.cleanup();
}
*info_lock = Some(new_info);
}
self.cached_pid.store(pid, Ordering::Release);
Ok(())
}
pub fn reinit(&self) -> ProcessResult<()> {
self.cleanup();
self.init()
}
pub fn cleanup(&self) {
if let Some(info) = self.info.write().ok().and_then(|mut lock| {
let info = lock.take();
drop(lock); info
}) {
match self.dc_mode {
DCMode::Desktop => {
if !info.hdc.0.is_null() {
crate::hdc::release_desktop_dc(info.hdc);
}
}
_ => {
if !info.hdc.0.is_null() && !info.hwnd.0.is_null() {
release_dc(info.hwnd, info.hdc);
}
}
}
if !info.handle.is_invalid() {
close_handle(info.handle);
}
}
self.cached_pid.store(0, Ordering::Release);
}
pub fn get_handle(&self) -> HANDLE {
self.info
.read()
.ok()
.and_then(|info| info.map(|i| i.handle))
.unwrap_or(HANDLE::default())
}
pub fn get_pid(&self) -> u32 {
self.cached_pid.load(Ordering::Acquire)
}
pub fn get_hwnd(&self) -> HWND {
self.info
.read()
.ok()
.and_then(|info| info.map(|i| i.hwnd))
.unwrap_or(HWND::default())
}
pub fn get_hdc(&self) -> HDC {
self.info
.read()
.ok()
.and_then(|info| info.map(|i| i.hdc))
.unwrap_or(HDC::default())
}
pub fn is_valid(&self) -> bool {
self.info
.read()
.ok()
.and_then(|info| info.as_ref().map(|i| i.is_valid()))
.unwrap_or(false)
}
}
impl Drop for Process {
fn drop(&mut self) {
self.cleanup();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_creation() {
let process = Process::new("test.exe");
assert_eq!(process.get_name(), "test.exe");
assert!(!process.is_valid());
assert_eq!(process.get_pid(), 0);
assert!(process.get_handle().is_invalid());
assert_eq!(process.get_hwnd(), HWND::default());
}
#[test]
fn test_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<Process>();
assert_sync::<Process>();
}
#[test]
fn test_init_nonexistent_process() {
let process = Process::new("definitely_not_existing_xyz123.exe");
let result = process.init();
assert!(result.is_err(), "Should fail for non-existent process");
if let Err(e) = result {
match e {
ProcessError::ProcessNotFound(name) => {
assert_eq!(name, "definitely_not_existing_xyz123.exe");
}
_ => panic!("Expected ProcessNotFound error"),
}
}
assert!(!process.is_valid());
}
#[test]
fn test_cleanup_without_init() {
let process = Process::new("test.exe");
process.cleanup();
assert!(!process.is_valid());
assert_eq!(process.get_pid(), 0);
}
#[test]
fn test_reinit_without_init() {
let process = Process::new("test.exe");
let result = process.reinit();
assert!(result.is_err());
}
#[test]
fn test_builder_simple() {
let process = Process::builder("notepad.exe").build();
assert_eq!(process.get_name(), "notepad.exe");
assert_eq!(process.get_dc_mode(), DCMode::Standard);
assert_eq!(process.get_hwnd_filter(), None);
assert!(!process.is_valid());
}
#[test]
fn test_builder_dc_mode_explicit() {
let process = Process::builder("app.exe")
.set_dc_mode(DCMode::Desktop)
.build();
assert_eq!(process.get_dc_mode(), DCMode::Desktop);
assert_eq!(process.get_dc_mode_value(), 3);
let process2 = Process::builder("app.exe")
.set_dc_mode(DCMode::WindowClient)
.build();
assert_eq!(process2.get_dc_mode(), DCMode::WindowClient);
assert_eq!(process2.get_dc_mode_value(), 2);
let process3 = Process::builder("app.exe").build();
assert_eq!(process3.get_dc_mode(), DCMode::Standard);
assert_eq!(process3.get_dc_mode_value(), 1);
}
#[test]
fn test_builder_dc_mode_numeric() {
let process1 = Process::builder("app.exe")
.set_dc_mode_num(3)
.build();
assert_eq!(process1.get_dc_mode_value(), 3);
let process2 = Process::builder("app.exe")
.set_dc_mode_num(2)
.build();
assert_eq!(process2.get_dc_mode_value(), 2);
let process3 = Process::builder("app.exe")
.set_dc_mode_num(1)
.build();
assert_eq!(process3.get_dc_mode_value(), 1);
}
#[test]
fn test_builder_try_dc_mode_value() {
let result = Process::builder("app.exe").try_set_dc_mode_num(3);
assert!(result.is_ok());
let process = result.unwrap().build();
assert_eq!(process.get_dc_mode_value(), 3);
let result = Process::builder("app.exe").try_set_dc_mode_num(99);
assert!(result.is_err());
}
#[test]
fn test_builder_complex_configuration() {
let filters = vec![("Google".to_string(), 1), ("Incognito".to_string(), 0)];
let process = Process::builder("chrome.exe")
.set_dc_mode(DCMode::Standard)
.set_hwnd_filter(filters.clone())
.build();
assert_eq!(process.get_name(), "chrome.exe");
assert_eq!(process.get_dc_mode(), DCMode::Standard);
assert_eq!(process.get_hwnd_filter(), Some(&filters));
}
#[test]
fn test_builder_clear_hwnd_filter() {
let filters = vec![("Test".to_string(), 1)];
let process = Process::builder("app.exe")
.set_hwnd_filter(filters)
.clear_hwnd_filter()
.build();
assert_eq!(process.get_hwnd_filter(), None);
}
#[test]
fn test_builder_method_chaining() {
let filters = vec![("Pattern".to_string(), 1)];
let _process = Process::builder("test.exe")
.desktop_mode()
.set_hwnd_filter(filters)
.set_dc_mode(DCMode::Standard)
.clear_hwnd_filter()
.window_client_mode()
.build();
}
#[test]
fn test_builder_vs_legacy_api_equivalence() {
let process1 = Process::new("test.exe");
let process2 = Process::builder("test.exe").build();
assert_eq!(process1.get_name(), process2.get_name());
assert_eq!(process1.get_dc_mode(), process2.get_dc_mode());
assert_eq!(process1.get_hwnd_filter(), process2.get_hwnd_filter());
}
#[test]
fn test_default_process_flags() {
let process = Process::new("test.exe");
assert_eq!(process.get_dc_mode(), DCMode::Standard);
assert_eq!(process.get_hwnd_filter(), None);
}
}