From 0de25ffa6cc8005be762c5dca777f48efd618064 Mon Sep 17 00:00:00 2001 From: Ben Hutchison Date: Tue, 20 Feb 2024 09:32:02 -0800 Subject: [PATCH] Fix high CPU usage in KeePass search results on Windows 10 --- KeePassTrayIconLockState/Fixes/Fix.cs | 12 + .../Fixes/FixHighCpuInSearchResults.cs | 34 +++ KeePassTrayIconLockState/Fixes/Fixes.cs | 21 ++ .../KeePassTrayIconLockState.csproj | 235 +++++++++--------- .../KeePassTrayIconLockStateExt.cs | 11 +- .../Properties/AssemblyInfo.cs | 8 +- Test.Elevated/Test.Elevated.csproj | 8 +- Test/Test.csproj | 8 +- 8 files changed, 205 insertions(+), 132 deletions(-) create mode 100644 KeePassTrayIconLockState/Fixes/Fix.cs create mode 100644 KeePassTrayIconLockState/Fixes/FixHighCpuInSearchResults.cs create mode 100644 KeePassTrayIconLockState/Fixes/Fixes.cs diff --git a/KeePassTrayIconLockState/Fixes/Fix.cs b/KeePassTrayIconLockState/Fixes/Fix.cs new file mode 100644 index 0000000..b6722f8 --- /dev/null +++ b/KeePassTrayIconLockState/Fixes/Fix.cs @@ -0,0 +1,12 @@ +#nullable enable + +using HarmonyLib; +using KeePass.Plugins; + +namespace KeePassTrayIconLockState.Fixes; + +public interface Fix { + + public void fix(IPluginHost pluginHost, Harmony harmony); + +} \ No newline at end of file diff --git a/KeePassTrayIconLockState/Fixes/FixHighCpuInSearchResults.cs b/KeePassTrayIconLockState/Fixes/FixHighCpuInSearchResults.cs new file mode 100644 index 0000000..0d24db9 --- /dev/null +++ b/KeePassTrayIconLockState/Fixes/FixHighCpuInSearchResults.cs @@ -0,0 +1,34 @@ +#nullable enable + +using HarmonyLib; +using KeePass.Plugins; +using KeePass.UI; +using System.Reflection; +using System.Windows.Forms; + +namespace KeePassTrayIconLockState.Fixes; + +/// +/// On Windows 10 but not 11, Windows' Accessibility/UI Automation gets into an infinite loop sending WM_GETOBJECT (0x3D), LVM_ISGROUPVIEWENABLED (0x10AF), and LVM_GETVIEW (0x108F) messages to the KeePass search results view when it has at least 1 result. +/// This results in high CPU usage (roughly 50% of one logical CPU core). It lasts until you hide the search results, for example by navigating to a different folder in your KeePass database. +/// To work around this issue and prevent the high CPU usage, this fix will prevent the window process of the search results view from handling LVM_ISGROUPVIEWENABLED messages by returning early. Blocking WM_GETOBJECT or LVM_GETVIEW messages are not necessary to fix this issue. +/// +public class FixHighCpuInSearchResults: Fix { + + private const int LVM_ISGROUPVIEWENABLED = 0x10AF; + + public void fix(IPluginHost pluginHost, Harmony harmony) { + MethodInfo wndProc = AccessTools.Method(typeof(CustomListViewEx), "WndProc", [typeof(Message).MakeByRefType()]); + + HarmonyMethod onBeforeCustomListViewExWndProc = new(AccessTools.Method(typeof(FixHighCpuInSearchResults), nameof(ignoreIsGroupViewEnabled), [typeof(Message).MakeByRefType()])); + + harmony.Patch(wndProc, prefix: onBeforeCustomListViewExWndProc); + } + + /// true if will not cause high CPU, which allows WndProc to run; or false if it will cause high CPU, which causes WndProc to return early + /// + internal static bool ignoreIsGroupViewEnabled(ref Message m) { + return m.Msg != LVM_ISGROUPVIEWENABLED; + } + +} \ No newline at end of file diff --git a/KeePassTrayIconLockState/Fixes/Fixes.cs b/KeePassTrayIconLockState/Fixes/Fixes.cs new file mode 100644 index 0000000..bcd3321 --- /dev/null +++ b/KeePassTrayIconLockState/Fixes/Fixes.cs @@ -0,0 +1,21 @@ +#nullable enable + +using HarmonyLib; +using KeePass.Plugins; +using System.Collections.Generic; + +namespace KeePassTrayIconLockState.Fixes; + +public static class Fixes { + + public static IEnumerable all { get; } = [ + new FixHighCpuInSearchResults() + ]; + + public static void fixAll(IPluginHost host, Harmony harmony) { + foreach (Fix fix in all) { + fix.fix(host, harmony); + } + } + +} \ No newline at end of file diff --git a/KeePassTrayIconLockState/KeePassTrayIconLockState.csproj b/KeePassTrayIconLockState/KeePassTrayIconLockState.csproj index 86247a1..cc5cf10 100644 --- a/KeePassTrayIconLockState/KeePassTrayIconLockState.csproj +++ b/KeePassTrayIconLockState/KeePassTrayIconLockState.csproj @@ -1,116 +1,121 @@ - - - - - Debug - AnyCPU - {E980ABDC-E56D-4E9C-A322-AFBE9D9092B4} - Library - Properties - KeePassTrayIconLockState - KeePassTrayIconLockState - v4.8 - 512 - true - 8524;8509 - latest - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - C:\Program Files\KeePass Password Safe 2\KeePass.exe - False - - - - - - - - ..\..\..\..\..\..\Programs\Security\KeePass\KeePass.exe - False - - - - - - - - - - True - True - Resources.resx - - - - - - 2.3.0 - - - 2.0.26 - - - 2.2.0 - - - 2.2.2 - - - - - ResXFileCodeGenerator - Resources.Designer.cs - - - - - - - - - - - - - - - - - - - - - - - - + + + + + Debug + AnyCPU + {E980ABDC-E56D-4E9C-A322-AFBE9D9092B4} + Library + Properties + KeePassTrayIconLockState + KeePassTrayIconLockState + v4.8 + 512 + true + 8524;8509 + latest + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + C:\Program Files\KeePass Password Safe 2\KeePass.exe + False + + + + + + + + ..\..\..\..\..\..\Programs\Security\KeePass\KeePass.exe + False + + + + + + + + + + + + + True + True + Resources.resx + + + + + + 2.3.0 + + + 2.0.26 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + 2.2.0 + + + 2.2.2 + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/KeePassTrayIconLockState/KeePassTrayIconLockStateExt.cs b/KeePassTrayIconLockState/KeePassTrayIconLockStateExt.cs index 61f6dd6..f55e584 100644 --- a/KeePassTrayIconLockState/KeePassTrayIconLockStateExt.cs +++ b/KeePassTrayIconLockState/KeePassTrayIconLockStateExt.cs @@ -27,6 +27,7 @@ public class KeePassTrayIconLockStateExt: Plugin { private readonly StoredProperty databaseOpenState = new(); private readonly IDarkNet darkNet = new DarkNet(); + private readonly Harmony harmony = new("com.aldaviva.keepasstrayiconlockstate"); private IPluginHost keePassHost = null!; private Property trayIcon = null!; @@ -91,6 +92,8 @@ public override bool Initialize(IPluginHost host) { hookKeepassTrayIconUpdates(); keepassRenderedTrayIcon += renderTrayIcon; + Fixes.Fixes.fixAll(keePassHost, harmony); + return true; } @@ -155,13 +158,11 @@ private static string getPluginInstallationDirectory() { /// I do this because KeePass sometimes renders its tray icon without exposing any events to indicate that it's doing so, therefore I can't receive any notifications to trigger my own icon to render. Two examples of this are clicking OK in the Options dialog box and canceling the Unlock Database dialog box when minimize to tray, minimize after locking, and minimize after opening are all enabled. /// Also, all of the types in KeePass are static, private, sealed, and don't implement interfaces, so there is no other way to receive notifications that it has rendered its tray icon. /// - private static void hookKeepassTrayIconUpdates() { - Harmony harmony = new("com.aldaviva.keepasstrayiconlockstate"); - - MethodInfo originalUpdateTrayIconMethod = AccessTools.Method(typeof(MainForm), "UpdateTrayIcon", new[] { typeof(bool) }) ?? + private void hookKeepassTrayIconUpdates() { + MethodInfo originalUpdateTrayIconMethod = AccessTools.Method(typeof(MainForm), "UpdateTrayIcon", [typeof(bool)]) ?? throw new MissingMethodException("Cannot find KeePass.Forms.MainForm.UpdateTrayIcon(bool) method"); - HarmonyMethod onAfterUpdateTrayIcon = new(AccessTools.Method(typeof(KeePassTrayIconLockStateExt), nameof(onKeepassRenderedTrayIcon))); + HarmonyMethod onAfterUpdateTrayIcon = new(SymbolExtensions.GetMethodInfo(() => onKeepassRenderedTrayIcon())); harmony.Patch(originalUpdateTrayIconMethod, postfix: onAfterUpdateTrayIcon); } diff --git a/KeePassTrayIconLockState/Properties/AssemblyInfo.cs b/KeePassTrayIconLockState/Properties/AssemblyInfo.cs index 42916ad..2be6290 100644 --- a/KeePassTrayIconLockState/Properties/AssemblyInfo.cs +++ b/KeePassTrayIconLockState/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -7,7 +7,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Ben Hutchison")] [assembly: AssemblyProduct("KeePass Plugin")] -[assembly: AssemblyCopyright("© 2023 Ben Hutchison")] +[assembly: AssemblyCopyright("© 2024 Ben Hutchison")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -17,8 +17,8 @@ // Remember to also update version.txt when changing these version attributes so that online update checking works. // https://keepass.info/help/v2_dev/plg_index.html#upd -[assembly: AssemblyVersion("1.3.0.0")] -[assembly: AssemblyFileVersion("1.3.0.0")] +[assembly: AssemblyVersion("1.4.0.0")] +[assembly: AssemblyFileVersion("1.4.0.0")] [assembly: InternalsVisibleTo("Test")] [assembly: InternalsVisibleTo("Test.Elevated")] \ No newline at end of file diff --git a/Test.Elevated/Test.Elevated.csproj b/Test.Elevated/Test.Elevated.csproj index a9f8002..0ca7959 100644 --- a/Test.Elevated/Test.Elevated.csproj +++ b/Test.Elevated/Test.Elevated.csproj @@ -11,13 +11,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Test/Test.csproj b/Test/Test.csproj index 07f3c0e..dfe4cb0 100644 --- a/Test/Test.csproj +++ b/Test/Test.csproj @@ -11,13 +11,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all