-
Notifications
You must be signed in to change notification settings - Fork 426
Add support for timeout in process termination handling #1756
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
Changes from 5 commits
a98a0ba
3ed2081
44d23f6
3c68643
aba07e4
dfd788e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,7 +20,7 @@ public class CancelOnProcessTerminationTests | |
| private const int SIGTERM = 15; | ||
|
|
||
| [LinuxOnlyTheory] | ||
| [InlineData(SIGINT, Skip = "https://github.com/dotnet/command-line-api/issues/1206")] // Console.CancelKeyPress | ||
| [InlineData(SIGINT/*, Skip = "https://github.com/dotnet/command-line-api/issues/1206"*/)] // Console.CancelKeyPress | ||
| [InlineData(SIGTERM)] // AppDomain.CurrentDomain.ProcessExit | ||
| public async Task CancelOnProcessTermination_cancels_on_process_termination(int signo) | ||
| { | ||
|
|
@@ -91,6 +91,170 @@ public async Task CancelOnProcessTermination_cancels_on_process_termination(int | |
| process.ExitCode.Should().Be(CancelledExitCode); | ||
| } | ||
|
|
||
| [LinuxOnlyTheory] | ||
| [InlineData(SIGINT)] | ||
| [InlineData(SIGTERM)] | ||
| public async Task CancelOnProcessTermination_null_timeout_on_cancel_processing(int signo) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this test redundant with the pre-existing test above?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is redundand with the current |
||
| { | ||
| const string ChildProcessWaiting = "Waiting for the command to be cancelled"; | ||
| const int CancelledExitCode = 42; | ||
|
|
||
| Func<string[], Task<int>> childProgram = (string[] args) => | ||
| { | ||
| var command = new Command("the-command"); | ||
|
|
||
| command.SetHandler(async context => | ||
| { | ||
| var cancellationToken = context.GetCancellationToken(); | ||
|
|
||
| try | ||
| { | ||
| context.Console.WriteLine(ChildProcessWaiting); | ||
| await Task.Delay(int.MaxValue, cancellationToken); | ||
| context.ExitCode = 1; | ||
| } | ||
| catch (OperationCanceledException) | ||
| { | ||
| // For Process.Exit handling the event must remain blocked as long as the | ||
| // command is executed. | ||
| // We are currently blocking that event because CancellationTokenSource.Cancel | ||
| // is called from the event handler. | ||
| // We'll do an async Yield now. This means the Cancel call will return | ||
| // and we're no longer actively blocking the event. | ||
| // The event handler is responsible to continue blocking until the command | ||
| // has finished executing. If it doesn't we won't get the CancelledExitCode. | ||
| await Task.Yield(); | ||
|
|
||
| // Exit code gets set here - but then execution continues and is let run till code voluntarily returns | ||
| // hence exit code gets overwritten below | ||
| context.ExitCode = 123; | ||
| } | ||
|
|
||
| // This is an example of bad pattern and reason why we need a timeout on termination processing | ||
| await Task.Delay(TimeSpan.FromMilliseconds(200)); | ||
|
|
||
| context.ExitCode = CancelledExitCode; | ||
| }); | ||
|
|
||
| return new CommandLineBuilder(new RootCommand | ||
| { | ||
| command | ||
| }) | ||
| // Unfortunately we cannot use test parameter here - RemoteExecutor currently doesn't capture the closure | ||
| .CancelOnProcessTermination(null) | ||
| .Build() | ||
| .InvokeAsync("the-command"); | ||
| }; | ||
|
|
||
| using RemoteExecution program = RemoteExecutor.Execute(childProgram, psi: new ProcessStartInfo { RedirectStandardOutput = true }); | ||
|
|
||
| Process process = program.Process; | ||
|
|
||
| // Wait for the child to be in the command handler. | ||
| string childState = await process.StandardOutput.ReadLineAsync(); | ||
| childState.Should().Be(ChildProcessWaiting); | ||
|
|
||
| // Request termination | ||
| kill(process.Id, signo).Should().Be(0); | ||
|
|
||
| // Verify the process terminates timely | ||
| bool processExited = process.WaitForExit(10000); | ||
| if (!processExited) | ||
| { | ||
| process.Kill(); | ||
| process.WaitForExit(); | ||
| } | ||
| processExited.Should().Be(true); | ||
|
|
||
| // Verify the process exit code | ||
| process.ExitCode.Should().Be(CancelledExitCode); | ||
| } | ||
|
|
||
| [LinuxOnlyTheory] | ||
| [InlineData(SIGINT)] | ||
| [InlineData(SIGTERM)] | ||
| public async Task CancelOnProcessTermination_timeout_on_cancel_processing(int signo) | ||
|
JanKrivanek marked this conversation as resolved.
Outdated
|
||
| { | ||
| const string ChildProcessWaiting = "Waiting for the command to be cancelled"; | ||
| const int CancelledExitCode = 42; | ||
| const int ForceTerminationCode = 130; | ||
|
|
||
| Func<string[], Task<int>> childProgram = (string[] args) => | ||
| { | ||
| var command = new Command("the-command"); | ||
|
|
||
| command.SetHandler(async context => | ||
| { | ||
| var cancellationToken = context.GetCancellationToken(); | ||
|
|
||
| try | ||
| { | ||
| context.Console.WriteLine(ChildProcessWaiting); | ||
| await Task.Delay(int.MaxValue, cancellationToken); | ||
| context.ExitCode = 1; | ||
| } | ||
| catch (OperationCanceledException) | ||
| { | ||
| // For Process.Exit handling the event must remain blocked as long as the | ||
| // command is executed. | ||
| // We are currently blocking that event because CancellationTokenSource.Cancel | ||
| // is called from the event handler. | ||
| // We'll do an async Yield now. This means the Cancel call will return | ||
| // and we're no longer actively blocking the event. | ||
| // The event handler is responsible to continue blocking until the command | ||
| // has finished executing. If it doesn't we won't get the CancelledExitCode. | ||
| await Task.Yield(); | ||
|
|
||
| context.ExitCode = CancelledExitCode; | ||
|
|
||
| // This is an example of bad pattern and reason why we need a timeout on termination processing | ||
| await Task.Delay(TimeSpan.FromMilliseconds(1000)); | ||
|
|
||
| // Execution should newer get here as termination processing has a timeout of 100ms | ||
| Environment.Exit(123); | ||
| } | ||
|
|
||
| // This is an example of bad pattern and reason why we need a timeout on termination processing | ||
| await Task.Delay(TimeSpan.FromMilliseconds(1000)); | ||
|
|
||
| // Execution should newer get here as termination processing has a timeout of 100ms | ||
| Environment.Exit(123); | ||
| }); | ||
|
|
||
| return new CommandLineBuilder(new RootCommand | ||
| { | ||
| command | ||
| }) | ||
| // Unfortunately we cannot use test parameter here - RemoteExecutor currently doesn't capture the closure | ||
| .CancelOnProcessTermination(TimeSpan.FromMilliseconds(100)) | ||
| .Build() | ||
| .InvokeAsync("the-command"); | ||
| }; | ||
|
|
||
| using RemoteExecution program = RemoteExecutor.Execute(childProgram, psi: new ProcessStartInfo { RedirectStandardOutput = true }); | ||
|
|
||
| Process process = program.Process; | ||
|
|
||
| // Wait for the child to be in the command handler. | ||
| string childState = await process.StandardOutput.ReadLineAsync(); | ||
| childState.Should().Be(ChildProcessWaiting); | ||
|
|
||
| // Request termination | ||
| kill(process.Id, signo).Should().Be(0); | ||
|
|
||
| // Verify the process terminates timely | ||
| bool processExited = process.WaitForExit(10000); | ||
| if (!processExited) | ||
| { | ||
| process.Kill(); | ||
| process.WaitForExit(); | ||
| } | ||
| processExited.Should().Be(true); | ||
|
|
||
| // Verify the process exit code | ||
| process.ExitCode.Should().Be(ForceTerminationCode); | ||
| } | ||
|
|
||
| [DllImport("libc", SetLastError = true)] | ||
| private static extern int kill(int pid, int sig); | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.