Skip to content
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

Support Ctrl+C handling #290

Merged
merged 45 commits into from
Dec 5, 2018
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
2d7843d
Support Ctrl+C handling
tmds Nov 5, 2018
235bf17
Reimplement using middleware
tmds Nov 13, 2018
bf6528f
Add CancelOnUnload
tmds Nov 13, 2018
1076155
Undo changes to InvocationPipeline
tmds Nov 13, 2018
0da0b47
Undo changes to InvocationContext
tmds Nov 13, 2018
5180d09
Update MiddlewareOrder
tmds Nov 13, 2018
3866b9e
Add CancelOnCancelKeyTests
tmds Nov 13, 2018
b7930c7
Make InvocationContext aware if cancellation is enabled
tmds Nov 14, 2018
7374992
Merge CancelOn* methods into UseConsoleLifetime and improve interacti…
tmds Nov 15, 2018
d53b5ec
Merge remote-tracking branch 'upstream/master' into cancel_key
tmds Nov 15, 2018
1fcab8b
Unwrap InvokeAsync TargetInvocationException
tmds Nov 15, 2018
256fb39
Bind CancellationToken
tmds Nov 15, 2018
48a9af8
Undo changes to SystemConsole
tmds Nov 15, 2018
952e314
UseExceptionHandler: also print a message when the invocation gets ca…
tmds Nov 15, 2018
d903c2f
Undo special handling of OperationCanceledException in UseExceptionHa…
tmds Nov 15, 2018
e5e8c32
Add Linux test for UseConsoleLifetime
tmds Nov 16, 2018
bdacb72
MethodBindingCommandHandler: restyle TargetInvocationException check
tmds Nov 16, 2018
17da00a
UseExceptionHandlerTests: undo changes
tmds Nov 16, 2018
9600be2
Add some comments to clarify how the termination event handlers work
tmds Nov 16, 2018
95f53e3
Use some new names
tmds Nov 16, 2018
0ae0881
Fix ExitCode when UseConsoleLifetime with non cancellable invocation
tmds Nov 16, 2018
85d5c07
More renaming
tmds Nov 16, 2018
3a078fa
Add another test
tmds Nov 16, 2018
2d28edf
DragonFruit: change TargetFramework to netcoreapp2.1 to fix CI build
tmds Nov 19, 2018
7e23c0b
Update DotNetCliVersion to 2.1.500
tmds Nov 20, 2018
fc5fdb1
Merge branch 'master' into cancel_key
tmds Nov 20, 2018
0ae2773
Update global.json sdk version to 2.1.500
tmds Nov 20, 2018
f6bed06
InvocationExtensionsTest: help the compiler pick a Create overload
tmds Nov 20, 2018
03a89de
AddCancellationHandling: return instead of using an out parameter
tmds Nov 20, 2018
1ea9e2f
Rename IsCancelled to IsCancellationRequested
tmds Nov 20, 2018
0ec7e8b
Revert back to 2.1.400 sdk
tmds Nov 20, 2018
be385fa
Remove duplicate DotNetCliVersion
tmds Nov 20, 2018
fd362ac
Add description for the wiki
tmds Nov 21, 2018
963c30d
Reword wiki feature description
tmds Nov 28, 2018
87d57ec
Move child process timeouts to the test process
tmds Nov 28, 2018
c5fe41b
Simplify tests
tmds Nov 28, 2018
2757674
Fix race between producers and consumers of cancellation
tmds Nov 28, 2018
1f6208d
Merge remote-tracking branch 'upstream/master' into cancel_key
tmds Nov 29, 2018
e0115d0
Tests: help compiler pick a CommandHandler.Create overload
tmds Nov 29, 2018
840f267
InvocationContext: ensure order of CancellationHandlerAdded event and…
tmds Nov 29, 2018
610ca21
Use FluentAssertion style
tmds Nov 29, 2018
6aa5bd1
Fix typo
tmds Nov 29, 2018
269bb1e
Make RemoteExceptionException directly derive from Exception
tmds Nov 30, 2018
a03dcdb
UseDefaults: include CancelOnProcessTermination
tmds Dec 4, 2018
6116ef9
Remove wiki feature description
tmds Dec 5, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion samples/DragonFruit/DragonFruit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp2.1</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp2.1</TargetFramework>
<StartupObject>AutoGeneratedProgram</StartupObject>
<!-- Ensure that an XML doc file is emitted to supply command line help -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
Expand Down
86 changes: 86 additions & 0 deletions src/System.CommandLine.Tests/CancelOnProcessTerminationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// 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 Xunit;

namespace System.CommandLine.Tests
{
public class CancelOnProcessTerminationTests
{
private const int SIGINT = 2;
private const int SIGTERM = 15;

[LinuxOnlyTheory]
tmds marked this conversation as resolved.
Show resolved Hide resolved
[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();
Assert.Equal(ChildProcessWaiting, childState);
tmds marked this conversation as resolved.
Show resolved Hide resolved

// Request termination
Assert.Equal(0, kill(process.Id, signo));

// Verify the process terminates timely
bool processExited = process.WaitForExit(10000);
if (!processExited)
{
process.Kill();
process.WaitForExit();
}
Assert.True(processExited);

// Verify the process exit code
Assert.Equal(CancelledExitCode, process.ExitCode);
}
}

[DllImport("libc", SetLastError = true)]
private static extern int kill(int pid, int sig);
}
}
18 changes: 18 additions & 0 deletions src/System.CommandLine.Tests/CollectionHelpers.cs
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);
}
}
}
}
5 changes: 5 additions & 0 deletions src/System.CommandLine.Tests/InvocationExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ public async Task RootCommand_InvokeAsync_returns_1_when_handler_throws()
{
wasCalled = true;
throw new Exception("oops!");

// Help the compiler pick a CommandHandler.Create overload.
#pragma warning disable CS0162 // Unreachable code detected
return 0;
#pragma warning restore CS0162
});

var resultCode = await rootCommand.InvokeAsync("");
Expand Down
4 changes: 2 additions & 2 deletions src/System.CommandLine.Tests/InvocationPipelineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using FluentAssertions;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

Expand Down Expand Up @@ -76,9 +77,8 @@ public void When_command_handler_throws_then_InvokeAsync_does_not_handle_the_exc
Func<Task> invoke = async () => await parser.InvokeAsync("the-command", _console);

invoke.Should()
.Throw<TargetInvocationException>()
.Throw<Exception>()
.Which
.InnerException
.Message
.Should()
.Be("oops!");
Expand Down
20 changes: 20 additions & 0 deletions src/System.CommandLine.Tests/LinuxOnlyTheory.cs
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";
}
}
}
}
74 changes: 74 additions & 0 deletions src/System.CommandLine.Tests/RemoteExecution.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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 : XunitException
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think the base exception type needs to be Xunit-specific.

{
internal RemoteExecutionException(string stackTrace) : base("Remote process failed with an unhandled exception.", stackTrace) { }
}
}
}
Loading