Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Add PipelineStopToken to PSCmdlet#24620

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
daxian-dbw merged 4 commits intoPowerShell:masterfromjborean93:cancellation-token
Mar 24, 2025

Conversation

jborean93
Copy link
Collaborator

@jborean93jborean93 commentedNov 26, 2024
edited
Loading

PR Summary

Adds the PipelineStopToken property to a PSCmdlet instance that makes it easier for cmdlets and advanced functions to integrate a CancellationToken in their code for calling .NET methods that support them.

PR Context

Fixes:#17444
Slight alternative to:#19685

I've opened an RFC to cover this approachPowerShell/PowerShell-RFC#382. This PR shows how it could be implemented and that it is not too intrusive.

This now makes it possible to do the following and have it signal theCancellationToken when the engine is set to stop.

FunctionTest-FunctionWithCancellation {    [CmdletBinding()]param ()    [MyType]::SomeMethodWithCancellation($PSCmdlet.PipelineStopToken)}

Before this was only really possible in a binary cmdlet and it required extra boilerplate. The binary cmdlet can now look like

[Cmdlet(VerbsDiagnostic.Test,"CmdletWIthCancellation")]publicsealedclassTestCmdletWithCancellation:PSCmdlet{protectedoverridevoidEndProcessing(){MyType.SomeMethodWithCancellation(PipelineStopToken);}}

PR Checklist

santisq, Maamue, trackd, HeyItsGilbert, ThomasNieto, gaelcolas, MatejKafka, alx9r, ArmaanMcleod, kasini3000, and 2 more reacted with thumbs up emoji
@microsoft-github-policy-servicemicrosoft-github-policy-servicebot added the Review - NeededThe PR is being reviewed labelDec 4, 2024
@SeeminglyScienceSeeminglyScience added WG-Enginecore PowerShell engine, interpreter, and runtime WG-NeedsReviewNeeds a review by the labeled Working Group labelsDec 4, 2024
@kasini3000

This comment was marked as off-topic.

@microsoft-github-policy-servicemicrosoft-github-policy-servicebot removed the Review - NeededThe PR is being reviewed labelJan 6, 2025
@SeeminglyScienceSeeminglyScience added CommunityDay-SmallA small PR that the PS team has identified to prioritize to review WG-ReviewedA Working Group has reviewed this and made a recommendation and removed WG-NeedsReviewNeeds a review by the labeled Working Group labelsJan 13, 2025
@daxian-dbwdaxian-dbw self-assigned thisJan 13, 2025
Copy link
Member

@daxian-dbwdaxian-dbw left a comment

Choose a reason for hiding this comment

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

Thank you@jborean93 for making this change. Love the new property!

@microsoft-github-policy-servicemicrosoft-github-policy-servicebot added the Review - NeededThe PR is being reviewed labelJan 25, 2025
Adds the PipelineStopToken property to a PSCmdlet instance that makes iteasier for cmdlets and advanced functions to integrate aCancellationToken in their code for calling .NET methods that supportthem.
@jborean93
Copy link
CollaboratorAuthor

@daxian-dbw thanks for the review, I've just rebased on the latest master and made the change to move the cancellation token to the pipeline processor. There are probably a few things we want to decide on:

  • Should the token be cancelled before or afterStopProcessing is called on each command in the pipeline
    • Currently it will cancel it after
  • Should the stop token be added theICommandRuntime interface through a newICommandRuntime3 and that is used as the check
    • Currently it just verifies theCommandRuntime of thePSCmdlet isMshCommandRuntime
    • This should always be the case so it's a simple Debug assertion but maybe that will change in the future
    • A new interface seems like a drastic change so I didn't go ahead with it

@iSazonov
Copy link
Collaborator

/azp run

@microsoft-github-policy-servicemicrosoft-github-policy-servicebot removed the Review - NeededThe PR is being reviewed labelJan 28, 2025
@azure-pipelinesAzure Pipelines
Copy link

Azure Pipelines successfully started running 4 pipeline(s).

@alx9r
Copy link

Should the token be cancelled before or after StopProcessing is called on each command in the pipeline

Cancelling the token after StopProcessing would result in fewer branches in user PowerShell code to achieve correct results, I think. That would be better ergonomics for authors of Advanced Functions.

I'm assuming here that token cancellation and StopProcessing both happen on a different thread from the execution of the PowerShell instance being stopped. If StopProcessing occurs after cancelling the token then the PowerShell thread can continue to execute user PowerShell code after token cancellation but before StopProcessing whenever unfortunate thread scheduling occurs. That would mean PowerShell authors have to consider cases where the PowerShell engine has canceled the token but not yet stopped the pipeline. I expect that would necessarily lead to checks of$PSCmdlet.PipelineStopToken.IsCancellationRequested in user code to avert undesired behavior and extraneous errors arising from that condition.

Consider the following user code:

functionInvoke-SomeCancellableWorkload {    [CmdletBinding()]param()$result= [MyCancellableWorkload]::Invoke($PSCmdlet.PipelineStopToken)$result|ConvertTo-SomeOtherFormat}

If the token is cancelled after StopProcessing, then the PowerShell engine will always interrupt PowerShell execution before$result | ConvertTo-SomeOtherFormat is executed whenInvoke() is canceled. In that scenario, the engine currently completes assignment to$result despite the pipeline already being in theStopping state, but does not execute$result | ConvertTo-SomeOtherFormat. (The behavior of assignments to PowerShell variables from .Net methods while stopping is contemplated in#24658. There are empirical test results demonstrating the behavior inthis answer).

If, on the other hand, the token is cancelled before StopProcessing, then$result would have the return value of a cancelled call toInvoke() assigned to it. Let's say that value is$null. That then means that the author ofInvoke-SomeCancellableWorkload has to consider the possibility of$result being$null when the line$result | ConvertTo-SomeOtherFormat is executed after token cancellation but before StopProcessing. That scenario is apt to cause at least an extraneous parameter binding error that would likely be observed by users whenever that unfortunate thread scheduling occurs. But the author might also deem that inputting$null toConvertTo-SomeOtherFormat is unnacceptable because that results in unknown, undesired, or destructive behavior. Then the author would probably have to resort to something like the following:

functionInvoke-SomeCancellableWorkload {    [CmdletBinding()]param()$result= [MyCancellableWorkload]::Invoke($PSCmdlet.PipelineStopToken). {}if ($PSCmdlet.PipelineStopToken.IsCancellationRequested) {throw [StopTokenCanceledBeforeStopProcessingException]::new()return    }$result|ConvertTo-SomeOtherFormat}

I think the former is more readable. But the latter would be necessary if cancellation occurs before StopProcessing.

@jborean93
Copy link
CollaboratorAuthor

@alx9r in the script based functions it won't matter because script functions cannot implementStopProcessing, so whether the token is cancelled before or after the effectively no-op StopProcessing function is called won't make a different. The question is more important for compiled cmdlets as they can have their ownStopProcessing implementation. Even then I don't think it matters too much.

Please keep in mind this PR is not trying to deal with any scenarios like variable assignment or the like. It is focused specifically on just exposing a CancellationToken for functions or cmdlets to use when calling methods that support a CancellationToken so it can stop its execution. In the majority of cases the cancellation will result in anOperationCanceledException meaning callers don't have to worry about checking the cancellation status of the token anyway.

@alx9r
Copy link

alx9r commentedJan 28, 2025
edited
Loading

@jborean93

in the script based functions it won't matter because script functions cannot implement StopProcessing, so whether the token is cancelled before or after the effectively no-op StopProcessing function is called won't make a different.

I think I might be miscommunicating, then. For script functions, how do you refer to the point in execution on the thread that invokes stopping that causes flow of control of an advanced function to jump toclean{}? I was referring to that asStopProcessing because in all testing I have done, the invocation ofStopProcessing on a compiledCmdlet corresponds to the same timing as flow of control jump toclean{} (and$PSCmdlet.Stopping changing from false to true). Whatever that event is called, the argument I madehere for StopProcessing surely applies to that event. In other words, whatever you call that event, there is a significant difference when authoring Advanced Functions whether that event happens before or after$PSCmdlet.PipelineStopToken is canceled.

@jborean93
Copy link
CollaboratorAuthor

I think you might be focusing too much on this cancellation/cleanup side. This PR enables two things for end users:

  • Removes the need for common boilerplate done in compiled cmdlets that manually setup a CancellationToken
  • Exposes the same functionality present in a compiled function into a script function
    • They can now call async functions with a cancellation token and do a blocking wait and it will respond to stop events
    • Currently you need to do a loop that frees up the engine every few ms to respond to a cancellation event

The talk about when clean is called in relation to stop is unrelated to this PR.

@alx9r
Copy link

The talk about when clean is called in relation to stop is unrelated to this PR.

My concern is not about whenclean{} is called in relation to stop. My concern is about when the$PSCmdlet.PipelineStopToken available in an Advanced Function is canceled in relation to when pipeline stopping affects the execution of the PowerShell code of that Advanced Function.

@jborean93
Copy link
CollaboratorAuthor

My concern is about when the $PSCmdlet.PipelineStopToken available in an Advanced Function is canceled in relation to when pipeline stopping affects the execution of the PowerShell code of that Advanced Function.

It's exactly the same as it is today, the only difference is that now there is a way to call .NET functions that accept a CancellationToken and have it respond to the stop request.

Normally this works just fine because a task that accepts aCancellationToken will raise theOperationCanceledException if set. For example

& {    [CmdletBinding()]param()    [System.Threading.Tasks.Task]::Delay(-1,$PSCmdlet.PipelineStopToken).GetAwaiter().GetResult()    [Console]::WriteLine("Will not be called")}

If the method does not raise the exception or you catch it yourself then it'll continue to run until it reaches a point where the engine checks if it is stopped. For example when calling a cmdlet, entering a loop.

& {    [CmdletBinding()]param()try {        [System.Threading.Tasks.Task]::Delay(-1,$PSCmdlet.PipelineStopToken).GetAwaiter().GetResult()    }catch {        [Console]::WriteLine("Exception$_")    }    [Console]::WriteLine("Will be called")Write-Host test# PowerShell checks here if it is stopped or not    [Console]::WriteLine("Will not be called")}# Exception Exception calling "GetResult" with "0" argument(s): "A task was canceled."# Will be called

If the API doesn't use that token properly then you need to handle that accordingly. This works exactly the same as in a compiled cmdlet, if you don't release control back to the engine somehow then things will continue to run.

But once again, this is off topic to this PR. This is not about the stop implementation but rather about adding theCancellationToken to thePSCmdlet to allow cmdlets and now script functions to use that. The former removing the boilerplate needed, the latter actually making it possible.

@alx9r
Copy link

@jborean93

I think we are still talking past one another. I'll try a different approach. With$PSCmdlet.PipelineStopToken there are now two separate events that can affect the execution of user code when stopping an Advanced Function:

  • cancellation of the token
  • interruption of executing PowerShell code

Which event happens first?

@jborean93
Copy link
CollaboratorAuthor

jborean93 commentedJan 29, 2025
edited
Loading

The pipeline is marked as stopped before the pipeline callsStopProcessing on each cmdlet and thus sets the token to be cancelled. This PR doesn't change any of that and this is why I'm trying to say this is off topic and just adds noise to the PR.

internalvoidStop()
{
PipelineProcessor[]copyStack;
lock(_syncRoot)
{
if(_stopping)
{
return;
}
_stopping=true;
copyStack=_stack.ToArray();
}
// Propagate error from the toplevel operation through to enclosing the LocalPipeline.
if(copyStack.Length>0)
{
PipelineProcessortopLevel=copyStack[copyStack.Length-1];
if(topLevel!=null&&_localPipeline!=null)
{
_localPipeline.SetHadErrors(topLevel.ExecutionFailed);
}
}
// Note: after _stopping is set to true, nothing can be pushed/popped
// from stack and it is safe to call stop on PipelineProcessors in stack
// outside the lock
// Note: you want to do below loop outside the lock so that
// pipeline execution thread doesn't get blocked on Push and Pop.
// Note: A copy of the stack is made because we "unstop" a stopped
// pipeline to execute finally blocks. We don't want to stop pipelines
// in the finally block though.
foreach(PipelineProcessorppincopyStack)
{
pp.Stop();
}
}

InStop() the_stopping field is set totrue beforepp.Stop() (what called StopProcessing on each cmdlet) is called.

@alx9r
Copy link

alx9r commentedJan 30, 2025
edited
Loading

The pipeline is marked as stopped before the pipeline calls StopProcessing on each cmdlet and thus sets the token to be cancelled.
...
InStop() the_stopping field is set to true beforepp.Stop() (what called StopProcessing on each cmdlet) is called.

Based on this and my attempt to trace through the source Ithink the thread invoking stopping completes the events in question in the following order:

  1. _stopping is set to true. This has the effect that flow of user PowerShell code of the pipeline undergoing stopping will subsequently be controlled according to the stopping rules.
  2. PipelineProcessor.Stop()is invoked whichcalls.Cancel() on theCancellationTokenSource from which$PSCmdlet.PipelineStopToken was produced.

Can you confirm that this is correct?

If that is correct, then my concern is addressed since that is the order that avoids the racesI was concerned about.

@microsoft-github-policy-servicemicrosoft-github-policy-servicebot added the Review - NeededThe PR is being reviewed labelFeb 6, 2025
@powercodepowercode added WG-ReviewedA Working Group has reviewed this and made a recommendation and removed Review - NeededThe PR is being reviewed WG-ReviewedA Working Group has reviewed this and made a recommendation labelsFeb 12, 2025
@daxian-dbw
Copy link
Member

I'm assuming here that token cancellation and StopProcessing both happen on a different thread from the execution of the PowerShell instance being stopped.
... that causes flow of control of an advanced function to jump to clean{}? I was referring to that as StopProcessing because ...

@alx9r If I understand correctly, you refer toclean {} asStopProcessing.clean {} will always be invoked on the pipeline thread, which is the same thread that runsbegin {},process {}, andend {}. When pipeline is being stopped,clean {} runs after all running scripts are actually stopped.PipelineProcessor.Stop, on the other hand, runs on a separate thread.

Can you confirm that this is correct?

The order is correct.

@@ -914,6 +922,8 @@ internal void Stop()
continue;
}
}

_pipelineStopTokenSource.Cancel();
Copy link
Member

@daxian-dbwdaxian-dbwFeb 19, 2025
edited
Loading

Choose a reason for hiding this comment

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

@jborean93 Sorry for my delay on getting back to this PR.

  • Should the token be cancelled before or afterStopProcessing is called on each command in the pipeline
    • Currently it will cancel it after

I prefer to moving it before the calls toDoStopProcessing(), so as to signal the cancellation sooner. With that, commands in pipeline that use the cancellation token could have started the cleanup while we are runningStopProcess for those that don't.

  • Should the stop token be added theICommandRuntime interface through a newICommandRuntime3 and that is used as the check
    • Currently it just verifies theCommandRuntime of thePSCmdlet isMshCommandRuntime
    • This should always be the case so it's a simple Debug assertion but maybe that will change in the future
    • A new interface seems like a drastic change so I didn't go ahead with it

I agree that a new interface is overkill. But this property should be available to commands that derive fromCmdlet too. There are many cmdlets whose implementation actually derives fromCmdlet, such asConvertFrom-Json,Get/Wait/Stop/Debug-Process, and quite some service cmdlets in PowerShell. When running in PowerShell, I believe we will assign anMshCommandRuntime instance to them. Also,Cmdlet has the public propertyStopping, which make it reasonable for it to have thePipelineStopToken property as well.

Maybe we should consider adding thePipelineStopToken property to the abstract base classInternalCommand (IsStopping is defined in it, which is returned fromCmdlet.Stopping). Something like the following:

// in InternalCommand class// internal bool IsStopping ...internalCancellationTokenStopToken=> commandRuntimeis MshCommandRuntime mcr? mcr.PipelineProcessor.PipelineStopToken: CancellationToken.Default;
// in Cmdlet class// public bool Stopping ...publicCancellationTokenPipelineStopToken=> StopToken;

@SeeminglyScience thoughts?

Copy link
CollaboratorAuthor

Choose a reason for hiding this comment

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

I can definitely move the cancellation to beforeDoStopProcessing is called on each pipeline command. I cannot think of a race condition here that would affect existing code so I think it is safe to do so.

I can certainly move it toCmdlet (accessed byInternalCommand as suggested). I just assumed we wanted things to usePSCmdlet overCmdlet and this would be a nice carrot to do so but I'll wait to see what@SeeminglyScience thinks.

I'll also need to dig in a big deeper into the CI failures as it seems like thePipelineProcessor was disposed before the cancellation token was stopped so I'm not 100% sure what is happening there yet.

Copy link

@alx9ralx9rFeb 20, 2025
edited
Loading

Choose a reason for hiding this comment

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

@daxian-dbw

If I understand correctly, you refer to clean {} as StopProcessing.

If I referred toclean {} asStopProcessing that was a mistake and I certainly didn't intend that.

I wrote

I'm assuming here that token cancellation and StopProcessing both happen on a different thread from the execution of the PowerShell instance being stopped.

because if they aren't on the thread executing user PowerShell then a race can arise from the time that elapses between_stopping getting set to true and$PSCmdlet.PipelineStopToken being cancelled. In particular the thread that does the stopping/cancelling can be preempted between setting_stopping and cancelling$PSCmdlet.PipelineStopToken while the thread executing user PowerShell code continues.

Can you confirm that this is correct?

The order is correct.

Thank you for confirming. This sounds good to me. I expect that order to work good.

Copy link
Member

Choose a reason for hiding this comment

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

@SeeminglyScience can you please review@jborean93 and my comments in this thread and share your thoughts? Thanks!

SeeminglyScience reacted with thumbs up emoji
Copy link
Collaborator

Choose a reason for hiding this comment

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

I prefer to moving it before the calls to DoStopProcessing(), so as to signal the cancellation sooner. With that, commands in pipeline that use the cancellation token could have started the cleanup while we are running StopProcess for those that don't.

Hmm my only worry with this is script based commands with a try finally. Since the cancellation token will be triggered in a different thread I wonder how likely the timing is forPSScriptCmdlet.StopProcessing to start occuring while the script command has just entered the finally block.

Something like

New-Item temp:\sometempfile$tcpClient= [Net.Sockets.TcpClient]::new()try {# will throw OperationCancelled on ctrl c$tcpClient.ConnectAsync($PSCmdlet.PipelineStopToken).GetAwaiter().GetResult()# do stuff with tcpclient}finally {# StopProcessing will probably be called somewhere in this `if` blockif ((Test-Path temp:\sometempfile) {Remove-Item temp:\sometempfile    }# is this likely to run?    ($tcpClient)?.Dispose()}

Copy link
CollaboratorAuthor

Choose a reason for hiding this comment

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

DoesStopProcessing do anything for script functions? IIRC last time I checked it was used for CDXML cmdlets but that's about it. It didn't seem to be user configurable hence this PR being opened.

At this point in time the pipeline has been marked as stopped so if the finally block was to call something which checks this state and also re-throws then the stop token being triggered before or after won't make a difference.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yep pretty sure you're 100% correct there lol

No objections from me then!

Copy link
CollaboratorAuthor

Choose a reason for hiding this comment

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

Thanks for confirming, I've pushed a commit that moved the cancellation to beforeDoStopProcessing and also exposed the CancellationToken on theCmdlet class.

@rkeithhillrkeithhill added WG-TriagedThe issue has been triaged by the designated WG. and removed WG-TriagedThe issue has been triaged by the designated WG. labelsFeb 22, 2025
@microsoft-github-policy-servicemicrosoft-github-policy-servicebot added the Review - NeededThe PR is being reviewed labelMar 1, 2025
Copy link
Member

@daxian-dbwdaxian-dbw left a comment

Choose a reason for hiding this comment

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

LGTM. Thanks@jborean93!

alx9r and kasini3000 reacted with hooray emoji
@daxian-dbwdaxian-dbw merged commit93e053c intoPowerShell:masterMar 24, 2025
33 checks passed
@microsoft-github-policy-servicemicrosoft-github-policy-servicebot removed the Review - NeededThe PR is being reviewed labelMar 24, 2025
@daxian-dbwdaxian-dbw added the CL-EngineIndicates that a PR should be marked as an engine change in the Change Log labelMar 24, 2025
@jborean93jborean93 deleted the cancellation-token branchMarch 24, 2025 21:41
@jborean93
Copy link
CollaboratorAuthor

Thanks for the reviews and comments on this one!

alx9r reacted with thumbs up emoji

Sysoiev-Yurii pushed a commit to Sysoiev-Yurii/PowerShell that referenced this pull requestMay 12, 2025
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@alx9ralx9ralx9r left review comments

@iSazonoviSazonoviSazonov left review comments

@SeeminglyScienceSeeminglyScienceSeeminglyScience left review comments

@daxian-dbwdaxian-dbwdaxian-dbw approved these changes

Assignees

@daxian-dbwdaxian-dbw

Labels
CL-EngineIndicates that a PR should be marked as an engine change in the Change LogCommunityDay-SmallA small PR that the PS team has identified to prioritize to reviewWG-Enginecore PowerShell engine, interpreter, and runtimeWG-ReviewedA Working Group has reviewed this and made a recommendation
Projects
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

Add aPipelineStopToken cancellation token property toPSCmdlet
8 participants
@jborean93@kasini3000@iSazonov@alx9r@daxian-dbw@SeeminglyScience@powercode@rkeithhill

[8]ページ先頭

©2009-2025 Movatter.jp