From ed458c398061c51f13e875f73c39217062d3e61c Mon Sep 17 00:00:00 2001 From: Moophic Date: Wed, 30 Oct 2019 18:37:32 +0200 Subject: [PATCH] Cancellation support (#28) --- CliFx.Benchmarks/Commands/CliFxCommand.cs | 5 ++-- CliFx.Demo/Commands/BookAddCommand.cs | 3 ++- CliFx.Demo/Commands/BookCommand.cs | 5 ++-- CliFx.Demo/Commands/BookListCommand.cs | 5 ++-- CliFx.Demo/Commands/BookRemoveCommand.cs | 5 ++-- CliFx.Tests/CliApplicationTests.cs | 26 +++++++++++++++++++ .../TestCommands/CancellableCommand.cs | 23 ++++++++++++++++ .../TestCommands/CommandExceptionCommand.cs | 5 ++-- CliFx.Tests/TestCommands/ConcatCommand.cs | 3 ++- CliFx.Tests/TestCommands/DivideCommand.cs | 5 ++-- .../DuplicateOptionNamesCommand.cs | 5 ++-- .../DuplicateOptionShortNamesCommand.cs | 5 ++-- .../EnvironmentVariableCommand.cs | 5 ++-- ...onmentVariableWithMultipleValuesCommand.cs | 3 ++- ...ariableWithoutCollectionPropertyCommand.cs | 3 ++- CliFx.Tests/TestCommands/ExceptionCommand.cs | 3 ++- .../TestCommands/HelloWorldDefaultCommand.cs | 5 ++-- .../TestCommands/HelpDefaultCommand.cs | 5 ++-- CliFx.Tests/TestCommands/HelpNamedCommand.cs | 5 ++-- CliFx.Tests/TestCommands/HelpSubCommand.cs | 5 ++-- .../TestCommands/NonAnnotatedCommand.cs | 5 ++-- CliFx/CliApplication.cs | 2 +- CliFx/CliFx.csproj | 2 +- CliFx/ICommand.cs | 5 ++-- CliFx/Services/IConsole.cs | 6 +++++ CliFx/Services/SystemConsole.cs | 20 ++++++++++++++ CliFx/Services/VirtualConsole.cs | 14 ++++++++++ 27 files changed, 146 insertions(+), 37 deletions(-) create mode 100644 CliFx.Tests/TestCommands/CancellableCommand.cs diff --git a/CliFx.Benchmarks/Commands/CliFxCommand.cs b/CliFx.Benchmarks/Commands/CliFxCommand.cs index a34659c..bb2e0b2 100644 --- a/CliFx.Benchmarks/Commands/CliFxCommand.cs +++ b/CliFx.Benchmarks/Commands/CliFxCommand.cs @@ -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; } } \ No newline at end of file diff --git a/CliFx.Demo/Commands/BookAddCommand.cs b/CliFx.Demo/Commands/BookAddCommand.cs index ecb7eb2..5f7a2fc 100644 --- a/CliFx.Demo/Commands/BookAddCommand.cs +++ b/CliFx.Demo/Commands/BookAddCommand.cs @@ -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) diff --git a/CliFx.Demo/Commands/BookCommand.cs b/CliFx.Demo/Commands/BookCommand.cs index 2a4fb81..4cc6c28 100644 --- a/CliFx.Demo/Commands/BookCommand.cs +++ b/CliFx.Demo/Commands/BookCommand.cs @@ -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); diff --git a/CliFx.Demo/Commands/BookListCommand.cs b/CliFx.Demo/Commands/BookListCommand.cs index e6077fe..cd333b8 100644 --- a/CliFx.Demo/Commands/BookListCommand.cs +++ b/CliFx.Demo/Commands/BookListCommand.cs @@ -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(); diff --git a/CliFx.Demo/Commands/BookRemoveCommand.cs b/CliFx.Demo/Commands/BookRemoveCommand.cs index ce7e771..ce33c4f 100644 --- a/CliFx.Demo/Commands/BookRemoveCommand.cs +++ b/CliFx.Demo/Commands/BookRemoveCommand.cs @@ -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); diff --git a/CliFx.Tests/CliApplicationTests.cs b/CliFx.Tests/CliApplicationTests.cs index c9c2b1b..b14985c 100644 --- a/CliFx.Tests/CliApplicationTests.cs +++ b/CliFx.Tests/CliApplicationTests.cs @@ -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"); + } + } } } \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/CancellableCommand.cs b/CliFx.Tests/TestCommands/CancellableCommand.cs new file mode 100644 index 0000000..4623afe --- /dev/null +++ b/CliFx.Tests/TestCommands/CancellableCommand.cs @@ -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"); + } + } +} diff --git a/CliFx.Tests/TestCommands/CommandExceptionCommand.cs b/CliFx.Tests/TestCommands/CommandExceptionCommand.cs index f8a3a5a..1502680 100644 --- a/CliFx.Tests/TestCommands/CommandExceptionCommand.cs +++ b/CliFx.Tests/TestCommands/CommandExceptionCommand.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/ConcatCommand.cs b/CliFx.Tests/TestCommands/ConcatCommand.cs index e357230..e1fde71 100644 --- a/CliFx.Tests/TestCommands/ConcatCommand.cs +++ b/CliFx.Tests/TestCommands/ConcatCommand.cs @@ -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; diff --git a/CliFx.Tests/TestCommands/DivideCommand.cs b/CliFx.Tests/TestCommands/DivideCommand.cs index 71dad4e..69caefd 100644 --- a/CliFx.Tests/TestCommands/DivideCommand.cs +++ b/CliFx.Tests/TestCommands/DivideCommand.cs @@ -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; diff --git a/CliFx.Tests/TestCommands/DuplicateOptionNamesCommand.cs b/CliFx.Tests/TestCommands/DuplicateOptionNamesCommand.cs index 6dc0a4e..8703994 100644 --- a/CliFx.Tests/TestCommands/DuplicateOptionNamesCommand.cs +++ b/CliFx.Tests/TestCommands/DuplicateOptionNamesCommand.cs @@ -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; } } \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/DuplicateOptionShortNamesCommand.cs b/CliFx.Tests/TestCommands/DuplicateOptionShortNamesCommand.cs index 7c6d65c..ee4c758 100644 --- a/CliFx.Tests/TestCommands/DuplicateOptionShortNamesCommand.cs +++ b/CliFx.Tests/TestCommands/DuplicateOptionShortNamesCommand.cs @@ -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; } } \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/EnvironmentVariableCommand.cs b/CliFx.Tests/TestCommands/EnvironmentVariableCommand.cs index 11f6d38..654dd57 100644 --- a/CliFx.Tests/TestCommands/EnvironmentVariableCommand.cs +++ b/CliFx.Tests/TestCommands/EnvironmentVariableCommand.cs @@ -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; } } diff --git a/CliFx.Tests/TestCommands/EnvironmentVariableWithMultipleValuesCommand.cs b/CliFx.Tests/TestCommands/EnvironmentVariableWithMultipleValuesCommand.cs index 92a93f7..ca9a379 100644 --- a/CliFx.Tests/TestCommands/EnvironmentVariableWithMultipleValuesCommand.cs +++ b/CliFx.Tests/TestCommands/EnvironmentVariableWithMultipleValuesCommand.cs @@ -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 Option { get; set; } - public Task ExecuteAsync(IConsole console) => Task.CompletedTask; + public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; } } diff --git a/CliFx.Tests/TestCommands/EnvironmentVariableWithoutCollectionPropertyCommand.cs b/CliFx.Tests/TestCommands/EnvironmentVariableWithoutCollectionPropertyCommand.cs index 8b61b8e..f1d6f5b 100644 --- a/CliFx.Tests/TestCommands/EnvironmentVariableWithoutCollectionPropertyCommand.cs +++ b/CliFx.Tests/TestCommands/EnvironmentVariableWithoutCollectionPropertyCommand.cs @@ -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; } } diff --git a/CliFx.Tests/TestCommands/ExceptionCommand.cs b/CliFx.Tests/TestCommands/ExceptionCommand.cs index 59ab4bf..6e9f8d5 100644 --- a/CliFx.Tests/TestCommands/ExceptionCommand.cs +++ b/CliFx.Tests/TestCommands/ExceptionCommand.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/HelloWorldDefaultCommand.cs b/CliFx.Tests/TestCommands/HelloWorldDefaultCommand.cs index fd8eb9b..201e9e0 100644 --- a/CliFx.Tests/TestCommands/HelloWorldDefaultCommand.cs +++ b/CliFx.Tests/TestCommands/HelloWorldDefaultCommand.cs @@ -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; diff --git a/CliFx.Tests/TestCommands/HelpDefaultCommand.cs b/CliFx.Tests/TestCommands/HelpDefaultCommand.cs index 638c309..627489e 100644 --- a/CliFx.Tests/TestCommands/HelpDefaultCommand.cs +++ b/CliFx.Tests/TestCommands/HelpDefaultCommand.cs @@ -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; } } \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/HelpNamedCommand.cs b/CliFx.Tests/TestCommands/HelpNamedCommand.cs index 72a2122..cf4c110 100644 --- a/CliFx.Tests/TestCommands/HelpNamedCommand.cs +++ b/CliFx.Tests/TestCommands/HelpNamedCommand.cs @@ -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; } } \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/HelpSubCommand.cs b/CliFx.Tests/TestCommands/HelpSubCommand.cs index fec6df8..1cb8e14 100644 --- a/CliFx.Tests/TestCommands/HelpSubCommand.cs +++ b/CliFx.Tests/TestCommands/HelpSubCommand.cs @@ -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; } } \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/NonAnnotatedCommand.cs b/CliFx.Tests/TestCommands/NonAnnotatedCommand.cs index 6977532..76747b9 100644 --- a/CliFx.Tests/TestCommands/NonAnnotatedCommand.cs +++ b/CliFx.Tests/TestCommands/NonAnnotatedCommand.cs @@ -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; } } \ No newline at end of file diff --git a/CliFx/CliApplication.cs b/CliFx/CliApplication.cs index 91cf4b1..ef81056 100644 --- a/CliFx/CliApplication.cs +++ b/CliFx/CliApplication.cs @@ -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; diff --git a/CliFx/CliFx.csproj b/CliFx/CliFx.csproj index dbbf230..8ebf360 100644 --- a/CliFx/CliFx.csproj +++ b/CliFx/CliFx.csproj @@ -23,4 +23,4 @@ - \ No newline at end of file + diff --git a/CliFx/ICommand.cs b/CliFx/ICommand.cs index 9fb2c78..f8efef7 100644 --- a/CliFx/ICommand.cs +++ b/CliFx/ICommand.cs @@ -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 . /// This method is called when the command is invoked by a user through command line interface. /// - Task ExecuteAsync(IConsole console); + Task ExecuteAsync(IConsole console, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/CliFx/Services/IConsole.cs b/CliFx/Services/IConsole.cs index 6d92bf2..94d670f 100644 --- a/CliFx/Services/IConsole.cs +++ b/CliFx/Services/IConsole.cs @@ -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. /// void ResetColor(); + + /// + /// Cancels when soft cancellation requested. + /// + CancellationToken CancellationToken { get; } } } \ No newline at end of file diff --git a/CliFx/Services/SystemConsole.cs b/CliFx/Services/SystemConsole.cs index 193b3fd..7604d87 100644 --- a/CliFx/Services/SystemConsole.cs +++ b/CliFx/Services/SystemConsole.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading; namespace CliFx.Services { @@ -8,6 +9,22 @@ namespace CliFx.Services /// public class SystemConsole : IConsole { + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + + /// + 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(); + }; + } + /// public TextReader Input => Console.In; @@ -42,5 +59,8 @@ namespace CliFx.Services /// public void ResetColor() => Console.ResetColor(); + + /// + public CancellationToken CancellationToken => _cancellationTokenSource.Token; } } \ No newline at end of file diff --git a/CliFx/Services/VirtualConsole.cs b/CliFx/Services/VirtualConsole.cs index 08498da..b27a150 100644 --- a/CliFx/Services/VirtualConsole.cs +++ b/CliFx/Services/VirtualConsole.cs @@ -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 /// public class VirtualConsole : IConsole { + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + /// public TextReader Input { get; } @@ -82,5 +85,16 @@ namespace CliFx.Services ForegroundColor = ConsoleColor.Gray; BackgroundColor = ConsoleColor.Black; } + + /// + public CancellationToken CancellationToken => _cancellationTokenSource.Token; + + /// + /// Simulates cancellation. + /// + public void Cancel() + { + _cancellationTokenSource.Cancel(); + } } } \ No newline at end of file