From 7aaaecc60f4d0b233d9518c86018044cb6542b14 Mon Sep 17 00:00:00 2001 From: tian Date: Sun, 29 Mar 2026 11:45:48 +0800 Subject: [PATCH 1/3] fix(windows): prevent mouse drag lag from hook timeout and silent removal Three root causes addressed: 1. Hook on UI thread: Low-level hooks (WH_MOUSE_LL / WH_KEYBOARD_LL) were installed on the WPF UI thread. Any UI stall (layout, GC, Dispatcher.Invoke) would delay the hook callback, eventually causing Windows to skip or silently remove the hook. Now hooks run on a dedicated background thread with its own message loop. 2. Expensive work in hook callback: GetActiveAppInfo() called Process.GetProcessById + FileVersionInfo synchronously in the hook callback. Now only lightweight hWnd + processId capture happens in the callback; full resolution is deferred to ThreadPool via the new ActiveWindowManager.ResolveAppInfo() method. Process-level cache further reduces cold-miss cost. 3. No recovery from silent hook removal: Windows can silently stop delivering events to a timed-out hook, with no notification. Added a watchdog timer that monitors cursor position vs hook activity and automatically reinstalls hooks when they appear dead. Amp-Thread-ID: https://ampcode.com/threads/T-019d37a1-587a-77ab-bdea-b9e2048057d6 Co-authored-by: Amp --- .../KeyStats/Helpers/ActiveWindowManager.cs | 56 +++- .../KeyStats/Helpers/NativeInterop.cs | 33 ++ .../KeyStats/Services/InputMonitorService.cs | 290 +++++++++++++----- 3 files changed, 302 insertions(+), 77 deletions(-) diff --git a/KeyStats.Windows/KeyStats/Helpers/ActiveWindowManager.cs b/KeyStats.Windows/KeyStats/Helpers/ActiveWindowManager.cs index c7985b9..e461b89 100644 --- a/KeyStats.Windows/KeyStats/Helpers/ActiveWindowManager.cs +++ b/KeyStats.Windows/KeyStats/Helpers/ActiveWindowManager.cs @@ -33,6 +33,11 @@ public static class ActiveWindowManager private static IntPtr _lastWindowHandle = IntPtr.Zero; private static ActiveAppInfo _lastAppInfo = ActiveAppInfo.Unknown; + // Cache process name + display name by process ID to avoid repeated + // Process.GetProcessById / FileVersionInfo lookups inside the hook callback. + private static readonly Dictionary _processCache = new(); + private const int MaxProcessCacheSize = 64; + /// /// Gets the foreground app identity for attribution and display. /// @@ -71,6 +76,39 @@ public static ActiveAppInfo GetActiveAppInfo() } } + /// + /// Resolves app info from a pre-captured window handle and process ID. + /// Use this when hWnd/pid were captured in a time-critical path (e.g. hook callback) + /// and the expensive resolution is deferred to a background thread. + /// + public static ActiveAppInfo ResolveAppInfo(IntPtr hWnd, uint processId) + { + if (hWnd == IntPtr.Zero || processId == 0) + { + return ActiveAppInfo.Unknown; + } + + try + { + lock (_lock) + { + if (hWnd == _lastWindowHandle && _lastAppInfo.IsKnown) + { + return _lastAppInfo; + } + + var appInfo = BuildAppInfo(hWnd, processId); + _lastWindowHandle = hWnd; + _lastAppInfo = appInfo; + return appInfo; + } + } + catch + { + return ActiveAppInfo.Unknown; + } + } + /// /// Backward-compatible accessor when only process identity is needed. /// @@ -83,14 +121,28 @@ private static ActiveAppInfo BuildAppInfo(IntPtr windowHandle, uint processId) { var windowTitle = GetWindowTitle(windowHandle); + // Use cached process identity when available to avoid expensive + // Process.GetProcessById + FileVersionInfo on every window switch. + if (_processCache.TryGetValue(processId, out var cached)) + { + var displayName = ResolveDisplayName(cached.ProcessName, cached.DisplayName, windowTitle); + return new ActiveAppInfo(cached.ProcessName, displayName, windowTitle, processId, windowHandle); + } + try { using var process = Process.GetProcessById((int)processId); var processName = NormalizeProcessName(process.ProcessName); var fileDisplayName = GetFileDisplayName(process, processName); - var displayName = ResolveDisplayName(processName, fileDisplayName, windowTitle); - return new ActiveAppInfo(processName, displayName, windowTitle, processId, windowHandle); + if (_processCache.Count >= MaxProcessCacheSize) + { + _processCache.Clear(); + } + _processCache[processId] = (processName, fileDisplayName); + + var resolvedDisplayName = ResolveDisplayName(processName, fileDisplayName, windowTitle); + return new ActiveAppInfo(processName, resolvedDisplayName, windowTitle, processId, windowHandle); } catch { diff --git a/KeyStats.Windows/KeyStats/Helpers/NativeInterop.cs b/KeyStats.Windows/KeyStats/Helpers/NativeInterop.cs index 0c46741..6e7f18c 100644 --- a/KeyStats.Windows/KeyStats/Helpers/NativeInterop.cs +++ b/KeyStats.Windows/KeyStats/Helpers/NativeInterop.cs @@ -144,6 +144,39 @@ public static short LoWord(int dword) [DllImport("user32.dll", SetLastError = true)] public static extern int GetWindowTextLength(IntPtr hWnd); + public const int WM_QUIT = 0x0012; + + [StructLayout(LayoutKind.Sequential)] + public struct MSG + { + public IntPtr hwnd; + public uint message; + public IntPtr wParam; + public IntPtr lParam; + public uint time; + public POINT pt; + } + + [DllImport("user32.dll")] + public static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); + + [DllImport("user32.dll")] + public static extern bool TranslateMessage(ref MSG lpMsg); + + [DllImport("user32.dll")] + public static extern IntPtr DispatchMessage(ref MSG lpMsg); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool PostThreadMessage(uint threadId, uint msg, IntPtr wParam, IntPtr lParam); + + [DllImport("kernel32.dll")] + public static extern uint GetCurrentThreadId(); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetCursorPos(out POINT lpPoint); + [DllImport("dwmapi.dll", PreserveSig = true)] private static extern int DwmSetWindowAttribute(IntPtr hwnd, int dwAttribute, ref int pvAttribute, int cbAttribute); diff --git a/KeyStats.Windows/KeyStats/Services/InputMonitorService.cs b/KeyStats.Windows/KeyStats/Services/InputMonitorService.cs index 42f0a46..892231b 100644 --- a/KeyStats.Windows/KeyStats/Services/InputMonitorService.cs +++ b/KeyStats.Windows/KeyStats/Services/InputMonitorService.cs @@ -24,6 +24,17 @@ public class InputMonitorService : IDisposable private System.Drawing.Point? _lastMousePosition; private double _accumulatedDistance = 0.0; + // 专用 hook 线程,避免 UI 线程卡顿导致 hook 超时 + private Thread? _hookThread; + private uint _hookThreadId; + + // hook 健康检查:watchdog 定时检测 hook 是否被 Windows 静默移除 + private Timer? _watchdogTimer; + private long _lastMouseHookTick; + private NativeInterop.POINT _lastCursorPos; + private const int WatchdogIntervalMs = 3000; + private const int HookDeadThresholdMs = 5000; + public event Action? KeyPressed; public event Action? LeftMouseClicked; public event Action? RightMouseClicked; @@ -42,53 +53,178 @@ public void StartMonitoring() _keyboardProc = KeyboardHookCallback; _mouseProc = MouseHookCallback; - using var curProcess = Process.GetCurrentProcess(); - using var curModule = curProcess.MainModule; + _lastMouseHookTick = Environment.TickCount64; + + // 在专用线程上安装 hook 并运行消息循环,使 hook 回调不受 UI 线程阻塞影响 + var readyEvent = new ManualResetEventSlim(false); + Exception? hookError = null; - if (curModule != null) + _hookThread = new Thread(() => { - var moduleHandle = NativeInterop.GetModuleHandle(curModule.ModuleName); - _keyboardHookId = NativeInterop.SetWindowsHookEx( - NativeInterop.WH_KEYBOARD_LL, - _keyboardProc, - moduleHandle, - 0); - - if (_keyboardHookId == IntPtr.Zero) + try { - var error = Marshal.GetLastWin32Error(); - Debug.WriteLine($"Failed to install keyboard hook. Error code: {error}"); - throw new System.ComponentModel.Win32Exception(error, "Failed to install keyboard hook"); + _hookThreadId = NativeInterop.GetCurrentThreadId(); + InstallHooks(); + readyEvent.Set(); + + // 低级钩子需要消息循环来分发回调 + while (NativeInterop.GetMessage(out var msg, IntPtr.Zero, 0, 0) > 0) + { + NativeInterop.TranslateMessage(ref msg); + NativeInterop.DispatchMessage(ref msg); + } } + catch (Exception ex) + { + hookError = ex; + readyEvent.Set(); + } + }); + _hookThread.IsBackground = true; + _hookThread.Name = "InputHookThread"; + _hookThread.Start(); + + readyEvent.Wait(); + if (hookError != null) + { + throw hookError; + } - _mouseHookId = NativeInterop.SetWindowsHookEx( - NativeInterop.WH_MOUSE_LL, - _mouseProc, - moduleHandle, - 0); + _isMonitoring = true; + + // 启动 watchdog 定时检测 hook 是否存活 + _watchdogTimer = new Timer(WatchdogCallback, null, WatchdogIntervalMs, WatchdogIntervalMs); + + Debug.WriteLine("Input monitoring started successfully (dedicated hook thread)"); + } + + private void InstallHooks() + { + var moduleHandle = NativeInterop.GetModuleHandle(null); + + _keyboardHookId = NativeInterop.SetWindowsHookEx( + NativeInterop.WH_KEYBOARD_LL, + _keyboardProc!, + moduleHandle, + 0); + + if (_keyboardHookId == IntPtr.Zero) + { + var error = Marshal.GetLastWin32Error(); + Debug.WriteLine($"Failed to install keyboard hook. Error code: {error}"); + throw new System.ComponentModel.Win32Exception(error, "Failed to install keyboard hook"); + } - if (_mouseHookId == IntPtr.Zero) + _mouseHookId = NativeInterop.SetWindowsHookEx( + NativeInterop.WH_MOUSE_LL, + _mouseProc!, + moduleHandle, + 0); + + if (_mouseHookId == IntPtr.Zero) + { + var error = Marshal.GetLastWin32Error(); + Debug.WriteLine($"Failed to install mouse hook. Error code: {error}"); + if (_keyboardHookId != IntPtr.Zero) { - var error = Marshal.GetLastWin32Error(); - Debug.WriteLine($"Failed to install mouse hook. Error code: {error}"); - // Clean up keyboard hook before throwing - if (_keyboardHookId != IntPtr.Zero) + NativeInterop.UnhookWindowsHookEx(_keyboardHookId); + _keyboardHookId = IntPtr.Zero; + } + throw new System.ComponentModel.Win32Exception(error, "Failed to install mouse hook"); + } + } + + private void ReinstallHooks() + { + Debug.WriteLine("Watchdog: reinstalling hooks..."); + + // 在 hook 线程上卸载旧 hook 并重新安装 + if (_hookThreadId != 0) + { + // 终止旧的消息循环 + NativeInterop.PostThreadMessage(_hookThreadId, NativeInterop.WM_QUIT, IntPtr.Zero, IntPtr.Zero); + } + + _hookThread?.Join(2000); + + // 清理可能残留的 hook handle + if (_keyboardHookId != IntPtr.Zero) + { + NativeInterop.UnhookWindowsHookEx(_keyboardHookId); + _keyboardHookId = IntPtr.Zero; + } + if (_mouseHookId != IntPtr.Zero) + { + NativeInterop.UnhookWindowsHookEx(_mouseHookId); + _mouseHookId = IntPtr.Zero; + } + + _lastMouseHookTick = Environment.TickCount64; + + var readyEvent = new ManualResetEventSlim(false); + + _hookThread = new Thread(() => + { + try + { + _hookThreadId = NativeInterop.GetCurrentThreadId(); + InstallHooks(); + readyEvent.Set(); + + while (NativeInterop.GetMessage(out var msg, IntPtr.Zero, 0, 0) > 0) { - NativeInterop.UnhookWindowsHookEx(_keyboardHookId); - _keyboardHookId = IntPtr.Zero; + NativeInterop.TranslateMessage(ref msg); + NativeInterop.DispatchMessage(ref msg); } - throw new System.ComponentModel.Win32Exception(error, "Failed to install mouse hook"); } - } + catch (Exception ex) + { + Debug.WriteLine($"Watchdog: hook reinstall failed: {ex.Message}"); + readyEvent.Set(); + } + }); + _hookThread.IsBackground = true; + _hookThread.Name = "InputHookThread"; + _hookThread.Start(); - _isMonitoring = true; - Debug.WriteLine("Input monitoring started successfully"); + readyEvent.Wait(); + Debug.WriteLine("Watchdog: hooks reinstalled"); + } + + private void WatchdogCallback(object? state) + { + if (!_isMonitoring) return; + + NativeInterop.GetCursorPos(out var currentPos); + var cursorMoved = currentPos.x != _lastCursorPos.x || currentPos.y != _lastCursorPos.y; + _lastCursorPos = currentPos; + + if (!cursorMoved) return; + + // 光标在移动,但 hook 回调长时间未被触发 → hook 可能已被 Windows 静默移除 + var elapsed = Environment.TickCount64 - Interlocked.Read(ref _lastMouseHookTick); + if (elapsed > HookDeadThresholdMs) + { + Debug.WriteLine($"Watchdog: mouse hook appears dead (no callback for {elapsed}ms), reinstalling..."); + ReinstallHooks(); + } } public void StopMonitoring() { if (!_isMonitoring) return; + _watchdogTimer?.Dispose(); + _watchdogTimer = null; + + // 终止 hook 线程的消息循环 + if (_hookThreadId != 0) + { + NativeInterop.PostThreadMessage(_hookThreadId, NativeInterop.WM_QUIT, IntPtr.Zero, IntPtr.Zero); + } + + _hookThread?.Join(2000); + if (_keyboardHookId != IntPtr.Zero) { NativeInterop.UnhookWindowsHookEx(_keyboardHookId); @@ -101,7 +237,6 @@ public void StopMonitoring() _mouseHookId = IntPtr.Zero; } - // 清空按下的键集合 _pressedKeys.Clear(); _isMonitoring = false; @@ -118,19 +253,23 @@ private IntPtr KeyboardHookCallback(int nCode, IntPtr wParam, IntPtr lParam) if (message == NativeInterop.WM_KEYDOWN || message == NativeInterop.WM_SYSKEYDOWN) { - // 只在键第一次按下时记录,忽略长按时的重复按下事件 if (!_pressedKeys.Contains(vkCode)) { _pressedKeys.Add(vkCode); + // GetKeyName 需在 hook 回调中同步调用以准确获取修饰键状态 var keyName = KeyNameMapper.GetKeyName(vkCode); - var activeApp = ActiveWindowManager.GetActiveAppInfo(); - // 异步触发事件,避免阻塞低级钩子回调 - ThreadPool.QueueUserWorkItem(_ => KeyPressed?.Invoke(keyName, activeApp.AppName, activeApp.DisplayName)); + // 捕获前台窗口句柄和进程 ID(轻量 P/Invoke),完整解析异步进行 + var hWnd = NativeInterop.GetForegroundWindow(); + NativeInterop.GetWindowThreadProcessId(hWnd, out uint pid); + ThreadPool.QueueUserWorkItem(_ => + { + var activeApp = ActiveWindowManager.ResolveAppInfo(hWnd, pid); + KeyPressed?.Invoke(keyName, activeApp.AppName, activeApp.DisplayName); + }); } } else if (message == NativeInterop.WM_KEYUP || message == NativeInterop.WM_SYSKEYUP) { - // 键释放时,从集合中移除 _pressedKeys.Remove(vkCode); } } @@ -142,46 +281,49 @@ private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0) { + Interlocked.Exchange(ref _lastMouseHookTick, Environment.TickCount64); + var message = (int)wParam; var hookStruct = Marshal.PtrToStructure(lParam); switch (message) { case NativeInterop.WM_LBUTTONDOWN: - { - // 在钩子回调中获取进程名,然后异步触发事件 - var activeApp = ActiveWindowManager.GetActiveAppInfo(); - ThreadPool.QueueUserWorkItem(_ => LeftMouseClicked?.Invoke(activeApp.AppName, activeApp.DisplayName)); - } - break; - case NativeInterop.WM_RBUTTONDOWN: - { - var activeApp = ActiveWindowManager.GetActiveAppInfo(); - ThreadPool.QueueUserWorkItem(_ => RightMouseClicked?.Invoke(activeApp.AppName, activeApp.DisplayName)); - } - break; - case NativeInterop.WM_MBUTTONDOWN: { - var activeApp = ActiveWindowManager.GetActiveAppInfo(); - ThreadPool.QueueUserWorkItem(_ => MiddleMouseClicked?.Invoke(activeApp.AppName, activeApp.DisplayName)); + var msg = message; + var hWnd = NativeInterop.GetForegroundWindow(); + NativeInterop.GetWindowThreadProcessId(hWnd, out uint pid); + ThreadPool.QueueUserWorkItem(_ => + { + var activeApp = ActiveWindowManager.ResolveAppInfo(hWnd, pid); + var appName = activeApp.AppName; + var displayName = activeApp.DisplayName; + if (msg == NativeInterop.WM_LBUTTONDOWN) + LeftMouseClicked?.Invoke(appName, displayName); + else if (msg == NativeInterop.WM_RBUTTONDOWN) + RightMouseClicked?.Invoke(appName, displayName); + else + MiddleMouseClicked?.Invoke(appName, displayName); + }); } break; case NativeInterop.WM_XBUTTONDOWN: { - var activeApp = ActiveWindowManager.GetActiveAppInfo(); - var button = NativeInterop.HiWord((int)hookStruct.mouseData); - if (button == NativeInterop.XBUTTON2) - { - ThreadPool.QueueUserWorkItem(_ => SideForwardMouseClicked?.Invoke(activeApp.AppName, activeApp.DisplayName)); - } - else + var mouseData = hookStruct.mouseData; + var hWnd = NativeInterop.GetForegroundWindow(); + NativeInterop.GetWindowThreadProcessId(hWnd, out uint pid); + ThreadPool.QueueUserWorkItem(_ => { - // Default unknown/legacy side buttons to back. - ThreadPool.QueueUserWorkItem(_ => SideBackMouseClicked?.Invoke(activeApp.AppName, activeApp.DisplayName)); - } + var activeApp = ActiveWindowManager.ResolveAppInfo(hWnd, pid); + var button = NativeInterop.HiWord((int)mouseData); + if (button == NativeInterop.XBUTTON2) + SideForwardMouseClicked?.Invoke(activeApp.AppName, activeApp.DisplayName); + else + SideBackMouseClicked?.Invoke(activeApp.AppName, activeApp.DisplayName); + }); } break; @@ -192,9 +334,14 @@ private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam) case NativeInterop.WM_MOUSEWHEEL: case NativeInterop.WM_MOUSEHWHEEL: { - var activeApp = ActiveWindowManager.GetActiveAppInfo(); var mouseData = hookStruct.mouseData; - ThreadPool.QueueUserWorkItem(_ => HandleScroll(mouseData, activeApp.AppName, activeApp.DisplayName)); + var hWnd = NativeInterop.GetForegroundWindow(); + NativeInterop.GetWindowThreadProcessId(hWnd, out uint pid); + ThreadPool.QueueUserWorkItem(_ => + { + var activeApp = ActiveWindowManager.ResolveAppInfo(hWnd, pid); + HandleScroll(mouseData, activeApp.AppName, activeApp.DisplayName); + }); } break; } @@ -205,38 +352,33 @@ private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam) private void HandleMouseMove(NativeInterop.POINT pt) { - var now = DateTime.Now; + var now = Environment.TickCount64; var currentPosition = new System.Drawing.Point(pt.x, pt.y); - // 初始化位置 if (!_lastMousePosition.HasValue) { _lastMousePosition = currentPosition; - _lastMouseSampleTime = now; + _lastMouseSampleTime = DateTime.MinValue; return; } - // 计算本次移动的距离 var dx = currentPosition.X - _lastMousePosition.Value.X; var dy = currentPosition.Y - _lastMousePosition.Value.Y; var segmentDistance = Math.Sqrt(dx * dx + dy * dy); - // 过滤异常大的单次移动(可能是鼠标跳跃或系统事件) - // 保留真实路径累计,但仍丢弃明显不合理的跳点,避免污染统计。 const double maxSegmentDistance = 250.0; if (segmentDistance > maxSegmentDistance) { _accumulatedDistance = 0.0; _lastMousePosition = currentPosition; - _lastMouseSampleTime = now; + _lastMouseSampleTime = DateTime.Now; return; } - // 直接累计每一小段位移,统计真实走过的路径长度。 _accumulatedDistance += segmentDistance; _lastMousePosition = currentPosition; - var elapsed = (now - _lastMouseSampleTime).TotalSeconds; + var elapsed = (DateTime.Now - _lastMouseSampleTime).TotalSeconds; if (elapsed < _mouseSampleInterval) { return; @@ -244,7 +386,7 @@ private void HandleMouseMove(NativeInterop.POINT pt) var reportedDistance = _accumulatedDistance; _accumulatedDistance = 0.0; - _lastMouseSampleTime = now; + _lastMouseSampleTime = DateTime.Now; if (reportedDistance <= 0) { @@ -257,8 +399,6 @@ private void HandleMouseMove(NativeInterop.POINT pt) private void HandleScroll(uint mouseData, string appName, string displayName) { - // mouseData contains the scroll delta in the high-order word - // WHEEL_DELTA is 120, so divide by 120 to get wheel ticks var delta = NativeInterop.HiWord((int)mouseData); var scrollDistance = Math.Abs(delta) / 120.0; MouseScrolled?.Invoke(scrollDistance, appName, displayName); From 6642db42cc0b801093f1da9ee431f26c48f0a240 Mon Sep 17 00:00:00 2001 From: tian Date: Sun, 29 Mar 2026 12:05:08 +0800 Subject: [PATCH 2/3] fix(windows): use net48-compatible tick count for hook watchdog --- .../KeyStats/Services/InputMonitorService.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/KeyStats.Windows/KeyStats/Services/InputMonitorService.cs b/KeyStats.Windows/KeyStats/Services/InputMonitorService.cs index 892231b..8ba27bf 100644 --- a/KeyStats.Windows/KeyStats/Services/InputMonitorService.cs +++ b/KeyStats.Windows/KeyStats/Services/InputMonitorService.cs @@ -30,7 +30,7 @@ public class InputMonitorService : IDisposable // hook 健康检查:watchdog 定时检测 hook 是否被 Windows 静默移除 private Timer? _watchdogTimer; - private long _lastMouseHookTick; + private int _lastMouseHookTick; private NativeInterop.POINT _lastCursorPos; private const int WatchdogIntervalMs = 3000; private const int HookDeadThresholdMs = 5000; @@ -53,7 +53,7 @@ public void StartMonitoring() _keyboardProc = KeyboardHookCallback; _mouseProc = MouseHookCallback; - _lastMouseHookTick = Environment.TickCount64; + _lastMouseHookTick = Environment.TickCount; // 在专用线程上安装 hook 并运行消息循环,使 hook 回调不受 UI 线程阻塞影响 var readyEvent = new ManualResetEventSlim(false); @@ -159,7 +159,7 @@ private void ReinstallHooks() _mouseHookId = IntPtr.Zero; } - _lastMouseHookTick = Environment.TickCount64; + _lastMouseHookTick = Environment.TickCount; var readyEvent = new ManualResetEventSlim(false); @@ -202,7 +202,7 @@ private void WatchdogCallback(object? state) if (!cursorMoved) return; // 光标在移动,但 hook 回调长时间未被触发 → hook 可能已被 Windows 静默移除 - var elapsed = Environment.TickCount64 - Interlocked.Read(ref _lastMouseHookTick); + var elapsed = unchecked((uint)(Environment.TickCount - Volatile.Read(ref _lastMouseHookTick))); if (elapsed > HookDeadThresholdMs) { Debug.WriteLine($"Watchdog: mouse hook appears dead (no callback for {elapsed}ms), reinstalling..."); @@ -281,7 +281,7 @@ private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0) { - Interlocked.Exchange(ref _lastMouseHookTick, Environment.TickCount64); + Interlocked.Exchange(ref _lastMouseHookTick, Environment.TickCount); var message = (int)wParam; var hookStruct = Marshal.PtrToStructure(lParam); @@ -352,7 +352,6 @@ private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam) private void HandleMouseMove(NativeInterop.POINT pt) { - var now = Environment.TickCount64; var currentPosition = new System.Drawing.Point(pt.x, pt.y); if (!_lastMousePosition.HasValue) From 23096588f61b510eb987b9c951a92c509e756a96 Mon Sep 17 00:00:00 2001 From: tian Date: Sun, 29 Mar 2026 12:11:33 +0800 Subject: [PATCH 3/3] fix(windows): avoid concurrent hook watchdog reinstalls --- .../KeyStats/Services/InputMonitorService.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/KeyStats.Windows/KeyStats/Services/InputMonitorService.cs b/KeyStats.Windows/KeyStats/Services/InputMonitorService.cs index 8ba27bf..d0f4d7b 100644 --- a/KeyStats.Windows/KeyStats/Services/InputMonitorService.cs +++ b/KeyStats.Windows/KeyStats/Services/InputMonitorService.cs @@ -31,6 +31,7 @@ public class InputMonitorService : IDisposable // hook 健康检查:watchdog 定时检测 hook 是否被 Windows 静默移除 private Timer? _watchdogTimer; private int _lastMouseHookTick; + private int _isReinstallingHooks; private NativeInterop.POINT _lastCursorPos; private const int WatchdogIntervalMs = 3000; private const int HookDeadThresholdMs = 5000; @@ -205,8 +206,20 @@ private void WatchdogCallback(object? state) var elapsed = unchecked((uint)(Environment.TickCount - Volatile.Read(ref _lastMouseHookTick))); if (elapsed > HookDeadThresholdMs) { + if (Interlocked.CompareExchange(ref _isReinstallingHooks, 1, 0) != 0) + { + return; + } + Debug.WriteLine($"Watchdog: mouse hook appears dead (no callback for {elapsed}ms), reinstalling..."); - ReinstallHooks(); + try + { + ReinstallHooks(); + } + finally + { + Volatile.Write(ref _isReinstallingHooks, 0); + } } }