Created
June 2, 2017 19:55
-
-
Save SeeminglyScience/5f5741b9adc68c9b512209210f97b0fc to your computer and use it in GitHub Desktop.
Force PS classes to be marshaled to a specific runspace.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
Simple script that forces all static methods of types made | |
with PowerShell to be bound to the current runspace. If you | |
are getting NullReference exceptions every now and then, this | |
might fix it. | |
I put this in my profile as a temporary workaround, I do not | |
recommend adding this to a module. It uses a large amount of | |
reflection and could be unpredictable. | |
#> | |
function Start-BoundRunspaceFixer { | |
[CmdletBinding()] | |
[OutputType([System.IAsyncResult])] | |
param() | |
end { | |
$script = ' | |
[CmdletBinding()] | |
param([Parameter(Mandatory)][runspace]$Runspace) | |
end {{ | |
function Set-BoundRunspace {{ | |
{0} | |
}} | |
while ($true) {{ | |
Set-BoundRunspace -Runspace $Runspace | |
Start-Sleep -Seconds 5 | |
}} | |
}} | |
' -f (Get-Command Set-BoundRunspace).Definition | |
$ps = [powershell]:: | |
Create('NewRunspace'). | |
AddScript($script, $false). | |
AddParameter('Runspace', [runspace]::DefaultRunspace) | |
[PSCustomObject]@{ | |
PowerShell = $ps | |
Runspace = $ps.Runspace | |
Handle = $ps.BeginInvoke() | |
} | |
} | |
} | |
function Set-BoundRunspace { | |
[CmdletBinding()] | |
param( | |
[runspace] | |
$Runspace = [runspace]::DefaultRunspace | |
) | |
end { | |
# Make the truck loads of reflection somewhat readable. | |
function GetProperty { | |
[CmdletBinding(PositionalBinding=$false)] | |
param( | |
[Parameter(Position=0)][string]$Name, | |
[Parameter(Position=1)][object]$Set, | |
[Parameter(Mandatory, ValueFromPipeline)][ValidateNotNull()][object]$Source | |
) | |
process { | |
$propertyInfo = $Source.GetType().GetProperty($Name, 61) | |
if (-not $Set) { return $propertyInfo.GetValue($Source) } | |
$null = $propertyInfo.SetValue($Source, $Set) | |
} | |
} | |
# All PS classes have a TypeName_<staticHelpers> non public class associated with them. It | |
# contains the SessionStateKeeper, and the script blocks for static and instance members. | |
$types = [AppDomain]::CurrentDomain.GetAssemblies(). | |
Where{ $_.IsDynamic }. | |
ForEach{ $_.GetTypes() }. | |
Where{ $_.Name.Contains('<staticHelpers>') } | |
$context = $Runspace | GetProperty ExecutionContext | |
$engineStateInternal = $context | GetProperty EngineSessionState | |
$engineState = $engineStateInternal | GetProperty PublicSessionState | |
foreach ($type in $types) { | |
# Static method fields are in <MemberName_X_X> format where X is some number. \.? checks | |
# for the . in .ctor. | |
$methods = $type.GetFields(60).Where{ $_.Name -match '<\.?\w+_\d+_\d+>' } | |
foreach ($method in $methods) { | |
# The wrapper is the ScriptBlockMemberMethodWrapper. | |
$wrapper = $method.GetValue($type) | |
# _scriptBlock contains the sb for a static method. _boundScriptBlock is for instances, | |
# which aren't affected. | |
$sb = $wrapper.GetType().GetField('_scriptBlock', 60).GetValue($wrapper).Value | |
if ($null -eq $sb) { | |
$PSCmdlet.WriteVerbose('No ScriptBlock found for {0}.' -f $method.Name) | |
continue | |
} | |
# Get info about the bound scriptblock | |
$internal = $sb | GetProperty SessionStateInternal | |
$sbContext = $internal | GetProperty ExecutionContext | |
$boundRunspace = $sbContext | GetProperty CurrentRunspace | |
# Stops here unless we need to fix. | |
if ($boundRunspace.Name -ne $Runspace.Name) { | |
$PSCmdlet.WriteVerbose(('Found method {0} with incorrect context (Runspace: {1})' -f $method.Name, $boundRunspace.Name)) | |
# If in a module, we need to get the SSI from that module instead of execution context. | |
$module = $internal | GetProperty Module | |
if ($module) { | |
# Can't use Get-Module because different runspace. | |
$moduleTable = $context | GetProperty Modules | GetProperty ModuleTable | |
$localModule = $moduleTable.($module.Path) | |
$sb | GetProperty SessionStateInternal -Set ($localModule.SessionState | GetProperty Internal) | |
$sb | GetProperty SessionState -Set $localModule.SessionState | |
$PSCmdlet.WriteVerbose('Set SSI to module {0}.' -f $module.Name) | |
continue | |
} | |
# Not a module so we can use default context. | |
$sb | GetProperty SessionStateInternal -Set $engineStateInternal | |
$sb | GetProperty SessionState -Set $engineState | |
$PSCmdlet.WriteVerbose('Set SSI to local context.') | |
continue | |
} | |
$PSCmdlet.WriteVerbose(('Method {0} appears to have the correct runspace ({1}).' -f $method.Name, $boundRunspace.Name)) | |
} | |
} | |
} | |
} | |
$global:RunspaceBinderInfo = Start-BoundRunspaceFixer |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment