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 13 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
62 changes: 60 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 Expand Up @@ -139,5 +139,63 @@ public async Task Invocation_can_be_short_circuited_by_middleware_by_not_calling
middlewareWasCalled.Should().BeTrue();
handlerWasCalled.Should().BeFalse();
}

[Fact]
public async Task Invocation_can_be_cancelled_by_middleware_before_cancellation_enabled()
{
var command = new Command("the-command");
command.Handler = CommandHandler.Create((InvocationContext context) =>
{
CancellationToken ct = context.EnableCancellation();
tmds marked this conversation as resolved.
Show resolved Hide resolved
// The middleware canceled the request
Assert.True(ct.IsCancellationRequested);
});

var parser = new CommandLineBuilder()
.UseMiddleware(async (context, next) =>
{
bool cancellationEnabled = context.Cancel();
// Cancellation is not yet enabled
Assert.False(cancellationEnabled);
await next(context);
})
.AddCommand(command)
.Build();

await parser.InvokeAsync("the-command", new TestConsole());
}

[Fact]
public async Task Invocation_can_be_cancelled_by_middleware_after_cancellation_enabled()
{
const int timeout = 5000;

var command = new Command("the-command");
command.Handler = CommandHandler.Create(async (InvocationContext context) =>
{
CancellationToken ct = context.EnableCancellation();
// The middleware hasn't cancelled our request yet.
Assert.False(ct.IsCancellationRequested);

await Task.Delay(timeout, ct);
});

var parser = new CommandLineBuilder()
.UseMiddleware(async (context, next) =>
{
Task task = next(context);

bool cancellationEnabled = context.Cancel();
// Cancellation is enabled by next
Assert.True(cancellationEnabled);

await task;
})
.AddCommand(command)
.Build();

var invocation = parser.InvokeAsync("the-command", new TestConsole());
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => invocation);
}
}
}
16 changes: 16 additions & 0 deletions src/System.CommandLine.Tests/UseExceptionHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.CommandLine.Builder;
using System.CommandLine.Invocation;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;
Expand Down Expand Up @@ -73,6 +74,21 @@ public async Task UseExceptionHandler_catches_command_handler_exceptions_and_wri
_console.Error.ToString().Should().Contain("System.Exception: oops!");
}

[Fact]
public async Task UseExceptionHandler_doesnt_write_operationcancelledexception_details_when_context_is_cancelled()
{
var parser = new CommandLineBuilder()
.UseMiddleware((InvocationContext context) => context.Cancel())
.AddCommand("the-command", "",
cmd => cmd.OnExecute((CancellationToken ct) => ct.ThrowIfCancellationRequested()))
.UseExceptionHandler()
.Build();

await parser.InvokeAsync("the-command", _console);

_console.Error.ToString().Should().Equals("");
}

[Fact]
public async Task Declaration_of_UseExceptionHandler_can_come_before_other_middleware()
{
Expand Down
3 changes: 2 additions & 1 deletion src/System.CommandLine/Builder/CommandLineBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ internal void AddMiddleware(

internal static class MiddlewareOrder
{
public const int ExceptionHandler = int.MinValue;
public const int ProcessExit = int.MinValue;
public const int ExceptionHandler = ProcessExit + 100;
public const int Configuration = ExceptionHandler + 100;
public const int Preprocessing = Configuration + 100;
public const int AfterPreprocessing = Preprocessing + 100;
Expand Down
4 changes: 4 additions & 0 deletions src/System.CommandLine/Invocation/CommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public static ICommandHandler Create<T>(
Action<T> action) =>
new MethodBindingCommandHandler(action);

public static ICommandHandler Create<T>(
Func<T, Task> action) =>
new MethodBindingCommandHandler(action);

tmds marked this conversation as resolved.
Show resolved Hide resolved
public static ICommandHandler Create<T1, T2>(
Action<T1, T2> action) =>
new MethodBindingCommandHandler(action);
Expand Down
41 changes: 41 additions & 0 deletions src/System.CommandLine/Invocation/InvocationContext.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
// 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.Threading;

namespace System.CommandLine.Invocation
{
public sealed class InvocationContext : IDisposable
{
private readonly IDisposable _onDispose;
private CancellationTokenSource _cts;
private bool _isCancellationEnabled;
private readonly object _gate = new object();

public InvocationContext(
ParseResult parseResult,
Expand Down Expand Up @@ -36,6 +41,42 @@ public InvocationContext(

public IInvocationResult InvocationResult { get; set; }

// Indicates the invocation can be cancelled.
// This returns a CancellationToken that will be set when the invocation
// is cancelled. The method may return a CancellationToken that is already
// cancelled.
public CancellationToken EnableCancellation()
{
lock (_gate)
tmds marked this conversation as resolved.
Show resolved Hide resolved
tmds marked this conversation as resolved.
Show resolved Hide resolved
{
_isCancellationEnabled = true;
if (_cts == null)
{
_cts = new CancellationTokenSource();
}
return _cts.Token;
}
}

// The return value indicates if the Invocation has cancellation enabled.
// When Cancel returns false, the Middleware may decide to forcefully
// end the process, for example, by calling Environment.Exit.
public bool Cancel()
{
lock (_gate)
{
if (_cts == null)
{
_cts = new CancellationTokenSource();
}
_cts.Cancel();
return _isCancellationEnabled;
}
}

public bool IsCancelled =>
_cts?.Token.IsCancellationRequested == true;

public void Dispose()
{
_onDispose?.Dispose();
Expand Down
52 changes: 47 additions & 5 deletions src/System.CommandLine/Invocation/InvocationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,51 @@
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static System.Environment;

namespace System.CommandLine.Invocation
{
public static class InvocationExtensions
{
public static CommandLineBuilder UseConsoleLifetime(this CommandLineBuilder builder)
tmds marked this conversation as resolved.
Show resolved Hide resolved
{
builder.AddMiddleware(async (context, next) =>
{
ConsoleCancelEventHandler consoleHandler = (_, args) =>
{
if (context.Cancel())
{
args.Cancel = true;
}
};
tmds marked this conversation as resolved.
Show resolved Hide resolved
var blockProcessExit = new ManualResetEventSlim(initialState: false);
EventHandler processExitHandler = (_1, _2) =>
{
if (context.Cancel())
{
blockProcessExit.Wait();
Environment.ExitCode = context.ResultCode;
}
};
try
{
Console.CancelKeyPress += consoleHandler;
AppDomain.CurrentDomain.ProcessExit += processExitHandler;
await next(context);
}
finally
{
Console.CancelKeyPress -= consoleHandler;
AppDomain.CurrentDomain.ProcessExit -= processExitHandler;
blockProcessExit.Set();
}
}, CommandLineBuilder.MiddlewareOrder.ProcessExit);

return builder;
}

public static CommandLineBuilder UseMiddleware(
this CommandLineBuilder builder,
InvocationMiddleware middleware)
Expand Down Expand Up @@ -75,11 +113,15 @@ public static CommandLineBuilder UseExceptionHandler(
}
catch (Exception exception)
{
context.Console.ResetColor();
context.Console.ForegroundColor = ConsoleColor.Red;
context.Console.Error.Write("Unhandled exception: ");
context.Console.Error.WriteLine(exception.ToString());
context.Console.ResetColor();
if (!(context.IsCancelled &&
exception is OperationCanceledException))
{
context.Console.ResetColor();
context.Console.ForegroundColor = ConsoleColor.Red;
context.Console.Error.Write("Unhandled exception: ");
context.Console.Error.WriteLine(exception.ToString());
context.Console.ResetColor();
}
context.ResultCode = 1;
}
}, order: CommandLineBuilder.MiddlewareOrder.ExceptionHandler);
Expand Down
6 changes: 6 additions & 0 deletions src/System.CommandLine/Invocation/MethodBinderBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace System.CommandLine.Invocation
Expand Down Expand Up @@ -76,6 +77,11 @@ public static object[] BindMethodArguments(
{
arguments.Add(context.Console);
}
else if (parameterInfo.ParameterType == typeof(CancellationToken))
{
CancellationToken ct = context.EnableCancellation();
arguments.Add(ct);
}
else
{
var argument = context.ParseResult
Expand Down
13 changes: 12 additions & 1 deletion src/System.CommandLine/Invocation/MethodBindingCommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,18 @@ public MethodBindingCommandHandler(MethodBinderBase methodBinder)

public Task<int> InvokeAsync(InvocationContext context)
{
return _methodBinder.InvokeAsync(context);
try
{
return _methodBinder.InvokeAsync(context);
}
catch (TargetInvocationException te)
tmds marked this conversation as resolved.
Show resolved Hide resolved
{
if (te.InnerException != null)
{
throw te.InnerException;
}
throw;
}
}
}
}