From 29a743d40fb530f8d7b8e628be136eac743c8a09 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 9 Oct 2026 09:32:47 -0700 Subject: [PATCH] Use `GetCurrentPackageFamilyNameNative` API instead of path matching to detect MSIX installation (#28029) --- .../host/msh/ConsoleHost.cs | 70 +++++++------------ .../Windows/GetCurrentPackageFamilyName.cs | 43 ++++++++++++ .../engine/Utils.cs | 21 ++++++ 3 files changed, 88 insertions(+), 46 deletions(-) create mode 100644 src/System.Management.Automation/engine/Interop/Windows/GetCurrentPackageFamilyName.cs diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs index 78028f816..57bc08147 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs @@ -378,20 +378,41 @@ namespace Microsoft.PowerShell #if UNIX const string pwshName = "pwsh"; const string dotnetToolPathSegment = "/.store/powershell/"; + const StringComparison procNameCompType = StringComparison.Ordinal; #else const string pwshName = "pwsh.exe"; const string dotnetToolPathSegment = @"\.store\powershell\"; + const StringComparison procNameCompType = StringComparison.OrdinalIgnoreCase; #endif string psExePath = Environment.ProcessPath; string psExeHome = Path.GetDirectoryName(psExePath); string processName = Path.GetFileName(psExePath); - // Use 'Environment.ProcessPath' if it points to 'pwsh.exe' or 'pwsh'. - if (pwshName.Equals(processName, StringComparison.Ordinal)) + // Use 'Environment.ProcessPath' if it points to 'pwsh.exe' or 'pwsh'. The 'ProcessPath' on Windows + // could be any case as it depends on the `lpCommandLine` argument passed to `CreateProcess`, so we + // compare with 'OrdinalIgnoreCase' on Windows. + if (pwshName.Equals(processName, procNameCompType)) { #if !UNIX - psExeHome = ResolveStablePathIfMsix(psExeHome); + // When running from an MSIX package, 'psExeHome' points to the MSIX package folder under "Program Files". + // + // That path contains a version string and will change with every update. Prepending that path to the PATH + // environment variable caused a problem for CMake-based build systems: CMake cached the path to 'pwsh.exe' + // when running for the first time from the MSIX PowerShell. That cached path became invalid after the MSIX + // PowerShell was updated, which broke CMake. + // + // So, instead of using the "Program Files" package folder path, we need to use the stable path that contains + // the execution alias for the specific MSIX package, e.g. use "%LOCALAPPDATA%\Microsoft\WindowsApps\Microsoft.PowerShell_8wekyb3d8bbwe" + // instead of "%ProgramFiles%\WindowsApps\Microsoft.PowerShell_7.x.x.0_x64__8wekyb3d8bbwe". + if (Utils.IsMSIXInstallation(out string packageFamilyName)) + { + string localAppDataDir = Environment.GetFolderPath( + Environment.SpecialFolder.LocalApplicationData, + Environment.SpecialFolderOption.DoNotVerify); + + psExeHome = $"{localAppDataDir}\\Microsoft\\WindowsApps\\{packageFamilyName}"; + } #endif return psExeHome; } @@ -439,49 +460,6 @@ namespace Microsoft.PowerShell } } #else - /// - /// Handle the MSIX package scenario where points to the MSIX package folder under "Program Files". - /// - /// That path contains a version string and will change with every update. Prepending that path to the PATH environment variable - /// caused a problem for CMake-based build systems: CMake cached the path to 'pwsh.exe' when running for the first time from the - /// MSIX PowerShell. That cached path became invalid after the MSIX PowerShell was updated, which broke CMake. - /// - /// So, instead of using the "Program Files" package folder path, we need to use the stable path that contains the execution alias - /// for the specific MSIX package, e.g. use "%LOCALAPPDATA%\Microsoft\WindowsApps\Microsoft.PowerShell_8wekyb3d8bbwe" instead of - /// "%ProgramFiles%\WindowsApps\Microsoft.PowerShell_7.x.x.0_x64__8wekyb3d8bbwe". - /// - /// Path to the directory that contains the pwsh executable. - private static string ResolveStablePathIfMsix(string psExeHome) - { - const string msixPublisherSuffix = "_8wekyb3d8bbwe"; - const string msixPackageBaseName = "Microsoft.PowerShell"; - - if (psExeHome.EndsWith(msixPublisherSuffix, StringComparison.Ordinal)) - { - string programFileDir = Environment.GetFolderPath( - Environment.SpecialFolder.ProgramFiles, - Environment.SpecialFolderOption.DoNotVerify); - - string prefix = $"{programFileDir}\\WindowsApps\\{msixPackageBaseName}"; - if (psExeHome.StartsWith(prefix, StringComparison.Ordinal)) - { - int startIndex = prefix.Length; - int underbarIndex = psExeHome.IndexOf('_', startIndex); - if (underbarIndex > 0) - { - ReadOnlySpan channelSuffix = psExeHome.AsSpan(startIndex, underbarIndex - startIndex); - string localAppDataDir = Environment.GetFolderPath( - Environment.SpecialFolder.LocalApplicationData, - Environment.SpecialFolderOption.DoNotVerify); - - psExeHome = $"{localAppDataDir}\\Microsoft\\WindowsApps\\{msixPackageBaseName}{channelSuffix}{msixPublisherSuffix}"; - } - } - } - - return psExeHome; - } - /// /// The break handler for the program. Dispatches a break event to the current Executor. /// diff --git a/src/System.Management.Automation/engine/Interop/Windows/GetCurrentPackageFamilyName.cs b/src/System.Management.Automation/engine/Interop/Windows/GetCurrentPackageFamilyName.cs new file mode 100644 index 000000000..562847a76 --- /dev/null +++ b/src/System.Management.Automation/engine/Interop/Windows/GetCurrentPackageFamilyName.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Windows + { + [LibraryImport("kernel32.dll", EntryPoint = "GetCurrentPackageFamilyName", StringMarshalling = StringMarshalling.Utf16)] + private static partial int GetCurrentPackageFamilyNameNative(ref uint packageFamilyNameLength, Span packageFamilyName); + + /// + /// Returns the package family name of the current process when it has package (MSIX) identity; otherwise null. + /// + internal static string? GetCurrentPackageFamilyName() + { + const int ErrorSuccess = 0; + const int PackageFamilyNameMaxLength = 65; // Maximum name plus null terminator. + + try + { + uint length = PackageFamilyNameMaxLength; + Span buffer = stackalloc char[PackageFamilyNameMaxLength]; + int result = GetCurrentPackageFamilyNameNative(ref length, buffer); + if (result is not ErrorSuccess || length is 0) + { + return null; + } + + // The returned length includes the null terminator, which we don't want in the managed string. + return new string(buffer[..(int)(length - 1)]); + } + catch (Exception exception) when (exception is DllNotFoundException or EntryPointNotFoundException) + { + return null; + } + } + } +} diff --git a/src/System.Management.Automation/engine/Utils.cs b/src/System.Management.Automation/engine/Utils.cs index 0de9fe0d5..d955b7854 100644 --- a/src/System.Management.Automation/engine/Utils.cs +++ b/src/System.Management.Automation/engine/Utils.cs @@ -475,6 +475,27 @@ namespace System.Management.Automation return string.Empty; } + + private static string s_packageFamilyName; + private static bool? s_isMSIXInstallation; + + /// + /// Determine whether the current process has package (MSIX) identity, caching the result + /// so the native API is only called once. + /// + /// Receives the package family name when the process has package identity; otherwise null. + /// True if the current process has package (MSIX) identity; otherwise false. + internal static bool IsMSIXInstallation(out string packageFamilyName) + { + if (s_isMSIXInstallation is null) + { + s_packageFamilyName = Interop.Windows.GetCurrentPackageFamilyName(); + s_isMSIXInstallation = s_packageFamilyName is not null; + } + + packageFamilyName = s_packageFamilyName; + return s_isMSIXInstallation.Value; + } #endif internal static string DefaultPowerShellAppBase => GetApplicationBase(DefaultPowerShellShellID);