-
Notifications
You must be signed in to change notification settings - Fork 381
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #290 from tmds/cancel_key
Support Ctrl+C handling
- Loading branch information
Showing
18 changed files
with
653 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
src/System.CommandLine.DragonFruit.Tests/System.CommandLine.DragonFruit.Tests.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
87 changes: 87 additions & 0 deletions
87
src/System.CommandLine.Tests/CancelOnProcessTerminationTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
// Copyright (c) .NET Foundation and contributors. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
using System.CommandLine.Builder; | ||
using System.CommandLine.Invocation; | ||
using System.Diagnostics; | ||
using System.Runtime.InteropServices; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using FluentAssertions; | ||
using Xunit; | ||
|
||
namespace System.CommandLine.Tests | ||
{ | ||
public class CancelOnProcessTerminationTests | ||
{ | ||
private const int SIGINT = 2; | ||
private const int SIGTERM = 15; | ||
|
||
[LinuxOnlyTheory] | ||
[InlineData(SIGINT)] // Console.CancelKeyPress | ||
[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) => | ||
new CommandLineBuilder() | ||
.AddCommand(new Command("the-command", | ||
handler: CommandHandler.Create<CancellationToken>(async ct => | ||
{ | ||
try | ||
{ | ||
Console.WriteLine(ChildProcessWaiting); | ||
await Task.Delay(int.MaxValue, ct); | ||
} | ||
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(); | ||
return CancelledExitCode; | ||
} | ||
return 1; | ||
}))) | ||
.CancelOnProcessTermination() | ||
.Build() | ||
.InvokeAsync("the-command"); | ||
|
||
using (RemoteExecution program = RemoteExecutor.Execute(childProgram, psi: new ProcessStartInfo { RedirectStandardOutput = true })) | ||
{ | ||
System.Diagnostics.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); | ||
} | ||
} | ||
|
||
[DllImport("libc", SetLastError = true)] | ||
private static extern int kill(int pid, int sig); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// Copyright (c) .NET Foundation and contributors. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
using System.Collections.Generic; | ||
|
||
namespace System.CommandLine.Tests | ||
{ | ||
public static class CollectionHelpers | ||
{ | ||
public static void AddRange<T>(this ICollection<T> destination, IEnumerable<T> source) | ||
{ | ||
foreach (T item in source) | ||
{ | ||
destination.Add(item); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// Copyright (c) .NET Foundation and contributors. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
using System.Runtime.InteropServices; | ||
using Microsoft.DotNet.PlatformAbstractions; | ||
using Xunit; | ||
|
||
namespace System.CommandLine.Tests | ||
{ | ||
public class LinuxOnlyTheory : TheoryAttribute | ||
{ | ||
public LinuxOnlyTheory() | ||
{ | ||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) | ||
{ | ||
this.Skip = "This test requires Linux to run"; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
// Copyright (c) .NET Foundation and contributors. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
using System.Diagnostics; | ||
using System.IO; | ||
using Xunit; | ||
using Xunit.Sdk; | ||
|
||
namespace System.CommandLine.Tests | ||
{ | ||
public class RemoteExecution : IDisposable | ||
{ | ||
private const int FailWaitTimeoutMilliseconds = 60 * 1000; | ||
private string _exceptionFile; | ||
|
||
public RemoteExecution(Process process, string className, string methodName, string exceptionFile) | ||
{ | ||
Process = process; | ||
ClassName = className; | ||
MethodName = methodName; | ||
_exceptionFile = exceptionFile; | ||
} | ||
|
||
public Process Process { get; private set; } | ||
public string ClassName { get; private set; } | ||
public string MethodName { get; private set; } | ||
|
||
public void Dispose() | ||
{ | ||
GC.SuppressFinalize(this); // before Dispose(true) in case the Dispose call throws | ||
Dispose(disposing: true); | ||
} | ||
|
||
private void Dispose(bool disposing) | ||
{ | ||
Assert.True(disposing, $"A test {ClassName}.{MethodName} forgot to Dispose() the result of RemoteInvoke()"); | ||
|
||
if (Process != null) | ||
{ | ||
Assert.True(Process.WaitForExit(FailWaitTimeoutMilliseconds), | ||
$"Timed out after {FailWaitTimeoutMilliseconds}ms waiting for remote process {Process.Id}"); | ||
|
||
// A bit unorthodox to do throwing operations in a Dispose, but by doing it here we avoid | ||
// needing to do this in every derived test and keep each test much simpler. | ||
try | ||
{ | ||
if (File.Exists(_exceptionFile)) | ||
{ | ||
throw new RemoteExecutionException(File.ReadAllText(_exceptionFile)); | ||
} | ||
} | ||
finally | ||
{ | ||
if (File.Exists(_exceptionFile)) | ||
{ | ||
File.Delete(_exceptionFile); | ||
} | ||
|
||
// Cleanup | ||
try { Process.Kill(); } | ||
catch { } // ignore all cleanup errors | ||
} | ||
|
||
Process.Dispose(); | ||
Process = null; | ||
} | ||
} | ||
|
||
private sealed class RemoteExecutionException : Exception | ||
{ | ||
private readonly string _stackTrace; | ||
|
||
internal RemoteExecutionException(string stackTrace) | ||
: base("Remote process failed with an unhandled exception.") | ||
{ | ||
_stackTrace = stackTrace; | ||
} | ||
|
||
public override string StackTrace | ||
{ | ||
get => _stackTrace ?? base.StackTrace; | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.