-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Process.WaitForExit: don't wait for standard streams when process was killed. #48101
Conversation
… killed. This is a common pattern: process.Kill(); process.WaitForExit(); When the process has redirected standard output or standard error WaitForExit will block until all descendants of the child process (that inherited these streams) have also terminated. This change will make WaitForExit no longer wait for these descendants when the process was killed first. Additionally, when the user has cancelled the reading by calling CancelOutputRead and CancelErrorRead, WaitForExit will no longer wait for the streams either.
Tagging subscribers to this area: Issue DetailsThis is a common pattern:
When the process has redirected standard output or standard error This change will make Additionally, when the user has cancelled the reading by calling @danmoseley @stephentoub @adamsitnik @eiriktsarpalis ptal
|
What's the reason for the proposed change? |
To better match with expectation that The current behavior can introduce unexpected long blocking. Users that are aware call |
The tests aren't passing on Windows.
I think it doesn't find "sleep". |
Is the current behavior the same on both Windows and Unix? What is the .NET Framework behavior? If we are matching that, it seems this change could be significantly breaking. |
Behavior is the same on Windows and Unix. The change is meant to be non-breaking: |
if (Interlocked.CompareExchange(ref _operationState, OperationStateReading, state) != state) | ||
{ | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does BeginReadLine
ever makes calls to FlushMessageQueue
?
I think that is ReadBufferAsync
's responsibility?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. I looked at the file history but it has only been modified twice: When it was introduced to .NET Core, and then when it was refactored to fix some async cancellation issues.
Maybe it's an optimization because in most cases, EOF is reached quite quickly? What do you think, @jozkee @adamsitnik ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your PR, @tmds. I left some feedback for you to consider.
@jozkee @adamsitnik let's talk about this PR in our next triage meeting to discuss any potential unintended breaking changes, and if we are ok to take them.
if (Interlocked.CompareExchange(ref _operationState, OperationStateReading, state) != state) | ||
{ | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. I looked at the file history but it has only been modified twice: When it was introduced to .NET Core, and then when it was refactored to fix some async cancellation issues.
Maybe it's an optimization because in most cases, EOF is reached quite quickly? What do you think, @jozkee @adamsitnik ?
}; | ||
process.BeginOutputReadLine(); | ||
process.BeginErrorReadLine(); | ||
string childOutput = await childOutputTcs.Task.TimeoutAfter(30_000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
Task asyncWaitTask = useAsyncAPI ? process.WaitForExitAsync() : | ||
Task.Run(() => process.WaitForExit()); | ||
await asyncWaitTask.TimeoutAfter(30_000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The sleep
child is meant to quit after 10 minutes. Is there a point in waiting 30 seconds? Seems like a lot.
@carlossanlop, @jozkee, @adamsitnik -- please take a look at this in your next triage meeting to decide if we should take the behavioral change. |
@adamsitnik, @carlossanlop, @jozkee -- merge conflicts have emerged on this, but we should decide if we want to take the behavioral change before we both resolving them. |
@adamsitnik This PR is assigned to you for follow-up/decision before the RC1 snap. |
@jeffhandley, @adamsitnik, what is the plan here? Last comment is from July talking about doing something with this prior to the RC1 snap, which has already happened. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello @tmds
First of all, please accept my apologies for a very big delay.
To be honest with you, I am always hesistant to review PRs that try to change something non-trivial without disucssing it first.
If we discuss things first, and agree that given scenario needs to be fixed, you won't waste your time by working on a fix. As a maintainer I am going to have some context (the review will be easier) and I am also going to have the chance to see what other users think about it (the more upvotes, the higher priority on my TODO list).
If we don't and it turns out that current implementation works as expected, we are both not happy about the outcome: you have spent time on a change that won't get merged, while I have to reject a PR knowing that someone had good intentions and spent some time working on it.
In this particular PR we have two changes:
make WaitForExit no longer wait for these descendants
when the process was killed first
Currently, we wait for these descendants only when the user does not provide any timeout value, meaning that the user can wait up to infinity. I don't believe that this behavior should be changed. It's by design.
runtime/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs
Lines 183 to 188 in ef85762
// If we have a hard timeout, we cannot wait for the streams | |
if (milliseconds == Timeout.Infinite) | |
{ | |
_output?.EOF.GetAwaiter().GetResult(); | |
_error?.EOF.GetAwaiter().GetResult(); | |
} |
runtime/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs
Lines 213 to 217 in ef85762
if (exited && milliseconds == Timeout.Infinite) // if we have a hard timeout, we cannot wait for the streams | |
{ | |
_output?.EOF.GetAwaiter().GetResult(); | |
_error?.EOF.GetAwaiter().GetResult(); | |
} |
when the user has cancelled the reading by calling
CancelOutputRead and CancelErrorRead
I am suprised that Cancel methods don't actualy cancel anything. It seems that we set the flag to true:
runtime/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/AsyncStreamReader.cs
Lines 83 to 85 in ef85762
internal void CancelOperation() | |
{ | |
_cancelOperation = true; |
But don't stop reading:
runtime/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/AsyncStreamReader.cs
Lines 218 to 219 in ef85762
// Keep going until we're out of data to process. | |
while (true) |
The actual async read operation cancellation is requested by Dispose
method:
runtime/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/AsyncStreamReader.cs
Lines 256 to 258 in ef85762
public void Dispose() | |
{ | |
_cts.Cancel(); |
Would it not be simpler to just actually use the _cancelOperation
flag in all while(true)
loops and call _cts.Cancel();
from CancelOperation()
?
But even with that, I am not sure if the stream used by async reader supports cancellation as on Windows we pass isAsync: false
to the FileStream
ctor:
runtime/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs
Line 639 in ef85762
_standardInput = new StreamWriter(new FileStream(parentInputPipeHandle!, FileAccess.Write, 4096, false), enc, 4096); |
May I ask you to: create a new issue that describes the cancellation problem and send a new PR that addresses that? Once we have that PR, I am going to take a look at the Windows part and ensure that we are using a stream that supports cancellation. Once we get there, the issue can be closed (and the fix included in .NET 7).
By design means this is intentional. This is how I came to making this PR: msbuild has some code like this: process.Kill();
bool exited = process.WaitForExit(timeout); The timeout is here to avoid the the infinite wait when a grand child still holds the terminal. The msbuild repo also had a long standing open issue where things went wrong for an unclear reason that was hard to reproduce. The root cause was a bug in the |
This is a common pattern:
When the process has redirected standard output or standard error
WaitForExit
will block until all descendants of the child process (thatinherited these streams) have also terminated.
This change will make
WaitForExit
no longer wait for these descendantswhen the process was killed first.
Additionally, when the user has cancelled the reading by calling
CancelOutputRead
andCancelErrorRead
,WaitForExit
will no longerwait for the streams either.
@danmoseley @stephentoub @adamsitnik @eiriktsarpalis ptal