Cancellation support (#28)

This commit is contained in:
Moophic
2019-10-30 18:37:32 +02:00
committed by Alexey Golub
parent 25538f99db
commit ed458c3980
27 changed files with 146 additions and 37 deletions

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
@@ -16,6 +17,6 @@ namespace CliFx.Benchmarks.Commands
[CommandOption("bool", 'b')]
public bool BoolOption { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask;
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Demo.Internal;
@@ -31,7 +32,7 @@ namespace CliFx.Demo.Commands
_libraryService = libraryService;
}
public Task ExecuteAsync(IConsole console)
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken)
{
// To make the demo simpler, we will just generate random publish date and ISBN if they were not set
if (Published == default)

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Demo.Internal;
using CliFx.Demo.Services;
@@ -20,7 +21,7 @@ namespace CliFx.Demo.Commands
_libraryService = libraryService;
}
public Task ExecuteAsync(IConsole console)
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken)
{
var book = _libraryService.GetBook(Title);

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Demo.Internal;
using CliFx.Demo.Services;
@@ -16,7 +17,7 @@ namespace CliFx.Demo.Commands
_libraryService = libraryService;
}
public Task ExecuteAsync(IConsole console)
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken)
{
var library = _libraryService.GetLibrary();

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Demo.Services;
using CliFx.Exceptions;
@@ -19,7 +20,7 @@ namespace CliFx.Demo.Commands
_libraryService = libraryService;
}
public Task ExecuteAsync(IConsole console)
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken)
{
var book = _libraryService.GetBook(Title);

View File

@@ -231,5 +231,31 @@ namespace CliFx.Tests
stderr.Should().NotBeNullOrWhiteSpace();
}
}
[Test]
public async Task RunAsync_Cancellation_Test()
{
// Arrange
using (var stdoutStream = new StringWriter())
{
var console = new VirtualConsole(stdoutStream);
var application = new CliApplicationBuilder()
.AddCommand(typeof(CancellableCommand))
.UseConsole(console)
.Build();
var args = new[] { "cancel" };
// Act
var runTask = application.RunAsync(args);
console.Cancel();
var exitCode = await runTask.ConfigureAwait(false);
var stdOut = stdoutStream.ToString().Trim();
// Assert
exitCode.Should().Be(-2146233029);
stdOut.Should().Be("Printed");
}
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands
{
[Command("cancel")]
public class CancellableCommand : ICommand
{
public async Task ExecuteAsync(IConsole console, CancellationToken cancellationToken)
{
await Task.Yield();
console.Output.WriteLine("Printed");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
console.Output.WriteLine("Never printed");
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Services;
@@ -14,6 +15,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption("msg", 'm')]
public string Message { get; set; }
public Task ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode);
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => throw new CommandException(Message, ExitCode);
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
@@ -14,7 +15,7 @@ namespace CliFx.Tests.TestCommands
[CommandOption('s', Description = "String separator.")]
public string Separator { get; set; } = "";
public Task ExecuteAsync(IConsole console)
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken)
{
console.Output.WriteLine(string.Join(Separator, Inputs));
return Task.CompletedTask;

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
@@ -16,7 +17,7 @@ namespace CliFx.Tests.TestCommands
// This property should be ignored by resolver
public bool NotAnOption { get; set; }
public Task ExecuteAsync(IConsole console)
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken)
{
console.Output.WriteLine(Dividend / Divisor);
return Task.CompletedTask;

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
@@ -13,6 +14,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption("fruits")]
public string Oranges { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask;
}
}

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
@@ -13,6 +14,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption('f')]
public string Oranges { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask;
}
}

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
@@ -10,6 +11,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption("opt", EnvironmentVariableName = "ENV_SINGLE_VALUE")]
public string Option { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask;
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
@@ -11,6 +12,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption("opt", EnvironmentVariableName = "ENV_MULTIPLE_VALUES")]
public IEnumerable<string> Option { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask;
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
@@ -11,6 +12,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption("opt", EnvironmentVariableName = "ENV_MULTIPLE_VALUES")]
public string Option { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask;
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
@@ -11,6 +12,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption("msg", 'm')]
public string Message { get; set; }
public Task ExecuteAsync(IConsole console) => throw new Exception(Message);
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => throw new Exception(Message);
}
}

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
@@ -7,7 +8,7 @@ namespace CliFx.Tests.TestCommands
[Command]
public class HelloWorldDefaultCommand : ICommand
{
public Task ExecuteAsync(IConsole console)
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken)
{
console.Output.WriteLine("Hello world.");
return Task.CompletedTask;

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
@@ -13,6 +14,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption("option-b", 'b', Description = "OptionB description.")]
public string OptionB { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask;
}
}

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
@@ -13,6 +14,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption("option-d", 'd', Description = "OptionD description.")]
public string OptionD { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask;
}
}

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
@@ -10,6 +11,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption("option-e", 'e', Description = "OptionE description.")]
public string OptionE { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask;
}
}

View File

@@ -1,10 +1,11 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Services;
namespace CliFx.Tests.TestCommands
{
public class NonAnnotatedCommand : ICommand
{
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask;
}
}

View File

@@ -171,7 +171,7 @@ namespace CliFx
_commandInitializer.InitializeCommand(command, targetCommandSchema, commandInput);
// Execute command
await command.ExecuteAsync(_console);
await command.ExecuteAsync(_console, _console.CancellationToken);
// Finish the chain with exit code 0
return 0;

View File

@@ -23,4 +23,4 @@
<None Include="../favicon.png" Pack="True" PackagePath="" />
</ItemGroup>
</Project>
</Project>

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Services;
namespace CliFx
@@ -12,6 +13,6 @@ namespace CliFx
/// Executes command using specified implementation of <see cref="IConsole"/>.
/// This method is called when the command is invoked by a user through command line interface.
/// </summary>
Task ExecuteAsync(IConsole console);
Task ExecuteAsync(IConsole console, CancellationToken cancellationToken);
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Threading;
namespace CliFx.Services
{
@@ -52,5 +53,10 @@ namespace CliFx.Services
/// Resets foreground and background color to default values.
/// </summary>
void ResetColor();
/// <summary>
/// Cancels when soft cancellation requested.
/// </summary>
CancellationToken CancellationToken { get; }
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Threading;
namespace CliFx.Services
{
@@ -8,6 +9,22 @@ namespace CliFx.Services
/// </summary>
public class SystemConsole : IConsole
{
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
/// <inheritdoc />
public SystemConsole()
{
// Subscribe to CancelKeyPress event with cancellation token source
// Kills app on second cancellation (hard cancellation)
Console.CancelKeyPress += (_, args) =>
{
if (_cancellationTokenSource.IsCancellationRequested)
return;
args.Cancel = true;
_cancellationTokenSource.Cancel();
};
}
/// <inheritdoc />
public TextReader Input => Console.In;
@@ -42,5 +59,8 @@ namespace CliFx.Services
/// <inheritdoc />
public void ResetColor() => Console.ResetColor();
/// <inheritdoc />
public CancellationToken CancellationToken => _cancellationTokenSource.Token;
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Threading;
using CliFx.Internal;
namespace CliFx.Services
@@ -11,6 +12,8 @@ namespace CliFx.Services
/// </summary>
public class VirtualConsole : IConsole
{
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
/// <inheritdoc />
public TextReader Input { get; }
@@ -82,5 +85,16 @@ namespace CliFx.Services
ForegroundColor = ConsoleColor.Gray;
BackgroundColor = ConsoleColor.Black;
}
/// <inheritdoc />
public CancellationToken CancellationToken => _cancellationTokenSource.Token;
/// <summary>
/// Simulates cancellation.
/// </summary>
public void Cancel()
{
_cancellationTokenSource.Cancel();
}
}
}