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..d0f4d7b 100644 --- a/KeyStats.Windows/KeyStats/Services/InputMonitorService.cs +++ b/KeyStats.Windows/KeyStats/Services/InputMonitorService.cs @@ -24,6 +24,18 @@ 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 int _lastMouseHookTick; + private int _isReinstallingHooks; + 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 +54,190 @@ public void StartMonitoring() _keyboardProc = KeyboardHookCallback; _mouseProc = MouseHookCallback; - using var curProcess = Process.GetCurrentProcess(); - using var curModule = curProcess.MainModule; + _lastMouseHookTick = Environment.TickCount; + + // 在专用线程上安装 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; + } + + _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); - _mouseHookId = NativeInterop.SetWindowsHookEx( - NativeInterop.WH_MOUSE_LL, - _mouseProc, - moduleHandle, - 0); + _keyboardHookId = NativeInterop.SetWindowsHookEx( + NativeInterop.WH_KEYBOARD_LL, + _keyboardProc!, + moduleHandle, + 0); - if (_mouseHookId == IntPtr.Zero) + 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"); + } + + _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.TickCount; + + 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 = 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..."); + try + { + ReinstallHooks(); + } + finally + { + Volatile.Write(ref _isReinstallingHooks, 0); + } + } } 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 +250,6 @@ public void StopMonitoring() _mouseHookId = IntPtr.Zero; } - // 清空按下的键集合 _pressedKeys.Clear(); _isMonitoring = false; @@ -118,19 +266,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 +294,49 @@ private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0) { + Interlocked.Exchange(ref _lastMouseHookTick, Environment.TickCount); + 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 +347,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 +365,32 @@ private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam) private void HandleMouseMove(NativeInterop.POINT pt) { - var now = DateTime.Now; 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 +398,7 @@ private void HandleMouseMove(NativeInterop.POINT pt) var reportedDistance = _accumulatedDistance; _accumulatedDistance = 0.0; - _lastMouseSampleTime = now; + _lastMouseSampleTime = DateTime.Now; if (reportedDistance <= 0) { @@ -257,8 +411,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);