From 0edf06bb5fdec6b0905019290af9dee8b757eb8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Tue, 23 Sep 2025 23:20:06 +0200 Subject: [PATCH] CmdPal: Window Walker - detect UWP apps and prevent "Unresponsive" tag on them (#41938) ## Summary of the Pull Request This PR introduces detection of UWP processes and skips evaluation of the Process.Responding property for them. The Process.Responding property is only reliable for Win32 apps. For UWP processes, relying on this property can produce incorrect results. With this change, UWP apps will no longer be flagged with an Unresponsive tag due to misleading property values. ## PR Checklist - [x] Closes: #38353 - [ ] **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 ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- .github/actions/spell-check/expect.txt | 1 + .../Components/ContextMenuHelper.cs | 2 +- .../Components/WindowProcess.cs | 12 +- .../Helpers/NativeMethods.cs | 38 +++++- .../Helpers/ProcessPackagingInfo.cs | 14 ++ .../Helpers/ProcessPackagingInspector.cs | 123 ++++++++++++++++++ .../Helpers/ProcessPackagingKind.cs | 13 ++ 7 files changed, 194 insertions(+), 9 deletions(-) create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ProcessPackagingInfo.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ProcessPackagingInspector.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ProcessPackagingKind.cs diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index c4c13282e4..a6615ca2b2 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -27,6 +27,7 @@ admx advancedpaste advancedpasteui advancedpasteuishortcut +advapi32 advfirewall AFeature affordances diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/ContextMenuHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/ContextMenuHelper.cs index 442f0b79db..cf2625c3a7 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/ContextMenuHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/ContextMenuHelper.cs @@ -30,7 +30,7 @@ internal sealed class ContextMenuHelper // Hide menu if Explorer.exe is the shell process or the process name is ApplicationFrameHost.exe // In the first case we would crash the windows ui and in the second case we would kill the generic process for uwp apps. - if (!windowData.Process.IsShellProcess && !(windowData.Process.IsUwpApp && string.Equals(windowData.Process.Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase)) + if (!windowData.Process.IsShellProcess && !(windowData.Process.IsUwpAppFrameHost && string.Equals(windowData.Process.Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase)) && !(windowData.Process.IsFullAccessDenied && SettingsManager.Instance.HideKillProcessOnElevatedProcesses)) { contextMenu.Add(new CommandContextItem(new EndTaskCommand(windowData)) diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/WindowProcess.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/WindowProcess.cs index f0e1639d68..af3730cada 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/WindowProcess.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/WindowProcess.cs @@ -23,7 +23,7 @@ internal sealed class WindowProcess /// /// An indicator if the window belongs to an 'Universal Windows Platform (UWP)' process /// - private readonly bool _isUwpApp; + private readonly bool _isUwpAppFrameHost; /// /// Gets the id of the process @@ -42,7 +42,8 @@ internal sealed class WindowProcess { try { - return Process.GetProcessById((int)ProcessID).Responding; + // Process.Responding doesn't work on UWP apps + return ProcessType.Kind == ProcessPackagingKind.UwpApp || Process.GetProcessById((int)ProcessID).Responding; } catch (InvalidOperationException) { @@ -76,7 +77,7 @@ internal sealed class WindowProcess /// /// Gets a value indicating whether the window belongs to an 'Universal Windows Platform (UWP)' process /// - internal bool IsUwpApp => _isUwpApp; + public bool IsUwpAppFrameHost => _isUwpAppFrameHost; /// /// Gets a value indicating whether this is the shell process or not @@ -134,9 +135,12 @@ internal sealed class WindowProcess internal WindowProcess(uint pid, uint tid, string name) { UpdateProcessInfo(pid, tid, name); - _isUwpApp = string.Equals(Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase); + ProcessType = ProcessPackagingInspector.Inspect((int)pid); + _isUwpAppFrameHost = string.Equals(Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase); } + public ProcessPackagingInfo ProcessType { get; private set; } + /// /// Updates the process information of the instance. /// diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/NativeMethods.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/NativeMethods.cs index 822c84bbde..382d1a56d1 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/NativeMethods.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/NativeMethods.cs @@ -5,7 +5,7 @@ using System; using System.Runtime.InteropServices; using System.Text; - +using Microsoft.Win32.SafeHandles; using SuppressMessageAttribute = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute; #pragma warning disable SA1649, CA1051, CA1707, CA1028, CA1714, CA1069, SA1402 @@ -98,6 +98,25 @@ public static partial class NativeMethods [DllImport("kernel32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetFirmwareType(ref FirmwareType FirmwareType); + + [DllImport("advapi32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool OpenProcessToken(SafeProcessHandle processHandle, TokenAccess desiredAccess, out SafeAccessTokenHandle tokenHandle); + + [DllImport("advapi32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool GetTokenInformation( + SafeAccessTokenHandle tokenHandle, + TOKEN_INFORMATION_CLASS tokenInformationClass, + out int tokenInformation, + int tokenInformationLength, + out int returnLength); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "GetPackageFullName")] + internal static extern int GetPackageFullName( + SafeProcessHandle hProcess, + ref uint packageFullNameLength, + StringBuilder? packageFullName); } [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "These are the names used by win32.")] @@ -383,7 +402,7 @@ public enum ShowWindowCommand /// /// Displays a window in its most recent size and position. This value - /// is similar to , except + /// is similar to , except /// the window is not activated. /// ShowNoActivate = 4, @@ -401,14 +420,14 @@ public enum ShowWindowCommand /// /// Displays the window as a minimized window. This value is similar to - /// , except the + /// , except the /// window is not activated. /// ShowMinNoActive = 7, /// /// Displays the window in its current size and position. This value is - /// similar to , except the + /// similar to , except the /// window is not activated. /// ShowNA = 8, @@ -1100,3 +1119,14 @@ public enum SIGDN : uint FILESYSPATH = 0x80058000, URL = 0x80068000, } + +internal enum TOKEN_INFORMATION_CLASS +{ + TokenIsAppContainer = 29, +} + +[Flags] +internal enum TokenAccess : uint +{ + TOKEN_QUERY = 0x0008, +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ProcessPackagingInfo.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ProcessPackagingInfo.cs new file mode 100644 index 0000000000..f1d3c5e09d --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ProcessPackagingInfo.cs @@ -0,0 +1,14 @@ +// 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. + +namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers; + +internal sealed record ProcessPackagingInfo( + int Pid, + ProcessPackagingKind Kind, + bool HasPackageIdentity, + bool IsAppContainer, + string? PackageFullName, + int? LastError +); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ProcessPackagingInspector.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ProcessPackagingInspector.cs new file mode 100644 index 0000000000..43d77d1aba --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ProcessPackagingInspector.cs @@ -0,0 +1,123 @@ +// 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. + +using System; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers; + +internal static class ProcessPackagingInspector +{ +#pragma warning disable SA1310 // Field names should not contain underscore + private const int ERROR_INSUFFICIENT_BUFFER = 122; + private const int APPMODEL_ERROR_NO_PACKAGE = 15700; +#pragma warning restore SA1310 // Field names should not contain underscore + + /// + /// Inspect a process by PID and classify its packaging. + /// + public static ProcessPackagingInfo Inspect(int pid) + { + var hProcess = NativeMethods.OpenProcess(ProcessAccessFlags.QueryLimitedInformation, false, pid); + using var process = new SafeProcessHandle(hProcess, true); + if (process.IsInvalid) + { + return new ProcessPackagingInfo( + pid, + ProcessPackagingKind.Unknown, + HasPackageIdentity: false, + IsAppContainer: false, + PackageFullName: null, + LastError: Marshal.GetLastPInvokeError()); + } + + // 1) Check package identity + var hasPackage = TryGetPackageFullName(process, out var packageFullName, out _); + + // 2) If packaged, check AppContainer -> strict UWP + var isAppContainer = false; + int? tokenErr = null; + if (hasPackage) + { + isAppContainer = TryIsAppContainer(process, out tokenErr); + } + + var kind = + !hasPackage ? ProcessPackagingKind.UnpackagedWin32 : + isAppContainer ? ProcessPackagingKind.UwpApp : + ProcessPackagingKind.PackagedWin32; + + return new ProcessPackagingInfo( + pid, + kind, + HasPackageIdentity: hasPackage, + IsAppContainer: isAppContainer, + PackageFullName: packageFullName, + LastError: null); + } + + private static bool TryGetPackageFullName(SafeProcessHandle hProcess, out string? packageFullName, out int? lastError) + { + packageFullName = null; + lastError = null; + + uint len = 0; + var rc = NativeMethods.GetPackageFullName(hProcess, ref len, null); + if (rc == APPMODEL_ERROR_NO_PACKAGE) + { + return false; // no package identity + } + + if (rc != ERROR_INSUFFICIENT_BUFFER && rc != 0) + { + lastError = rc; + return false; // unexpected error + } + + if (len == 0) + { + return false; + } + + var sb = new StringBuilder((int)len); + rc = NativeMethods.GetPackageFullName(hProcess, ref len, sb); + if (rc == 0) + { + packageFullName = sb.ToString(); + return true; + } + + lastError = rc; + return false; + } + + private static bool TryIsAppContainer(SafeProcessHandle hProcess, out int? lastError) + { + lastError = null; + + if (!NativeMethods.OpenProcessToken(hProcess, TokenAccess.TOKEN_QUERY, out var token)) + { + lastError = Marshal.GetLastPInvokeError(); + return false; // can't decide; treat as not-UWP for classification + } + + using (token) + { + if (!NativeMethods.GetTokenInformation( + token, + TOKEN_INFORMATION_CLASS.TokenIsAppContainer, + out var val, + sizeof(int), + out _)) + { + lastError = Marshal.GetLastPInvokeError(); + return false; + } + + return val != 0; // true => AppContainer (UWP) + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ProcessPackagingKind.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ProcessPackagingKind.cs new file mode 100644 index 0000000000..dddfeaeb26 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ProcessPackagingKind.cs @@ -0,0 +1,13 @@ +// 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. + +namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers; + +internal enum ProcessPackagingKind +{ + Unknown = 0, + UnpackagedWin32, + PackagedWin32, + UwpApp, +}