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 @@ -372,20 +372,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))
{
#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 @@ -433,49 +454,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;

/// <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)
{
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