From 1e59d5b240383bdbd19c9faa5b32893f520655d2 Mon Sep 17 00:00:00 2001 From: Lenus Walker Date: Sat, 13 Jun 2026 17:10:14 -0400 Subject: [PATCH] Gate Lenovo-only features off on unsupported laptops and cut background CPU On unsupported (non-Lenovo) machines, several Lenovo-specific components started unconditionally and consumed resources: - Seven Lenovo WMI/driver event listeners were auto-activated during IoC container build regardless of hardware. - Hybrid mode init, AIController, the HWiNFO sensor integration, and the battery discharge-rate monitor ran outside the compatibility gate. Changes: - Add Compatibility.IsBasicCompatible (synchronous view of the cached basic-compat result) and an AutoActivateLenovoListener() registration extension that skips auto-activation on unsupported hardware. App now resolves compatibility before building the container so the gate is set at module-load time. The 7 Lenovo listeners use the gated extension; the 6 generic listeners keep normal auto-activation. - Move hybrid mode, AIController, battery monitor, HWiNFO, and macro init inside the compatible block. Cross-platform features (power-mode Windows fallback, processor TDP automation, CLI/IPC, GPU keep-off) stay available on any laptop. Performance: - MacroController installs the system-wide WH_KEYBOARD_LL hook only when macros are enabled (default off), eliminating per-keystroke managed callbacks on every machine. Hook install/uninstall is confined to the UI thread (startup + MacroPage toggle); SetEnabled only flips the setting because automation steps run off-thread. Added Stop() and shutdown cleanup. - Remove dead-code guards in BatteryDischargeRateMonitorService. Co-Authored-By: Claude Opus 4.8 --- .../MacroController.cs | 14 +++++++++ .../RegistrationBuilderExtensions.cs | 14 +++++++++ LenovoLegionToolkit.Lib/IoCModule.cs | 18 ++++++----- .../BatteryDischargeRateMonitorService.cs | 6 ---- .../Utils/Compatibility.cs | 7 +++++ LenovoLegionToolkit.WPF/App.xaml.cs | 31 +++++++++++++++---- .../Pages/MacroPage.xaml.cs | 10 +++++- 7 files changed, 80 insertions(+), 20 deletions(-) diff --git a/LenovoLegionToolkit.Lib.Macro/MacroController.cs b/LenovoLegionToolkit.Lib.Macro/MacroController.cs index 7841489659..b9a2661cb7 100644 --- a/LenovoLegionToolkit.Lib.Macro/MacroController.cs +++ b/LenovoLegionToolkit.Lib.Macro/MacroController.cs @@ -71,12 +71,26 @@ public void SetSequences(Dictionary sequences) public void Start() { + // Don't install a system-wide keyboard hook unless macros are enabled. When disabled (the + // default) the hook would otherwise route every keystroke through managed code for nothing. + if (!IsEnabled) + return; + if (_kbHook != default) return; _kbHook = PInvoke.SetWindowsHookEx(WINDOWS_HOOK_ID.WH_KEYBOARD_LL, _kbProc, HINSTANCE.Null, 0); } + public void Stop() + { + if (_kbHook == default) + return; + + PInvoke.UnhookWindowsHookEx(_kbHook); + _kbHook = default; + } + public void StartRecording(MacroRecorderSettings settings = MacroRecorderSettings.Keyboard) => _recorder.StartRecording(settings); public void StopRecording() => _recorder.StopRecording(); diff --git a/LenovoLegionToolkit.Lib/Extensions/RegistrationBuilderExtensions.cs b/LenovoLegionToolkit.Lib/Extensions/RegistrationBuilderExtensions.cs index 3b6f766132..1303064ad7 100644 --- a/LenovoLegionToolkit.Lib/Extensions/RegistrationBuilderExtensions.cs +++ b/LenovoLegionToolkit.Lib/Extensions/RegistrationBuilderExtensions.cs @@ -2,6 +2,7 @@ using Autofac; using Autofac.Builder; using LenovoLegionToolkit.Lib.Listeners; +using LenovoLegionToolkit.Lib.Utils; namespace LenovoLegionToolkit.Lib.Extensions; @@ -11,4 +12,17 @@ public static void AutoActivateListener(this IRegistrationBuilder e.Instance.StartAsync().AsValueTask()).AutoActivate(); } + + /// + /// Auto-activates the listener only on Lenovo-compatible hardware. On unsupported machines the + /// listener is still registered (so it can be resolved on demand) but is never started, so it + /// never subscribes to its Lenovo-only WMI/driver event source and consumes no resources. + /// + public static void AutoActivateLenovoListener(this IRegistrationBuilder, ConcreteReflectionActivatorData, SingleRegistrationStyle> registration) where T : EventArgs + { + if (!Compatibility.IsBasicCompatible) + return; + + registration.OnActivating(e => e.Instance.StartAsync().AsValueTask()).AutoActivate(); + } } diff --git a/LenovoLegionToolkit.Lib/IoCModule.cs b/LenovoLegionToolkit.Lib/IoCModule.cs index 5d205e5212..dfb34695f5 100644 --- a/LenovoLegionToolkit.Lib/IoCModule.cs +++ b/LenovoLegionToolkit.Lib/IoCModule.cs @@ -86,19 +86,23 @@ protected override void Load(ContainerBuilder builder) builder.Register(true); builder.Register(true); + // Generic listeners: work on any laptop, always auto-started. builder.Register().AutoActivateListener(); builder.Register().AutoActivateListener(); - builder.Register().AutoActivateListener(); - builder.Register().AutoActivateListener(); builder.Register().AutoActivateListener(); - builder.Register().AutoActivateListener(); builder.Register().AutoActivateListener(); - builder.Register().AutoActivateListener(); builder.Register().AutoActivateListener(); - builder.Register().AutoActivateListener(); builder.Register().AutoActivateListener(); - builder.Register().AutoActivateListener(); - builder.Register().AutoActivateListener(); + + // Lenovo-only listeners: subscribe to Lenovo WMI/driver event sources. Auto-started only on + // compatible hardware so they consume zero resources on unsupported machines. + builder.Register().AutoActivateLenovoListener(); + builder.Register().AutoActivateLenovoListener(); + builder.Register().AutoActivateLenovoListener(); + builder.Register().AutoActivateLenovoListener(); + builder.Register().AutoActivateLenovoListener(); + builder.Register().AutoActivateLenovoListener(); + builder.Register().AutoActivateLenovoListener(); builder.Register(); builder.Register(); diff --git a/LenovoLegionToolkit.Lib/Services/BatteryDischargeRateMonitorService.cs b/LenovoLegionToolkit.Lib/Services/BatteryDischargeRateMonitorService.cs index 4cd84b2960..87697ea225 100644 --- a/LenovoLegionToolkit.Lib/Services/BatteryDischargeRateMonitorService.cs +++ b/LenovoLegionToolkit.Lib/Services/BatteryDischargeRateMonitorService.cs @@ -15,12 +15,6 @@ public async Task StartStopIfNeededAsync() { await StopAsync().ConfigureAwait(false); - if (_refreshTask != null) - return; - - if (_cts is not null) - await _cts.CancelAsync().ConfigureAwait(false); - _cts = new CancellationTokenSource(); var token = _cts.Token; diff --git a/LenovoLegionToolkit.Lib/Utils/Compatibility.cs b/LenovoLegionToolkit.Lib/Utils/Compatibility.cs index b54a8cf659..73f8b3df96 100644 --- a/LenovoLegionToolkit.Lib/Utils/Compatibility.cs +++ b/LenovoLegionToolkit.Lib/Utils/Compatibility.cs @@ -76,6 +76,13 @@ public static partial class Compatibility private static MachineInformation? _machineInformation; private static bool? _basicCompatibility; + /// + /// Synchronous view of the basic (Lenovo hardware) compatibility result. + /// Returns false until has run at least once. + /// Used to gate Lenovo-only background components so they consume no resources on unsupported machines. + /// + public static bool IsBasicCompatible => _basicCompatibility ?? false; + public static async Task CheckBasicCompatibilityAsync() => _basicCompatibility ??= await WMI.LenovoGameZoneData.ExistsAsync().ConfigureAwait(false); public static async Task<(bool isCompatible, MachineInformation machineInformation)> IsCompatibleAsync() diff --git a/LenovoLegionToolkit.WPF/App.xaml.cs b/LenovoLegionToolkit.WPF/App.xaml.cs index d404bb1a72..6d192a6ec3 100644 --- a/LenovoLegionToolkit.WPF/App.xaml.cs +++ b/LenovoLegionToolkit.WPF/App.xaml.cs @@ -104,6 +104,10 @@ private async void Application_Startup(object sender, StartupEventArgs e) WinFormsApp.SetHighDpiMode(WinFormsHighDpiMode.PerMonitorV2); RenderOptions.ProcessRenderMode = RenderMode.SoftwareOnly; + // Determine basic (Lenovo hardware) compatibility BEFORE building the IoC container, so that + // Lenovo-only listeners are not auto-activated on unsupported machines. + _isCompatible = await Compatibility.CheckBasicCompatibilityAsync(); + IoCContainer.Initialize( new Lib.IoCModule(), new Lib.Automation.IoCModule(), @@ -125,7 +129,6 @@ private async void Application_Startup(object sender, StartupEventArgs e) AutomationPage.EnableHybridModeAutomation = flags.EnableHybridModeAutomation; - _isCompatible = await Compatibility.CheckBasicCompatibilityAsync(); if (_isCompatible) { await LogSoftwareStatusAsync(); @@ -133,17 +136,24 @@ private async void Application_Startup(object sender, StartupEventArgs e) await InitRgbKeyboardControllerAsync(); await InitSpectrumKeyboardControllerAsync(); await InitGpuOverclockControllerAsync(); + + // Lenovo-only features: only initialize on compatible hardware. + await InitHybridModeAsync(); + InitMacroController(); + + await IoCContainer.Resolve().StartIfNeededAsync(); + await IoCContainer.Resolve().StartStopIfNeededAsync(); + + // HWiNFO integration exposes Lenovo sensor readings, so it is only meaningful here. + await IoCContainer.Resolve().StartStopIfNeededAsync(); } + // Cross-platform features (Windows power overlay fallback, processor TDP automation, CLI/IPC, + // discrete-GPU power management): available on any supported laptop. await InitPowerModeFeatureAsync(); - await InitHybridModeAsync(); await InitAutomationProcessorAsync(); - InitMacroController(); - await IoCContainer.Resolve().StartIfNeededAsync(); - await IoCContainer.Resolve().StartStopIfNeededAsync(); await IoCContainer.Resolve().StartStopIfNeededAsync(); - await IoCContainer.Resolve().StartStopIfNeededAsync(); await IoCContainer.Resolve().StartStopIfNeededAsync(); #if !DEBUG @@ -276,6 +286,15 @@ public async Task ShutdownAsync() } catch { /* Ignored. */ } + try + { + if (IoCContainer.TryResolve() is { } macroController) + { + macroController.Stop(); + } + } + catch { /* Ignored. */ } + Shutdown(); } diff --git a/LenovoLegionToolkit.WPF/Pages/MacroPage.xaml.cs b/LenovoLegionToolkit.WPF/Pages/MacroPage.xaml.cs index 8384638efe..9507615043 100644 --- a/LenovoLegionToolkit.WPF/Pages/MacroPage.xaml.cs +++ b/LenovoLegionToolkit.WPF/Pages/MacroPage.xaml.cs @@ -30,7 +30,15 @@ private void MacroPage_Initialized(object? sender, EventArgs e) private void EnableMacroToggle_Click(object sender, RoutedEventArgs e) { - _controller.SetEnabled(_enableMacroToggle.IsChecked ?? false); + var enabled = _enableMacroToggle.IsChecked ?? false; + _controller.SetEnabled(enabled); + + // Install/remove the low-level keyboard hook live. This runs on the UI thread, which owns the + // message loop the hook callback is dispatched on, so the hook only exists while macros are on. + if (enabled) + _controller.Start(); + else + _controller.Stop(); } private void NumberPadButton_Click(object sender, RoutedEventArgs e)