[InlineData(SIGTERM)] // AppDomain.CurrentDomain.ProcessExit public async Task CancelOnProcessTermination_cancels_on_process_termination(int signo) { 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(); context.ExitCode = CancelledExitCode; } }); return(new CommandLineBuilder(new RootCommand { command }) .CancelOnProcessTermination() .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); }
public async Task CancelOnProcessTermination_provides_CancellationToken_that_signals_termination_when_null_timeout_is_specified(int signo) { 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); }