Skip to content

Add a PipelineStopToken cancellation token property to PSCmdlet #17444

@SeeminglyScience

Description

@SeeminglyScience

Summary of the new feature / enhancement

A common pattern for binary cmdlets is to create a cancellation token source that is then cancelled in PSCmdlet.StopProcessing().

This is a very useful pattern to enable cancelling a method that takes CancellationToken without having to poll for completion. Having this available by default would reduce a lot of boilerplate in binary cmdlets. Boilerplate example:

namespace MyModule;

[Cmdlet(VerbsDiagnostic.Test, "MyCommand")]
public class TestMyCommand : PSCmdlet, IDisposable
{
    private readonly CancellationTokenSource _stopping = new();

    protected override void ProcessRecord()
    {
        try
        {
            Task.Delay(2000, _stopping.Token).GetAwaiter().GetResult();
        }
        catch (OperationCancelledException)
        {
            throw new PipelineStoppedException();
        }
    }

    protected override void StopProcessing() => _stopping.Cancel();

    public void Dispose() => _stopping.Dispose();
}

This pattern is not possible in PowerShell directly, instead a script must poll a task for completion if it wants to respect pipeline stops. Polling example:

$task = [System.Threading.Tasks.Task]::Delay(2000)
while (-not $task.AsyncWaitHandle.WaitOne(200)) { }
$null = $task.GetAwaiter().GetResult();

Even then all you can do is honor pipeline stops, you can't request cancellation of the work being done in another thread.

Proposed technical implementation details (optional)

namespace System.Management.Automation;

public abstract class PSCmdlet
{
+   private CancellationTokenSource? _pipelineStopTokenSource;

+   public CancellationToken PipelineStopToken => (_pipelineStopTokenSource ??= new()).Token;

    protected virtual void StopProcessing()
    {
        using (PSTransactionManager.GetEngineProtectionScope())
        {
+           _pipelineStopTokenSource?.Cancel();
        }
    }

    internal void InternalDispose(bool isDisposing)
    {
        _myInvocation = null;
        _state = null;
        _commandInfo = null;
        _context = null;
+       _pipelineStopTokenSource?.Dispose();
+       _pipelineStopTokenSource = null;
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    In-PRIndicates that a PR is out for the issueIssue-Enhancementthe issue is more of a feature request than a bugWG-Enginecore PowerShell engine, interpreter, and runtime

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions