Skip to content

Process.OutputReadNotifyUser does not respect the synchronization context when invoking DataReceivedEventHandler #2024

Closed
@daxian-dbw

Description

@daxian-dbw

Summary

This was reported in PowerShell and turned out to be a .NET Core issue: PowerShell/PowerShell#11658

In .NET Framework, Process.OutputReadNotifyUser respects the synchronization context and depends on SynchronizingObject to invoke the handler on the desired thread.

However, in .NET Core 3.1, Process.OutputReadNotifyUser doesn't respect the SynchronizingObject anymore. The callback will eventually be invoked in AsyncStreamReader.ReadBufferAsync, after await _stream.ReadAsync(...).ConfigureAwait(false). Be noted about the ConfigureAwait(false) part, it means everything after this will be continued on a thread pool thread, and thus the DataReceivedEventHandler will always be invoked on a thread pool thread instead.

Reproduce steps

I don't have a simpler repro handy, and below is the original repro from the PowerShell issue.
Paste below code into Windows PowerShell 5.1 and pwsh 7.0.0-rc.2, then click Run button on the form window.

[reflection.assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
[reflection.assembly]::LoadWithPartialName("System.Drawing") | Out-Null

[System.Windows.Forms.Application]::EnableVisualStyles()
$form = New-Object 'System.Windows.Forms.Form'
$buttonRunProcess = New-Object 'System.Windows.Forms.Button'
$richtextboxOutput = New-Object 'System.Windows.Forms.RichTextBox'
$buttonExit = New-Object 'System.Windows.Forms.Button'
$InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'

$buttonExit_Click={
    $form.Close()
}

$buttonRunProcess_Click = {
    $buttonRunProcess.Enabled = $false

    $richtextboxOutput.Clear()

    $process = New-Object System.Diagnostics.Process
    $process.StartInfo.FileName = 'ping.exe'
    $process.StartInfo.Arguments = 'google.com'
    $process.StartInfo.UseShellExecute = $false
    $process.StartInfo.CreateNoWindow = $true
    $process.StartInfo.RedirectStandardInput = $false
    $process.StartInfo.RedirectStandardOutput = $true
    $process.EnableRaisingEvents = $true
    $process.SynchronizingObject = $buttonRunProcess
    $process.add_OutputDataReceived( {
        # Use $_.Data to access the output text
        $richtextboxOutput.AppendText($_.Data)
        $richtextboxOutput.AppendText("`r`n") }
)

    $process.Start() | Out-Null
    $process.BeginOutputReadLine()
}


$Form_StateCorrection_Load=
{
    #Correct the initial state of the form to prevent the .Net maximized form issue
    $form.WindowState = $InitialFormWindowState
}

$Form_Cleanup_FormClosed=
{
    try
    {
        $buttonRunProcess.remove_Click($buttonRunProcess_Click)
        $buttonExit.remove_Click($buttonExit_Click)
        $form.remove_FormClosed($processTracker_FormClosed)
        $form.remove_Load($Form_StateCorrection_Load)
        $form.remove_FormClosed($Form_Cleanup_FormClosed)
    }
    catch { Out-Null <# Prevent PSScriptAnalyzer warning #> }
}

$form.SuspendLayout()

$form.Controls.Add($buttonRunProcess)
$form.Controls.Add($richtextboxOutput)
$form.Controls.Add($buttonExit)
$form.ClientSize = [System.Drawing.Size]::new(584, 362)
$form.Margin = '4, 4, 4, 4'
$form.MinimumSize = [System.Drawing.Size]::new(304, 315)
$form.Name = 'Redirect Process Output'
$form.StartPosition = 'CenterScreen'
$form.Text = 'Redirect Process Output'

$buttonRunProcess.Anchor = 'Bottom, Left'
$buttonRunProcess.Location = [System.Drawing.Point]::new(12, 327)
$buttonRunProcess.Name = 'buttonRunProcess'
$buttonRunProcess.Size = [System.Drawing.Size]::new(75, 23)
$buttonRunProcess.TabIndex = 0
$buttonRunProcess.Text = 'Run'
$buttonRunProcess.UseCompatibleTextRendering = $True
$buttonRunProcess.UseVisualStyleBackColor = $True
$buttonRunProcess.add_Click($buttonRunProcess_Click)

$richtextboxOutput.Anchor = 'Top, Bottom, Left, Right'
$richtextboxOutput.HideSelection = $False
$richtextboxOutput.Location = [System.Drawing.Point]::new(12, 12)
$richtextboxOutput.Name = 'richtextboxOutput'
$richtextboxOutput.ReadOnly = $True
$richtextboxOutput.Size = [System.Drawing.Size]::new(559, 305)
$richtextboxOutput.TabIndex = 6
$richtextboxOutput.Text = ''
$richtextboxOutput.WordWrap = $False

$buttonExit.Anchor = 'Bottom, Right'
$buttonExit.Location = [System.Drawing.Point]::new(497, 327)
$buttonExit.Name = 'buttonExit'
$buttonExit.Size =  [System.Drawing.Size]::new(75, 23)
$buttonExit.TabIndex = 2
$buttonExit.Text = 'E&xit'
$buttonExit.UseCompatibleTextRendering = $True
$buttonExit.UseVisualStyleBackColor = $True
$buttonExit.add_Click($buttonExit_Click)

$form.ResumeLayout()

$InitialFormWindowState = $form.WindowState
$form.add_Load($Form_StateCorrection_Load)
$form.add_FormClosed($Form_Cleanup_FormClosed)
$form.ShowDialog()

Expected behavior

After clicking the Run button the output from ping google.com is displayed inside the text box on both Windows PowerShell and pwsh 7.

Actual behavior

It works fine on Windows PowerShell.

However on pwsh 7, no output is displayed and pwsh crashes with the following exception.

Unhandled exception. System.Management.Automation.PSInvalidOperationException: There is no Runspace available to run scripts in this thread. You can provide one in the DefaultRunspace property of the System.Management.Automation.Runspaces.Runspace type. The script block you attempted to invoke was:
        # Use $_.AppendText("`r`n")
   at System.Management.Automation.ScriptBlock.GetContextFromTLS()
   at System.Management.Automation.ScriptBlock.InvokeAsDelegateHelper(Object dollarUnder, Object dollarThis, Object[] args)
   at lambda_method(Closure , Object , DataReceivedEventArgs )
   at System.Diagnostics.Process.OutputReadNotifyUser(String data)
   at System.Diagnostics.AsyncStreamReader.FlushMessageQueue(Boolean rethrowInNewThread)
--- End of stack trace from previous location where exception was thrown ---
   at System.Diagnostics.AsyncStreamReader.<>c.<FlushMessageQueue>b__18_0(Object edi)
   at System.Threading.QueueUserWorkItemCallback.<>c.<.cctor>b__6_0(QueueUserWorkItemCallback quwi)
   at System.Threading.ExecutionContext.RunForThreadPoolUnsafe[TState](ExecutionContext executionContext, Action`1 callback, TState& state)
   at System.Threading.QueueUserWorkItemCallback.Execute()
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()

This is because the event handler to OutputDataReceived is a delegate created from a PowerShell script block, and it requires a default Runspace from the current thread to execute.
On Windows PowerShell, the handler is invoked on the right thread thanks to the sync context.
On .NET Core 3.1, it's invoked on a thread pool thread, which doesn't have a default Runspace and hence the exception.

$process.add_OutputDataReceived( {
        # Use $_.Data to access the output text
        $richtextboxOutput.AppendText($_.Data)
        $richtextboxOutput.AppendText("`r`n") }
)

Environment data

Windows PowerShell 5.1 runs on .NET Framework, and pwsh 7.0.0-rc.2 runs on .NET Core 3.1

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions