Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/System.Management.Automation/engine/pipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -904,7 +904,24 @@ internal void Stop()
return;
}

_pipelineStopTokenSource.Cancel();
try
{
_pipelineStopTokenSource.Cancel();
}
catch (ObjectDisposedException)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how this would be possible without adding test only hooks that lock the state to a particular set of steps. This would be very complicated and means we start testing an implementation detail. Ultimately this is a race condition that I don't believe we can really test against.

{
// Dispose() can race with this asynchronous Stop() call. Dispose() only
// disposes the token source after the pipeline has finished and all the
// commands have been disposed, so there is nothing left to stop and we
// return early rather than call StopProcessing() on disposed commands.
// https://github.com/PowerShell/PowerShell/issues/28299
return;
}
catch (AggregateException)
{
// Swallow callback failures like StopProcessing() below, e.g. a
// ScriptBlock callback run on a thread without a Runspace.
}

// Call StopProcessing() for all the commands.
foreach (CommandProcessorBase commandProcessor in commands)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Describe 'PipelineStopToken tests' -Tags 'CI' {
)

$ps = [PowerShell]::Create()
$null = $ps.AddScript("'start'`n" + $ScriptBlock.ToString())
$null = $ps.AddScript($ScriptBlock.ToString())
foreach ($arg in $ArgumentList) {
$null = $ps.AddArgument($arg)
}
Expand All @@ -26,7 +26,7 @@ Describe 'PipelineStopToken tests' -Tags 'CI' {
$inPipe.Complete()
$outPipe = [System.Management.Automation.PSDataCollection[object]]::new()

# Use an event to make sure Stop is called once the pipeline has started
# Use an event to make sure Stop is called once the script outputs its first object
# and not before.
$eventId = [Guid]::NewGuid().ToString()
Register-ObjectEvent -InputObject $outPipe -EventName DataAdded -SourceIdentifier $eventId
Expand All @@ -53,6 +53,27 @@ Describe 'PipelineStopToken tests' -Tags 'CI' {
[CmdletBinding()]
param ([Parameter()][int]$Timeout)

'start'
[System.Threading.Tasks.Task]::Delay($Timeout * 1000, $PSCmdlet.PipelineStopToken).GetAwaiter().GetResult()
}

Test-FunctionWithStop -Timeout 10
}

$end = (Get-Date) - $start
$end.TotalSeconds | Should -BeLessThan 10
}

It 'Stops pipeline when a registered callback throws' {
$start = Get-Date

Invoke-WithStop -ScriptBlock {
Function Test-FunctionWithStop {
[CmdletBinding()]
param ([Parameter()][int]$Timeout)

$null = $PSCmdlet.PipelineStopToken.Register([Action]{ throw 'callback failure' })
'start'
[System.Threading.Tasks.Task]::Delay($Timeout * 1000, $PSCmdlet.PipelineStopToken).GetAwaiter().GetResult()
}

Expand Down Expand Up @@ -87,6 +108,7 @@ public sealed class TestCmdletWithStop : Cmdlet
$start = Get-Date
Invoke-WithStop -ScriptBlock {
Import-Module -Assembly $args[0]
'start'

Test-CmdletWithStop -Timeout 10
} -ArgumentList $binaryAssembly
Expand Down
Loading