From 94f14851fa8192a66c9ab505984336798071f7d2 Mon Sep 17 00:00:00 2001 From: Twest2 Date: Mon, 15 Jun 2026 18:29:12 -0500 Subject: [PATCH] Keep mouse-hook process out of EcoQoS so clicks aren't missed in power-saving mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The real cause of "only the first couple of clicks were captured while on power saving mode": Windows Power Throttling (EcoQoS) CPU-starves background processes under a power-saving plan. The low-level WH_MOUSE_LL hook lives in the spawned PowerShell watcher; when its callback is starved past the system LowLevelHooksTimeout, Windows silently stops delivering mouse events to it — the process keeps running (so no exit/crash, no fallback to interval capture fires), it just misses clicks, and those clicks never become steps. The previous powerSaveBlocker('prevent-app-suspension') only maps to SetThreadExecutionState, which blocks system sleep but does NOT opt a process out of EcoQoS, so it couldn't fix this. Have the watcher process opt itself out of execution-speed throttling via SetProcessInformation(ProcessPowerThrottling, ...) and raise itself to HIGH_PRIORITY_CLASS at startup, so the hook callback always runs at full speed and every click is delivered regardless of the laptop's power mode. --- app/capture.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/app/capture.js b/app/capture.js index b380094..8120470 100644 --- a/app/capture.js +++ b/app/capture.js @@ -798,6 +798,16 @@ public static class SFMouseHook { private const int WM_MBUTTONDOWN = 0x0207; private const int WM_XBUTTONDOWN = 0x020B; private const long UnixEpochMilliseconds = 62135596800000L; + // Opting this process out of Windows Power Throttling (EcoQoS). In a + // power-saving plan the OS CPU-starves background processes; a starved + // low-level mouse hook whose callback exceeds LowLevelHooksTimeout is + // silently skipped by Windows (events stop arriving) while the process + // stays alive, so clicks are missed with no error — the "only the first + // couple of clicks were captured" symptom. + private const int ProcessPowerThrottling = 4; + private const uint PROCESS_POWER_THROTTLING_CURRENT_VERSION = 1; + private const uint PROCESS_POWER_THROTTLING_EXECUTION_SPEED = 0x1; + private const uint HIGH_PRIORITY_CLASS = 0x00000080; private static IntPtr hook = IntPtr.Zero; private static LowLevelMouseProc proc = HookCallback; @@ -829,6 +839,13 @@ public static class SFMouseHook { public POINT pt; } + [StructLayout(LayoutKind.Sequential)] + private struct PROCESS_POWER_THROTTLING_STATE { + public uint Version; + public uint ControlMask; + public uint StateMask; + } + private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", SetLastError = true)] @@ -855,7 +872,33 @@ public static class SFMouseHook { [DllImport("user32.dll")] private static extern bool SetProcessDpiAwarenessContext(IntPtr value); + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool SetProcessInformation(IntPtr hProcess, int ProcessInformationClass, ref PROCESS_POWER_THROTTLING_STATE ProcessInformation, uint ProcessInformationSize); + + [DllImport("kernel32.dll")] + private static extern IntPtr GetCurrentProcess(); + + [DllImport("kernel32.dll")] + private static extern bool SetPriorityClass(IntPtr hProcess, uint dwPriorityClass); + + // Force this process to run at full CPU speed regardless of the power plan, + // so the mouse-hook callback never trips LowLevelHooksTimeout and clicks + // keep being delivered while the laptop is in eco / power-saving mode. + private static void KeepProcessResponsive() { + try { + PROCESS_POWER_THROTTLING_STATE state = new PROCESS_POWER_THROTTLING_STATE(); + state.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION; + // Control EXECUTION_SPEED and set its state bit to 0 => throttling off + // (the documented way to opt a process out of EcoQoS). + state.ControlMask = PROCESS_POWER_THROTTLING_EXECUTION_SPEED; + state.StateMask = 0; + SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, ref state, (uint)Marshal.SizeOf(state)); + } catch { } + try { SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS); } catch { } + } + public static void Run() { + KeepProcessResponsive(); try { SetProcessDpiAwarenessContext(new IntPtr(-4)); } catch { } Thread writer = new Thread(WriterLoop);