From 030cfd80f214a15759696fc5c68a814f30f9dabf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Nov 2026 17:41:25 +0000 Subject: [PATCH 1/3] Initial plan From 571c90a02474df8a4057bc391a42857c9184b340 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Nov 2026 17:53:09 +0000 Subject: [PATCH 2/3] Prevent StreamingHost messages from feeding back through Receive-Job Co-authored-by: SteveL-MSFT <11859881+SteveL-MSFT@users.noreply.github.com> --- help/Start-ThreadJob.md | 3 + src/code/Microsoft.PowerShell.ThreadJob.cs | 38 ++++++------- test/Microsoft.PowerShell.ThreadJob.Tests.ps1 | 55 +++++++++++++++++++ 3 files changed, 75 insertions(+), 21 deletions(-) diff --git a/help/Start-ThreadJob.md b/help/Start-ThreadJob.md index 41bcfde..e437cc4 100644 --- a/help/Start-ThreadJob.md +++ b/help/Start-ThreadJob.md @@ -171,6 +171,9 @@ Accept wildcard characters: False ### -StreamingHost Optional host object to which job output can directed. +Progress, verbose, warning, debug, and information messages are streamed to this host +instead of being buffered for `Receive-Job`. Success output and errors remain buffered +in the job. ```yaml Type: PSHost diff --git a/src/code/Microsoft.PowerShell.ThreadJob.cs b/src/code/Microsoft.PowerShell.ThreadJob.cs index c967e9d..882bb17 100644 --- a/src/code/Microsoft.PowerShell.ThreadJob.cs +++ b/src/code/Microsoft.PowerShell.ThreadJob.cs @@ -648,14 +648,7 @@ public ThreadJob( iss.LanguageMode = enforceLockdown ? PSLanguageMode.ConstrainedLanguage : PSLanguageMode.FullLanguage; } - if (_streamingHost != null) - { - _rs = RunspaceFactory.CreateRunspace(_streamingHost, iss); - } - else - { - _rs = RunspaceFactory.CreateRunspace(iss); - } + _rs = RunspaceFactory.CreateRunspace(iss); _ps = System.Management.Automation.PowerShell.Create(); _ps.Runspace = _rs; _ps.InvocationStateChanged += (sender, psStateChanged) => @@ -708,25 +701,26 @@ public ThreadJob( this.Error = _ps.Streams.Error; this.Error.EnumeratorNeverBlocks = true; - this.Progress = _ps.Streams.Progress; - this.Progress.EnumeratorNeverBlocks = true; + // Host-streamed records must not be replayed by Receive-Job. + if (_streamingHost == null) + { + this.Progress = _ps.Streams.Progress; + this.Verbose = _ps.Streams.Verbose; + this.Warning = _ps.Streams.Warning; + this.Debug = _ps.Streams.Debug; + this.Information = _ps.Streams.Information; + } - this.Verbose = _ps.Streams.Verbose; + this.Progress.EnumeratorNeverBlocks = true; this.Verbose.EnumeratorNeverBlocks = true; - - this.Warning = _ps.Streams.Warning; this.Warning.EnumeratorNeverBlocks = true; + this.Debug.EnumeratorNeverBlocks = true; + this.Information.EnumeratorNeverBlocks = true; if (lockdownWarning != null) { this.Warning.Add(lockdownWarning); } - this.Debug = _ps.Streams.Debug; - this.Debug.EnumeratorNeverBlocks = true; - - this.Information = _ps.Streams.Information; - this.Information.EnumeratorNeverBlocks = true; - // Create the JobManager job definition and job specification, and add to the JobManager. ThreadJobDefinition = new JobDefinition(typeof(ThreadJobSourceAdapter), "", Name); Dictionary parameterCollection = new Dictionary(); @@ -770,7 +764,8 @@ public override void StartJob() _ps.Commands.Clear(); _ps.AddScript(_initSb.ToString()); _runningInitScript = true; - _ps.BeginInvoke(_input, _output); + var settings = _streamingHost == null ? null : new PSInvocationSettings { Host = _streamingHost }; + _ps.BeginInvoke(_input, _output, settings, null, null); } else { @@ -1078,7 +1073,8 @@ private void RunScript() _ps.AddParameter(VERBATIM_ARGUMENT, _usingValuesMap); } - _ps.BeginInvoke(_input, _output); + var settings = _streamingHost == null ? null : new PSInvocationSettings { Host = _streamingHost }; + _ps.BeginInvoke(_input, _output, settings, null, null); } private ScriptBlock GetScriptBlockFromFile(string filePath, PSCmdlet psCmdlet) diff --git a/test/Microsoft.PowerShell.ThreadJob.Tests.ps1 b/test/Microsoft.PowerShell.ThreadJob.Tests.ps1 index 1b58596..05e6c71 100644 --- a/test/Microsoft.PowerShell.ThreadJob.Tests.ps1 +++ b/test/Microsoft.PowerShell.ThreadJob.Tests.ps1 @@ -23,6 +23,61 @@ function Wait-ForJobRunning } } +Describe 'ThreadJob StreamingHost Tests' -Tags 'CI' { + + It 'Streams output once while Receive-Job waits (initialization: )' -TestCases @( + foreach ($initialization in $false, $true) + { + @{ Stream = 'Verbose'; Prefix = 'VERBOSE: '; Command = 'Write-Verbose "StreamingHostMessage" -Verbose'; Initialization = $initialization } + @{ Stream = 'Debug'; Prefix = 'DEBUG: '; Command = 'Write-Debug "StreamingHostMessage" -Debug'; Initialization = $initialization } + @{ Stream = 'Warning'; Prefix = 'WARNING: '; Command = 'Write-Warning "StreamingHostMessage"'; Initialization = $initialization } + @{ Stream = 'Information'; Prefix = ''; Command = 'Write-Information "StreamingHostMessage" -InformationAction Continue'; Initialization = $initialization } + @{ Stream = 'Host'; Prefix = ''; Command = 'Write-Host "StreamingHostMessage"'; Initialization = $initialization } + } + ) { + param ($Stream, $Prefix, $Command, $Initialization) + + $modulePath = (Get-Command Start-ThreadJob).Module.Path.Replace("'", "''") + if ($Initialization) + { + $scriptParameters = "-InitializationScript { $Command; Start-Sleep -Milliseconds 300 } -ScriptBlock { 'SuccessOutput' }" + } + else + { + $scriptParameters = "-ScriptBlock { $Command; Start-Sleep -Milliseconds 300; 'SuccessOutput' }" + } + + $script = @" +if (`$null -ne `$PSStyle) { `$PSStyle.OutputRendering = 'PlainText' } +Import-Module '$modulePath' +Start-ThreadJob -StreamingHost `$Host $scriptParameters | Receive-Job -Wait -Verbose -Debug -InformationAction Continue +"@ + + $results = & (Get-Process -Id $PID).Path -NoProfile -NonInteractive -Command $script + $LASTEXITCODE | Should -Be 0 + @($results | Where-Object { $_ -eq 'SuccessOutput' }) | Should -HaveCount 1 + @($results | Where-Object { $_ -eq "${Prefix}StreamingHostMessage" }) | + Should -HaveCount 1 + } + + It 'Retains success output and errors when using StreamingHost' { + $job = Start-ThreadJob -StreamingHost $Host -ScriptBlock { + 'SuccessOutput' + Write-Error 'ErrorOutput' + } | Wait-Job + + try + { + $job.Output | Should -Be 'SuccessOutput' + $job.Error | Should -Be 'ErrorOutput' + } + finally + { + $job | Remove-Job -Force + } + } +} + # Basic thread job tests Describe 'Basic ThreadJob Tests' -Tags 'CI' { From fb98b3a37aa4ecd533a7d0ce89de6b8525dc1427 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Nov 2026 17:56:47 +0000 Subject: [PATCH 3/3] Preserve information records when streaming job diagnostics Co-authored-by: SteveL-MSFT <11859881+SteveL-MSFT@users.noreply.github.com> --- help/Start-ThreadJob.md | 6 +++--- src/code/Microsoft.PowerShell.ThreadJob.cs | 2 +- test/Microsoft.PowerShell.ThreadJob.Tests.ps1 | 16 ++++++++++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/help/Start-ThreadJob.md b/help/Start-ThreadJob.md index e437cc4..642d570 100644 --- a/help/Start-ThreadJob.md +++ b/help/Start-ThreadJob.md @@ -171,9 +171,9 @@ Accept wildcard characters: False ### -StreamingHost Optional host object to which job output can directed. -Progress, verbose, warning, debug, and information messages are streamed to this host -instead of being buffered for `Receive-Job`. Success output and errors remain buffered -in the job. +Progress, verbose, warning, and debug messages are streamed to this host instead of +being buffered for `Receive-Job`. Success output, errors, and information records +remain buffered in the job. ```yaml Type: PSHost diff --git a/src/code/Microsoft.PowerShell.ThreadJob.cs b/src/code/Microsoft.PowerShell.ThreadJob.cs index 882bb17..8b7b479 100644 --- a/src/code/Microsoft.PowerShell.ThreadJob.cs +++ b/src/code/Microsoft.PowerShell.ThreadJob.cs @@ -708,9 +708,9 @@ public ThreadJob( this.Verbose = _ps.Streams.Verbose; this.Warning = _ps.Streams.Warning; this.Debug = _ps.Streams.Debug; - this.Information = _ps.Streams.Information; } + this.Information = _ps.Streams.Information; this.Progress.EnumeratorNeverBlocks = true; this.Verbose.EnumeratorNeverBlocks = true; this.Warning.EnumeratorNeverBlocks = true; diff --git a/test/Microsoft.PowerShell.ThreadJob.Tests.ps1 b/test/Microsoft.PowerShell.ThreadJob.Tests.ps1 index 05e6c71..c34e2ba 100644 --- a/test/Microsoft.PowerShell.ThreadJob.Tests.ps1 +++ b/test/Microsoft.PowerShell.ThreadJob.Tests.ps1 @@ -31,12 +31,16 @@ Describe 'ThreadJob StreamingHost Tests' -Tags 'CI' { @{ Stream = 'Verbose'; Prefix = 'VERBOSE: '; Command = 'Write-Verbose "StreamingHostMessage" -Verbose'; Initialization = $initialization } @{ Stream = 'Debug'; Prefix = 'DEBUG: '; Command = 'Write-Debug "StreamingHostMessage" -Debug'; Initialization = $initialization } @{ Stream = 'Warning'; Prefix = 'WARNING: '; Command = 'Write-Warning "StreamingHostMessage"'; Initialization = $initialization } - @{ Stream = 'Information'; Prefix = ''; Command = 'Write-Information "StreamingHostMessage" -InformationAction Continue'; Initialization = $initialization } - @{ Stream = 'Host'; Prefix = ''; Command = 'Write-Host "StreamingHostMessage"'; Initialization = $initialization } } ) { param ($Stream, $Prefix, $Command, $Initialization) + if ($Stream -eq 'Debug' -and $PSVersionTable.PSVersion.Major -eq 5) + { + # Windows PowerShell's -Debug parameter prompts instead of continuing. + $Command = '$DebugPreference = "Continue"; Write-Debug "StreamingHostMessage"' + } + $modulePath = (Get-Command Start-ThreadJob).Module.Path.Replace("'", "''") if ($Initialization) { @@ -50,7 +54,7 @@ Describe 'ThreadJob StreamingHost Tests' -Tags 'CI' { $script = @" if (`$null -ne `$PSStyle) { `$PSStyle.OutputRendering = 'PlainText' } Import-Module '$modulePath' -Start-ThreadJob -StreamingHost `$Host $scriptParameters | Receive-Job -Wait -Verbose -Debug -InformationAction Continue +Start-ThreadJob -StreamingHost `$Host $scriptParameters | Receive-Job -Wait -Verbose -Debug "@ $results = & (Get-Process -Id $PID).Path -NoProfile -NonInteractive -Command $script @@ -60,16 +64,20 @@ Start-ThreadJob -StreamingHost `$Host $scriptParameters | Receive-Job -Wait -Ver Should -HaveCount 1 } - It 'Retains success output and errors when using StreamingHost' { + It 'Retains success output, errors, and silent information when using StreamingHost' { $job = Start-ThreadJob -StreamingHost $Host -ScriptBlock { 'SuccessOutput' Write-Error 'ErrorOutput' + Write-Information 'InformationOutput' -Tags 'TestTag' } | Wait-Job try { $job.Output | Should -Be 'SuccessOutput' $job.Error | Should -Be 'ErrorOutput' + $job.Information | Should -HaveCount 1 + $job.Information[0].MessageData | Should -Be 'InformationOutput' + $job.Information[0].Tags | Should -Contain 'TestTag' } finally {