commit 831b698c6f64a3b593899fc09bbec3da9db4e4c7 Author: isark Date: Wed Jul 5 21:24:13 2023 +0200 Initial commit. Mostly done with the windows implementation diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1585201 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "rust-targets.targets": [ + "system", + "x86_64-pc-windows-gnu", + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..08bef83 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "Underlayer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "Underlayer" +path = "src/lib.rs" + +[[example]] +name = "test" +path = "src/examples/main.rs" + +[dependencies] +crossbeam = "0.8.2" +log = "0.4.19" +raw-window-handle = "0.5.2" +windows = {version = "0.48.0", features = ["Win32_UI_Accessibility", "Win32_Foundation", "Win32_UI_WindowsAndMessaging", "Win32_Graphics_Gdi", "Win32_System_Com", "Win32_System_Ole"]} + +[dev-dependencies] +ctor = "0.2.3" +simple_logger = "4.2.0" diff --git a/src/examples/main.rs b/src/examples/main.rs new file mode 100644 index 0000000..599e3e6 --- /dev/null +++ b/src/examples/main.rs @@ -0,0 +1,19 @@ +use Underlayer::{UnderlayEvent, register}; + +fn init() { + simple_logger::init().expect("Could not initialize simple_logger"); +} + +fn main() { + init(); + let rx: std::sync::mpsc::Receiver = register("Untitled - Notepad"); + + + let mut value = rx.recv(); + while let Ok(event) = value { + log::info!("Got event: {event:?}"); + value = rx.recv(); + } + + log::error!("{value:?}"); +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9e93569 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,35 @@ +use std::{sync::mpsc::Receiver}; + + +#[cfg_attr(unix, path = "linux/mod.rs")] +#[cfg_attr(windows, path = "windows/mod.rs")] +#[cfg_attr(mac, path = "windows/mod.rs")] +pub mod platform_impl; + +#[derive(Debug, Clone, Copy)] +pub struct Bounds { + x: i32, + y: i32, + width: i32, + height: i32, +} + +#[derive(Debug, Clone, Copy)] +pub enum UnderlayEvent { + Attach { bounds: Bounds, has_access: bool }, + Detach, + MoveResize(Bounds), + Focus, + Blur, +} + +pub fn register>(window_title: T) -> Receiver { + let (tx,rx) = std::sync::mpsc::channel(); + + platform_impl::init(window_title.into(), tx).expect("Catastrophe"); + + return rx; +} + + + diff --git a/src/linux/mod.rs b/src/linux/mod.rs new file mode 100644 index 0000000..37d6580 --- /dev/null +++ b/src/linux/mod.rs @@ -0,0 +1,6 @@ + + +pub fn init(window_title: String, tx: std::sync::mpsc::Sender) -> Result<(), String> { + + Err(format!("Failed init underlayer for window: {}", window_title)) +} diff --git a/src/macos/mod.rs b/src/macos/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/windows/mod.rs b/src/windows/mod.rs new file mode 100644 index 0000000..394ace7 --- /dev/null +++ b/src/windows/mod.rs @@ -0,0 +1,520 @@ +use windows::Win32::Graphics::Gdi::ClientToScreen; +use windows::Win32::System::Com::VT_I4; +use windows::Win32::System::Ole::{VariantClear, VariantInit}; +use windows::Win32::UI::Accessibility::{ + AccessibleObjectFromEvent, AccessibleObjectFromWindow, IAccessible, +}; + +use std::sync::atomic::AtomicPtr; +use std::sync::{Arc, Mutex}; +use windows::core::{Error, PCWSTR}; +use windows::Win32::Foundation::{ + GetLastError, SetLastError, ERROR_ACCESS_DENIED, LPARAM, POINT, RECT, WPARAM, +}; +use windows::Win32::UI::WindowsAndMessaging::{ + DispatchMessageW, GetClientRect, GetForegroundWindow, GetMessageW, GetWindowTextW, + GetWindowThreadProcessId, IsHungAppWindow, PostMessageW, RegisterWindowMessageW, + TranslateMessage, CHILDID_SELF, EVENT_OBJECT_DESTROY, EVENT_OBJECT_LOCATIONCHANGE, + EVENT_OBJECT_NAMECHANGE, EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_MINIMIZEEND, MSG, + OBJECT_IDENTIFIER, OBJID_WINDOW, STATE_SYSTEM_FOCUSED, WINEVENT_OUTOFCONTEXT, +}; +use windows::Win32::{ + Foundation::HWND, + UI::Accessibility::{SetWinEventHook, UnhookWinEvent, HWINEVENTHOOK}, +}; + +use crate::{Bounds, UnderlayEvent}; + +static GLOBAL_CALLBACK_SENDER: AtomicPtr>> = + AtomicPtr::new(std::ptr::null_mut()); + +fn constant_name(value: u32) -> String { + match value { + EVENT_OBJECT_DESTROY => "EVENT_OBJECT_DESTROY".into(), + EVENT_OBJECT_LOCATIONCHANGE => "EVENT_OBJECT_LOCATIONCHANGE".into(), + EVENT_OBJECT_NAMECHANGE => "EVENT_OBJECT_NAMECHANGE".into(), + EVENT_SYSTEM_FOREGROUND => "EVENT_SYSTEM_FOREGROUND".into(), + EVENT_SYSTEM_MINIMIZEEND => "EVENT_SYSTEM_MINIMIZEEND".into(), + WINEVENT_OUTOFCONTEXT => "WINEVENT_OUTOFCONTEXT".into(), + _ => format!("Unknown constant ({value})"), + } +} + +#[derive(Clone)] +struct HookInfo { + pub id: HWINEVENTHOOK, + pub min: u32, + pub max: u32, + pub idprocess: u32, + pub idthread: u32, +} + +impl std::fmt::Debug for HookInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("HookInfo") + .field("id", &self.id) + .field("min", &constant_name(self.min)) + .field("max", &constant_name(self.max)) + .field("idprocess", &self.idprocess) + .field("idthread", &self.idthread) + .finish() + } +} + +impl HookInfo { + pub fn new(min: u32, max: u32) -> Self { + Self { + id: Default::default(), + min, + max, + idprocess: Default::default(), + idthread: Default::default(), + } + } + + pub fn bind(&mut self) { + log::trace!("Binding hook: {self:?}"); + + if self.id != Default::default() { + log::warn!( + "Binding {} to {} without unbinding the previous one", + constant_name(self.min), + constant_name(self.max) + ) + } + + unsafe { + self.id = SetWinEventHook( + self.min, + self.max, + None, + Some(global_hook), + self.idprocess, + self.idthread, + WINEVENT_OUTOFCONTEXT, + ); + } + } + + pub fn unbind(&mut self) { + if self.id == Default::default() { + log::warn!("Unbinding unbind hook..."); + return; + }; + + unsafe { UnhookWinEvent(self.id) }; + self.id = HWINEVENTHOOK::default(); + } +} + +impl Drop for HookInfo { + fn drop(&mut self) { + log::trace!("Dropping hook: {self:?}"); + self.unbind(); + } +} + +impl Event { + fn from_data(event: u32, hwnd: HWND, idobject: OBJECT_IDENTIFIER, idchild: u32) -> Self { + match event { + EVENT_OBJECT_DESTROY => Self::Destroyed { + hwnd, + idobject, + idchild, + }, + EVENT_OBJECT_LOCATIONCHANGE => Self::LocationChange { + hwnd, + idobject, + idchild, + }, + EVENT_OBJECT_NAMECHANGE => Self::NameChange { + hwnd, + idobject, + idchild, + }, + EVENT_SYSTEM_FOREGROUND | EVENT_SYSTEM_MINIMIZEEND => Event::ForegroundChange { hwnd }, + _ => Event::Unrepresented(event), + } + } +} + +#[derive(Debug, Clone)] +enum Event { + ForegroundChange { + hwnd: HWND, + }, + NameChange { + hwnd: HWND, + idobject: OBJECT_IDENTIFIER, + idchild: u32, + }, + LocationChange { + hwnd: HWND, + idobject: OBJECT_IDENTIFIER, + idchild: u32, + }, + Destroyed { + hwnd: HWND, + idobject: OBJECT_IDENTIFIER, + idchild: u32, + }, + Unrepresented(u32), +} + +unsafe extern "system" fn global_hook( + _hwineventhook: HWINEVENTHOOK, + event: u32, + hwnd: HWND, + idobject: i32, + idchild: i32, + _ideventthread: u32, + _dwmseventtime: u32, +) { + if let Ok(mut runtime) = + (*GLOBAL_CALLBACK_SENDER.load(std::sync::atomic::Ordering::Relaxed)).lock() + { + runtime.on_event(Event::from_data( + event, + hwnd, + OBJECT_IDENTIFIER(idobject), + idchild as u32, + )); + } +} + +unsafe extern "system" fn global_timer_hook( + _hwnd: HWND, + _msg: u32, + _timer_id: usize, + _dwms_event_time: u32, +) { + if let Ok(mut runtime) = + (*GLOBAL_CALLBACK_SENDER.load(std::sync::atomic::Ordering::Relaxed)).lock() + { + let system_foreground_window = GetForegroundWindow(); + if runtime.foreground_window != system_foreground_window + && runtime.msaa_check_window_focused_state(system_foreground_window) + { + runtime.on_new_foreground(system_foreground_window); + } + } +} + +struct Runtime { + foreground_change: HookInfo, + unminimize_change: HookInfo, + + foreground_name_change: HookInfo, + foreground_window: HWND, + + target: TargetWindow, + + underlay_tx: std::sync::mpsc::Sender, + uipi_msg_id: u32, +} + +struct TargetWindow { + hwnd: HWND, + title: String, + location_change: HookInfo, + destroyed_change: HookInfo, + is_destroyed: bool, + is_focused: bool, +} + +trait Valid { + fn is_valid(&self) -> bool; +} + +impl Valid for HWND { + fn is_valid(&self) -> bool { + *self != Default::default() + } +} + +impl Runtime { + fn on_event(&mut self, event: Event) { + log::trace!("Got event: {event:?}"); + match event { + Event::ForegroundChange { hwnd } => { + log::trace!("ForegroundChange: {hwnd:?}"); + + //Sometimes focus changes doesn't keep up, check manually. + if unsafe { GetForegroundWindow() } != hwnd { + if !self.msaa_check_window_focused_state(hwnd) { + return; + } + } + + self.on_new_foreground(hwnd) + } + Event::NameChange { + hwnd, + idobject, + idchild, + } => { + log::trace!("NameChange: {hwnd:?}"); + if self.target.hwnd == hwnd && idobject == OBJID_WINDOW && idchild == CHILDID_SELF { + self.check_and_handle(self.foreground_window) + } + } + Event::LocationChange { + hwnd, + idobject, + idchild, + } => { + log::trace!("LocationChange: {hwnd:?}"); + if self.target.hwnd == hwnd && idobject == OBJID_WINDOW && idchild == CHILDID_SELF { + self.handle_movesize(); + } + } + Event::Destroyed { + hwnd, + idobject, + idchild, + } => { + log::trace!("Destroy: {hwnd:?}"); + if self.target.hwnd == hwnd && idobject == OBJID_WINDOW && idchild == CHILDID_SELF { + self.target.is_destroyed = true; + log::trace!("target.hwnd == destroyed hwnd"); + self.check_and_handle(Default::default()); + } + } + Event::Unrepresented(event_code) => { + log::trace!("Unrepresented event: {event_code}"); + } + }; + } + + fn on_new_foreground(&mut self, hwnd: HWND) { + log::trace!( + "Got new foreground: {hwnd:?} - {}", + self.get_title(hwnd).unwrap_or("".into()) + ); + self.foreground_window = hwnd; + + self.foreground_name_change.unbind(); + + if self.foreground_window.is_valid() && self.foreground_window == self.target.hwnd { + self.foreground_name_change.idthread = + unsafe { GetWindowThreadProcessId(self.foreground_window, None) }; + self.foreground_name_change.bind(); + } + + self.check_and_handle(self.foreground_window); + } + + fn get_title(&mut self, window: HWND) -> Result { + //Allow some extra space in case of undocumented higher limit / little bit of safety for newer versions + const BUFSIZE: usize = 512; + let mut buffer: [u16; 512] = [0_u16; BUFSIZE]; + + unsafe { SetLastError(Default::default()) }; + let num_stored = unsafe { GetWindowTextW(window, &mut buffer) }; + if num_stored == 0 { + unsafe { GetLastError() }.ok()?; + return Ok("".into()); + } + + Ok(String::from_utf16_lossy(&buffer) + .trim_end_matches(char::from(0)) + .to_string()) + } + + fn check_and_handle(&mut self, hwnd: HWND) { + if unsafe { IsHungAppWindow(hwnd).into() } { + return; + } + + if self.target.hwnd.is_valid() { + if self.target.hwnd == hwnd { + if !self.target.is_focused { + self.target.is_focused = true; + self.underlay_tx.send(UnderlayEvent::Focus).ok(); + } + return; + } + + if self.target.is_focused { + self.target.is_focused = false; + self.underlay_tx.send(UnderlayEvent::Blur).ok(); + } + + if self.target.is_destroyed { + self.target.hwnd = Default::default(); + self.target.is_destroyed = false; + self.underlay_tx.send(UnderlayEvent::Detach).ok(); + } + } + + log::trace!("Checking and handling pre get_title"); + + match self.get_title(hwnd) { + Ok(title) if title == self.target.title => {} + _ => return, + } + + log::info!("Got target window!"); + + if self.target.hwnd.is_valid() { + self.target.location_change.unbind(); + self.target.destroyed_change.unbind(); + } + + self.target.hwnd = hwnd; + let thread_id = unsafe { GetWindowThreadProcessId(hwnd, None) }; + + self.target.location_change.idthread = thread_id; + self.target.location_change.bind(); + self.target.destroyed_change.idthread = thread_id; + self.target.destroyed_change.bind(); + + let has_access = self.has_uipi_access(); + if let Some(bounds) = self.get_content_bounds() { + self.underlay_tx + .send(UnderlayEvent::Attach { bounds, has_access }) + .ok(); + + self.target.is_focused = true; + self.underlay_tx.send(UnderlayEvent::Focus).ok(); + } else { + self.target.hwnd = Default::default(); + } + } + + fn msaa_check_window_focused_state(&mut self, hwnd: HWND) -> bool { + log::trace!("msaa_check_window_focused_state"); + let mut var_child_self = unsafe { VariantInit() }; + let mut p_acc: Option = None; + let hr = unsafe { + AccessibleObjectFromEvent( + hwnd, + OBJID_WINDOW.0 as u32, + CHILDID_SELF, + &mut p_acc, + &mut var_child_self, + ) + }; + if hr.is_err() || p_acc.is_none() { + unsafe { VariantClear(&mut var_child_self) }.ok(); + return false; + } + let p_acc = p_acc.unwrap(); + let var_state = unsafe { p_acc.get_accState(var_child_self) }; + + let mut is_focused = false; + + if let Ok(mut var_state) = var_state { + if unsafe { var_state.Anonymous.Anonymous.vt } == VT_I4 { + let value = unsafe { var_state.Anonymous.Anonymous.Anonymous.lVal }; + is_focused = (value & STATE_SYSTEM_FOCUSED as i32) != 0; + } + unsafe { + VariantClear(&mut var_state).ok(); + }; + } + + return is_focused; + } + + fn handle_movesize(&self) { + if let Some(bounds) = self.get_content_bounds() { + self.underlay_tx + .send(UnderlayEvent::MoveResize(bounds)) + .ok(); + } + } + + fn has_uipi_access(&self) -> bool { + unsafe { + SetLastError(Default::default()); + PostMessageW( + self.target.hwnd, + self.uipi_msg_id, + WPARAM::default(), + LPARAM::default(), + ); + GetLastError() != ERROR_ACCESS_DENIED + } + } + + fn get_content_bounds(&self) -> Option { + let mut rect: RECT = unsafe { std::mem::zeroed() }; + if !unsafe { GetClientRect(self.target.hwnd, &mut rect).into() } { + return None; + } + + let mut pt_client_ul: POINT = POINT { + x: rect.left, + y: rect.top, + }; + if !unsafe { ClientToScreen(self.target.hwnd, &mut pt_client_ul).into() } { + return None; + } + + Some(Bounds { + x: pt_client_ul.x, + y: pt_client_ul.y, + width: rect.right, + height: rect.bottom, + }) + } +} + +pub fn init( + window_title: String, + tx: std::sync::mpsc::Sender, +) -> Result<(), String> { + std::thread::spawn(move || { + let uipi_msg_id = register_message("UNDERLAY_UIPI_TEST"); + + let mut runtime = Runtime { + foreground_change: HookInfo::new(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND), + unminimize_change: HookInfo::new(EVENT_SYSTEM_MINIMIZEEND, EVENT_SYSTEM_MINIMIZEEND), + + foreground_name_change: HookInfo::new(EVENT_OBJECT_NAMECHANGE, EVENT_OBJECT_NAMECHANGE), + foreground_window: Default::default(), + + target: TargetWindow { + hwnd: Default::default(), + title: window_title, + location_change: HookInfo::new( + EVENT_OBJECT_LOCATIONCHANGE, + EVENT_OBJECT_LOCATIONCHANGE, + ), + destroyed_change: HookInfo::new(EVENT_OBJECT_DESTROY, EVENT_OBJECT_DESTROY), + is_destroyed: false, + is_focused: false, + }, + + uipi_msg_id, + + underlay_tx: tx, + }; + runtime.foreground_change.bind(); + runtime.unminimize_change.bind(); + + GLOBAL_CALLBACK_SENDER.store( + Box::into_raw(Box::new(Arc::new(Mutex::new(runtime)))), + std::sync::atomic::Ordering::Relaxed, + ); + + let mut msg = MSG::default(); + while unsafe { GetMessageW(&mut msg, HWND(0), 0, 0) } != windows::Win32::Foundation::FALSE { + log::trace!("Got message"); + unsafe { TranslateMessage(&msg) }; + log::trace!("Translated message"); + unsafe { DispatchMessageW(&msg) }; + log::trace!("Dispatched message"); + } + log::trace!("GetMessageW caused loop to exit"); + }); + + Ok(()) +} + +fn register_message>(message: T) -> u32 { + let message_wide = windows::core::HSTRING::from(message.into()); + let result = unsafe { RegisterWindowMessageW(PCWSTR(message_wide.as_wide().as_ptr())) }; + result +}