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.