Files
PowerToys/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/Window.cs
Dave Rayment 5e30caa674 [WindowWalker] Fix race condition in UWP app enumeration (#45601)
## Summary of the Pull Request
This fixes a race condition in the WindowWalker component in both
**Command Palette** and **Run**. The lack of a lock around a cache
update could potentially lead to inaccurate information about UWP
applications being returned.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #45600
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
More details about the root cause can be found in the original issue:
#45600.

In summary, a `Task` is created and started as part of the
`CreateWindowProcessInstance()` code. The creation is inside a lock, but
there is no lock around access to the `_handlesToProcessCache` cache
within the Task itself, which will run on a different thread.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

- Ensured unit tests still pass (NB: I cannot currently run CmdPal UI
tests for some reason)
- Manually ran the applications, testing the Window Walker component in
both
2026-02-20 09:49:42 -06:00

428 lines
16 KiB
C#

// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Wox.Plugin.Common.VirtualDesktop.Helper;
using Wox.Plugin.Common.Win32;
using Wox.Plugin.Logger;
namespace Microsoft.Plugin.WindowWalker.Components
{
/// <summary>
/// Represents a specific open window
/// </summary>
internal class Window
{
/// <summary>
/// The handle to the window
/// </summary>
private readonly IntPtr hwnd;
/// <summary>
/// A static cache for the process data of all known windows
/// that we don't have to query the data every time
/// </summary>
private static readonly Dictionary<IntPtr, WindowProcess> _handlesToProcessCache = new Dictionary<IntPtr, WindowProcess>();
/// <summary>
/// An instance of <see cref="WindowProcess"/> that contains the process information for the window
/// </summary>
private readonly WindowProcess processInfo;
/// <summary>
/// An instance of <see cref="VDesktop"/> that contains the desktop information for the window
/// </summary>
private readonly VDesktop desktopInfo;
/// <summary>
/// Gets the title of the window (the string displayed at the top of the window)
/// </summary>
internal string Title
{
get
{
int sizeOfTitle = NativeMethods.GetWindowTextLength(hwnd);
if (sizeOfTitle++ > 0)
{
StringBuilder titleBuffer = new StringBuilder(sizeOfTitle);
var numCharactersWritten = NativeMethods.GetWindowText(hwnd, titleBuffer, sizeOfTitle);
if (numCharactersWritten == 0)
{
return string.Empty;
}
return titleBuffer.ToString();
}
else
{
return string.Empty;
}
}
}
/// <summary>
/// Gets the handle to the window
/// </summary>
internal IntPtr Hwnd
{
get { return hwnd; }
}
/// <summary>
/// Gets the object of with the process information of the window
/// </summary>
internal WindowProcess Process
{
get { return processInfo; }
}
/// <summary>
/// Gets the object of with the desktop information of the window
/// </summary>
internal VDesktop Desktop
{
get { return desktopInfo; }
}
/// <summary>
/// Gets the name of the class for the window represented
/// </summary>
internal string ClassName
{
get
{
return GetWindowClassName(Hwnd);
}
}
/// <summary>
/// Gets a value indicating whether the window is visible (might return false if it is a hidden IE tab)
/// </summary>
internal bool Visible
{
get
{
return NativeMethods.IsWindowVisible(Hwnd);
}
}
/// <summary>
/// Gets a value indicating whether the window is cloaked (true) or not (false).
/// (A cloaked window is not visible to the user. But the window is still composed by DWM.)
/// </summary>
internal bool IsCloaked
{
get
{
return GetWindowCloakState() != WindowCloakState.None;
}
}
/// <summary>
/// Gets a value indicating whether the specified window handle identifies an existing window.
/// </summary>
internal bool IsWindow
{
get
{
return NativeMethods.IsWindow(Hwnd);
}
}
/// <summary>
/// Gets a value indicating whether the window is a toolwindow
/// </summary>
internal bool IsToolWindow
{
get
{
return (NativeMethods.GetWindowLong(Hwnd, Win32Constants.GWL_EXSTYLE) &
(uint)ExtendedWindowStyles.WS_EX_TOOLWINDOW) ==
(uint)ExtendedWindowStyles.WS_EX_TOOLWINDOW;
}
}
/// <summary>
/// Gets a value indicating whether the window is an appwindow
/// </summary>
internal bool IsAppWindow
{
get
{
return (NativeMethods.GetWindowLong(Hwnd, Win32Constants.GWL_EXSTYLE) &
(uint)ExtendedWindowStyles.WS_EX_APPWINDOW) ==
(uint)ExtendedWindowStyles.WS_EX_APPWINDOW;
}
}
/// <summary>
/// Gets a value indicating whether the window has ITaskList_Deleted property
/// </summary>
internal bool TaskListDeleted
{
get
{
return NativeMethods.GetProp(Hwnd, "ITaskList_Deleted") != IntPtr.Zero;
}
}
/// <summary>
/// Gets a value indicating whether the specified windows is the owner (i.e. doesn't have an owner)
/// </summary>
internal bool IsOwner
{
get
{
return NativeMethods.GetWindow(Hwnd, GetWindowCmd.GW_OWNER) == IntPtr.Zero;
}
}
/// <summary>
/// Gets a value indicating whether the window is minimized
/// </summary>
internal bool Minimized
{
get
{
return GetWindowSizeState() == WindowSizeState.Minimized;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="Window"/> class.
/// Initializes a new Window representation
/// </summary>
/// <param name="hwnd">the handle to the window we are representing</param>
internal Window(IntPtr hwnd)
{
// TODO: Add verification as to whether the window handle is valid
this.hwnd = hwnd;
processInfo = CreateWindowProcessInstance(hwnd);
desktopInfo = Main.VirtualDesktopHelperInstance.GetWindowDesktop(hwnd);
}
/// <summary>
/// Switches desktop focus to the window
/// </summary>
internal void SwitchToWindow()
{
// The following block is necessary because
// 1) There is a weird flashing behavior when trying
// to use ShowWindow for switching tabs in IE
// 2) SetForegroundWindow fails on minimized windows
// Using Ordinal since this is internal
if (processInfo.Name.ToUpperInvariant().Equals("IEXPLORE.EXE", StringComparison.Ordinal) || !Minimized)
{
NativeMethods.SetForegroundWindow(Hwnd);
}
else
{
if (!NativeMethods.ShowWindow(Hwnd, ShowWindowCommand.Restore))
{
// ShowWindow doesn't work if the process is running elevated: fall back to SendMessage
_ = NativeMethods.SendMessage(Hwnd, Win32Constants.WM_SYSCOMMAND, Win32Constants.SC_RESTORE);
}
}
NativeMethods.FlashWindow(Hwnd, true);
}
/// <summary>
/// Helper function to close the window
/// </summary>
internal void CloseThisWindowHelper()
{
_ = NativeMethods.SendMessageTimeout(Hwnd, Win32Constants.WM_SYSCOMMAND, Win32Constants.SC_CLOSE, 0, 0x0000, 5000, out _);
}
/// <summary>
/// Closes the window
/// </summary>
internal void CloseThisWindow()
{
Thread thread = new(new ThreadStart(CloseThisWindowHelper));
thread.Start();
}
/// <summary>
/// Converts the window name to string along with the process name
/// </summary>
/// <returns>The title of the window</returns>
public override string ToString()
{
// Using CurrentCulture since this is user facing
return Title + " (" + processInfo.Name.ToUpper(CultureInfo.CurrentCulture) + ")";
}
/// <summary>
/// Returns what the window size is
/// </summary>
/// <returns>The state (minimized, maximized, etc..) of the window</returns>
internal WindowSizeState GetWindowSizeState()
{
NativeMethods.GetWindowPlacement(Hwnd, out WINDOWPLACEMENT placement);
switch (placement.ShowCmd)
{
case ShowWindowCommand.Normal:
return WindowSizeState.Normal;
case ShowWindowCommand.Minimize:
case ShowWindowCommand.ShowMinimized:
return WindowSizeState.Minimized;
case ShowWindowCommand.Maximize: // No need for ShowMaximized here since its also of value 3
return WindowSizeState.Maximized;
default:
// throw new Exception("Don't know how to handle window state = " + placement.ShowCmd);
return WindowSizeState.Unknown;
}
}
/// <summary>
/// Enum to simplify the state of the window
/// </summary>
internal enum WindowSizeState
{
Normal,
Minimized,
Maximized,
Unknown,
}
/// <summary>
/// Returns the window cloak state from DWM
/// (A cloaked window is not visible to the user. But the window is still composed by DWM.)
/// </summary>
/// <returns>The state (none, app, ...) of the window</returns>
internal WindowCloakState GetWindowCloakState()
{
_ = NativeMethods.DwmGetWindowAttribute(Hwnd, (int)DwmWindowAttributes.Cloaked, out int isCloakedState, sizeof(uint));
switch (isCloakedState)
{
case (int)DwmWindowCloakStates.None:
return WindowCloakState.None;
case (int)DwmWindowCloakStates.CloakedApp:
return WindowCloakState.App;
case (int)DwmWindowCloakStates.CloakedShell:
return Main.VirtualDesktopHelperInstance.IsWindowCloakedByVirtualDesktopManager(hwnd, Desktop.Id) ? WindowCloakState.OtherDesktop : WindowCloakState.Shell;
case (int)DwmWindowCloakStates.CloakedInherited:
return WindowCloakState.Inherited;
default:
return WindowCloakState.Unknown;
}
}
/// <summary>
/// Enum to simplify the cloak state of the window
/// </summary>
internal enum WindowCloakState
{
None,
App,
Shell,
Inherited,
OtherDesktop,
Unknown,
}
/// <summary>
/// Returns the class name of a window.
/// </summary>
/// <param name="hwnd">Handle to the window.</param>
/// <returns>Class name</returns>
private static string GetWindowClassName(IntPtr hwnd)
{
StringBuilder windowClassName = new StringBuilder(300);
var numCharactersWritten = NativeMethods.GetClassName(hwnd, windowClassName, windowClassName.MaxCapacity);
if (numCharactersWritten == 0)
{
return string.Empty;
}
return windowClassName.ToString();
}
/// <summary>
/// Gets an instance of <see cref="WindowProcess"/> form process cache or creates a new one. A new one will be added to the cache.
/// </summary>
/// <param name="hWindow">The handle to the window</param>
/// <returns>A new Instance of type <see cref="WindowProcess"/></returns>
private static WindowProcess CreateWindowProcessInstance(IntPtr hWindow)
{
lock (_handlesToProcessCache)
{
if (_handlesToProcessCache.Count > 7000)
{
Debug.Print("Clearing Process Cache because its size is " + _handlesToProcessCache.Count);
_handlesToProcessCache.Clear();
}
// Add window's process to cache if missing
if (!_handlesToProcessCache.ContainsKey(hWindow))
{
// Get process ID and name
var processId = WindowProcess.GetProcessIDFromWindowHandle(hWindow);
var threadId = WindowProcess.GetThreadIDFromWindowHandle(hWindow);
var processName = WindowProcess.GetProcessNameFromProcessID(processId);
if (processName.Length != 0)
{
_handlesToProcessCache.Add(hWindow, new WindowProcess(processId, threadId, processName));
}
else
{
// For the dwm process we cannot receive the name. This is no problem because the window isn't part of result list.
Log.Debug($"Invalid process {processId} ({processName}) for window handle {hWindow}.", typeof(Window));
_handlesToProcessCache.Add(hWindow, new WindowProcess(0, 0, string.Empty));
}
}
// Correct the process data if the window belongs to a uwp app hosted by 'ApplicationFrameHost.exe'
// (This only works if the window isn't minimized. For minimized windows the required child window isn't assigned.)
if (string.Equals(_handlesToProcessCache[hWindow].Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase))
{
new Task(() =>
{
EnumWindowsProc callbackptr = new EnumWindowsProc((IntPtr hwnd, IntPtr lParam) =>
{
// Every uwp app main window has at least three child windows. Only the one we are interested in has a class starting with "Windows.UI.Core." and is assigned to the real app process.
// (The other ones have a class name that begins with the string "ApplicationFrame".)
if (GetWindowClassName(hwnd).StartsWith("Windows.UI.Core.", StringComparison.OrdinalIgnoreCase))
{
var childProcessId = WindowProcess.GetProcessIDFromWindowHandle(hwnd);
var childThreadId = WindowProcess.GetThreadIDFromWindowHandle(hwnd);
var childProcessName = WindowProcess.GetProcessNameFromProcessID(childProcessId);
// Update process info in cache
lock (_handlesToProcessCache)
{
_handlesToProcessCache[hWindow].UpdateProcessInfo(childProcessId, childThreadId, childProcessName);
}
return false;
}
else
{
return true;
}
});
_ = NativeMethods.EnumChildWindows(hWindow, callbackptr, 0);
}).Start();
}
return _handlesToProcessCache[hWindow];
}
}
}
}