Bug: InvocationStateChanged never fires Completed after BeginInvoke() on custom runspace
Summary
When a PowerShell instance is run via BeginInvoke() on a custom runspace created with
RunspaceFactory::CreateRunspace(), the InvocationStateChanged event fires once with state
Running when execution begins but never fires with Completed, Failed, or Stopped
after the script finishes — even for a trivially short script that exits cleanly.
Environment
- PowerShell version: 7.6.0 (Core)
- OS: Microsoft Windows 10.0.19045 (Win32NT)
- Repro mode: Interactive
pwsh.exe session (no WPF, no additional threads)
Steps to reproduce
Both variants below produce the same result. ThreadOptions does not affect the behaviour.
Variant A (with ReuseThread):
$Rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$Rs.ApartmentState = 'MTA'
$Rs.ThreadOptions = 'ReuseThread'
$Rs.Open()
$Ps = [System.Management.Automation.PowerShell]::Create()
$Ps.Runspace = $Rs
$Ps.add_InvocationStateChanged({
param($s, $e)
Write-Host "State: $($e.InvocationStateInfo.State)"
})
$null = $Ps.AddScript('1..3 | ForEach-Object { Start-Sleep -Milliseconds 100 }; "done"')
$null = $Ps.BeginInvoke()
Start-Sleep -Seconds 2
Write-Host "Final PS state: $($Ps.InvocationStateInfo.State)"
Variant B (without ThreadOptions — default NewThread):
$Rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$Rs.ApartmentState = 'MTA'
$Rs.Open()
$Ps = [System.Management.Automation.PowerShell]::Create()
$Ps.Runspace = $Rs
$Ps.add_InvocationStateChanged({
param($s, $e)
Write-Host "State: $($e.InvocationStateInfo.State)"
})
$null = $Ps.AddScript('1..3 | ForEach-Object { Start-Sleep -Milliseconds 100 }; "done"')
$null = $Ps.BeginInvoke()
Start-Sleep -Seconds 2
Write-Host "Final PS state: $($Ps.InvocationStateInfo.State)"
Actual output (both variants)
State: Running
Final PS state: Running
Expected output
State: Running
State: Completed
Final PS state: Completed
The InvocationStateChanged event should fire a second time with state Completed once the
script finishes executing. It does not. Polling $Ps.InvocationStateInfo.State directly after
the script has had time to run also returns Running permanently. The behaviour is identical
regardless of ThreadOptions.
Impact
Any code that relies on InvocationStateChanged to detect script completion — for example,
re-enabling UI controls or disposing resources after a background task finishes — will never
receive the completion notification. The PowerShell instance and its Runspace cannot be
safely disposed, and there is no reliable built-in mechanism to know when execution has ended.
This pattern is commonly used in WPF/WinForms applications to run background PowerShell work
off the UI thread. The broken event makes that pattern unusable without a workaround.
Workaround
Have the script signal its own completion through an out-of-band mechanism — for example,
enqueueing a sentinel value into a ConcurrentQueue that is polled by a timer on the UI thread:
$Q = [System.Collections.Concurrent.ConcurrentQueue[String]]::new()
$Rs.SessionStateProxy.SetVariable('_Q', $Q)
$null = $Ps.AddScript('
1..3 | ForEach-Object { $env:COMPUTERNAME | Out-Null }
$_Q.Enqueue("--- Done ---")
')
$null = $Ps.BeginInvoke()
# Poll until done (or use a DispatcherTimer / background thread)
while ($true) {
$item = ''
if ($Q.TryDequeue([ref]$item) -and $item -eq '--- Done ---') { break }
Start-Sleep -Milliseconds 100
}
Write-Host "Script finished (via workaround)"
This is an unreasonable burden on callers. The event should work.
Additional notes
$Ps.InvocationStateInfo.State never transitions from Running even after the script
completes and the background thread has exited.
- The issue reproduces with both
ReuseThread and the default NewThread — ThreadOptions
is not a factor.
- The issue was first noticed in a WPF GUI application running on PS 7.6.0 under Windows 10,
but the minimal repro above confirms it is not WPF-specific — it reproduces in a plain
interactive pwsh.exe session with no UI framework involved.
- Using a
RunspacePool instead of a single custom Runspace has not been tested and may
or may not exhibit the same behaviour.
UseCurrentThread has not been tested.
Bug: InvocationStateChanged never fires
Completedafter BeginInvoke() on custom runspaceSummary
When a
PowerShellinstance is run viaBeginInvoke()on a custom runspace created withRunspaceFactory::CreateRunspace(), theInvocationStateChangedevent fires once with stateRunningwhen execution begins but never fires withCompleted,Failed, orStoppedafter the script finishes — even for a trivially short script that exits cleanly.
Environment
pwsh.exesession (no WPF, no additional threads)Steps to reproduce
Both variants below produce the same result.
ThreadOptionsdoes not affect the behaviour.Variant A (with
ReuseThread):Variant B (without
ThreadOptions— defaultNewThread):Actual output (both variants)
Expected output
The
InvocationStateChangedevent should fire a second time with stateCompletedonce thescript finishes executing. It does not. Polling
$Ps.InvocationStateInfo.Statedirectly afterthe script has had time to run also returns
Runningpermanently. The behaviour is identicalregardless of
ThreadOptions.Impact
Any code that relies on
InvocationStateChangedto detect script completion — for example,re-enabling UI controls or disposing resources after a background task finishes — will never
receive the completion notification. The
PowerShellinstance and itsRunspacecannot besafely disposed, and there is no reliable built-in mechanism to know when execution has ended.
This pattern is commonly used in WPF/WinForms applications to run background PowerShell work
off the UI thread. The broken event makes that pattern unusable without a workaround.
Workaround
Have the script signal its own completion through an out-of-band mechanism — for example,
enqueueing a sentinel value into a
ConcurrentQueuethat is polled by a timer on the UI thread:This is an unreasonable burden on callers. The event should work.
Additional notes
$Ps.InvocationStateInfo.Statenever transitions fromRunningeven after the scriptcompletes and the background thread has exited.
ReuseThreadand the defaultNewThread—ThreadOptionsis not a factor.
but the minimal repro above confirms it is not WPF-specific — it reproduces in a plain
interactive
pwsh.exesession with no UI framework involved.RunspacePoolinstead of a single customRunspacehas not been tested and mayor may not exhibit the same behaviour.
UseCurrentThreadhas not been tested.