Piping ListView items in separate runspace

烈酒焚心 提交于 2019-12-17 21:27:28

问题


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()
}


来源:https://stackoverflow.com/questions/34451759/piping-listview-items-in-separate-runspace

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!