From 4c50f5ac22abfc7114b5fee88576030e92796192 Mon Sep 17 00:00:00 2001 From: Rohan Barar Date: Tue, 7 Oct 2025 22:21:53 +1100 Subject: [PATCH 1/3] Update .gitignore Signed-off-by: Rohan Barar --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index c2819b03..297aac4f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,9 @@ Cargo.lock # We don't want to commit IDE configuration files. .idea/ .vscode/ +*.iml + +# Operating system generated metadata files +.DS_Store +Thumbs.db +Desktop.ini \ No newline at end of file From 3e0267f2c25f191fd17437d59683a067f65901c7 Mon Sep 17 00:00:00 2001 From: Rohan Barar Date: Tue, 7 Oct 2025 22:38:34 +1100 Subject: [PATCH 2/3] Initial commit for winapps-monitor Signed-off-by: Rohan Barar --- Cargo.toml | 1 + winapps-monitor/Cargo.toml | 23 + .../assets/icons/system_tray_icon.ico | Bin 0 -> 14837 bytes .../assets/icons/system_tray_icon.svg | 4 + winapps-monitor/src/main.rs | 453 ++++++++++++++++++ winapps-monitor/src/svg2ico.py | 20 + 6 files changed, 501 insertions(+) create mode 100644 winapps-monitor/Cargo.toml create mode 100644 winapps-monitor/assets/icons/system_tray_icon.ico create mode 100644 winapps-monitor/assets/icons/system_tray_icon.svg create mode 100644 winapps-monitor/src/main.rs create mode 100644 winapps-monitor/src/svg2ico.py diff --git a/Cargo.toml b/Cargo.toml index 0e5b9072..221f7a06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,5 +3,6 @@ members = [ "winapps", "winapps-cli", "winapps-gui", + "winapps-monitor", ] resolver = "2" diff --git a/winapps-monitor/Cargo.toml b/winapps-monitor/Cargo.toml new file mode 100644 index 00000000..8762165a --- /dev/null +++ b/winapps-monitor/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "winapps-monitor" +version = "0.1.0" +edition = "2024" + +[dependencies] +tray-icon = "0.21.1" +ico = "0.4.0" +anyhow = "1.0.100" +chrono = "0.4.42" + +[dependencies.windows] +version = "0.62.1" +features = [ + "Win32_Foundation", + "Win32_UI_WindowsAndMessaging", + "Win32_System_Threading", + "Win32_Storage_FileSystem", + "Win32_System_ProcessStatus", + "Win32_Graphics_Dwm", + "Win32_UI_Accessibility", + "Win32_System_Com" +] \ No newline at end of file diff --git a/winapps-monitor/assets/icons/system_tray_icon.ico b/winapps-monitor/assets/icons/system_tray_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..ebb54847056e028dda36491c4a8dd9dd1613c6be GIT binary patch literal 14837 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 F{{S>5L1X{` literal 0 HcmV?d00001 diff --git a/winapps-monitor/assets/icons/system_tray_icon.svg b/winapps-monitor/assets/icons/system_tray_icon.svg new file mode 100644 index 00000000..548c4159 --- /dev/null +++ b/winapps-monitor/assets/icons/system_tray_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/winapps-monitor/src/main.rs b/winapps-monitor/src/main.rs new file mode 100644 index 00000000..7014b80d --- /dev/null +++ b/winapps-monitor/src/main.rs @@ -0,0 +1,453 @@ +use anyhow::Result; +use chrono::Local; +use std::collections::HashMap; +use std::sync::{Mutex, MutexGuard, OnceLock}; +use std::ffi::OsString; +use std::os::windows::ffi::OsStringExt; +use std::cell::Cell; +use tray_icon::{TrayIconBuilder, Icon}; +use windows::{ + core::BOOL, + Win32::{ + Foundation::{HWND, LPARAM}, + Graphics::Dwm::{DwmGetWindowAttribute, DWMWA_CLOAKED}, + UI::{ + Accessibility::{SetWinEventHook, UnhookWinEvent, HWINEVENTHOOK}, + WindowsAndMessaging::{ + EnumWindows, GetAncestor, GetLastActivePopup, GetWindowLongPtrW, + GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId, IsWindowVisible, + GetClassNameW, EnumChildWindows, GetMessageW, TranslateMessage, DispatchMessageW, + GA_ROOTOWNER, GWL_EXSTYLE, WS_EX_TOOLWINDOW, WS_EX_APPWINDOW, EVENT_OBJECT_CREATE, + EVENT_OBJECT_DESTROY, EVENT_OBJECT_SHOW, EVENT_OBJECT_HIDE, EVENT_OBJECT_CLOAKED, + EVENT_OBJECT_UNCLOAKED, OBJID_WINDOW, EVENT_OBJECT_NAMECHANGE, + WINEVENT_OUTOFCONTEXT, WINEVENT_SKIPOWNPROCESS, OBJECT_IDENTIFIER, MSG + } + } + } +}; + +type WindowKey = isize; +#[derive(Clone, Debug)] +struct WindowEntry { + pid: u32, + title: String, +} +#[repr(C)] +struct UwpPidSearch<'a> { + frame_pid: u32, + found_pid: &'a Cell, +} +static WINDOWS: OnceLock>> = OnceLock::new(); + +/// Name: windows_map +/// Purpose: Lazy global initialiser for 'WINDOWS'. +fn windows_map() -> &'static Mutex> { + WINDOWS.get_or_init(|| Mutex::new(HashMap::new())) +} + +/// Name: on_win_event +/// Purpose: Callback function registered via SetWinEventHook. +/// Events: +/// - WINDOW APPEARED: EVENT_OBJECT_CREATE, EVENT_OBJECT_SHOW, EVENT_OBJECT_UNCLOAKED +/// - WINDOW DISAPPEARED: EVENT_OBJECT_HIDE, EVENT_OBJECT_DESTROY, EVENT_OBJECT_CLOAKED +/// - WINDOW NAME CHANGED: EVENT_OBJECT_NAMECHANGE +extern "system" fn on_win_event ( + _hook: HWINEVENTHOOK, + event: u32, + hwnd: HWND, + id_object: i32, + _id_child: i32, + _evt_thread: u32, + _evt_time: u32 +) { + // Debugging + //println!("[evt {event:#x}] hwnd={:?} id_object={}", hwnd, id_object); + //return; + + // Ignore non-window events + if OBJECT_IDENTIFIER(id_object) != OBJID_WINDOW { + return; + } + + // Ignore events without an associated window to inspect + if hwnd == HWND::default() { + return; + } + + // Check if a window appeared or disappeared + if matches!(event, EVENT_OBJECT_CREATE | EVENT_OBJECT_SHOW | EVENT_OBJECT_UNCLOAKED) { + // Check the new window is a user-facing main/top-level application window + if !is_candidate_window(hwnd) { + return; + } + + /* + // Require non-empty title + unsafe { + if GetWindowTextLengthW(hwnd) == 0 { + return; + } + } + */ + + // Identify the process ID + let pid: u32 = + if is_application_frame_window(hwnd) { + // UWP + uwp_pid(hwnd) + } else { + // Win32 + let mut p: u32 = 0u32; + unsafe { + GetWindowThreadProcessId(hwnd, Some(&mut p)); + } + p + }; + + // Grab the current title (can empty on window creation) + let title: String = window_title(hwnd); + + // Add window + add_window(hwnd.0 as isize, pid, title); + debug_dump_windows(); + } else if matches!(event, EVENT_OBJECT_NAMECHANGE) { + // Store the new window title + let title: String = window_title(hwnd); + update_window_title(hwnd.0 as isize, title); + debug_dump_windows(); + } else if matches!(event, EVENT_OBJECT_HIDE | EVENT_OBJECT_DESTROY | EVENT_OBJECT_CLOAKED) { + // Remove window + remove_window(hwnd.0 as isize); + debug_dump_windows(); + } +} + +/// Name: add_window +/// Purpose: Add a window to the window list. +fn add_window(hwnd: isize, pid: u32, title: String) { + let mut map: MutexGuard> = windows_map().lock().unwrap(); + (&mut *map).insert(hwnd, WindowEntry { pid, title }); +} + +/// Name: update_window_title +/// Purpose: Update a window title in the window list. +fn update_window_title(hwnd: isize, new_title: String) { + let mut map: MutexGuard> = windows_map().lock().unwrap(); + if let Some(entry) = (&mut *map).get_mut(&hwnd) { + entry.title = new_title; + } +} + +/// Name: remove_window +/// Purpose: Remove a window from the window list. +fn remove_window(hwnd: isize) { + let mut map: MutexGuard> = windows_map().lock().unwrap(); + (&mut *map).remove(&hwnd); +} + +/// Name: window_title +/// Purpose: Return the title of a window given a window handle. +fn window_title(hwnd: HWND) -> String { + unsafe { + let len = GetWindowTextLengthW(hwnd); + if len == 0 { + return String::new(); + } + let mut buf = vec![0u16; len as usize + 1]; + let written = GetWindowTextW(hwnd, &mut buf); + buf.truncate(written as usize); + OsString::from_wide(&buf).to_string_lossy().into_owned() + } +} + +/// Name: debug_dump_windows +/// Purpose: Print the current contents of the window list. +pub fn debug_dump_windows() { + // Take a stable snapshot of the window list + let snapshot: Vec<(WindowKey, WindowEntry)> = { + let map: MutexGuard> = windows_map().lock().unwrap(); + map.iter().map(|(&k, v)| (k, v.clone())).collect() + }; + + // Sort entries + let mut rows: Vec<(WindowKey, WindowEntry)> = snapshot; + (&mut *rows).sort_by(|a, b| a.1.pid.cmp(&b.1.pid).then(a.0.cmp(&b.0))); + + // Timestamp + let ts: String = Local::now().format("%Y-%m-%d %H:%M:%S%.3f").to_string(); + + // Print Table Header + println!( + "WinApps Monitor Window List - {} Entr{} - Last Refresh: {}\r\n", + rows.len(), + if rows.len() == 1 { "y" } else { "ies" }, + ts + ); + println!("{:<14} {:<8} {}", "HWND", "PID", "TITLE"); + println!("{:-<14} {:-<8} {:-<80}", "", "", ""); + + // Print rows with aligned columns + for (hwnd, entry) in rows { + println!( + "{:<14} {:<8} {}", + format!("{:#x}", hwnd), + entry.pid, + truncate(&entry.title, 80) + ); + } + + // Closing horizontal rule + let total_width = 14 + 1 + 8 + 1 + 80; // widths + spaces + println!("{:-<1$}", "", total_width); + println!(); +} + +fn main() -> Result<()> { + // Display System Tray Icon + let icon = icon_from_ico(); + let _tray_icon = TrayIconBuilder::new() + .with_tooltip("WinApps Monitor") + .with_icon(icon) + .build()?; + + // Flags: + // - Call the callback function from outside the target application's process + // - Suppress events originating from this process + let flags: u32 = WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS; + + // Install hooks for the specific events we care about + let hook_create: HWINEVENTHOOK = unsafe { SetWinEventHook(EVENT_OBJECT_CREATE, EVENT_OBJECT_CREATE, None, Some(on_win_event), 0, 0, flags) }; + let hook_show: HWINEVENTHOOK = unsafe { SetWinEventHook(EVENT_OBJECT_SHOW, EVENT_OBJECT_SHOW, None, Some(on_win_event), 0, 0, flags) }; + let hook_hide: HWINEVENTHOOK = unsafe { SetWinEventHook(EVENT_OBJECT_HIDE, EVENT_OBJECT_HIDE, None, Some(on_win_event), 0, 0, flags) }; + let hook_destroy: HWINEVENTHOOK = unsafe { SetWinEventHook(EVENT_OBJECT_DESTROY, EVENT_OBJECT_DESTROY, None, Some(on_win_event), 0, 0, flags) }; + let hook_cloaked: HWINEVENTHOOK = unsafe { SetWinEventHook(EVENT_OBJECT_CLOAKED, EVENT_OBJECT_CLOAKED, None, Some(on_win_event), 0, 0, flags) }; + let hook_uncloaked: HWINEVENTHOOK = unsafe { SetWinEventHook(EVENT_OBJECT_UNCLOAKED, EVENT_OBJECT_UNCLOAKED, None, Some(on_win_event), 0, 0, flags) }; + + // Hook verification (non-null handles mean success) + println!("WINDOW HOOKS INSTALLED:"); + println!("- CREATE = {}", (!hook_create.0.is_null()).to_string().to_uppercase()); + println!("- SHOW = {}", (!hook_show.0.is_null()).to_string().to_uppercase()); + println!("- HIDE = {}", (!hook_hide.0.is_null()).to_string().to_uppercase()); + println!("- DESTROY = {}", (!hook_destroy.0.is_null()).to_string().to_uppercase()); + println!("- CLOAKED = {}", (!hook_cloaked.0.is_null()).to_string().to_uppercase()); + println!("- UNCLOAKED = {}", (!hook_uncloaked.0.is_null()).to_string().to_uppercase()); + println!(); + + // Seed the current state so already-open windows are present + seed_open_windows(); + + // Dump once to see the baseline state + debug_dump_windows(); + + // Keep the app alive AND pump messages so WinEvent callbacks can be delivered + unsafe { + let mut msg: MSG = MSG::default(); + // GetMessageW returns >0 until WM_QUIT; 0 on WM_QUIT; <0 on error. + while GetMessageW(&mut msg, None, 0, 0).into() { + let _ = TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + + // TODO Unreachable - Need to implement unhook on shutdown + #[allow(unreachable_code)] + unsafe { + if !hook_create.0.is_null() { let _ = UnhookWinEvent(hook_create); } + if !hook_show.0.is_null() { let _ = UnhookWinEvent(hook_show); } + if !hook_hide.0.is_null() { let _ = UnhookWinEvent(hook_hide); } + if !hook_destroy.0.is_null() { let _ = UnhookWinEvent(hook_destroy); } + if !hook_cloaked.0.is_null() { let _ = UnhookWinEvent(hook_cloaked); } + if !hook_uncloaked.0.is_null() { let _ = UnhookWinEvent(hook_uncloaked); } + } + + Ok(()) +} + +/// Name: is_application_frame_window +/// Purpose: Returns 'true' if the given window belongs to the UWP ApplicationFrameHost. This is +/// important to check, as UWP applications run under ApplicationFrameHost and need to be +/// enumerated differently. +/// Input: Window Handle (HWND) +/// Output: Boolean (True if UWP application, false if not) +fn is_application_frame_window(hwnd: HWND) -> bool { + let mut buf = [0u16; 128]; + unsafe { + let len = GetClassNameW(hwnd, &mut buf) as usize; + if len == 0 { + return false; + } + let class_name = String::from_utf16_lossy(&buf[..len]); + class_name == "ApplicationFrameWindow" + } +} + +/// Name: is_candidate_window +/// Purpose: Given a window handle, this function decides (using a few heuristics) whether the +/// window is likely to be a user-facing main/top-level application window, excluding +/// tool palettes, dialogs, popups, hidden windows, etc. +/// Input: Window Handle (HWND) +/// Output: Boolean (True if window of interest, false if not) +fn is_candidate_window(hwnd: HWND) -> bool { + unsafe { + // Exclude windows that are not visible + // Note: Minimised windows are considered visible - use IsIconic instead to exclude them + if IsWindowVisible(hwnd).as_bool() == false { + return false; + } + + // Exclude tool windows (palette/floaters) + let ex = GetWindowLongPtrW(hwnd, GWL_EXSTYLE) as u32; + if (ex & WS_EX_TOOLWINDOW.0) != 0 { + return false; + } + + // Detect common UWP host frame class + let is_uwp_frame = is_application_frame_window(hwnd); + + // Prefer top-level (no owner) to exclude popups/dialogs attached to a parent window, BUT: + // - Allow windows that explicitly ask for taskbar presence (WS_EX_APPWINDOW) + // - Allow UWP frames even if they have an owner + let root_owner: HWND = GetAncestor(hwnd, GA_ROOTOWNER); + let has_appwindow: bool = (ex & WS_EX_APPWINDOW.0) != 0; + if root_owner != hwnd && !has_appwindow && !is_uwp_frame { + return false; + } + + // Skip “hidden by owner” popups (Raymond Chen heuristic), + // but keep UWP frames which can have quirky popup chains. + let last_active_popup: HWND = GetLastActivePopup(hwnd); + if !is_uwp_frame + && last_active_popup != hwnd + && !IsWindowVisible(last_active_popup).as_bool() + { + return false; + } + + // Skip cloaked windows + let mut cloaked: u32 = 0; + let _ = DwmGetWindowAttribute( + hwnd, + DWMWA_CLOAKED, + &mut cloaked as *mut _ as *mut _, + size_of::() as u32, + ); + if cloaked != 0 { + return false; + } + } + true +} + +/// Name: seed_open_windows +/// Purpose: Enumerate current top-level windows and add them to WINDOWS. +fn seed_open_windows() { + unsafe { + // Write straight into the global map + let _ = EnumWindows(Some(enum_windows_seed_proc), LPARAM(0)); + } +} + +/// Name: enum_windows_seed_proc +/// Purpose: Enumerate current top-level windows and add them to WINDOWS. +extern "system" fn enum_windows_seed_proc(hwnd: HWND, _lparam: LPARAM) -> BOOL { + // Apply the same filters used in the hook callback + if !is_candidate_window(hwnd) { + return BOOL(1); + } + + // Store title + let title = window_title(hwnd); + + // Resolve true PID (handles ApplicationFrameWindow/UWP) + let pid = if is_application_frame_window(hwnd) { + uwp_pid(hwnd) + } else { + let mut p = 0u32; + unsafe { GetWindowThreadProcessId(hwnd, Some(&mut p)); } + p + }; + if pid == 0 { + return BOOL(1); + } + + // Insert into the map keyed by hWnd + add_window(hwnd.0 as isize, pid, title); + + BOOL(1) // Continue enumeration +} + +/// Name: enum_child_find_different_pid +/// Purpose: Discover the real/true process ID behind a 'modern' UWP window. +extern "system" fn enum_child_find_different_pid(hwnd: HWND, lparam: LPARAM) -> BOOL { + unsafe { + let ctx: &mut UwpPidSearch = &mut *(lparam.0 as *mut UwpPidSearch); + let mut child_pid = 0u32; + GetWindowThreadProcessId(hwnd, Some(&mut child_pid)); + + if child_pid != 0 && child_pid != ctx.frame_pid { + ctx.found_pid.set(child_pid); + return BOOL(0); // Stop enumeration + } + } + BOOL(1) // Continue +} + +/// Name: uwp_pid +/// Rationale: The top-level frame window for 'modern' UWP applications is owned by the host process +/// 'ApplicationFrameWindow'. Additional logic is required to identify the 'true' process +/// ID since the application content lives within a child window belonging to a different +/// process. +fn uwp_pid(hwnd: HWND) -> u32 { + unsafe { + let mut frame_pid: u32 = 0u32; + GetWindowThreadProcessId(hwnd, Some(&mut frame_pid)); + let found: Cell = Cell::new(0u32); + let mut ctx = UwpPidSearch { + frame_pid, + found_pid: &found, + }; + + let _ = EnumChildWindows( + Some(hwnd), + Some(enum_child_find_different_pid), + LPARAM(&mut ctx as *mut _ as isize), + ); + + let pid: u32 = found.get(); + if pid != 0 { pid } else { frame_pid } + } +} + +/// Name: truncate +/// Purpose: For tidy console output. +fn truncate(s: &str, max: usize) -> String { + if s.len() <= max { + s.to_string() + } else { + let mut t = s.chars().take(max.saturating_sub(1)).collect::(); + t.push('…'); + t + } +} + +/// Name: icon_from_ico +/// Purpose: Embed and prepare the system tray icon for use. +fn icon_from_ico() -> Icon { + // Embed ICO at compile time + let bytes = include_bytes!("../assets/icons/system_tray_icon.ico"); + + // Process .ico file -> Pick the highest resolution -> Convert to RGBA + let mut cursor = std::io::Cursor::new(&bytes[..]); + let dir = ico::IconDir::read(&mut cursor).expect("Invalid .ico"); + let best = dir.entries() + .iter() + .max_by_key(|e| (e.width(), e.height(), e.bits_per_pixel())) + .expect("No entries in .ico"); + let image = best.decode().expect("Failed to decode .ico image"); + let width = image.width(); + let height = image.height(); + let rgba = image.rgba_data().to_vec(); // RGBA8 + + // Return the RGBA Icon + Icon::from_rgba(rgba, width, height).expect("tray_icon::Icon::from_rgba failed") +} diff --git a/winapps-monitor/src/svg2ico.py b/winapps-monitor/src/svg2ico.py new file mode 100644 index 00000000..ab7a1bf2 --- /dev/null +++ b/winapps-monitor/src/svg2ico.py @@ -0,0 +1,20 @@ +# conda install pillow +# conda install -c conda-forge cairosvg +from PIL import Image +import io +import cairosvg + +# File Paths +svg_path = "../assets/icons/system_tray_icon.svg" +ico_path = "../assets/icons/system_tray_icon.ico" + +# Convert SVG file to PNG bytes +with open(svg_path, "rb") as f: + svg_data = f.read() +png_bytes = cairosvg.svg2png(bytestring=svg_data, output_width=256, output_height=256) + +# Open PNG bytes as Pillow image +image = Image.open(io.BytesIO(png_bytes)) + +# Save as ICO with multiple sizes +image.save(ico_path, format="ICO", sizes=[(16,16),(32,32),(48,48),(64,64),(128,128),(256,256)]) \ No newline at end of file From 7d42d1cfea1f9ab83a7b34c8a51062548dce3a26 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 12:24:59 +0000 Subject: [PATCH 3/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .gitignore | 2 +- winapps-cli/src/main.rs | 4 +- winapps-monitor/Cargo.toml | 2 +- winapps-monitor/src/main.rs | 200 ++++++++++++++++++++++++--------- winapps-monitor/src/svg2ico.py | 2 +- winapps/src/freerdp.rs | 2 +- winapps/src/quickemu.rs | 2 +- 7 files changed, 156 insertions(+), 58 deletions(-) diff --git a/.gitignore b/.gitignore index 297aac4f..2606d3e8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,4 @@ Cargo.lock # Operating system generated metadata files .DS_Store Thumbs.db -Desktop.ini \ No newline at end of file +Desktop.ini diff --git a/winapps-cli/src/main.rs b/winapps-cli/src/main.rs index 3fb78ee0..140be7d1 100644 --- a/winapps-cli/src/main.rs +++ b/winapps-cli/src/main.rs @@ -1,7 +1,7 @@ -use clap::{arg, Command}; +use clap::{Command, arg}; use winapps::freerdp::freerdp_back::Freerdp; use winapps::quickemu::{create_vm, kill_vm, start_vm}; -use winapps::{unwrap_or_panic, RemoteClient}; +use winapps::{RemoteClient, unwrap_or_panic}; fn cli() -> Command { Command::new("winapps-cli") diff --git a/winapps-monitor/Cargo.toml b/winapps-monitor/Cargo.toml index 8762165a..9b80b900 100644 --- a/winapps-monitor/Cargo.toml +++ b/winapps-monitor/Cargo.toml @@ -20,4 +20,4 @@ features = [ "Win32_Graphics_Dwm", "Win32_UI_Accessibility", "Win32_System_Com" -] \ No newline at end of file +] diff --git a/winapps-monitor/src/main.rs b/winapps-monitor/src/main.rs index 7014b80d..e4a50684 100644 --- a/winapps-monitor/src/main.rs +++ b/winapps-monitor/src/main.rs @@ -1,29 +1,29 @@ use anyhow::Result; use chrono::Local; +use std::cell::Cell; use std::collections::HashMap; -use std::sync::{Mutex, MutexGuard, OnceLock}; use std::ffi::OsString; use std::os::windows::ffi::OsStringExt; -use std::cell::Cell; -use tray_icon::{TrayIconBuilder, Icon}; +use std::sync::{Mutex, MutexGuard, OnceLock}; +use tray_icon::{Icon, TrayIconBuilder}; use windows::{ - core::BOOL, Win32::{ Foundation::{HWND, LPARAM}, - Graphics::Dwm::{DwmGetWindowAttribute, DWMWA_CLOAKED}, + Graphics::Dwm::{DWMWA_CLOAKED, DwmGetWindowAttribute}, UI::{ - Accessibility::{SetWinEventHook, UnhookWinEvent, HWINEVENTHOOK}, + Accessibility::{HWINEVENTHOOK, SetWinEventHook, UnhookWinEvent}, WindowsAndMessaging::{ - EnumWindows, GetAncestor, GetLastActivePopup, GetWindowLongPtrW, + DispatchMessageW, EVENT_OBJECT_CLOAKED, EVENT_OBJECT_CREATE, EVENT_OBJECT_DESTROY, + EVENT_OBJECT_HIDE, EVENT_OBJECT_NAMECHANGE, EVENT_OBJECT_SHOW, + EVENT_OBJECT_UNCLOAKED, EnumChildWindows, EnumWindows, GA_ROOTOWNER, GWL_EXSTYLE, + GetAncestor, GetClassNameW, GetLastActivePopup, GetMessageW, GetWindowLongPtrW, GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId, IsWindowVisible, - GetClassNameW, EnumChildWindows, GetMessageW, TranslateMessage, DispatchMessageW, - GA_ROOTOWNER, GWL_EXSTYLE, WS_EX_TOOLWINDOW, WS_EX_APPWINDOW, EVENT_OBJECT_CREATE, - EVENT_OBJECT_DESTROY, EVENT_OBJECT_SHOW, EVENT_OBJECT_HIDE, EVENT_OBJECT_CLOAKED, - EVENT_OBJECT_UNCLOAKED, OBJID_WINDOW, EVENT_OBJECT_NAMECHANGE, - WINEVENT_OUTOFCONTEXT, WINEVENT_SKIPOWNPROCESS, OBJECT_IDENTIFIER, MSG - } - } - } + MSG, OBJECT_IDENTIFIER, OBJID_WINDOW, TranslateMessage, WINEVENT_OUTOFCONTEXT, + WINEVENT_SKIPOWNPROCESS, WS_EX_APPWINDOW, WS_EX_TOOLWINDOW, + }, + }, + }, + core::BOOL, }; type WindowKey = isize; @@ -51,14 +51,14 @@ fn windows_map() -> &'static Mutex> { /// - WINDOW APPEARED: EVENT_OBJECT_CREATE, EVENT_OBJECT_SHOW, EVENT_OBJECT_UNCLOAKED /// - WINDOW DISAPPEARED: EVENT_OBJECT_HIDE, EVENT_OBJECT_DESTROY, EVENT_OBJECT_CLOAKED /// - WINDOW NAME CHANGED: EVENT_OBJECT_NAMECHANGE -extern "system" fn on_win_event ( +extern "system" fn on_win_event( _hook: HWINEVENTHOOK, event: u32, hwnd: HWND, id_object: i32, _id_child: i32, _evt_thread: u32, - _evt_time: u32 + _evt_time: u32, ) { // Debugging //println!("[evt {event:#x}] hwnd={:?} id_object={}", hwnd, id_object); @@ -75,7 +75,10 @@ extern "system" fn on_win_event ( } // Check if a window appeared or disappeared - if matches!(event, EVENT_OBJECT_CREATE | EVENT_OBJECT_SHOW | EVENT_OBJECT_UNCLOAKED) { + if matches!( + event, + EVENT_OBJECT_CREATE | EVENT_OBJECT_SHOW | EVENT_OBJECT_UNCLOAKED + ) { // Check the new window is a user-facing main/top-level application window if !is_candidate_window(hwnd) { return; @@ -91,18 +94,17 @@ extern "system" fn on_win_event ( */ // Identify the process ID - let pid: u32 = - if is_application_frame_window(hwnd) { - // UWP - uwp_pid(hwnd) - } else { - // Win32 - let mut p: u32 = 0u32; - unsafe { - GetWindowThreadProcessId(hwnd, Some(&mut p)); - } - p - }; + let pid: u32 = if is_application_frame_window(hwnd) { + // UWP + uwp_pid(hwnd) + } else { + // Win32 + let mut p: u32 = 0u32; + unsafe { + GetWindowThreadProcessId(hwnd, Some(&mut p)); + } + p + }; // Grab the current title (can empty on window creation) let title: String = window_title(hwnd); @@ -115,7 +117,10 @@ extern "system" fn on_win_event ( let title: String = window_title(hwnd); update_window_title(hwnd.0 as isize, title); debug_dump_windows(); - } else if matches!(event, EVENT_OBJECT_HIDE | EVENT_OBJECT_DESTROY | EVENT_OBJECT_CLOAKED) { + } else if matches!( + event, + EVENT_OBJECT_HIDE | EVENT_OBJECT_DESTROY | EVENT_OBJECT_CLOAKED + ) { // Remove window remove_window(hwnd.0 as isize); debug_dump_windows(); @@ -216,21 +221,99 @@ fn main() -> Result<()> { let flags: u32 = WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS; // Install hooks for the specific events we care about - let hook_create: HWINEVENTHOOK = unsafe { SetWinEventHook(EVENT_OBJECT_CREATE, EVENT_OBJECT_CREATE, None, Some(on_win_event), 0, 0, flags) }; - let hook_show: HWINEVENTHOOK = unsafe { SetWinEventHook(EVENT_OBJECT_SHOW, EVENT_OBJECT_SHOW, None, Some(on_win_event), 0, 0, flags) }; - let hook_hide: HWINEVENTHOOK = unsafe { SetWinEventHook(EVENT_OBJECT_HIDE, EVENT_OBJECT_HIDE, None, Some(on_win_event), 0, 0, flags) }; - let hook_destroy: HWINEVENTHOOK = unsafe { SetWinEventHook(EVENT_OBJECT_DESTROY, EVENT_OBJECT_DESTROY, None, Some(on_win_event), 0, 0, flags) }; - let hook_cloaked: HWINEVENTHOOK = unsafe { SetWinEventHook(EVENT_OBJECT_CLOAKED, EVENT_OBJECT_CLOAKED, None, Some(on_win_event), 0, 0, flags) }; - let hook_uncloaked: HWINEVENTHOOK = unsafe { SetWinEventHook(EVENT_OBJECT_UNCLOAKED, EVENT_OBJECT_UNCLOAKED, None, Some(on_win_event), 0, 0, flags) }; + let hook_create: HWINEVENTHOOK = unsafe { + SetWinEventHook( + EVENT_OBJECT_CREATE, + EVENT_OBJECT_CREATE, + None, + Some(on_win_event), + 0, + 0, + flags, + ) + }; + let hook_show: HWINEVENTHOOK = unsafe { + SetWinEventHook( + EVENT_OBJECT_SHOW, + EVENT_OBJECT_SHOW, + None, + Some(on_win_event), + 0, + 0, + flags, + ) + }; + let hook_hide: HWINEVENTHOOK = unsafe { + SetWinEventHook( + EVENT_OBJECT_HIDE, + EVENT_OBJECT_HIDE, + None, + Some(on_win_event), + 0, + 0, + flags, + ) + }; + let hook_destroy: HWINEVENTHOOK = unsafe { + SetWinEventHook( + EVENT_OBJECT_DESTROY, + EVENT_OBJECT_DESTROY, + None, + Some(on_win_event), + 0, + 0, + flags, + ) + }; + let hook_cloaked: HWINEVENTHOOK = unsafe { + SetWinEventHook( + EVENT_OBJECT_CLOAKED, + EVENT_OBJECT_CLOAKED, + None, + Some(on_win_event), + 0, + 0, + flags, + ) + }; + let hook_uncloaked: HWINEVENTHOOK = unsafe { + SetWinEventHook( + EVENT_OBJECT_UNCLOAKED, + EVENT_OBJECT_UNCLOAKED, + None, + Some(on_win_event), + 0, + 0, + flags, + ) + }; // Hook verification (non-null handles mean success) println!("WINDOW HOOKS INSTALLED:"); - println!("- CREATE = {}", (!hook_create.0.is_null()).to_string().to_uppercase()); - println!("- SHOW = {}", (!hook_show.0.is_null()).to_string().to_uppercase()); - println!("- HIDE = {}", (!hook_hide.0.is_null()).to_string().to_uppercase()); - println!("- DESTROY = {}", (!hook_destroy.0.is_null()).to_string().to_uppercase()); - println!("- CLOAKED = {}", (!hook_cloaked.0.is_null()).to_string().to_uppercase()); - println!("- UNCLOAKED = {}", (!hook_uncloaked.0.is_null()).to_string().to_uppercase()); + println!( + "- CREATE = {}", + (!hook_create.0.is_null()).to_string().to_uppercase() + ); + println!( + "- SHOW = {}", + (!hook_show.0.is_null()).to_string().to_uppercase() + ); + println!( + "- HIDE = {}", + (!hook_hide.0.is_null()).to_string().to_uppercase() + ); + println!( + "- DESTROY = {}", + (!hook_destroy.0.is_null()).to_string().to_uppercase() + ); + println!( + "- CLOAKED = {}", + (!hook_cloaked.0.is_null()).to_string().to_uppercase() + ); + println!( + "- UNCLOAKED = {}", + (!hook_uncloaked.0.is_null()).to_string().to_uppercase() + ); println!(); // Seed the current state so already-open windows are present @@ -252,12 +335,24 @@ fn main() -> Result<()> { // TODO Unreachable - Need to implement unhook on shutdown #[allow(unreachable_code)] unsafe { - if !hook_create.0.is_null() { let _ = UnhookWinEvent(hook_create); } - if !hook_show.0.is_null() { let _ = UnhookWinEvent(hook_show); } - if !hook_hide.0.is_null() { let _ = UnhookWinEvent(hook_hide); } - if !hook_destroy.0.is_null() { let _ = UnhookWinEvent(hook_destroy); } - if !hook_cloaked.0.is_null() { let _ = UnhookWinEvent(hook_cloaked); } - if !hook_uncloaked.0.is_null() { let _ = UnhookWinEvent(hook_uncloaked); } + if !hook_create.0.is_null() { + let _ = UnhookWinEvent(hook_create); + } + if !hook_show.0.is_null() { + let _ = UnhookWinEvent(hook_show); + } + if !hook_hide.0.is_null() { + let _ = UnhookWinEvent(hook_hide); + } + if !hook_destroy.0.is_null() { + let _ = UnhookWinEvent(hook_destroy); + } + if !hook_cloaked.0.is_null() { + let _ = UnhookWinEvent(hook_cloaked); + } + if !hook_uncloaked.0.is_null() { + let _ = UnhookWinEvent(hook_uncloaked); + } } Ok(()) @@ -363,7 +458,9 @@ extern "system" fn enum_windows_seed_proc(hwnd: HWND, _lparam: LPARAM) -> BOOL { uwp_pid(hwnd) } else { let mut p = 0u32; - unsafe { GetWindowThreadProcessId(hwnd, Some(&mut p)); } + unsafe { + GetWindowThreadProcessId(hwnd, Some(&mut p)); + } p }; if pid == 0 { @@ -439,7 +536,8 @@ fn icon_from_ico() -> Icon { // Process .ico file -> Pick the highest resolution -> Convert to RGBA let mut cursor = std::io::Cursor::new(&bytes[..]); let dir = ico::IconDir::read(&mut cursor).expect("Invalid .ico"); - let best = dir.entries() + let best = dir + .entries() .iter() .max_by_key(|e| (e.width(), e.height(), e.bits_per_pixel())) .expect("No entries in .ico"); diff --git a/winapps-monitor/src/svg2ico.py b/winapps-monitor/src/svg2ico.py index ab7a1bf2..6663d1bd 100644 --- a/winapps-monitor/src/svg2ico.py +++ b/winapps-monitor/src/svg2ico.py @@ -17,4 +17,4 @@ image = Image.open(io.BytesIO(png_bytes)) # Save as ICO with multiple sizes -image.save(ico_path, format="ICO", sizes=[(16,16),(32,32),(48,48),(64,64),(128,128),(256,256)]) \ No newline at end of file +image.save(ico_path, format="ICO", sizes=[(16,16),(32,32),(48,48),(64,64),(128,128),(256,256)]) diff --git a/winapps/src/freerdp.rs b/winapps/src/freerdp.rs index b88936aa..cc51e1d2 100644 --- a/winapps/src/freerdp.rs +++ b/winapps/src/freerdp.rs @@ -2,7 +2,7 @@ pub mod freerdp_back { use std::process::{Command, Stdio}; use tracing::{info, warn}; - use crate::{unwrap_or_exit, Config, RemoteClient}; + use crate::{Config, RemoteClient, unwrap_or_exit}; pub struct Freerdp {} diff --git a/winapps/src/quickemu.rs b/winapps/src/quickemu.rs index d2cb54cd..aed7175e 100644 --- a/winapps/src/quickemu.rs +++ b/winapps/src/quickemu.rs @@ -1,4 +1,4 @@ -use crate::{get_data_dir, save_config, unwrap_or_exit, Config}; +use crate::{Config, get_data_dir, save_config, unwrap_or_exit}; use std::fs; use std::process::Command; use tracing::info;