8000 Add check if STA is supported before using on console startup by SteveL-MSFT · Pull Request #15106 · PowerShell/PowerShell · GitHub
[go: up one dir, main page]

Skip to content

Add check if STA is supported before using on console startup #15106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 22, 2021

Conversation

SteveL-MSFT
Copy link
Member
@SteveL-MSFT SteveL-MSFT commented Mar 26, 2021

PR Summary

Some specific builds of Windows do not support STA. However, pwsh defaults to STA to support WinForms/WPF (same as Windows PowerShell). This results in an error and unusable PowerShell runspace. Fix is to have a check whether -STA is explicitly used or implicitly where -MTA is not specified by attempting to create a STA thread.

Validated manually.

PR Context

Reported by internal Msft partner

PR Checklist

@ghost ghost added Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept and removed Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept labels Mar 26, 2021
Copy link
Contributor
@PaulHigin PaulHigin left a comment

Choose a reason for hiding this comment

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

LGTM

@ghost ghost added the Review - Needed The PR is being reviewed label Apr 3, 2021
@ghost
Copy link
ghost commented Apr 3, 2021

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

@rjmholt
Copy link
Collaborator
rjmholt commented Apr 12, 2021

I've opened an issue to ask the .NET team about this, however as I wrote it some thoughts occurred to me:

  1. Why are we trying to detect and override a user misconfiguration? Shouldn't we just let it fail and let the user fix it? If we do it through a series of internal logic, I feel we're just going to end up with complicated, hard-to-explain behaviour that's hard to justify for users.
  2. If we are determined to do it this way, why do we need to set a boolean early on? Could we instead do one of:
    • Wait until the first thread we must create and then add logic to catch and retry
    • Make the current boolean getter a cached lazy value that only creates and tests the thread on demand

@ghost ghost removed the Review - Needed The PR is being reviewed label Apr 12, 2021
@SteveL-MSFT
Copy link
Member Author

The problem is that we default to STA for compatibility with Windows PowerShell which is needed for winforms/WPF which requires STA. This was a breaking change from PowerShell Core 6 (which defaulted to MTA) with PowerShell 7 and is affecting some users who don't even care about winforms/WPF.

Whether STA or MTA is used is needed to be determined before we create the first runspace. The exception occurs when the console host tries to open the runspace as that is when the thread is actually created. I don't think it makes sense for the API to create the runspace to override STA and magically make it MTA. Doing it in the host makes more sense to me as the user isn't explicitly requesting it (unless -STA is specified)

@iSazonov
Copy link
Collaborator
iSazonov commented Apr 13, 2021

This results in an error and unusable PowerShell runspace.

I wonder - no exception is on the SKUs from SetApartmentState()?

I see we use StaMode property only in one place.


And I guess the issue is in
if (apartmentState != ApartmentState.Unknown && !Platform.IsNanoServer && !Platform.IsIoT)

and

if (apartmentState != ApartmentState.Unknown && !Platform.IsNanoServer && !Platform.IsIoT)

I believe we could not add a test in StaMode but add a fix in the two places. It would be more reliable solution.

@SteveL-MSFT
Copy link
Member Author

The exception doesn't occur with calling SetApartmentState() (unless it's an already running thread), it's at thread start. However, I've reached out to the COM team to see if there's a better way to detect if STA is supported rather than checking the ProductName in the registry.

@iSazonov
Copy link
Collaborator

The exception doesn't occur with calling SetApartmentState() (unless it's an already running thread), it's at thread start.

My thought just to start a thread and process the exception. Cases (I don't know that is right):

  • we can write an error and continue
  • we can write an error and terminate
  • we can fallback to MTA and cache the fact.

@SteveL-MSFT
Copy link
Member Author

Got an answer from COM team on a better way to check if STA is supported but requires a PInvoke.

Copy link
Collaborator
@rjmholt rjmholt left a comment

Choose a reason for hiding this comment

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

Based on the suggestions in the .NET issue, it might be more efficient and threadsafe if this is done lazily:

public static bool IsStaSupported
{
    get
    {
#if UNIX
        return false;
#else
        return s_isStaSupportedLazy.Value;
#endif
    }
}

private static readonly Lazy<bool> s_isStaSupported = new Lazy<bool>(GetStaSupportedValue);

private static bool GetStaSupportedValue()
{
    // See objbase.h
    const int COINIT_APARTMENTTHREADED = 0x2;
    const int E_NOTIMPL = unchecked((int)0X80004001);
    int result = Windows.NativeMethods.CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);

    // If 0 is returned the thread has been initialized for the first time
    // as an STA and therefore must remain that way.
    if (result > 0)
    {
        Windows.NativeMethods.CoUninitialize();
    }

    return result != E_NOTIMPL;
}

@SteveL-MSFT SteveL-MSFT added the Review - Committee The PR/Issue needs a review from the PowerShell Committee label Apr 15, 2021
@rjmholt
Copy link
Collaborator
rjmholt commented Apr 15, 2021

Another question if we want to migrate from WPF to MAUI.

I suspect that despite being able to make our own migration (although that would require more work that currently isn't planned), users with existing scripts that use WPF expect continued compatibility.

@rjmholt
Copy link
Collaborator
rjmholt commented Apr 15, 2021

However, pwsh defaults to STA to support WinForms/WPF (same as Windows PowerShell)

I think it would be useful for us to summarise the state of things so far in terms of which PowerShell version has what behaviour and what are the motivating scenarios.

Is the following accurate?

  • Non-Windows has no concept of apartment state, because it's a COM concept
  • WinPS defaults to STA, and -mta overrides that to MTA. The default of STA is to enable WPF by default.
  • PS 6 defaulted to MTA because .NET Core 2 didn't have WPF and we had no reason to think it would return. There was no -sta or -mta flag.
  • PS 7 and .NET Core 3 reintroduced WPF. PowerShell changed its default back to STA to make WPF work, breaking SKUs where STA was unavailable.

Some questions:

  • What's the purpose and behaviour of the -mta and -sta flags when:
    • They are used to override the default?
    • They match the default?
    • They set an unsupported behaviour?
  • What's the full explanation for PS 6 defaulting to MTA? Absent WPF it was still a breaking change and COM is not something where I would think we'd benefit greatly from configuring differently, so I would imagine we'd want a good reason for subtly breaking things there.
  • Were there existing scenarios depending on MTA that were broken when PS 7 switched the default?

@iSazonov
Copy link
Collaborator
iSazonov commented Apr 15, 2021

Is the following accurate?

Yes.

Were there existing scenarios depending on MTA that were broken when PS 7 switched the default?

No.
(We could think about MTA as "no threading limitations" and STA as "apply some threading limitations")

What's the full explanation for PS 6 defaulting to MTA?

We just followed default .Net behavior that is MTA on all platforms. It was ok until .Net ported WPF to .Net Core and we retrieved WPF to pwsh too.

Absent WPF it was still a breaking change and COM is not something where I would think we'd benefit greatly from configuring differently, so I would imagine we'd want a good reason for subtly breaking things there.

I am not sure I understand the phrase. While pwsh was MTA the COM subsystem did set STA explicitly so COM support was still working. And this keeps on working the same today.
As I said earlier, other subsystems work in a similar way, or should work in a similar way if possible.

What's the purpose and behaviour of the -mta and -sta flags when:
They are used to override the default?

Good question :-)
STA allows to use some GUI controls https://www.sapien.com/blog/2015/03/09/powershell-studio-knowing-when-to-use-sta-mode/
MTA allows specific thread scenarios (I have no examples)

They match the default?

Interesting, PowerShell 2 default is MTA. But it raised issues with GUI scripts and default was changed to STA in PowerShell 3.
ISE always is STA as GUI tool.

They set an unsupported behavior?

If we set MTA and call an GUI control we get an exception.
If we set STA and a script uses an advanced threading the script will stop working.

SAPIEN Blog
Follow @SAPIENDavid Recently, on our forums, a user asked why certain controls didn’t function correctly or threw an error when he unchecked the STA option in PowerShell Studio. STA (Single Threaded Apartment) and MTA (Multi-Threaded Apartment) determine how a process manages legacy COM objects and

@SteveL-MSFT
Copy link
Member Author

Regarding PSCore6, it was changed to MTA because PowerShell Core 5 (what was shipped on NanoServer originally) was MTA and this is because NanoServer doesn't support STA. I'm pretty sure .NET Core didn't support STA initially until later when it was at "parity" with .NET Framework. For PowerShell 7, we did change the default back to STA because of support for WPF/WinForms that was added to .NET Core.

Once STA or MTA is set on a thread, you can't change it. So once the default runspace created by ConsoleHost (in this case) has set the ApartmentState, that's it. This is why the switches exist for pwsh.exe and allow to be set if the user knows they are calling some COM API that requires one or the other to work successfully.

I think the only scenario PowerShell users have encountered STA vs MTA is using winforms/WPF from script. The cases where Windows itself doesn't support STA, the usage of PowerShell is as a console or automation and I haven't heard of any scenarios where they needed STA (or aware of the problem until they try PS7).

@rjmholt
Copy link
Collaborator
rjmholt commented Apr 15, 2021

So for example, a set of behaviours that would make sense to me is:

  • No apartment state flag: PS tries to be STA on Windows but falls back to MTA, always MTA on Unix.
  • -sta: PS starts in STA, if STA is not supported it crashes with a helpful message
  • -mta: PS starts in MTA, if you used WPF and hit issues it's time to read the docs

I don't love that the default isn't simply the implicit version of an explicit configuration, and I've seen plenty of docs where the default or undefined configuration seems innocent enough but ends up being very complex because it tortuously tries to "just work". In this circumstance though, I don't think the default is that complicated -- you either end up in STA, or if that's not supported, MTA.

The important part for me is that when you explicitly set an option, you either get the configured result or it crashes, there's no "suggestion noted" behaviour where we quietly just give you MTA when you use -sta on a platform where it doesn't work.

It does raise the question to me of providing the -sta flag at all; on Unix the decision was made that since you can't configure it you don't get a switch. But now our matrix is more complicated.

@iSazonov
Copy link
Collaborator

So for example, a set of behaviours that would make sense to me is:

  • No apartment state flag: PS tries to be STA on Windows but falls back to MTA, always MTA on Unix.
  • -sta: PS starts in STA, if STA is not supported it crashes with a helpful message
  • -mta: PS starts in MTA, if you used WPF and hit issues it's time to read the docs

It is ideal.
A complicity with default will be only on GUI-less Windows (Nano) but this is not the version that ill-informed users are using.

It does raise the question to me of providing the -sta flag at all; on Unix the decision was made that since you can't configure it you don't get a switch. But now our matrix is more complicated.

In history the default was changed thrice! So it is better to keep the parameters :-)

// See objbase.h
const int COINIT_APARTMENTTHREADED = 0x2;
const int E_NOTIMPL = unchecked((int)0X80004001);
int result = Windows.NativeMethods.CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if this PInvoke is anymore efficient than spinning up a thread before. Has any perf testing been done?

// See objbase.h
const int COINIT_APARTMENTTHREADED = 0x2;
const int E_NOTIMPL = unchecked((int)0X80004001);
int result = Windows.NativeMethods.CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can an exception be thrown here?

Copy link
Member Author

Choose a reason for hiding this comment

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

CoInitializeEx only returns an HRESULT, so there should never be an exception here unless .NET throws.

@joeyaiello
Copy link
Contributor

Leaving a question here in anticipation of the @PowerShell/powershell-committee on this.

I'm fairly ignorant of apartment state in general, and my knowledge is limited to the kind of historical context that @SteveL-MSFT referenced above.

In what sort of situations would a user care about their apartment state beyond the two scenarios relevant in this issue (i.e. whether or not WPF/WinForms is supported, and whether or not pwsh starts up in the default case on some Windows SKUs).

@rjmholt, you alluded to this a little bit in some of your responses around the user needing to hold a flow chart in their head around apartment state. But what sort of user needs to do that, and for what?

@SteveL-MSFT
Copy link
Member Author

@PowerShell/powershell-committee reviewed this and is ok with handling the check in the consolehost so that it starts on Windows skus that don't support STA and explicitly specifying -sta on such a system fails

@SteveL-MSFT SteveL-MSFT added Committee-Reviewed PS-Committee has reviewed this and made a decision and removed Review - Committee The PR/Issue needs a review from the PowerShell Committee labels Apr 21, 2021
@ghost ghost added Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept and removed Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept labels Apr 21, 2021
Comment on lines +171 to +176
// If 0 is returned the thread has been initialized for the first time
// as an STA and thus supported and needs to be uninitialized.
if (result > 0)
{
Windows.NativeMethods.CoUninitialize();
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder why we need the check if it is used in startup scenario.
If we want to have universal code the condition looks wrong:
S_Ok = 0
S_False = 1
RPC_E_CHANGED_MODE = -2147417850

Copy link
Member Author

Choose a reason for hiding this comment

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

It's needed in the startup scenario as the main console runspace needs to start as either STA or MTA. So we need to know if STA is supported before trying STA otherwise pwsh ends up in an unusable state. S_False will never apply here as CoInitialize() won't be called before this.

@iSazonov
Copy link
Collaborator
iSazonov commented Apr 22, 2021

Isn't it easier to just check this System.Threading.Thread.CurrentThread.ApartmentState?

By default we start in STA on Windows but if an Windows build doesn't support STA it start in MTA mode. Yes?

@SteveL-MSFT
Copy link
Member Author
SteveL-MSFT commented Apr 22, 2021

No, by default ApartmentState is Unknown unless explicitly set to STA or MTA. You only need STA or MTA to interop with COM.

@iSazonov
Copy link
Collaborator

Interes 10000 ting, in debugger I see MTA for main thread. This brings us back to the idea that try-fallback (and cache result) approach should work even more reliable and predictable.

@SteveL-MSFT
Copy link
Member Author

The @PowerShell/powershell-committee discussed the option to have runspace creation handle the problem of STA support, but decided against it as ApartmentState is set via the Runspace API and should be respected. The console, however, is not something most users need to think about STA vs MTA and should just start and for advanced users, they can rely on the -sta and -mta switches.

@iSazonov
Copy link
Collaborator
iSazonov commented Apr 22, 2021

ApartmentState is set via the Runspace API and should be respected

If it is all about "You only need STA or MTA to interop with COM." the proposal with fallback in _worker.SetApartmentState(apartmentState); will work very well since the fallback will be raised only on Windows builds without COM.

(Such fallback is used already in New-Object but in another scenario.)

@rjmholt rjmholt merged commit fc1d5ef into PowerShell:master Apr 22, 2021
@SteveL-MSFT SteveL-MSFT deleted the sta-mta branch April 22, 2021 22:48
rkeithhill pushed a commit to rkeithhill/PowerShell that referenced this pull request May 3, 2021
@daxian-dbw daxian-dbw added the CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log label May 26, 2021
@ghost
Copy link
ghost commented May 27, 2021

🎉v7.2.0-preview.6 has been released which incorporates this pull request.:tada:

Handy links:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log Committee-Reviewed PS-Committee has reviewed this and made a decision
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants
0