Skip to content

Instantly share code, notes, and snippets.

@SeeminglyScience
Created June 2, 2017 19:55
Show Gist options
  • Save SeeminglyScience/5f5741b9adc68c9b512209210f97b0fc to your computer and use it in GitHub Desktop.
Save SeeminglyScience/5f5741b9adc68c9b512209210f97b0fc to your computer and use it in GitHub Desktop.
Force PS classes to be marshaled to a specific runspace.
<#
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