This is the problem with PowerShell event system. It deadlocks when event handler generate event with wait to completion option.
Register-EngineEvent -SourceIdentifier MyEvent1 -Action {Write-Host 'Does not get called.'}
Register-EngineEvent -SourceIdentifier MyEvent2 -Action {$ExecutionContext.Events.GenerateEvent('MyEvent1',$null,$null,$null,$true,$true)}
New-Event -SourceIdentifier MyEvent2
So, what your setup have to do with events?
Script blocks literals remembers current session state (including Runspace
) at creation time. And if at script block invocation time current Runspace
for current thread ([Runspace]::DefaultRunspace
) is different or busy processing something in different thread, then script block invoked by generating event to original Runspace
with wait to completion option.
Another interesting thing is that: if target Runspace
does not start processing events in 250 milliseconds, then PowerShell start event processing in the thread waiting for event completion.
In your code you have two nested script block invokations:
- Script block passed as delegate to
$listview.Invoke
method.
- Script block passed to
Where-Object
cmdlet.
As I see, your code have three possible outcomes:
Script block's original Runspace
is free, so script block executed in different thread (not UI thread).
$PowerShell=[PowerShell]::Create()
$PowerShell.Runspace=[RunspaceFactory]::CreateRunspace($Host)
$PowerShell.Runspace.Open()
$Stopwatch=[Diagnostics.Stopwatch]::new()
$PowerShell.Runspace.SessionStateProxy.PSVariable.Set('Stopwatch',$Stopwatch)
$ScriptBlock=$PowerShell.AddScript{{
Write-Host "$($Stopwatch.ElapsedMilliseconds): $([Threading.Thread]::CurrentThread.ManagedThreadId)"
{Write-Host OK}.Invoke()
}}.Invoke()
$PowerShell.Commands.Clear()
& {
$Stopwatch.Start()
Write-Host "$($Stopwatch.ElapsedMilliseconds): $([Threading.Thread]::CurrentThread.ManagedThreadId)"
$ScriptBlock.Invoke()
}
Script block's original Runspace
is busy, so script block executed in current thread and deadlocks on nested script block invocation.
$PowerShell=[PowerShell]::Create()
$PowerShell.Runspace=[RunspaceFactory]::CreateRunspace($Host)
$PowerShell.Runspace.Open()
$Stopwatch=[Diagnostics.Stopwatch]::new()
$PowerShell.Runspace.SessionStateProxy.PSVariable.Set('Stopwatch',$Stopwatch)
$ScriptBlock=$PowerShell.AddScript{{
Write-Host "$($Stopwatch.ElapsedMilliseconds): $([Threading.Thread]::CurrentThread.ManagedThreadId)"
{Write-Host Deadlock}.Invoke()
}}.Invoke()
$PowerShell.Commands.Clear()
& {
$AsyncResult=$PowerShell.AddScript{Start-Sleep 10}.BeginInvoke()
$Stopwatch.Start()
Write-Host "$($Stopwatch.ElapsedMilliseconds): $([Threading.Thread]::CurrentThread.ManagedThreadId)"
$ScriptBlock.Invoke()
}
Script block's original Runspace
initially busy, but get free before nested script block invocation, so script block executed in current thread and does not deadlocks.
$PowerShell=[PowerShell]::Create()
$PowerShell.Runspace=[RunspaceFactory]::CreateRunspace($Host)
$PowerShell.Runspace.Open()
$Stopwatch=[Diagnostics.Stopwatch]::new()
$PowerShell.Runspace.SessionStateProxy.PSVariable.Set('Stopwatch',$Stopwatch)
$ScriptBlock=$PowerShell.AddScript{{
Write-Host "$($Stopwatch.ElapsedMilliseconds): $([Threading.Thread]::CurrentThread.ManagedThreadId)"
Start-Sleep 10
{Write-Host OK}.Invoke()
}}.Invoke()
$PowerShell.Commands.Clear()
& {
$AsyncResult=$PowerShell.AddScript{Start-Sleep 5}.BeginInvoke()
$Stopwatch.Start()
Write-Host "$($Stopwatch.ElapsedMilliseconds): $([Threading.Thread]::CurrentThread.ManagedThreadId)"
$ScriptBlock.Invoke()
}
To workaround this issue you need to detach script block from its original Runspace
. You can achieve that by creating new script block from string using [ScriptBlock]::Create
method.
$PowerShell=[PowerShell]::Create()
$PowerShell.Runspace=[RunspaceFactory]::CreateRunspace($Host)
$PowerShell.Runspace.Open()
$Stopwatch=[Diagnostics.Stopwatch]::new()
$PowerShell.Runspace.SessionStateProxy.PSVariable.Set('Stopwatch',$Stopwatch)
$ScriptBlock=$PowerShell.AddScript{[ScriptBlock]::Create{
Write-Host "$($Stopwatch.ElapsedMilliseconds): $([Threading.Thread]::CurrentThread.ManagedThreadId)"
{Write-Host OK}.Invoke()
}}.Invoke()
$PowerShell.Commands.Clear()
& {
$AsyncResult=$PowerShell.AddScript{Start-Sleep 10}.BeginInvoke()
$Stopwatch.Start()
Write-Host "$($Stopwatch.ElapsedMilliseconds): $([Threading.Thread]::CurrentThread.ManagedThreadId)"
$ScriptBlock.Invoke()
}