Piping ListView items in separate runspace

2019-01-29 09:19发布

问题:

I have a form written in PowerShell (as it uses PS to run commands over hundreds of servers) but this bit had me stumped for awhile:

$listview.Invoke([System.Action[String]]{
    Param
    (
        [String]$data
    )
    write-warning "1"
    $LVitem = ($LVresults.Items | Where { $_.Name -eq "Test Account" })
    write-warning "2"
    $LVitem.Tag.Output += $data + "`n"
}, "Testing")

It's in a separate runspace that runs an Invoke-Command to a specific server and pipes the output to this code block.

Now if I run the Where statement in the main runspace (where the form is created) it works perfectly fine. In the separate runspace however it locks up the form. Warning 1 is displayed, Warning 2 is not.

Piping to a Foreach statement has the same problem, but I can use:

Foreach ($item in $listview.Items) { 
    if ($item.Name -eq "Test Account") { $LVitem = $item }
}

Can anyone explain this? I'm not doing anything fancy with the ListView or its items, it just seems the ListView doesn't like its items being piped in another runspace.

回答1:

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:

  1. Script block passed as delegate to $listview.Invoke method.
  2. Script block passed to Where-Object cmdlet.

As I see, your code have three possible outcomes:

  1. 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()
    }
    
  2. 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()
    }
    
  3. 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()
}