Visitar URL original
Use `GetCurrentPackageFamilyNameNative` API instead of path matching to detect MSIX installation by daxian-dbw · Pull Request #28029 · PowerShell/PowerShell · GitHub
Skip to content

Use GetCurrentPackageFamilyNameNative API instead of path matching to detect MSIX installation - #28029

Open
Dongbo Wang (daxian-dbw) wants to merge 3 commits into
PowerShell:masterfrom
daxian-dbw:string-comparison
Open

Dongbo Wang (daxian-dbw) wants to merge 3 commits into
PowerShell:masterfrom
daxian-dbw:string-comparison

Conversation

@daxian-dbw

@daxian-dbw Dongbo Wang (daxian-dbw) commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

PR Summary

This is a follow-up PR based on the discussions with Jordan Borean (@jborean93) in

This PR contains 2 changes:

  1. Compare the process name using StringComparison.OrdinalIgnoreCase because Environment.ProcessPath may contain any case as it depends on the comamndline argument passed to CreateProcess.
  2. Use GetCurrentPackageFamilyName function to detect if the running pwsh is a MSIX installation and get its package family name if so.

Validated with the .msixbundle package produced by the ADO build: https://dev.azure.com/mscodehub/PowerShellCore/_build/results?buildId=720977&view=results.

image

PR Checklist

Copilot AI lite review requested due to automatic review settings September 18, 2026 18:15
@daxian-dbw
Dongbo Wang (daxian-dbw) requested a review from a team as a code owner September 18, 2026 18:15
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@daxian-dbw Dongbo Wang (daxian-dbw) added CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log Backport-7.6.x-Consider labels Sep 18, 2026
@daxian-dbw

Copy link
Copy Markdown
Member Author

Jordan Borean (@jborean93) Can you please do a review when you have time? Thank you!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

No unresolved blocking issues were identified.

Pull request overview

Updates MSIX detection to use the native package identity API and makes Windows process-name matching case-insensitive.

Changes:

  • Adds cached package-family-name detection and native API interop.
  • Uses stable MSIX execution-alias paths.
  • Applies ordinal case-insensitive process matching.
File summaries
File Description
src/System.Management.Automation/engine/Utils.cs Caches MSIX identity and package family name.
src/System.Management.Automation/engine/Interop/Windows/GetCurrentPackageFamilyName.cs Adds Windows API interop.
src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs Uses stable MSIX paths and updated process matching.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

@jborean93 Jordan Borean (jborean93) left a comment

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've manually verified the PInvoking logic with a manual test MSIX package and it seems to work fine. I personally might consider the pre-allocated span method on PACKAGE_FAMILY_NAME_MAX_LENGTH + 1 to avoid having to do 2 PInvoke calls as I know this is code running at startup.

When doing some manual testing with the 3 different methods; original one in PR, pre allocated, string.Create, I did find that it's mostly a difference of nanoseconds. Technically the pre-allocated method is faster but still a very small, on average:

  • Original 53ns
  • Preallocated 38ns
  • String.Create 49ns

Up to you as to whether you think either of those other options are worth considering but logically the changes you have here in the PR seem good to me.


try
{
uint length = 0;

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.

Considering we are fairly confident at the size of the buffer that would be needed here for PowerShell/PowerShell.Preview and the theoretical max of the package family name is PACKAGE_FAMILY_NAME_MAX_LENGTH == 64 would it better to just allocate the 64 chars and call this once? The wording is a bit vague as to whether we would want 65 so it can cover the null terminator that PACKAGE_FAMILY_NAME_MAX_LENGTH seems to imply it doesn't.

This is really a stretch but, the other added benefit over only calling the method once is we've set a hard ceiling over what will be allocated from the stack. This is in case something changes in the future we are sure that it'll be 64/65 and not anything up to int.MaxValue that GetCurrentPackageFamilyName could provide.

// 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 WindowsPreallocated
    {
        [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;

            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;
            }
        }
    }
}

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 looks good! I have updated the PR based on this suggestion.

return null;
}

Span<char> buffer = stackalloc char[(int)length];

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.

This is a pretty minor nit but just as an FYI if you are interested you could use the string.Create overload that accepts the SpanAction.

This you avoid having to copy the memory from the stack to the string heap by having C# provide you a Span<char> to that heaped memory. You don't find any benefits in allocation but you do remove a copy operation. It's still a tiny amount of memory so in the long run it doesn't matter and the pre-allocated method I also mentioned provides a faster implementation due to removing the extra PInvoke call.

I tested the below in a mock MSIX package on Windows. I still think that maybe just pre-allocating the defined max length is a better option here but I just left it here in case you were interested/curious.

// 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 ErrorInsufficientBuffer = 122;
            const int AppModelErrorNoPackage = 15700;

            try
            {
                uint length = 0;
                int result = GetCurrentPackageFamilyNameNative(ref length, Span<char>.Empty);
                if (result is AppModelErrorNoPackage)
                {
                    return null;
                }

                if (result is not ErrorInsufficientBuffer || length is 0)
                {
                    return null;
                }

                // The returned length includes the null terminator, which we don't want in the managed string.
                // The actual Span<char> still guarantees there is an extra char designed for the null terminator.
                // We also provide a simple span to store the inner result while keeping the action static.
                Span<int> writeResult = stackalloc int[1];
                string packageFamilyName = string.Create((int)length - 1, writeResult, static (chars, state) =>
                {
                    uint bufferLength = (uint)chars.Length + 1;
                    state[0] = GetCurrentPackageFamilyNameNative(ref bufferLength, chars);
                });

                return writeResult[0] is 0 ? packageFamilyName : null;
            }
            catch (Exception exception) when (exception is DllNotFoundException or EntryPointNotFoundException)
            {
                return null;
            }
        }
    }
}

@daxian-dbw Dongbo Wang (daxian-dbw) Sep 21, 2026 •

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.

It's good to know that for this string.Create overload, the Span<char> passed to the delegate already points to the new string allocated in heap.

Comment thread src/System.Management.Automation/engine/Utils.cs
}

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.

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

@microsoft-github-policy-service microsoft-github-policy-service Bot added the Review - Needed The PR is being reviewed label Sep 30, 2026
@microsoft-github-policy-service

Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days.
Maintainer, please provide feedback and/or mark it as Waiting on Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backport-7.6.x-Consider CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log Review - Needed The PR is being reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants