mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Cancellation support (#28)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
CliFx.Tests/TestCommands/CancellableCommand.cs
Normal file
23
CliFx.Tests/TestCommands/CancellableCommand.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -23,4 +23,4 @@
|
||||
<None Include="../favicon.png" Pack="True" PackagePath="" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user