I'm adding PowerShell support to RD Tabs to create a scripting environment where the application can be automated in many wonderful ways. I followed the super easy instructions at the PolyMon project for adding PowerShell support. The basic tests succeeded and I was happy. Then I tried actively integrating RD Tabs with the PowerShell script by providing a pre-defined variable that contained various methods for controlling aspects of RD Tabs. The simplest example was creating a new tab from within the script itself. When I ran the script and got to the step that executed that function, I got the following exception.
Current thread must be set to single thread apartment (STA) mode before OLE calls can be made
That's quite strange, because VB.Net Windows Forms applications are STA by default. If you don't know what STA is, don't worry about. Just know that it's related to how COM handles multithreading. Since RD Tabs itself extensively P/Invokes things, I do make heavy use of COM interop. However, there are plenty of built-in framework classes that also wrap COM, so mismatched threading models may happen even without any P/Invokes.
Why was I getting an error that implied I was not set to STA? It turns out the problem is with PowerShell. PowerShell runs in MTA mode by default (the other threading model). When you create a new RunSpace and Pipeline, you are effectively creating a new thread for PowerShell to run on. Since it's a brand new thread, it sets it to the default that PowerShell uses, MTA.
Thankfully, The Google did not fail me. I found somebody else having a similar problem with PowerShell when creating a snap-in for MMC. The solution is to cleverly dig into the RunSpace and essentially tell PowerShell to execute scripts on the hosting application's thread. The original author used C#, so below is a translated VB.Net summary without any exception handling.
Dim rs As Runspace = Nothing
rs = RunspaceFactory.CreateRunspace
rs.Open()
' Set the default runspace to the one we just created
Runspace.DefaultRunspace = rs
' Fetch the EngineIntrinsics from the runspace (this is needed so we run the engine on the current thread
' (which will be STA instead of MTA) which is required because VB.Net defaults to STA)
Dim ei As EngineIntrinsics = CType(rs.SessionStateProxy.GetVariable("ExecutionContext"), EngineIntrinsics)
' Create a scriptblock from the engine, supply the user code
Dim sb As ScriptBlock = ei.InvokeCommand.NewScriptBlock("Insert Your PowerShell Code Here")
' Invoke the script
sb.Invoke()
I'm glad there's a solution, but I'm also a bit perplexed that there isn't an easy property you can use to change PowerShell's default threading model. Such as rs.SetThreadingModel() or something. Oh well. Such is life!