From 3ad72b67a4178057d4a4ff94d10b0297649457a9 Mon Sep 17 00:00:00 2001 From: fix2it Date: Sat, 12 Sep 2026 22:57:47 +0300 Subject: [PATCH 1/4] fix: prevent deadlock on Explorer hang and ensure taskbar recovery - Replace synchronous SHAppBarMessage in get_taskbar_rect with non-blocking get_window_rect_safe to prevent thread deadlock when explorer.exe hangs (Application Hang 1002) - In spawn_taskbar_watchdog, verify parent window validity via GetParent() when shell_hosted to detect orphaned child windows across explorer restarts - Re-dock taskbar surfaces and re-render on TaskbarCreated shell broadcast --- .github/workflows/ci.yml | 40 ++++++++++++++++++++++++++++++++++++++ src/native_interop.rs | 28 ++++++-------------------- src/window.rs | 21 +++++++++++++++++--- src/window/message_loop.rs | 2 ++ 4 files changed, 66 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..9a79df8e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + branches: [ "**" ] + pull_request: + branches: [ "**" ] + +permissions: + contents: read + +jobs: + build-and-test: + name: Build, Test & Package + runs-on: windows-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + + - name: Check code + run: cargo check --all-targets + + - name: Run unit tests + run: cargo test --all + + - name: Build release binary + run: cargo build --release + + - name: Upload release artifact + uses: actions/upload-artifact@v4 + with: + name: claude-code-usage-monitor-fixed + path: target/release/claude-code-usage-monitor.exe + if-no-files-found: error diff --git a/src/native_interop.rs b/src/native_interop.rs index 6dabe605..8aeea0a8 100644 --- a/src/native_interop.rs +++ b/src/native_interop.rs @@ -143,29 +143,13 @@ pub fn find_child_window(parent: HWND, class_name: &str) -> Option { } } -/// Get taskbar position via SHAppBarMessage +/// Get taskbar position safely. +/// We use get_window_rect_safe directly because GetWindowRect is a non-blocking +/// kernel-mode query that returns immediately even if explorer.exe is hung or unresponsive. +/// SHAppBarMessage sends a synchronous LPC message to explorer.exe's UI message loop, +/// which deadlocks the monitor thread if Explorer hangs (Event 1002). pub fn get_taskbar_rect(taskbar_hwnd: HWND) -> Option { - unsafe { - let mut class_name = [0u16; 64]; - let len = GetClassNameW(taskbar_hwnd, &mut class_name); - if len > 0 { - let class_name = String::from_utf16_lossy(&class_name[..len as usize]); - if class_name == "Shell_SecondaryTrayWnd" { - return get_window_rect_safe(taskbar_hwnd); - } - } - - let mut abd = APPBARDATA { - cbSize: std::mem::size_of::() as u32, - hWnd: taskbar_hwnd, - ..Default::default() - }; - let result = SHAppBarMessage(ABM_GETTASKBARPOS, &mut abd); - if result == 0 { - return None; - } - Some(abd.rc) - } + get_window_rect_safe(taskbar_hwnd) } /// Get the bounding rectangle of a window diff --git a/src/window.rs b/src/window.rs index 4325ce06..d7c68a30 100644 --- a/src/window.rs +++ b/src/window.rs @@ -433,9 +433,24 @@ fn spawn_taskbar_watchdog() { if !shell_hosted { continue; } - let invalid = windows - .iter() - .any(|window| unsafe { !IsWindow(Some(window.to_hwnd())).as_bool() }); + let invalid = windows.iter().any(|window| unsafe { + let hwnd = window.to_hwnd(); + if !IsWindow(Some(hwnd)).as_bool() { + return true; + } + if shell_hosted { + // When hosted inside a shell window (like Shell_TrayWnd or Progman), + // Windows does not always destroy cross-process child windows when Explorer restarts. + // Verify that the parent window is still alive and valid. + let parent = GetParent(hwnd).ok(); + match parent { + Some(p) if !p.is_invalid() => !IsWindow(Some(p)).as_bool(), + _ => true, + } + } else { + false + } + }); if invalid && !native_interop::find_taskbars().is_empty() { diagnose::log("watchdog: shell-hosted surface was destroyed -> relaunching"); relaunch_self(); diff --git a/src/window/message_loop.rs b/src/window/message_loop.rs index f8d7043c..d24b3a46 100644 --- a/src/window/message_loop.rs +++ b/src/window/message_loop.rs @@ -545,6 +545,8 @@ pub(super) unsafe extern "system" fn wnd_proc( // and tray-icon-only themes keep their owner HWND, so restore the // registrations when the shell broadcasts its return. sync_tray_icon(hwnd); + position_at_taskbar(); + render_layered(); LRESULT(0) } WM_DESTROY => { From a40d85c75c418c97735fc1ad91d0789c4fde5160 Mon Sep 17 00:00:00 2001 From: fix2it Date: Sat, 12 Sep 2026 23:35:03 +0300 Subject: [PATCH 2/4] fix(watchdog): treat unparented windows as valid to prevent relaunch loop --- src/window.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/window.rs b/src/window.rs index d7c68a30..77e01a0b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -441,11 +441,10 @@ fn spawn_taskbar_watchdog() { if shell_hosted { // When hosted inside a shell window (like Shell_TrayWnd or Progman), // Windows does not always destroy cross-process child windows when Explorer restarts. - // Verify that the parent window is still alive and valid. - let parent = GetParent(hwnd).ok(); - match parent { + // If this window has a parent that is now destroyed, flag it as invalid. + match GetParent(hwnd).ok() { Some(p) if !p.is_invalid() => !IsWindow(Some(p)).as_bool(), - _ => true, + _ => false, } } else { false From 1acaa5823d286323d6d285afbf8c077eae07ced7 Mon Sep 17 00:00:00 2001 From: fix2it Date: Sat, 12 Sep 2026 23:40:08 +0300 Subject: [PATCH 3/4] clean(native_interop): remove unused SHAppBarMessage imports --- src/native_interop.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/native_interop.rs b/src/native_interop.rs index 8aeea0a8..febdd663 100644 --- a/src/native_interop.rs +++ b/src/native_interop.rs @@ -7,7 +7,6 @@ use windows::Win32::Graphics::Gdi::{ EnumDisplayMonitors, GetMonitorInfoW, HDC, HMONITOR, MONITORINFO, }; use windows::Win32::UI::Accessibility::{SetWinEventHook, UnhookWinEvent, HWINEVENTHOOK}; -use windows::Win32::UI::Shell::{SHAppBarMessage, ABM_GETTASKBARPOS, APPBARDATA}; use windows::Win32::UI::WindowsAndMessaging::*; // Window style constants From 80d99989bed8d495bcd90032af4d445675db67d2 Mon Sep 17 00:00:00 2001 From: fix2it Date: Sun, 13 Sep 2026 00:18:22 +0300 Subject: [PATCH 4/4] refactor(review): optimize watchdog allocations, clean TaskbarCreated dispatch, and fix CI BOM --- .github/workflows/ci.yml | 8 +++---- src/native_interop.rs | 2 +- src/window.rs | 48 ++++++++++++++++--------------------- src/window/host_geometry.rs | 2 +- src/window/message_loop.rs | 1 - 5 files changed, 26 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a79df8e..d9640531 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,10 @@ -name: CI +name: CI on: push: - branches: [ "**" ] + branches: [ main, "fix/**", "feat/**" ] pull_request: - branches: [ "**" ] + branches: [ main ] permissions: contents: read @@ -35,6 +35,6 @@ jobs: - name: Upload release artifact uses: actions/upload-artifact@v4 with: - name: claude-code-usage-monitor-fixed + name: claude-code-usage-monitor-release path: target/release/claude-code-usage-monitor.exe if-no-files-found: error diff --git a/src/native_interop.rs b/src/native_interop.rs index febdd663..6c1da838 100644 --- a/src/native_interop.rs +++ b/src/native_interop.rs @@ -103,7 +103,7 @@ pub fn find_taskbars() -> Vec { if len > 0 { let class_name = String::from_utf16_lossy(&class_name[..len as usize]); if class_name == "Shell_TrayWnd" || class_name == "Shell_SecondaryTrayWnd" { - if let Some(rect) = get_taskbar_rect(hwnd).or_else(|| get_window_rect_safe(hwnd)) { + if let Some(rect) = get_taskbar_rect(hwnd) { taskbars.push(TaskbarWindow { hwnd, rect }); } } diff --git a/src/window.rs b/src/window.rs index 77e01a0b..452e1c52 100644 --- a/src/window.rs +++ b/src/window.rs @@ -406,7 +406,7 @@ fn relaunch_self() { fn spawn_taskbar_watchdog() { std::thread::spawn(move || loop { std::thread::sleep(Duration::from_secs(TASKBAR_WATCH_INTERVAL_SECS)); - let (shell_hosted, windows) = { + let invalid = { let state = lock_state(); let Some(state) = state.as_ref() else { continue; @@ -422,34 +422,26 @@ fn spawn_taskbar_watchdog() { ) }) }); - ( - shell_hosted, - std::iter::once(state.hwnd) - .chain(state.mirror_hwnds.iter().copied()) - .chain(state.desktop_hwnds.iter().flatten().copied()) - .collect::>(), - ) - }; - if !shell_hosted { - continue; - } - let invalid = windows.iter().any(|window| unsafe { - let hwnd = window.to_hwnd(); - if !IsWindow(Some(hwnd)).as_bool() { - return true; - } - if shell_hosted { - // When hosted inside a shell window (like Shell_TrayWnd or Progman), - // Windows does not always destroy cross-process child windows when Explorer restarts. - // If this window has a parent that is now destroyed, flag it as invalid. - match GetParent(hwnd).ok() { - Some(p) if !p.is_invalid() => !IsWindow(Some(p)).as_bool(), - _ => false, - } - } else { - false + if !shell_hosted { + continue; } - }); + std::iter::once(state.hwnd) + .chain(state.mirror_hwnds.iter().copied()) + .chain(state.desktop_hwnds.iter().flatten().copied()) + .any(|window| unsafe { + let hwnd = window.to_hwnd(); + if !IsWindow(Some(hwnd)).as_bool() { + return true; + } + // When hosted inside a shell window (like Shell_TrayWnd or Progman), + // Windows does not always destroy cross-process child windows when Explorer restarts. + // If this window has a parent that is now destroyed, flag it as invalid. + match GetParent(hwnd).ok() { + Some(p) if !p.is_invalid() => !IsWindow(Some(p)).as_bool(), + _ => false, + } + }) + }; if invalid && !native_interop::find_taskbars().is_empty() { diagnose::log("watchdog: shell-hosted surface was destroyed -> relaunching"); relaunch_self(); diff --git a/src/window/host_geometry.rs b/src/window/host_geometry.rs index b4a4d365..4bf743f0 100644 --- a/src/window/host_geometry.rs +++ b/src/window/host_geometry.rs @@ -34,7 +34,7 @@ pub(super) fn refresh_theme_host_geometry() { } fn refresh_with(query: impl FnOnce() -> Vec) { - // SHAppBarMessage can dispatch another layout notification before returning. + // Layout queries can dispatch another layout notification before returning. // Reentrant readers keep using the last complete snapshot; never wait here. if REFRESHING.swap(true, Ordering::Acquire) { return; diff --git a/src/window/message_loop.rs b/src/window/message_loop.rs index d24b3a46..6abcaabb 100644 --- a/src/window/message_loop.rs +++ b/src/window/message_loop.rs @@ -545,7 +545,6 @@ pub(super) unsafe extern "system" fn wnd_proc( // and tray-icon-only themes keep their owner HWND, so restore the // registrations when the shell broadcasts its return. sync_tray_icon(hwnd); - position_at_taskbar(); render_layered(); LRESULT(0) }