Skip to content

Commit

Permalink
Fix high CPU usage in KeePass search results on Windows 10
Browse files Browse the repository at this point in the history
  • Loading branch information
Aldaviva committed Feb 20, 2024
1 parent 279fab1 commit 0de25ff
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 132 deletions.
12 changes: 12 additions & 0 deletions KeePassTrayIconLockState/Fixes/Fix.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#nullable enable

using HarmonyLib;
using KeePass.Plugins;

namespace KeePassTrayIconLockState.Fixes;

public interface Fix {

public void fix(IPluginHost pluginHost, Harmony harmony);

}
34 changes: 34 additions & 0 deletions KeePassTrayIconLockState/Fixes/FixHighCpuInSearchResults.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#nullable enable

using HarmonyLib;
using KeePass.Plugins;
using KeePass.UI;
using System.Reflection;
using System.Windows.Forms;

namespace KeePassTrayIconLockState.Fixes;

/// <summary>
/// <para>On Windows 10 but not 11, Windows' Accessibility/UI Automation gets into an infinite loop sending <c>WM_GETOBJECT</c> (0x3D), <c>LVM_ISGROUPVIEWENABLED</c> (0x10AF), and <c>LVM_GETVIEW</c> (0x108F) messages to the KeePass search results view when it has at least 1 result.</para>
/// <para>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.</para>
/// <para>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 <c>LVM_ISGROUPVIEWENABLED</c> messages by returning early. Blocking <c>WM_GETOBJECT</c> or <c>LVM_GETVIEW</c> messages are not necessary to fix this issue.</para>
/// </summary>
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);
}

/// <returns><c>true</c> if <paramref name="m"/> will not cause high CPU, which allows WndProc to run; or <c>false</c> if it will cause high CPU, which causes WndProc to return early</returns>
/// <remarks><see href="https://harmony.pardeike.net/articles/patching-prefix.html#changing-the-result-and-skipping-the-original"/></remarks>
internal static bool ignoreIsGroupViewEnabled(ref Message m) {
return m.Msg != LVM_ISGROUPVIEWENABLED;
}

}
21 changes: 21 additions & 0 deletions KeePassTrayIconLockState/Fixes/Fixes.cs
Original file line number Diff line number Diff line change
@@ -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<Fix> all { get; } = [
new FixHighCpuInSearchResults()
];

public static void fixAll(IPluginHost host, Harmony harmony) {
foreach (Fix fix in all) {
fix.fix(host, harmony);
}
}

}
235 changes: 120 additions & 115 deletions KeePassTrayIconLockState/KeePassTrayIconLockState.csproj
Original file line number Diff line number Diff line change
@@ -1,116 +1,121 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{E980ABDC-E56D-4E9C-A322-AFBE9D9092B4}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>KeePassTrayIconLockState</RootNamespace>
<AssemblyName>KeePassTrayIconLockState</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<NoWarn>8524;8509</NoWarn>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
</ItemGroup>
<Choose>
<When Condition="'$(GITHUB_ACTIONS)' == 'true'">
<ItemGroup>
<!-- In GitHub Actions CI, KeePass is installed by Chocolatey to the default installation directory -->
<!-- The "KeePass" NuGet package is unfortunately unusable because it's not an official build with the same strong signing key, so this project's DLL won't be able to refer to KeePass at runtime -->
<!-- PLGX packaging would fix this because KeePass would rewrite the dependency during runtime JIT compilation, but would also require the project to be build with C# 5, which is old and gross -->
<Reference Include="KeePass">
<HintPath>C:\Program Files\KeePass Password Safe 2\KeePass.exe</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</When>
<Otherwise>
<ItemGroup>
<!-- During development, use the KeePass installation directory on the development machine -->
<Reference Include="KeePass">
<HintPath>..\..\..\..\..\..\Programs\Security\KeePass\KeePass.exe</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Otherwise>
</Choose>
<ItemGroup>
<Compile Include="DatabaseOpenState.cs" />
<Compile Include="KeePassTrayIconLockStateExt.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="TrayIcon.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DarkNet">
<Version>2.3.0</Version>
</PackageReference>
<PackageReference Include="ILRepack.Lib.MSBuild.Task">
<Version>2.0.26</Version>
</PackageReference>
<PackageReference Include="KoKo">
<Version>2.2.0</Version>
</PackageReference>
<PackageReference Include="Lib.Harmony">
<Version>2.2.2</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Include="Resources\locked.ico" />
</ItemGroup>
<ItemGroup>
<None Include="Resources\unlocked.ico" />
</ItemGroup>
<ItemGroup>
<None Include="Resources\unlocking.ico" />
</ItemGroup>
<ItemGroup>
<None Include="Resources\plugin image.png" />
</ItemGroup>
<ItemGroup>
<None Include="version.txt" />
</ItemGroup>
<ItemGroup>
<None Include="Resources\unlocked-light.ico" />
</ItemGroup>
<ItemGroup>
<None Include="Resources\unlocking-light.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{E980ABDC-E56D-4E9C-A322-AFBE9D9092B4}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>KeePassTrayIconLockState</RootNamespace>
<AssemblyName>KeePassTrayIconLockState</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<NoWarn>8524;8509</NoWarn>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
</ItemGroup>
<Choose>
<When Condition="'$(GITHUB_ACTIONS)' == 'true'">
<ItemGroup>
<!-- In GitHub Actions CI, KeePass is installed by Chocolatey to the default installation directory -->
<!-- The "KeePass" NuGet package is unfortunately unusable because it's not an official build with the same strong signing key, so this project's DLL won't be able to refer to KeePass at runtime -->
<!-- PLGX packaging would fix this because KeePass would rewrite the dependency during runtime JIT compilation, but would also require the project to be build with C# 5, which is old and gross -->
<Reference Include="KeePass">
<HintPath>C:\Program Files\KeePass Password Safe 2\KeePass.exe</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</When>
<Otherwise>
<ItemGroup>
<!-- During development, use the KeePass installation directory on the development machine -->
<Reference Include="KeePass">
<HintPath>..\..\..\..\..\..\Programs\Security\KeePass\KeePass.exe</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Otherwise>
</Choose>
<ItemGroup>
<Compile Include="DatabaseOpenState.cs" />
<Compile Include="Fixes\Fix.cs" />
<Compile Include="Fixes\Fixes.cs" />
<Compile Include="Fixes\FixHighCpuInSearchResults.cs" />
<Compile Include="KeePassTrayIconLockStateExt.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="TrayIcon.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DarkNet">
<Version>2.3.0</Version>
</PackageReference>
<PackageReference Include="ILRepack.Lib.MSBuild.Task">
<Version>2.0.26</Version>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="KoKo">
<Version>2.2.0</Version>
</PackageReference>
<PackageReference Include="Lib.Harmony">
<Version>2.2.2</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Include="Resources\locked.ico" />
</ItemGroup>
<ItemGroup>
<None Include="Resources\unlocked.ico" />
</ItemGroup>
<ItemGroup>
<None Include="Resources\unlocking.ico" />
</ItemGroup>
<ItemGroup>
<None Include="Resources\plugin image.png" />
</ItemGroup>
<ItemGroup>
<None Include="version.txt" />
</ItemGroup>
<ItemGroup>
<None Include="Resources\unlocked-light.ico" />
</ItemGroup>
<ItemGroup>
<None Include="Resources\unlocking-light.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
11 changes: 6 additions & 5 deletions KeePassTrayIconLockState/KeePassTrayIconLockStateExt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class KeePassTrayIconLockStateExt: Plugin {

private readonly StoredProperty<DatabaseOpenState> databaseOpenState = new();
private readonly IDarkNet darkNet = new DarkNet();
private readonly Harmony harmony = new("com.aldaviva.keepasstrayiconlockstate");

private IPluginHost keePassHost = null!;
private Property<TrayIcon> trayIcon = null!;
Expand Down Expand Up @@ -91,6 +92,8 @@ public override bool Initialize(IPluginHost host) {
hookKeepassTrayIconUpdates();
keepassRenderedTrayIcon += renderTrayIcon;

Fixes.Fixes.fixAll(keePassHost, harmony);

return true;
}

Expand Down Expand Up @@ -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.
/// </summary>
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);
}
Expand Down
8 changes: 4 additions & 4 deletions KeePassTrayIconLockState/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Reflection;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

Expand All @@ -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("")]

Expand All @@ -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")]
8 changes: 4 additions & 4 deletions Test.Elevated/Test.Elevated.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.console" Version="2.5.3">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.console" Version="2.7.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
Loading

0 comments on commit 0de25ff

Please sign in to comment.