Saturday, February 25, 2017

Powershell Transcript cmdlets and secondary runspaces gotcha

So, I had some fun this past week, with a piece of code that, stripped to its essentials, looked like


which yields

Stop-Transcript : An error occurred stopping transcription: The host is not currently transcribing.
At line:1 char:1
+ Stop-Transcript
+ ~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [Stop-Transcript], PSInvalidOperationException
    + FullyQualifiedErrorId : InvalidOperation,Microsoft.PowerShell.Commands.StopTranscriptCommand

with a transcript (again, stripped to its essentials) of

Transcript started, output file is ...
PS>$pool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(1,1)
PS>$pool.Open()
PS># do stuff...
PS>$pool.Close()
**********************
Windows PowerShell transcript end

with nothing after the Close() showing up.

It turns out that, however you create a runspace, even if using an instance of a custom PSHost subclass and explicitly minting one through CreateRunspace(), the runspace will still get attached to an internal PSHost subtype, which couples it to a whole web of other internal and/or sealed types, eventually linking it to the transcription state of the overall PowerShell session. And when a runspace closes, it closes all open transcripts attached to it.

WTF FAIL!

Fortunately, there is one public API available to us that can sever this link, and one that makes a perverse sort of sense, after you've run through all the plumbing:


which finishes with

Transcript stopped, output file is...

and a transcript that looks like

Transcript started, output file is ...

PS>$pool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(1,1)
PS>$save = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace
PS>try {
    [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace = $null
    $pool.Open()
    # do stuff...
    $pool.Close()
    # do more stuff...
}
finally {
    [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace = $save
}
PS>Stop-Transcript
**********************
Windows PowerShell transcript end

because it turns out that, via a long chain of indirections, it is the -- fortunately thread-static -- global default runspace which is the thing that contaminates our intended-to-be-isolated worker environment.

Now, it would be understandable if runspace construction were to directly use the default runspace as a prototype, but it's nothing so obvious. It actually comes in via the UI object that is tenuously attached to the runspace reaching out to the default runspace. That's not so good, and speaks of excessive internal coupling.

Checking metrics on the assembly System.Management.Automation 3.0.0.0, we can see that it is indeed highly internally coupled, with a relational cohesion of 7.22, which is a level that is not so much coherent as positively incestuous. So while I didn't spot any other obvious booby-traps waiting to be sprung, I'm sure there are others that will rise up and bite the occasional edge-case.


No comments :