Files
lan-mouse/input-capture/src/windows/event_thread.rs
2025-04-02 02:39:49 +02:00

550 lines
18 KiB
Rust

use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::ptr::addr_of_mut;
use std::default::Default;
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::thread;
use tokio::sync::mpsc::error::TrySendError;
use tokio::sync::mpsc::Sender;
use windows::core::{w, PCWSTR};
use windows::Win32::Foundation::{FALSE, HINSTANCE, HWND, LPARAM, LRESULT, RECT, WPARAM};
use windows::Win32::Graphics::Gdi::{
EnumDisplayDevicesW, EnumDisplaySettingsW, DEVMODEW, DISPLAY_DEVICEW,
DISPLAY_DEVICE_ATTACHED_TO_DESKTOP, ENUM_CURRENT_SETTINGS,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::UI::WindowsAndMessaging::{
CallNextHookEx, CreateWindowExW, DispatchMessageW, GetMessageW, PostThreadMessageW,
RegisterClassW, SetWindowsHookExW, TranslateMessage, EDD_GET_DEVICE_INTERFACE_NAME, HHOOK,
HMENU, HOOKPROC, KBDLLHOOKSTRUCT, LLKHF_EXTENDED, MSG, MSLLHOOKSTRUCT, WH_KEYBOARD_LL,
WH_MOUSE_LL, WINDOW_STYLE, WM_DISPLAYCHANGE, WM_KEYDOWN, WM_KEYUP, WM_LBUTTONDOWN,
WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL,
WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER, WM_XBUTTONDOWN,
WM_XBUTTONUP, WNDCLASSW, WNDPROC,
};
use input_event::{
scancode::{self, Linux},
Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
};
use super::{display_util, CaptureEvent, Position};
pub(crate) struct EventThread {
request_buffer: Arc<Mutex<Vec<ClientUpdate>>>,
thread: Option<thread::JoinHandle<()>>,
thread_id: u32,
}
impl EventThread {
pub(crate) fn new(event_tx: Sender<(Position, CaptureEvent)>) -> Self {
let request_buffer = Default::default();
let (thread, thread_id) = start(event_tx, Arc::clone(&request_buffer));
Self {
request_buffer,
thread: Some(thread),
thread_id,
}
}
pub(crate) fn release_capture(&self) {
self.signal(RequestType::Release);
}
pub(crate) fn create(&self, pos: Position) {
self.client_update(ClientUpdate::Create(pos));
}
pub(crate) fn destroy(&self, pos: Position) {
self.client_update(ClientUpdate::Destroy(pos));
}
fn exit(&self) {
self.signal(RequestType::Exit);
}
fn client_update(&self, request: ClientUpdate) {
{
let mut requests = self.request_buffer.lock().unwrap();
requests.push(request);
}
self.signal(RequestType::ClientUpdate);
}
fn signal(&self, event_type: RequestType) {
let id = self.thread_id;
unsafe { PostThreadMessageW(id, WM_USER, WPARAM(event_type as usize), LPARAM(0)).unwrap() };
}
}
impl Drop for EventThread {
fn drop(&mut self) {
self.exit();
let _ = self.thread.take().expect("thread").join();
}
}
enum RequestType {
ClientUpdate = 0,
Release = 1,
Exit = 2,
}
enum ClientUpdate {
Create(Position),
Destroy(Position),
}
fn blocking_send_event(pos: Position, event: CaptureEvent) {
EVENT_TX.with_borrow_mut(|tx| tx.as_mut().unwrap().blocking_send((pos, event)).unwrap())
}
fn try_send_event(
pos: Position,
event: CaptureEvent,
) -> Result<(), TrySendError<(Position, CaptureEvent)>> {
EVENT_TX.with_borrow_mut(|tx| tx.as_mut().unwrap().try_send((pos, event)))
}
thread_local! {
/// all configured clients
static CLIENTS: RefCell<HashSet<Position>> = RefCell::new(HashSet::new());
/// currently active client
static ACTIVE_CLIENT: Cell<Option<Position>> = const { Cell::new(None) };
/// input event channel
static EVENT_TX: RefCell<Option<Sender<(Position, CaptureEvent)>>> = const { RefCell::new(None) };
/// position of barrier entry
static ENTRY_POINT: Cell<(i32, i32)> = const { Cell::new((0, 0)) };
/// previous mouse position
static PREV_POS: Cell<Option<(i32, i32)>> = const { Cell::new(None) };
/// displays and generation counter
static DISPLAYS: RefCell<(Vec<RECT>, i32)> = const { RefCell::new((Vec::new(), 0)) };
}
fn get_msg() -> Option<MSG> {
unsafe {
let mut msg = std::mem::zeroed();
let ret = GetMessageW(addr_of_mut!(msg), HWND::default(), 0, 0);
match ret.0 {
0 => None,
x if x > 0 => Some(msg),
_ => panic!("error in GetMessageW"),
}
}
}
fn start(
event_tx: Sender<(Position, CaptureEvent)>,
request_buffer: Arc<Mutex<Vec<ClientUpdate>>>,
) -> (thread::JoinHandle<()>, u32) {
/* condition variable to wait for thead id */
let thread_id = Arc::new((Condvar::new(), Mutex::new(None)));
let thread_id_ = Arc::clone(&thread_id);
let msg_thread = thread::spawn(|| start_routine(thread_id_, event_tx, request_buffer));
/* wait for thread to set its id */
let (cond, thread_id) = &*thread_id;
let mut thread_id = thread_id.lock().unwrap();
while (*thread_id).is_none() {
thread_id = cond.wait(thread_id).expect("channel closed");
}
(msg_thread, thread_id.expect("thread id"))
}
fn start_routine(
ready: Arc<(Condvar, Mutex<Option<u32>>)>,
event_tx: Sender<(Position, CaptureEvent)>,
request_buffer: Arc<Mutex<Vec<ClientUpdate>>>,
) {
EVENT_TX.replace(Some(event_tx));
/* communicate thread id */
{
let (cnd, mtx) = &*ready;
let mut ready = mtx.lock().unwrap();
*ready = Some(unsafe { GetCurrentThreadId() });
cnd.notify_one();
}
let mouse_proc: HOOKPROC = Some(mouse_proc);
let kybrd_proc: HOOKPROC = Some(kybrd_proc);
let window_proc: WNDPROC = Some(window_proc);
/* register hooks */
unsafe {
let _ = SetWindowsHookExW(WH_MOUSE_LL, mouse_proc, HINSTANCE::default(), 0).unwrap();
let _ = SetWindowsHookExW(WH_KEYBOARD_LL, kybrd_proc, HINSTANCE::default(), 0).unwrap();
}
let instance = unsafe { GetModuleHandleW(None).unwrap() };
let window_class: WNDCLASSW = WNDCLASSW {
lpfnWndProc: window_proc,
hInstance: instance.into(),
lpszClassName: w!("lan-mouse-message-window-class"),
..Default::default()
};
static WINDOW_CLASS_REGISTERED: AtomicBool = AtomicBool::new(false);
if WINDOW_CLASS_REGISTERED
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
/* register window class if not yet done so */
unsafe {
let ret = RegisterClassW(&window_class);
if ret == 0 {
panic!("RegisterClassW");
}
}
}
/* window is used ro receive WM_DISPLAYCHANGE messages */
unsafe {
CreateWindowExW(
Default::default(),
w!("lan-mouse-message-window-class"),
w!("lan-mouse-msg-window"),
WINDOW_STYLE::default(),
0,
0,
0,
0,
HWND::default(),
HMENU::default(),
instance,
None,
)
.expect("CreateWindowExW");
}
/* run message loop */
loop {
// mouse / keybrd proc do not actually return a message
let Some(msg) = get_msg() else {
break;
};
if msg.hwnd.0.is_null() {
/* messages sent via PostThreadMessage */
match msg.wParam.0 {
x if x == RequestType::Exit as usize => break,
x if x == RequestType::Release as usize => {
ACTIVE_CLIENT.take();
}
x if x == RequestType::ClientUpdate as usize => {
let requests = {
let mut res = vec![];
let mut requests = request_buffer.lock().unwrap();
for request in requests.drain(..) {
res.push(request);
}
res
};
for request in requests {
update_clients(request)
}
}
_ => {}
}
} else {
/* other messages for window_procs */
unsafe {
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
}
}
fn check_client_activation(wparam: WPARAM, lparam: LPARAM) -> bool {
if wparam.0 != WM_MOUSEMOVE as usize {
return ACTIVE_CLIENT.get().is_some();
}
let mouse_low_level: MSLLHOOKSTRUCT = unsafe { *(lparam.0 as *const MSLLHOOKSTRUCT) };
let curr_pos = (mouse_low_level.pt.x, mouse_low_level.pt.y);
let prev_pos = PREV_POS.get().unwrap_or(curr_pos);
PREV_POS.replace(Some(curr_pos));
/* next event is the first actual event */
let ret = ACTIVE_CLIENT.get().is_some();
/* client already active, no need to check */
if ACTIVE_CLIENT.get().is_some() {
return ret;
}
/* check if a client was activated */
let entered = DISPLAYS.with_borrow_mut(|(displays, generation)| {
update_display_regions(displays, generation);
display_util::entered_barrier(prev_pos, curr_pos, displays)
});
let Some(pos) = entered else {
return ret;
};
/* check if a client is registered for the barrier */
if !CLIENTS.with_borrow(|clients| clients.contains(&pos)) {
return ret;
}
/* update active client and entry point */
ACTIVE_CLIENT.replace(Some(pos));
let entry_point = DISPLAYS.with_borrow(|(displays, _)| {
display_util::clamp_to_display_bounds(displays, prev_pos, curr_pos)
});
ENTRY_POINT.replace(entry_point);
/* notify main thread */
log::debug!("ENTERED @ {prev_pos:?} -> {curr_pos:?}");
let active = ACTIVE_CLIENT.get().expect("active client");
blocking_send_event(active, CaptureEvent::Begin);
ret
}
unsafe extern "system" fn mouse_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
let active = check_client_activation(wparam, lparam);
/* no client was active */
if !active {
return CallNextHookEx(HHOOK::default(), ncode, wparam, lparam);
}
/* get active client if any */
let Some(pos) = ACTIVE_CLIENT.get() else {
return LRESULT(1);
};
/* convert to lan-mouse event */
let Some(pointer_event) = to_mouse_event(wparam, lparam) else {
return LRESULT(1);
};
/* notify mainthread (drop events if sending too fast) */
if let Err(e) = try_send_event(pos, CaptureEvent::Input(Event::Pointer(pointer_event))) {
log::warn!("e: {e}");
}
/* don't pass event to applications */
LRESULT(1)
}
unsafe extern "system" fn kybrd_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
/* get active client if any */
let Some(client) = ACTIVE_CLIENT.get() else {
return CallNextHookEx(HHOOK::default(), ncode, wparam, lparam);
};
/* convert to key event */
let Some(key_event) = to_key_event(wparam, lparam) else {
return LRESULT(1);
};
if let Err(e) = try_send_event(client, CaptureEvent::Input(Event::Keyboard(key_event))) {
log::warn!("e: {e}");
}
/* don't pass event to applications */
LRESULT(1)
}
unsafe extern "system" fn window_proc(
_hwnd: HWND,
uint: u32,
_wparam: WPARAM,
_lparam: LPARAM,
) -> LRESULT {
if uint == WM_DISPLAYCHANGE {
log::debug!("display resolution changed");
DISPLAY_RESOLUTION_GENERATION.fetch_add(1, Ordering::Release);
}
LRESULT(1)
}
static DISPLAY_RESOLUTION_GENERATION: AtomicI32 = AtomicI32::new(1);
fn update_display_regions(displays: &mut Vec<RECT>, generation: &mut i32) {
let global_generation = DISPLAY_RESOLUTION_GENERATION.load(Ordering::Acquire);
if *generation != global_generation {
enumerate_displays(displays);
log::debug!("displays: {displays:?}");
*generation = global_generation;
}
}
fn enumerate_displays(display_rects: &mut Vec<RECT>) {
display_rects.clear();
unsafe {
let mut devices = vec![];
for i in 0.. {
let mut device: DISPLAY_DEVICEW = std::mem::zeroed();
device.cb = std::mem::size_of::<DISPLAY_DEVICEW>() as u32;
let ret = EnumDisplayDevicesW(None, i, &mut device, EDD_GET_DEVICE_INTERFACE_NAME);
if ret == FALSE {
break;
}
if device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP != 0 {
devices.push(device.DeviceName);
}
}
for device in devices {
let mut dev_mode: DEVMODEW = std::mem::zeroed();
dev_mode.dmSize = std::mem::size_of::<DEVMODEW>() as u16;
let ret = EnumDisplaySettingsW(
PCWSTR::from_raw(&device as *const _),
ENUM_CURRENT_SETTINGS,
&mut dev_mode,
);
if ret == FALSE {
log::warn!("no display mode");
}
let pos = dev_mode.Anonymous1.Anonymous2.dmPosition;
let (x, y) = (pos.x, pos.y);
let (width, height) = (dev_mode.dmPelsWidth, dev_mode.dmPelsHeight);
display_rects.push(RECT {
left: x,
right: x + width as i32,
top: y,
bottom: y + height as i32,
});
}
}
}
fn update_clients(request: ClientUpdate) {
match request {
ClientUpdate::Create(pos) => {
CLIENTS.with_borrow_mut(|clients| clients.insert(pos));
}
ClientUpdate::Destroy(pos) => {
if let Some(active_pos) = ACTIVE_CLIENT.get() {
if pos == active_pos {
let _ = ACTIVE_CLIENT.take();
}
}
CLIENTS.with_borrow_mut(|clients| clients.remove(&pos));
}
}
}
fn to_key_event(wparam: WPARAM, lparam: LPARAM) -> Option<KeyboardEvent> {
let kybrdllhookstruct: KBDLLHOOKSTRUCT = unsafe { *(lparam.0 as *const KBDLLHOOKSTRUCT) };
let mut scan_code = kybrdllhookstruct.scanCode;
log::trace!("scan_code: {scan_code}");
if kybrdllhookstruct.flags.contains(LLKHF_EXTENDED) {
scan_code |= 0xE000;
}
let Ok(win_scan_code) = scancode::Windows::try_from(scan_code) else {
log::warn!("failed to translate to windows scancode: {scan_code}");
return None;
};
log::trace!("windows_scan: {win_scan_code:?}");
let Ok(linux_scan_code): Result<Linux, ()> = win_scan_code.try_into() else {
log::warn!("failed to translate into linux scancode: {win_scan_code:?}");
return None;
};
log::trace!("windows_scan: {linux_scan_code:?}");
let scan_code = linux_scan_code as u32;
match wparam {
WPARAM(p) if p == WM_KEYDOWN as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 1,
}),
WPARAM(p) if p == WM_KEYUP as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 0,
}),
WPARAM(p) if p == WM_SYSKEYDOWN as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 1,
}),
WPARAM(p) if p == WM_SYSKEYUP as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 0,
}),
_ => None,
}
}
fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> {
let mouse_low_level: MSLLHOOKSTRUCT = unsafe { *(lparam.0 as *const MSLLHOOKSTRUCT) };
match wparam {
WPARAM(p) if p == WM_LBUTTONDOWN as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_LEFT,
state: 1,
}),
WPARAM(p) if p == WM_MBUTTONDOWN as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_MIDDLE,
state: 1,
}),
WPARAM(p) if p == WM_RBUTTONDOWN as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_RIGHT,
state: 1,
}),
WPARAM(p) if p == WM_LBUTTONUP as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_LEFT,
state: 0,
}),
WPARAM(p) if p == WM_MBUTTONUP as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_MIDDLE,
state: 0,
}),
WPARAM(p) if p == WM_RBUTTONUP as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_RIGHT,
state: 0,
}),
WPARAM(p) if p == WM_MOUSEMOVE as usize => {
let (x, y) = (mouse_low_level.pt.x, mouse_low_level.pt.y);
let (ex, ey) = ENTRY_POINT.get();
let (dx, dy) = (x - ex, y - ey);
let (dx, dy) = (dx as f64, dy as f64);
Some(PointerEvent::Motion { time: 0, dx, dy })
}
WPARAM(p) if p == WM_MOUSEWHEEL as usize => Some(PointerEvent::AxisDiscrete120 {
axis: 0,
value: -(mouse_low_level.mouseData as i32 >> 16),
}),
WPARAM(p) if p == WM_XBUTTONDOWN as usize || p == WM_XBUTTONUP as usize => {
let hb = mouse_low_level.mouseData >> 16;
let button = match hb {
1 => BTN_BACK,
2 => BTN_FORWARD,
_ => {
log::warn!("unknown mouse button");
return None;
}
};
Some(PointerEvent::Button {
time: 0,
button,
state: if p == WM_XBUTTONDOWN as usize { 1 } else { 0 },
})
}
WPARAM(p) if p == WM_MOUSEHWHEEL as usize => Some(PointerEvent::AxisDiscrete120 {
axis: 1, // Horizontal
value: mouse_low_level.mouseData as i32 >> 16,
}),
w => {
log::warn!("unknown mouse event: {w:?}");
None
}
}
}