Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 24 additions & 46 deletions src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -378,20 +378,41 @@ private static string GetPSExecutableHome()
#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))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not the best naming style:

Suggested change
if (pwshName.Equals(processName, procNameCompType))
if (pwshName.Equals(processName, proccessNameComparisonType))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the name is clear enough and relatively short :)

{
#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;
}
Expand Down Expand Up @@ -439,49 +460,6 @@ private static void MyBreakHandler(object sender, ConsoleCancelEventArgs args)
}
}
#else
/// <summary>
/// Handle the MSIX package scenario where <paramref name="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".
/// </summary>
/// <param name="psExeHome">Path to the directory that contains the pwsh executable.</param>
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<char> 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;
}

/// <summary>
/// The break handler for the program. Dispatches a break event to the current Executor.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<char> packageFamilyName);

/// <summary>
/// Returns the package family name of the current process when it has package (MSIX) identity; otherwise null.
/// </summary>
internal static string? GetCurrentPackageFamilyName()
{
const int ErrorSuccess = 0;
const int PackageFamilyNameMaxLength = 65; // Maximum name plus null terminator.

try
{
uint length = PackageFamilyNameMaxLength;
Span<char> 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;
}
}
}
}
21 changes: 21 additions & 0 deletions src/System.Management.Automation/engine/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,27 @@ internal static string GetWindowsPowerShellVersionFromRegistry()

return string.Empty;
}

private static string s_packageFamilyName;
private static bool? s_isMSIXInstallation;

@iSazonov Ilya (iSazonov) Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could avoid s_isMSIXInstallation altogether and use s_packageFamilyName as flag:

  • null - is not initialized yet
  • string.Empty - we couldn't get the MSIX name (it can't be empty)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be used by other code in future, for example the configuration migration that Justin is working on. We don't want to call that API over and over again when it's not running in a MSIX installation.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that maybe Ilya (@iSazonov) is suggesting instead of using s_isMSIXInstallation as the flag for if this has or has not run use s_packageFamilyName. It would have 3 states:

  • null - Has not run so call the API
  • "" (empty string) - Has run but no package family name
  • "..." - Has run and this is the package family name

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sorry I misunderstood the intent :)
Yeah, that works too, but I don't think it matters much. I will stick with the current implementation.


/// <summary>
/// Determine whether the current process has package (MSIX) identity, caching the result
/// so the native API is only called once.
/// </summary>
/// <param name="packageFamilyName">Receives the package family name when the process has package identity; otherwise null.</param>
/// <returns>True if the current process has package (MSIX) identity; otherwise false.</returns>
internal static bool IsMSIXInstallation(out string packageFamilyName)
{
if (s_isMSIXInstallation is null)
Comment thread
daxian-dbw marked this conversation as resolved.
Resolved
{
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);
Expand Down
Loading