diff --git a/samples/HostingPlayground/Program.cs b/samples/HostingPlayground/Program.cs index c1105bf04d..90c1096676 100644 --- a/samples/HostingPlayground/Program.cs +++ b/samples/HostingPlayground/Program.cs @@ -12,7 +12,7 @@ namespace HostingPlayground { class Program { - static async Task Main(string[] args) => await BuildCommandLine() + static Task Main(string[] args) => BuildCommandLine() .UseHost(_ => Host.CreateDefaultBuilder(), host => { diff --git a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_Hosting_api_is_not_changed.approved.txt b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_Hosting_api_is_not_changed.approved.txt index 2cbc0bafdb..fb206743ff 100644 --- a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_Hosting_api_is_not_changed.approved.txt +++ b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_Hosting_api_is_not_changed.approved.txt @@ -12,7 +12,7 @@ System.CommandLine.Hosting public static System.CommandLine.CommandLineBuilder UseHost(this System.CommandLine.CommandLineBuilder builder, System.Func hostBuilderFactory, System.Action configureHost = null) public static Microsoft.Extensions.Hosting.IHostBuilder UseInvocationLifetime(this Microsoft.Extensions.Hosting.IHostBuilder host, System.CommandLine.Invocation.InvocationContext invocation, System.Action configureOptions = null) public class InvocationLifetime, Microsoft.Extensions.Hosting.IHostLifetime - .ctor(Microsoft.Extensions.Options.IOptions options, Microsoft.Extensions.Hosting.IHostEnvironment environment, Microsoft.Extensions.Hosting.IHostApplicationLifetime applicationLifetime, System.CommandLine.Invocation.InvocationContext context = null, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory = null) + .ctor(Microsoft.Extensions.Options.IOptions options, Microsoft.Extensions.Hosting.IHostEnvironment environment, Microsoft.Extensions.Hosting.IHostApplicationLifetime applicationLifetime, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory = null) public Microsoft.Extensions.Hosting.IHostApplicationLifetime ApplicationLifetime { get; } public Microsoft.Extensions.Hosting.IHostEnvironment Environment { get; } public InvocationLifetimeOptions Options { get; } diff --git a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_NamingConventionBinder_api_is_not_changed.approved.txt b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_NamingConventionBinder_api_is_not_changed.approved.txt index e8a6a52be0..a8c045b3dd 100644 --- a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_NamingConventionBinder_api_is_not_changed.approved.txt +++ b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_NamingConventionBinder_api_is_not_changed.approved.txt @@ -99,7 +99,7 @@ public System.Void BindParameter(System.Reflection.ParameterInfo param, System.CommandLine.Argument argument) public System.Void BindParameter(System.Reflection.ParameterInfo param, System.CommandLine.Option option) public System.Int32 Invoke(System.CommandLine.Invocation.InvocationContext context) - public System.Threading.Tasks.Task InvokeAsync(System.CommandLine.Invocation.InvocationContext context) + public System.Threading.Tasks.Task InvokeAsync(System.CommandLine.Invocation.InvocationContext context, System.Threading.CancellationToken cancellationToken = null) public class ModelDescriptor public static ModelDescriptor FromType() public static ModelDescriptor FromType(System.Type type) diff --git a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt index 26cf6ad038..3af763db30 100644 --- a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt +++ b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt @@ -116,27 +116,27 @@ System.CommandLine public static class Handler public static System.Void SetHandler(this Command command, System.Action handle) public static System.Void SetHandler(this Command command, System.Action handle) - public static System.Void SetHandler(this Command command, System.Func handle) - public static System.Void SetHandler(this Command command, System.Func handle) + public static System.Void SetHandler(this Command command, System.Func handle) + public static System.Void SetHandler(this Command command, System.Func handle) public static System.Void SetHandler(this Command command, Action handle, IValueDescriptor symbol) - public static System.Void SetHandler(this Command command, Func handle, IValueDescriptor symbol) + public static System.Void SetHandler(this Command command, Func handle, IValueDescriptor symbol) public static System.Void SetHandler(this Command command, Action handle, IValueDescriptor symbol1, IValueDescriptor symbol2) - public static System.Void SetHandler(this Command command, Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2) + public static System.Void SetHandler(this Command command, Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2) public static System.Void SetHandler(this Command command, Action handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3) - public static System.Void SetHandler(this Command command, Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3) + public static System.Void SetHandler(this Command command, Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3) public static System.Void SetHandler(this Command command, Action handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, IValueDescriptor symbol4) - public static System.Void SetHandler(this Command command, Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, IValueDescriptor symbol4) + public static System.Void SetHandler(this Command command, Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, IValueDescriptor symbol4) public static System.Void SetHandler(this Command command, Action handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, IValueDescriptor symbol4, IValueDescriptor symbol5) - public static System.Void SetHandler(this Command command, Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, IValueDescriptor symbol4, IValueDescriptor symbol5) + public static System.Void SetHandler(this Command command, Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, IValueDescriptor symbol4, IValueDescriptor symbol5) public static System.Void SetHandler(this Command command, Action handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, IValueDescriptor symbol4, IValueDescriptor symbol5, IValueDescriptor symbol6) - public static System.Void SetHandler(this Command command, Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, IValueDescriptor symbol4, IValueDescriptor symbol5, IValueDescriptor symbol6) + public static System.Void SetHandler(this Command command, Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, IValueDescriptor symbol4, IValueDescriptor symbol5, IValueDescriptor symbol6) public static System.Void SetHandler(this Command command, Action handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, IValueDescriptor symbol4, IValueDescriptor symbol5, IValueDescriptor symbol6, IValueDescriptor symbol7) - public static System.Void SetHandler(this Command command, Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, IValueDescriptor symbol4, IValueDescriptor symbol5, IValueDescriptor symbol6, IValueDescriptor symbol7) + public static System.Void SetHandler(this Command command, Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, IValueDescriptor symbol4, IValueDescriptor symbol5, IValueDescriptor symbol6, IValueDescriptor symbol7) public static System.Void SetHandler(this Command command, Action handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, IValueDescriptor symbol4, IValueDescriptor symbol5, IValueDescriptor symbol6, IValueDescriptor symbol7, IValueDescriptor symbol8) - public static System.Void SetHandler(this Command command, Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, IValueDescriptor symbol4, IValueDescriptor symbol5, IValueDescriptor symbol6, IValueDescriptor symbol7, IValueDescriptor symbol8) + public static System.Void SetHandler(this Command command, Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, IValueDescriptor symbol4, IValueDescriptor symbol5, IValueDescriptor symbol6, IValueDescriptor symbol7, IValueDescriptor symbol8) public interface ICommandHandler public System.Int32 Invoke(System.CommandLine.Invocation.InvocationContext context) - public System.Threading.Tasks.Task InvokeAsync(System.CommandLine.Invocation.InvocationContext context) + public System.Threading.Tasks.Task InvokeAsync(System.CommandLine.Invocation.InvocationContext context, System.Threading.CancellationToken cancellationToken = null) public interface IConsole : System.CommandLine.IO.IStandardError, System.CommandLine.IO.IStandardIn, System.CommandLine.IO.IStandardOut public abstract class IdentifierSymbol : Symbol public System.Collections.Generic.IReadOnlyCollection Aliases { get; } @@ -324,8 +324,8 @@ System.CommandLine.Help public System.Boolean Equals(TwoColumnHelpRow other) public System.Int32 GetHashCode() System.CommandLine.Invocation - public class InvocationContext, System.IDisposable - .ctor(System.CommandLine.ParseResult parseResult, System.CommandLine.IConsole console = null, System.Threading.CancellationToken cancellationToken = null) + public class InvocationContext + .ctor(System.CommandLine.ParseResult parseResult, System.CommandLine.IConsole console = null) public System.CommandLine.Binding.BindingContext BindingContext { get; } public System.CommandLine.IConsole Console { get; set; } public System.Int32 ExitCode { get; set; } @@ -334,17 +334,15 @@ System.CommandLine.Invocation public System.CommandLine.LocalizationResources LocalizationResources { get; } public System.CommandLine.Parsing.Parser Parser { get; } public System.CommandLine.ParseResult ParseResult { get; set; } - public System.Threading.CancellationToken GetCancellationToken() public System.Object GetValue(System.CommandLine.Option option) public T GetValue(Option option) public System.Object GetValue(System.CommandLine.Argument argument) public T GetValue(Argument argument) - public System.Void LinkToken(System.Threading.CancellationToken token) public delegate InvocationMiddleware : System.MulticastDelegate, System.ICloneable, System.Runtime.Serialization.ISerializable .ctor(System.Object object, System.IntPtr method) - public System.IAsyncResult BeginInvoke(InvocationContext context, System.Func next, System.AsyncCallback callback, System.Object object) + public System.IAsyncResult BeginInvoke(InvocationContext context, System.Threading.CancellationToken cancellationToken, System.Func next, System.AsyncCallback callback, System.Object object) public System.Threading.Tasks.Task EndInvoke(System.IAsyncResult result) - public System.Threading.Tasks.Task Invoke(InvocationContext context, System.Func next) + public System.Threading.Tasks.Task Invoke(InvocationContext context, System.Threading.CancellationToken cancellationToken, System.Func next) public enum MiddlewareOrder : System.Enum, System.IComparable, System.IConvertible, System.IFormattable Default=0 ErrorReporting=1000 diff --git a/src/System.CommandLine.Benchmarks/CommandLine/Perf_Parser_Directives_Suggest.cs b/src/System.CommandLine.Benchmarks/CommandLine/Perf_Parser_Directives_Suggest.cs index 0df2796867..14760434fa 100644 --- a/src/System.CommandLine.Benchmarks/CommandLine/Perf_Parser_Directives_Suggest.cs +++ b/src/System.CommandLine.Benchmarks/CommandLine/Perf_Parser_Directives_Suggest.cs @@ -46,8 +46,8 @@ public void Setup() public string TestCmdArgs; [Benchmark] - public async Task InvokeSuggest() - => await _testParser.InvokeAsync(TestCmdArgs, _nullConsole); + public Task InvokeSuggest() + => _testParser.InvokeAsync(TestCmdArgs, _nullConsole); } } diff --git a/src/System.CommandLine.Benchmarks/CommandLine/Perf_Parser_TypoCorrection.cs b/src/System.CommandLine.Benchmarks/CommandLine/Perf_Parser_TypoCorrection.cs index fe00c87af8..9549dc340f 100644 --- a/src/System.CommandLine.Benchmarks/CommandLine/Perf_Parser_TypoCorrection.cs +++ b/src/System.CommandLine.Benchmarks/CommandLine/Perf_Parser_TypoCorrection.cs @@ -53,7 +53,7 @@ public IEnumerable> GenerateTestParseResults() [Benchmark] [ArgumentsSource(nameof(GenerateTestParseResults))] - public async Task TypoCorrection(BdnParam parseResult) - => await parseResult.Value.InvokeAsync(_nullConsole); + public Task TypoCorrection(BdnParam parseResult) + => parseResult.Value.InvokeAsync(_nullConsole); } } diff --git a/src/System.CommandLine.Benchmarks/DragonFruit/Perf_CommandLine_EntryPoint.cs b/src/System.CommandLine.Benchmarks/DragonFruit/Perf_CommandLine_EntryPoint.cs index df53f13f54..87fe2309c1 100644 --- a/src/System.CommandLine.Benchmarks/DragonFruit/Perf_CommandLine_EntryPoint.cs +++ b/src/System.CommandLine.Benchmarks/DragonFruit/Perf_CommandLine_EntryPoint.cs @@ -129,8 +129,8 @@ public void Setup() } [Benchmark(Description = "ExecuteAssemblyAsync entry point search.")] - public async Task SearchForStartingPointUsingReflection() - => await System.CommandLine.DragonFruit.CommandLine.ExecuteAssemblyAsync( + public Task SearchForStartingPointUsingReflection() + => System.CommandLine.DragonFruit.CommandLine.ExecuteAssemblyAsync( _testAssembly, new string[] { }, null, @@ -138,8 +138,8 @@ public async Task SearchForStartingPointUsingReflection() _nullConsole); [Benchmark(Description = "ExecuteAssemblyAsync explicit entry point.")] - public async Task SearchForStartingPointWhenGivenEntryPointClass() - => await System.CommandLine.DragonFruit.CommandLine.ExecuteAssemblyAsync( + public Task SearchForStartingPointWhenGivenEntryPointClass() + => System.CommandLine.DragonFruit.CommandLine.ExecuteAssemblyAsync( _testAssembly, new string[] { }, "PerfTestApp.Program", diff --git a/src/System.CommandLine.Benchmarks/DragonFruit/Perf_CommandLine_Help.cs b/src/System.CommandLine.Benchmarks/DragonFruit/Perf_CommandLine_Help.cs index 0bf9d23e9e..0812ef2dc4 100644 --- a/src/System.CommandLine.Benchmarks/DragonFruit/Perf_CommandLine_Help.cs +++ b/src/System.CommandLine.Benchmarks/DragonFruit/Perf_CommandLine_Help.cs @@ -39,8 +39,8 @@ public void Setup() } [Benchmark(Description = "--help")] - public async Task SearchForStartingPointWhenGivenEntryPointClass_Help() - => await System.CommandLine.DragonFruit.CommandLine.ExecuteAssemblyAsync( + public Task SearchForStartingPointWhenGivenEntryPointClass_Help() + => System.CommandLine.DragonFruit.CommandLine.ExecuteAssemblyAsync( _testAssembly, new[] { "--help" }, null, diff --git a/src/System.CommandLine.DragonFruit/CommandLine.cs b/src/System.CommandLine.DragonFruit/CommandLine.cs index 40f8a55fc9..9c2a7bf9f7 100644 --- a/src/System.CommandLine.DragonFruit/CommandLine.cs +++ b/src/System.CommandLine.DragonFruit/CommandLine.cs @@ -28,7 +28,7 @@ public static class CommandLine /// Explicitly defined path to xml file containing XML Docs /// Output console /// The exit code. - public static async Task ExecuteAssemblyAsync( + public static Task ExecuteAssemblyAsync( Assembly entryAssembly, string[] args, string entryPointFullTypeName, @@ -46,7 +46,7 @@ public static async Task ExecuteAssemblyAsync( MethodInfo entryMethod = EntryPointDiscoverer.FindStaticEntryMethod(entryAssembly, entryPointFullTypeName); //TODO The xml docs file name and location can be customized using project property. - return await InvokeMethodAsync(args, entryMethod, xmlDocsFilePath, null, console); + return InvokeMethodAsync(args, entryMethod, xmlDocsFilePath, null, console); } /// @@ -79,7 +79,7 @@ public static int ExecuteAssembly( return InvokeMethod(args, entryMethod, xmlDocsFilePath, null, console); } - public static async Task InvokeMethodAsync( + public static Task InvokeMethodAsync( string[] args, MethodInfo method, string xmlDocsFilePath = null, @@ -88,7 +88,7 @@ public static async Task InvokeMethodAsync( { Parser parser = BuildParser(method, xmlDocsFilePath, target); - return await parser.InvokeAsync(args, console); + return parser.InvokeAsync(args, console); } public static int InvokeMethod( diff --git a/src/System.CommandLine.Generator/CommandHandlerSourceGenerator.cs b/src/System.CommandLine.Generator/CommandHandlerSourceGenerator.cs index 42ad13ea03..d4ffd1d016 100644 --- a/src/System.CommandLine.Generator/CommandHandlerSourceGenerator.cs +++ b/src/System.CommandLine.Generator/CommandHandlerSourceGenerator.cs @@ -115,10 +115,10 @@ private class GeneratedHandler_{handlerCount} : {ICommandHandlerType} } builder.Append($@" - public int Invoke(global::System.CommandLine.Invocation.InvocationContext context) => InvokeAsync(context).GetAwaiter().GetResult();"); + public int Invoke(global::System.CommandLine.Invocation.InvocationContext context) => InvokeAsync(context, global::System.Threading.CancellationToken.None).GetAwaiter().GetResult();"); builder.Append($@" - public async global::System.Threading.Tasks.Task InvokeAsync(global::System.CommandLine.Invocation.InvocationContext context) + public async global::System.Threading.Tasks.Task InvokeAsync(global::System.CommandLine.Invocation.InvocationContext context, global::System.Threading.CancellationToken cancellationToken) {{"); builder.Append($@" {invocation.InvokeContents()}"); diff --git a/src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs b/src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs index efe928533c..72a4cb4995 100644 --- a/src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs +++ b/src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs @@ -1,6 +1,7 @@ using System.CommandLine; using System.CommandLine.Invocation; using System.CommandLine.Parsing; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; @@ -143,7 +144,7 @@ public int Invoke(InvocationContext context) return Act(); } - public Task InvokeAsync(InvocationContext context) + public Task InvokeAsync(InvocationContext context, CancellationToken cancellationToken) { return Task.FromResult(Act()); } @@ -176,7 +177,7 @@ public int Invoke(InvocationContext context) return IntOption; } - public Task InvokeAsync(InvocationContext context) + public Task InvokeAsync(InvocationContext context, CancellationToken cancellationToken) { service.Value = IntOption; return Task.FromResult(IntOption); @@ -222,9 +223,9 @@ public MyHandler(MyService service) public string One { get; set; } - public int Invoke(InvocationContext context) => InvokeAsync(context).GetAwaiter().GetResult(); + public int Invoke(InvocationContext context) => InvokeAsync(context, CancellationToken.None).GetAwaiter().GetResult(); - public Task InvokeAsync(InvocationContext context) + public Task InvokeAsync(InvocationContext context, CancellationToken cancellationToken) { service.Value = IntOption; service.StringValue = One; diff --git a/src/System.CommandLine.Hosting.Tests/HostingTests.cs b/src/System.CommandLine.Hosting.Tests/HostingTests.cs index 83b6210606..a1e3d2b362 100644 --- a/src/System.CommandLine.Hosting.Tests/HostingTests.cs +++ b/src/System.CommandLine.Hosting.Tests/HostingTests.cs @@ -300,10 +300,10 @@ public static void GetInvocationContext_returns_same_instance_as_outer_middlewar InvocationContext ctxHosting = null; var parser = new CommandLineBuilder() - .AddMiddleware((context, next) => + .AddMiddleware((context, cancellationToken, next) => { ctxCustom = context; - return next(context); + return next(context, cancellationToken); }) .UseHost(hostBuilder => { @@ -323,10 +323,10 @@ public static void GetInvocationContext_in_ConfigureServices_returns_same_instan InvocationContext ctxConfigureServices = null; var parser = new CommandLineBuilder() - .AddMiddleware((context, next) => + .AddMiddleware((context, cancellationToken, next) => { ctxCustom = context; - return next(context); + return next(context, cancellationToken); }) .UseHost(hostBuilder => { @@ -373,13 +373,13 @@ public static void GetHost_returns_non_null_instance_in_subsequent_middleware() bool hostAsserted = false; var parser = new CommandLineBuilder() .UseHost() - .AddMiddleware((invCtx, next) => + .AddMiddleware((invCtx, cancellationToken, next) => { IHost host = invCtx.GetHost(); host.Should().NotBeNull(); hostAsserted = true; - return next(invCtx); + return next(invCtx, cancellationToken); }) .Build(); @@ -393,13 +393,13 @@ public static void GetHost_returns_null_when_no_host_in_invocation() { bool hostAsserted = false; var parser = new CommandLineBuilder() - .AddMiddleware((invCtx, next) => + .AddMiddleware((invCtx, cancellationToken, next) => { IHost host = invCtx.GetHost(); host.Should().BeNull(); hostAsserted = true; - return next(invCtx); + return next(invCtx, cancellationToken); }) .Build(); diff --git a/src/System.CommandLine.Hosting/HostingExtensions.cs b/src/System.CommandLine.Hosting/HostingExtensions.cs index 70c7969f57..1a4eb4637a 100644 --- a/src/System.CommandLine.Hosting/HostingExtensions.cs +++ b/src/System.CommandLine.Hosting/HostingExtensions.cs @@ -18,7 +18,7 @@ public static class HostingExtensions public static CommandLineBuilder UseHost(this CommandLineBuilder builder, Func hostBuilderFactory, Action configureHost = null) => - builder.AddMiddleware(async (invocation, next) => + builder.AddMiddleware(async (invocation, cancellationToken, next) => { var argsRemaining = invocation.ParseResult.UnmatchedTokens.ToArray(); var hostBuilder = hostBuilderFactory?.Invoke(argsRemaining) @@ -44,11 +44,11 @@ public static CommandLineBuilder UseHost(this CommandLineBuilder builder, invocation.BindingContext.AddService(typeof(IHost), _ => host); - await host.StartAsync(); + await host.StartAsync(cancellationToken); - await next(invocation); + await next(invocation, cancellationToken); - await host.StopAsync(); + await host.StopAsync(cancellationToken); }); public static CommandLineBuilder UseHost(this CommandLineBuilder builder, diff --git a/src/System.CommandLine.Hosting/InvocationLifetime.cs b/src/System.CommandLine.Hosting/InvocationLifetime.cs index 80798191d7..7cd7477c43 100644 --- a/src/System.CommandLine.Hosting/InvocationLifetime.cs +++ b/src/System.CommandLine.Hosting/InvocationLifetime.cs @@ -1,7 +1,6 @@ // 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.Invocation; using System.Threading; using System.Threading.Tasks; @@ -19,7 +18,6 @@ namespace System.CommandLine.Hosting { public class InvocationLifetime : IHostLifetime { - private readonly CancellationToken invokeCancelToken; private CancellationTokenRegistration invokeCancelReg; private CancellationTokenRegistration appStartedReg; private CancellationTokenRegistration appStoppingReg; @@ -28,7 +26,6 @@ public InvocationLifetime( IOptions options, IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, - InvocationContext context = null, ILoggerFactory loggerFactory = null) { Options = options?.Value ?? throw new ArgumentNullException(nameof(options)); @@ -37,11 +34,6 @@ public InvocationLifetime( ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime)); - // if InvocationLifetime is added outside of a System.CommandLine - // invocation pipeline context will be null. - // Use default cancellation token instead, and become a noop lifetime. - invokeCancelToken = context?.GetCancellationToken() ?? default; - Logger = (loggerFactory ?? NullLoggerFactory.Instance) .CreateLogger("Microsoft.Hosting.Lifetime"); } @@ -65,7 +57,9 @@ public Task WaitForStartAsync(CancellationToken cancellationToken) }, this); } - invokeCancelReg = invokeCancelToken.Register(state => + // The token comes from HostingExtensions.UseHost middleware + // and it's the invocation cancellation token. + invokeCancelReg = cancellationToken.Register(state => { ((InvocationLifetime)state).OnInvocationCancelled(); }, this); diff --git a/src/System.CommandLine.NamingConventionBinder.Tests/ModelBindingCommandHandlerTests.BindingByName.cs b/src/System.CommandLine.NamingConventionBinder.Tests/ModelBindingCommandHandlerTests.BindingByName.cs index e3fd786c39..2c5625d20b 100644 --- a/src/System.CommandLine.NamingConventionBinder.Tests/ModelBindingCommandHandlerTests.BindingByName.cs +++ b/src/System.CommandLine.NamingConventionBinder.Tests/ModelBindingCommandHandlerTests.BindingByName.cs @@ -6,6 +6,7 @@ using System.CommandLine.Tests.Binding; using System.CommandLine.Utility; using System.IO; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Xunit; @@ -41,7 +42,8 @@ public async Task Option_arguments_are_bound_by_name_to_method_parameters( var console = new TestConsole(); await handler.InvokeAsync( - new InvocationContext(command.Parse(commandLine), console)); + new InvocationContext(command.Parse(commandLine), console), + CancellationToken.None); console.Out.ToString().Should().Be(expectedValue.ToString()); } @@ -73,7 +75,8 @@ public async Task Option_arguments_are_bound_by_name_to_the_properties_of_method var console = new TestConsole(); await handler.InvokeAsync( - new InvocationContext(command.Parse(commandLine), console)); + new InvocationContext(command.Parse(commandLine), console), + CancellationToken.None); console.Out.ToString().Should().Be($"ClassWithSetter<{type.Name}>: {expectedValue}"); } @@ -105,7 +108,8 @@ public async Task Option_arguments_are_bound_by_name_to_the_constructor_paramete var console = new TestConsole(); await handler.InvokeAsync( - new InvocationContext(command.Parse(commandLine), console)); + new InvocationContext(command.Parse(commandLine), console), + CancellationToken.None); console.Out.ToString().Should().Be($"ClassWithCtorParameter<{type.Name}>: {expectedValue}"); } @@ -133,7 +137,7 @@ public async Task Command_arguments_are_bound_by_name_to_handler_method_paramete var console = new TestConsole(); await handler.InvokeAsync( - new InvocationContext(command.Parse(commandLine), console)); + new InvocationContext(command.Parse(commandLine), console), CancellationToken.None); console.Out.ToString().Should().Be(expectedValue.ToString()); } diff --git a/src/System.CommandLine.NamingConventionBinder.Tests/ModelBindingCommandHandlerTests.cs b/src/System.CommandLine.NamingConventionBinder.Tests/ModelBindingCommandHandlerTests.cs index c50e566f65..14d8748525 100644 --- a/src/System.CommandLine.NamingConventionBinder.Tests/ModelBindingCommandHandlerTests.cs +++ b/src/System.CommandLine.NamingConventionBinder.Tests/ModelBindingCommandHandlerTests.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Execution; @@ -44,7 +45,7 @@ public async Task Unspecified_option_arguments_with_no_default_value_are_bound_t var invocationContext = new InvocationContext(parseResult); - await handler.InvokeAsync(invocationContext); + await handler.InvokeAsync(invocationContext, CancellationToken.None); BoundValueCapturer.GetBoundValue(invocationContext).Should().Be(expectedValue); } @@ -121,7 +122,7 @@ public async Task Handler_method_receives_option_arguments_bound_to_the_specifie var invocationContext = new InvocationContext(parseResult); - await handler.InvokeAsync(invocationContext); + await handler.InvokeAsync(invocationContext, CancellationToken.None); var boundValue = BoundValueCapturer.GetBoundValue(invocationContext); @@ -184,7 +185,7 @@ public async Task Handler_method_receives_command_arguments_bound_to_the_specifi var invocationContext = new InvocationContext(parseResult); - await handler.InvokeAsync(invocationContext); + await handler.InvokeAsync(invocationContext, CancellationToken.None); var boundValue = BoundValueCapturer.GetBoundValue(invocationContext); @@ -236,7 +237,7 @@ public async Task Handler_method_receives_command_arguments_explicitly_bound_to_ var invocationContext = new InvocationContext(parseResult); - await handler.InvokeAsync(invocationContext); + await handler.InvokeAsync(invocationContext, CancellationToken.None); var boundValue = BoundValueCapturer.GetBoundValue(invocationContext); @@ -289,7 +290,7 @@ public async Task Handler_method_receive_option_arguments_explicitly_bound_to_th var invocationContext = new InvocationContext(parseResult); - await handler.InvokeAsync(invocationContext); + await handler.InvokeAsync(invocationContext, CancellationToken.None); var boundValue = BoundValueCapturer.GetBoundValue(invocationContext); diff --git a/src/System.CommandLine.NamingConventionBinder.Tests/ParameterBindingTests.cs b/src/System.CommandLine.NamingConventionBinder.Tests/ParameterBindingTests.cs index ae4e8bca79..fd104c8e0b 100644 --- a/src/System.CommandLine.NamingConventionBinder.Tests/ParameterBindingTests.cs +++ b/src/System.CommandLine.NamingConventionBinder.Tests/ParameterBindingTests.cs @@ -7,6 +7,7 @@ using System.CommandLine.IO; using System.CommandLine.Parsing; using System.IO; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Xunit; @@ -447,9 +448,9 @@ public abstract class AbstractTestCommandHandler : ICommandHandler { public abstract Task DoJobAsync(); - public int Invoke(InvocationContext context) => InvokeAsync(context).GetAwaiter().GetResult(); + public int Invoke(InvocationContext context) => InvokeAsync(context, CancellationToken.None).GetAwaiter().GetResult(); - public Task InvokeAsync(InvocationContext context) + public Task InvokeAsync(InvocationContext context, CancellationToken cancellationToken) => DoJobAsync(); } @@ -463,13 +464,13 @@ public class VirtualTestCommandHandler : ICommandHandler { public int Invoke(InvocationContext context) => 42; - public virtual Task InvokeAsync(InvocationContext context) + public virtual Task InvokeAsync(InvocationContext context, CancellationToken cancellationToken) => Task.FromResult(42); } public class OverridenVirtualTestCommandHandler : VirtualTestCommandHandler { - public override Task InvokeAsync(InvocationContext context) + public override Task InvokeAsync(InvocationContext context, CancellationToken cancellationToken) => Task.FromResult(41); } } \ No newline at end of file diff --git a/src/System.CommandLine.NamingConventionBinder/ModelBindingCommandHandler.cs b/src/System.CommandLine.NamingConventionBinder/ModelBindingCommandHandler.cs index 451cb1591a..97bf39bae4 100644 --- a/src/System.CommandLine.NamingConventionBinder/ModelBindingCommandHandler.cs +++ b/src/System.CommandLine.NamingConventionBinder/ModelBindingCommandHandler.cs @@ -6,6 +6,7 @@ using System.CommandLine.Invocation; using System.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; namespace System.CommandLine.NamingConventionBinder; @@ -54,8 +55,9 @@ internal ModelBindingCommandHandler( /// Binds values for the underlying user-defined method and uses them to invoke that method. /// /// The current invocation context. + /// A token that can be used to cancel the invocation. /// A task whose value can be used to set the process exit code. - public async Task InvokeAsync(InvocationContext context) + public async Task InvokeAsync(InvocationContext context, CancellationToken cancellationToken = default) { var bindingContext = context.BindingContext; @@ -130,5 +132,5 @@ private void BindValueSource(ParameterInfo param, IValueSource valueSource) x.ValueType == param.ParameterType); /// - public int Invoke(InvocationContext context) => InvokeAsync(context).GetAwaiter().GetResult(); + public int Invoke(InvocationContext context) => InvokeAsync(context, CancellationToken.None).GetAwaiter().GetResult(); } \ No newline at end of file diff --git a/src/System.CommandLine.Rendering.Tests/TerminalModeTests.cs b/src/System.CommandLine.Rendering.Tests/TerminalModeTests.cs index bf6caf42cb..6f068f63a9 100644 --- a/src/System.CommandLine.Rendering.Tests/TerminalModeTests.cs +++ b/src/System.CommandLine.Rendering.Tests/TerminalModeTests.cs @@ -42,7 +42,7 @@ public async Task Sets_output_mode_to_Ansi_when_specified_by_output_directive(Ou OutputMode detectedOutputMode = OutputMode.Auto; var command = new Command("hello"); - command.SetHandler(ctx => + command.SetHandler((ctx, cancellationToken) => { detectedOutputMode = ctx.Console.DetectOutputMode(); return Task.FromResult(0); diff --git a/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/Program.cs b/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/Program.cs index b45c16929e..02f4da77fd 100644 --- a/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/Program.cs +++ b/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/Program.cs @@ -1,6 +1,7 @@ using System.CommandLine; using System.CommandLine.Parsing; using System.Threading.Tasks; +using System.Threading; namespace EndToEndTestApp { @@ -22,7 +23,7 @@ static async Task Main(string[] args) }; rootCommand.SetHandler( - (string apple, string banana, string cherry, string durian) => Task.CompletedTask, + (string apple, string banana, string cherry, string durian, CancellationToken cancellationToken) => Task.CompletedTask, appleOption, bananaOption, cherryOption, diff --git a/src/System.CommandLine.Suggest/SuggestionDispatcher.cs b/src/System.CommandLine.Suggest/SuggestionDispatcher.cs index 6c36afb9e8..4f2e6a7df1 100644 --- a/src/System.CommandLine.Suggest/SuggestionDispatcher.cs +++ b/src/System.CommandLine.Suggest/SuggestionDispatcher.cs @@ -7,6 +7,7 @@ using System.CommandLine.Parsing; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace System.CommandLine.Suggest @@ -40,7 +41,7 @@ public SuggestionDispatcher(ISuggestionRegistration suggestionRegistration, ISug { Description = "Lists apps registered for suggestions", }; - ListCommand.SetHandler(ctx => + ListCommand.SetHandler((ctx, cancellationToken) => { ctx.Console.Out.WriteLine(ShellPrefixesToMatch(_suggestionRegistration)); return Task.FromResult(0); @@ -61,7 +62,7 @@ public SuggestionDispatcher(ISuggestionRegistration suggestionRegistration, ISug new Option("--suggestion-command", "The command to invoke to retrieve suggestions") }; - RegisterCommand.SetHandler(context => + RegisterCommand.SetHandler((context, cancellationToken) => { Register(context.ParseResult.GetValue(commandPathOption), context.Console); return Task.FromResult(0); diff --git a/src/System.CommandLine.Tests/Binding/SetHandlerTests.cs b/src/System.CommandLine.Tests/Binding/SetHandlerTests.cs index 2a546b900d..5b421460a9 100644 --- a/src/System.CommandLine.Tests/Binding/SetHandlerTests.cs +++ b/src/System.CommandLine.Tests/Binding/SetHandlerTests.cs @@ -4,7 +4,9 @@ using System.Collections.Generic; using System.CommandLine.Binding; using System.CommandLine.IO; +using System.CommandLine.Parsing; using System.Linq; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Xunit; @@ -184,29 +186,29 @@ public void Binding_is_correct_for_Func_overload_having_arity_(int arity) var receivedValues = new List(); Delegate handlerFunc = arity switch { - 1 => new Func( - i1 => + 1 => new Func( + (i1, cancellationToken) => Received(i1)), - 2 => new Func( - (i1, i2) => + 2 => new Func( + (i1, i2, cancellationToken) => Received(i1, i2)), - 3 => new Func( - (i1, i2, i3) => + 3 => new Func( + (i1, i2, i3, cancellationToken) => Received(i1, i2, i3)), - 4 => new Func( - (i1, i2, i3, i4) => + 4 => new Func( + (i1, i2, i3, i4, cancellationToken) => Received(i1, i2, i3, i4)), - 5 => new Func( - (i1, i2, i3, i4, i5) => + 5 => new Func( + (i1, i2, i3, i4, i5, cancellationToken) => Received(i1, i2, i3, i4, i5)), - 6 => new Func( - (i1, i2, i3, i4, i5, i6) => + 6 => new Func( + (i1, i2, i3, i4, i5, i6, cancellationToken) => Received(i1, i2, i3, i4, i5, i6)), - 7 => new Func( - (i1, i2, i3, i4, i5, i6, i7) => + 7 => new Func( + (i1, i2, i3, i4, i5, i6, i7, cancellationToken) => Received(i1, i2, i3, i4, i5, i6, i7)), - 8 => new Func( - (i1, i2, i3, i4, i5, i6, i7, i8) => + 8 => new Func( + (i1, i2, i3, i4, i5, i6, i7, i8, cancellationToken) => Received(i1, i2, i3, i4, i5, i6, i7, i8)), _ => throw new ArgumentOutOfRangeException() @@ -257,7 +259,7 @@ public async Task Unexpected_return_types_result_in_exit_code_0_if_no_exception_ var command = new Command("wat"); - var handle = () => + var handle = (CancellationToken cancellationToken) => { wasCalled = true; return Task.FromResult(new { NovelType = true }); @@ -269,5 +271,36 @@ public async Task Unexpected_return_types_result_in_exit_code_0_if_no_exception_ wasCalled.Should().BeTrue(); exitCode.Should().Be(0); } + + [Fact] + public async Task When_User_Requests_Cancellation_Its_Reflected_By_The_Token_Passed_To_Handler() + { + const int ExpectedExitCode = 123; + + Command command = new ("the-command"); + command.SetHandler(async (context, cancellationToken) => + { + try + { + await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + context.ExitCode = ExpectedExitCode * -1; + } + catch (OperationCanceledException) + { + context.ExitCode = ExpectedExitCode; + } + }); + + using CancellationTokenSource cts = new (); + + Parser parser = new CommandLineBuilder(new RootCommand { command }) + .Build(); + + Task invokeResult = parser.InvokeAsync("the-command", null, cts.Token); + + cts.Cancel(); + + (await invokeResult).Should().Be(ExpectedExitCode); + } } } \ No newline at end of file diff --git a/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs b/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs index aaae8d2b2b..6e62a9c432 100644 --- a/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs +++ b/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs @@ -2,11 +2,11 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using FluentAssertions; -using System.CommandLine.Invocation; using System.CommandLine.Parsing; using System.CommandLine.Tests.Utility; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Xunit; using Process = System.Diagnostics.Process; @@ -15,222 +15,76 @@ namespace System.CommandLine.Tests.Invocation { public class CancelOnProcessTerminationTests { - private const int SIGINT = 2; - private const int SIGTERM = 15; + private const string ChildProcessWaiting = "Waiting for the command to be cancelled"; + private const int SIGINT_EXIT_CODE = 130; + private const int SIGTERM_EXIT_CODE = 143; + private const int GracefulExitCode = 42; - [LinuxOnlyTheory] - [InlineData(SIGINT/*, Skip = "https://github.com/dotnet/command-line-api/issues/1206"*/)] // Console.CancelKeyPress - [InlineData(SIGTERM)] // AppDomain.CurrentDomain.ProcessExit - public async Task CancelOnProcessTermination_provides_CancellationToken_that_signals_termination_when_no_timeout_is_specified(int signo) + public enum Signals { - const string ChildProcessWaiting = "Waiting for the command to be cancelled"; - const int CancelledExitCode = 42; - - Func> childProgram = (string[] args) => - { - var command = new Command("the-command"); - - command.SetHandler(async context => - { - var cancellationToken = context.GetCancellationToken(); - - try - { - context.Console.WriteLine(ChildProcessWaiting); - await Task.Delay(int.MaxValue, cancellationToken); - context.ExitCode = 1; - } - 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(); - - context.ExitCode = CancelledExitCode; - } - - }); - - return new CommandLineBuilder(new RootCommand - { - command - }) - .CancelOnProcessTermination() - .Build() - .InvokeAsync("the-command"); - }; - - using RemoteExecution program = RemoteExecutor.Execute(childProgram, psi: new ProcessStartInfo { RedirectStandardOutput = true }); - - Process process = program.Process; - - // Wait for the child to be in the command handler. - string childState = await process.StandardOutput.ReadLineAsync(); - childState.Should().Be(ChildProcessWaiting); - - // Request termination - kill(process.Id, signo).Should().Be(0); + SIGINT = 2, // Console.CancelKeyPress + SIGTERM = 15 // AppDomain.CurrentDomain.ProcessExit + } - // Verify the process terminates timely - bool processExited = process.WaitForExit(10000); - if (!processExited) + [Fact] + public async Task CancellableHandler_is_cancelled_on_process_termination() + { + // The feature is supported on Windows, but it's simply harder to send SIGINT to test it properly. + // Same for macOS, where RemoteExecutor does not support getting application arguments. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - process.Kill(); - process.WaitForExit(); + await StartKillAndVerify(new[] { "--infiniteDelay", "false" }, Signals.SIGINT, GracefulExitCode); } - processExited.Should().Be(true); - - // Verify the process exit code - process.ExitCode.Should().Be(CancelledExitCode); } - [LinuxOnlyTheory] - [InlineData(SIGINT)] - [InlineData(SIGTERM)] - public async Task CancelOnProcessTermination_provides_CancellationToken_that_signals_termination_when_null_timeout_is_specified(int signo) + [Fact] + public async Task NonCancellableHandler_is_interrupted_on_process_termination() { - const string ChildProcessWaiting = "Waiting for the command to be cancelled"; - const int CancelledExitCode = 42; - - Func> childProgram = (string[] args) => + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - var command = new Command("the-command"); - - command.SetHandler(async context => - { - var cancellationToken = context.GetCancellationToken(); - - try - { - context.Console.WriteLine(ChildProcessWaiting); - await Task.Delay(int.MaxValue, cancellationToken); - context.ExitCode = 1; - } - 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(); - - // Exit code gets set here - but then execution continues and is let run till code voluntarily returns - // hence exit code gets overwritten below - context.ExitCode = 123; - } - - // This is an example of bad pattern and reason why we need a timeout on termination processing - await Task.Delay(TimeSpan.FromMilliseconds(200)); - - context.ExitCode = CancelledExitCode; - }); - - return new CommandLineBuilder(new RootCommand - { - command - }) - // Unfortunately we cannot use test parameter here - RemoteExecutor currently doesn't capture the closure - .CancelOnProcessTermination(null) - .Build() - .InvokeAsync("the-command"); - }; - - using RemoteExecution program = RemoteExecutor.Execute(childProgram, psi: new ProcessStartInfo { RedirectStandardOutput = true }); - - Process process = program.Process; - - // Wait for the child to be in the command handler. - string childState = await process.StandardOutput.ReadLineAsync(); - childState.Should().Be(ChildProcessWaiting); - - // Request termination - kill(process.Id, signo).Should().Be(0); - - // Verify the process terminates timely - bool processExited = process.WaitForExit(10000); - if (!processExited) - { - process.Kill(); - process.WaitForExit(); + await StartKillAndVerify(new[] { "--infiniteDelay", "true" }, Signals.SIGTERM, SIGTERM_EXIT_CODE); } - processExited.Should().Be(true); - - // Verify the process exit code - process.ExitCode.Should().Be(CancelledExitCode); } - [LinuxOnlyTheory] - [InlineData(SIGINT)] - [InlineData(SIGTERM)] - public async Task CancelOnProcessTermination_provides_CancellationToken_that_signals_termination_and_execution_is_terminated_at_the_specified_timeout(int signo) + private static Task Program(string[] args) { - const string ChildProcessWaiting = "Waiting for the command to be cancelled"; - const int CancelledExitCode = 42; - const int ForceTerminationCode = 130; - - Func> childProgram = (string[] args) => + Option infiniteDelayOption = new ("--infiniteDelay"); + RootCommand command = new () { - var command = new Command("the-command"); - - command.SetHandler(async context => - { - var cancellationToken = context.GetCancellationToken(); - - try - { - context.Console.WriteLine(ChildProcessWaiting); - await Task.Delay(int.MaxValue, cancellationToken); - context.ExitCode = 1; - } - 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(); - - context.ExitCode = CancelledExitCode; - - // This is an example of bad pattern and reason why we need a timeout on termination processing - await Task.Delay(TimeSpan.FromMilliseconds(1000)); - - // Execution should newer get here as termination processing has a timeout of 100ms - Environment.Exit(123); - } - - // This is an example of bad pattern and reason why we need a timeout on termination processing - await Task.Delay(TimeSpan.FromMilliseconds(1000)); + infiniteDelayOption + }; - // Execution should newer get here as termination processing has a timeout of 100ms - Environment.Exit(123); - }); + command.SetHandler(async (context, cancellationToken) => + { + context.Console.WriteLine(ChildProcessWaiting); - return new CommandLineBuilder(new RootCommand - { - command - }) - // Unfortunately we cannot use test parameter here - RemoteExecutor currently doesn't capture the closure - .CancelOnProcessTermination(TimeSpan.FromMilliseconds(100)) - .Build() - .InvokeAsync("the-command"); - }; + bool infiniteDelay = context.GetValue(infiniteDelayOption); - using RemoteExecution program = RemoteExecutor.Execute(childProgram, psi: new ProcessStartInfo { RedirectStandardOutput = true }); + try + { + // Passing CancellationToken.None here is an example of bad pattern + // and reason why we need a timeout on termination processing. + CancellationToken token = infiniteDelay ? CancellationToken.None : cancellationToken; + await Task.Delay(Timeout.InfiniteTimeSpan, token); + } + catch (OperationCanceledException) + { + context.ExitCode = GracefulExitCode; + } + }); + + return new CommandLineBuilder(command) + .CancelOnProcessTermination() + .Build() + .InvokeAsync(args); + } + + private async Task StartKillAndVerify(string[] args, Signals signal, int expectedExitCode) + { + using RemoteExecution program = RemoteExecutor.Execute( + Program, + args, + new ProcessStartInfo { RedirectStandardOutput = true }); Process process = program.Process; @@ -239,22 +93,19 @@ public async Task CancelOnProcessTermination_provides_CancellationToken_that_sig childState.Should().Be(ChildProcessWaiting); // Request termination - kill(process.Id, signo).Should().Be(0); + kill(process.Id, (int)signal).Should().Be(0); // Verify the process terminates timely - bool processExited = process.WaitForExit(10000); - if (!processExited) + if (!process.WaitForExit((int)TimeSpan.FromSeconds(10).TotalMilliseconds)) { process.Kill(); - process.WaitForExit(); } - processExited.Should().Be(true); // Verify the process exit code - process.ExitCode.Should().Be(ForceTerminationCode); + process.ExitCode.Should().Be(expectedExitCode); + + [DllImport("libc", SetLastError = true)] + static extern int kill(int pid, int sig); } - - [DllImport("libc", SetLastError = true)] - private static extern int kill(int pid, int sig); } -} +} \ No newline at end of file diff --git a/src/System.CommandLine.Tests/Invocation/InvocationContextTests.cs b/src/System.CommandLine.Tests/Invocation/InvocationContextTests.cs deleted file mode 100644 index 5e168e7191..0000000000 --- a/src/System.CommandLine.Tests/Invocation/InvocationContextTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -using FluentAssertions; -using System.CommandLine; -using System.CommandLine.Invocation; -using System.CommandLine.Parsing; -using System.Threading; -using Xunit; - -namespace System.CommandLine.Tests.Invocation -{ - public class InvocationContextTests - { - [Fact] - public void InvocationContext_with_cancellation_token_returns_it() - { - using CancellationTokenSource cts = new(); - var parseResult = new CommandLineBuilder(new RootCommand()) - .Build() - .Parse(""); - using InvocationContext context = new(parseResult, cancellationToken: cts.Token); - - var token = context.GetCancellationToken(); - - token.IsCancellationRequested.Should().BeFalse(); - cts.Cancel(); - token.IsCancellationRequested.Should().BeTrue(); - } - - [Fact] - public void InvocationContext_with_linked_cancellation_token_can_cancel_by_passed_token() - { - using CancellationTokenSource cts1 = new(); - using CancellationTokenSource cts2 = new(); - var parseResult = new CommandLineBuilder(new RootCommand()) - .Build() - .Parse(""); - using InvocationContext context = new(parseResult, cancellationToken: cts1.Token); - context.LinkToken(cts2.Token); - - var token = context.GetCancellationToken(); - - token.IsCancellationRequested.Should().BeFalse(); - cts1.Cancel(); - token.IsCancellationRequested.Should().BeTrue(); - } - - [Fact] - public void InvocationContext_with_linked_cancellation_token_can_cancel_by_linked_token() - { - using CancellationTokenSource cts1 = new(); - using CancellationTokenSource cts2 = new(); - var parseResult = new CommandLineBuilder(new RootCommand()) - .Build() - .Parse(""); - using InvocationContext context = new(parseResult, cancellationToken: cts1.Token); - context.LinkToken(cts2.Token); - - var token = context.GetCancellationToken(); - - token.IsCancellationRequested.Should().BeFalse(); - cts2.Cancel(); - token.IsCancellationRequested.Should().BeTrue(); - } - } -} diff --git a/src/System.CommandLine.Tests/Invocation/InvocationExtensionsTests.cs b/src/System.CommandLine.Tests/Invocation/InvocationExtensionsTests.cs index 4f6d2e7361..9308314cc1 100644 --- a/src/System.CommandLine.Tests/Invocation/InvocationExtensionsTests.cs +++ b/src/System.CommandLine.Tests/Invocation/InvocationExtensionsTests.cs @@ -80,7 +80,7 @@ public async Task RootCommand_InvokeAsync_returns_1_when_handler_throws() var wasCalled = false; var rootCommand = new RootCommand(); - rootCommand.SetHandler(() => + rootCommand.SetHandler(_ => { wasCalled = true; throw new Exception("oops!"); @@ -103,7 +103,7 @@ public void RootCommand_Invoke_returns_1_when_handler_throws() var wasCalled = false; var rootCommand = new RootCommand(); - rootCommand.SetHandler(() => + rootCommand.SetHandler(_ => { wasCalled = true; throw new Exception("oops!"); @@ -125,7 +125,7 @@ public async Task RootCommand_InvokeAsync_can_set_custom_result_code() { var rootCommand = new RootCommand(); - rootCommand.SetHandler(context => + rootCommand.SetHandler((context, cancellationToken) => { context.ExitCode = 123; return Task.CompletedTask; @@ -141,7 +141,7 @@ public void RootCommand_Invoke_can_set_custom_result_code() { var rootCommand = new RootCommand(); - rootCommand.SetHandler(context => + rootCommand.SetHandler((context, cancellationToken) => { context.ExitCode = 123; return Task.CompletedTask; @@ -157,9 +157,8 @@ public async Task Command_InvokeAsync_with_cancelation_token_invokes_command_han { CancellationTokenSource cts = new(); var command = new Command("test"); - command.SetHandler((InvocationContext context) => + command.SetHandler((InvocationContext context, CancellationToken cancellationToken) => { - CancellationToken cancellationToken = context.GetCancellationToken(); Assert.True(cancellationToken.CanBeCanceled); if (cancellationToken.IsCancellationRequested) { diff --git a/src/System.CommandLine.Tests/Invocation/InvocationPipelineTests.cs b/src/System.CommandLine.Tests/Invocation/InvocationPipelineTests.cs index d67eda24d4..c19f899f3f 100644 --- a/src/System.CommandLine.Tests/Invocation/InvocationPipelineTests.cs +++ b/src/System.CommandLine.Tests/Invocation/InvocationPipelineTests.cs @@ -120,7 +120,7 @@ public void When_middleware_throws_then_Invoke_does_not_handle_the_exception() public void When_command_handler_throws_then_InvokeAsync_does_not_handle_the_exception() { var command = new Command("the-command"); - command.SetHandler(() => + command.SetHandler(_ => { throw new Exception("oops!"); // Help the compiler pick a CommandHandler.Create overload. @@ -149,7 +149,7 @@ public void When_command_handler_throws_then_InvokeAsync_does_not_handle_the_exc public void When_command_handler_throws_then_Invoke_does_not_handle_the_exception() { var command = new Command("the-command"); - command.SetHandler(() => + command.SetHandler(_ => { throw new Exception("oops!"); // Help the compiler pick a CommandHandler.Create overload. @@ -181,7 +181,7 @@ public async Task ParseResult_can_be_replaced_by_middleware() var command = new Command("the-command"); var implicitInnerCommand = new Command("implicit-inner-command"); command.Subcommands.Add(implicitInnerCommand); - implicitInnerCommand.SetHandler(context => + implicitInnerCommand.SetHandler((context, cancellationToken) => { wasCalled = true; context.ParseResult.Errors.Should().BeEmpty(); @@ -192,7 +192,7 @@ public async Task ParseResult_can_be_replaced_by_middleware() { command }) - .AddMiddleware(async (context, next) => + .AddMiddleware(async (context, token, next) => { var tokens = context.ParseResult .Tokens @@ -201,7 +201,7 @@ public async Task ParseResult_can_be_replaced_by_middleware() .ToArray(); context.ParseResult = context.Parser.Parse(tokens); - await next(context); + await next(context, token); }) .Build(); @@ -217,7 +217,7 @@ public async Task Invocation_can_be_short_circuited_by_middleware_by_not_calling var handlerWasCalled = false; var command = new Command("the-command"); - command.SetHandler(context => + command.SetHandler((context, cancellationToken) => { handlerWasCalled = true; context.ParseResult.Errors.Should().BeEmpty(); @@ -228,7 +228,7 @@ public async Task Invocation_can_be_short_circuited_by_middleware_by_not_calling { command }) - .AddMiddleware(async (_, _) => + .AddMiddleware(async (_, _, _) => { middlewareWasCalled = true; await Task.Yield(); @@ -248,7 +248,7 @@ public void Synchronous_invocation_can_be_short_circuited_by_async_middleware_by var handlerWasCalled = false; var command = new Command("the-command"); - command.SetHandler(context => + command.SetHandler((context, cancellationToken) => { handlerWasCalled = true; context.ParseResult.Errors.Should().BeEmpty(); @@ -259,7 +259,7 @@ public void Synchronous_invocation_can_be_short_circuited_by_async_middleware_by { command }) - .AddMiddleware(async (context, next) => + .AddMiddleware(async (context, cancellationToken, next) => { middlewareWasCalled = true; await Task.Yield(); @@ -278,7 +278,7 @@ public async Task When_no_help_builder_is_specified_it_uses_default_implementati bool handlerWasCalled = false; var command = new Command("help-command"); - command.SetHandler(context => + command.SetHandler((context, cancellationToken) => { handlerWasCalled = true; context.HelpBuilder.Should().NotBeNull(); @@ -305,7 +305,7 @@ public async Task When_help_builder_factory_is_specified_it_is_used_to_create_th HelpBuilder createdHelpBuilder = null; var command = new Command("help-command"); - command.SetHandler(context => + command.SetHandler((context, cancellationToken) => { handlerWasCalled = true; context.HelpBuilder.Should().Be(createdHelpBuilder); @@ -331,37 +331,33 @@ public async Task When_help_builder_factory_is_specified_it_is_used_to_create_th } [Fact] - public async Task Command_InvokeAsync_can_cancel_from_middleware() + public async Task Middleware_can_throw_OperationCancelledException() { var handlerWasCalled = false; - var isCancelRequested = false; var command = new Command("the-command"); - command.SetHandler((InvocationContext context) => + command.SetHandler((InvocationContext context, CancellationToken cancellationToken) => { handlerWasCalled = true; - isCancelRequested = context.GetCancellationToken().IsCancellationRequested; return Task.FromResult(0); }); - - using CancellationTokenSource cts = new(); var parser = new CommandLineBuilder(new RootCommand { command }) - .AddMiddleware(async (context, next) => + .AddMiddleware((context, cancellationToken, next) => { - context.LinkToken(cts.Token); - cts.Cancel(); - await next(context); + throw new OperationCanceledException(cancellationToken); }) + .UseExceptionHandler((ex, ctx) => ctx.ExitCode = ex is OperationCanceledException ? 123 : 456) .Build(); - await parser.InvokeAsync("the-command"); + int result = await parser.InvokeAsync("the-command"); - handlerWasCalled.Should().BeTrue(); - isCancelRequested.Should().BeTrue(); + // when the middleware throws, we never get to handler + handlerWasCalled.Should().BeFalse(); + result.Should().Be(123); } } } diff --git a/src/System.CommandLine.Tests/UseExceptionHandlerTests.cs b/src/System.CommandLine.Tests/UseExceptionHandlerTests.cs index d0e3c1d93b..04bf66f321 100644 --- a/src/System.CommandLine.Tests/UseExceptionHandlerTests.cs +++ b/src/System.CommandLine.Tests/UseExceptionHandlerTests.cs @@ -53,7 +53,7 @@ public async Task UseExceptionHandler_catches_middleware_exceptions_and_writes_d public async Task UseExceptionHandler_catches_command_handler_exceptions_and_sets_result_code_to_1() { var command = new Command("the-command"); - command.SetHandler(() => + command.SetHandler(_ => { throw new Exception("oops!"); // Help the compiler pick a CommandHandler.Create overload. @@ -78,7 +78,7 @@ public async Task UseExceptionHandler_catches_command_handler_exceptions_and_set public async Task UseExceptionHandler_catches_command_handler_exceptions_and_writes_details_to_standard_error() { var command = new Command("the-command"); - command.SetHandler(() => + command.SetHandler(_ => { throw new Exception("oops!"); // Help the compiler pick a CommandHandler.Create overload. diff --git a/src/System.CommandLine/Binding/BindingContext.cs b/src/System.CommandLine/Binding/BindingContext.cs index 435decc4f2..aa6144aec3 100644 --- a/src/System.CommandLine/Binding/BindingContext.cs +++ b/src/System.CommandLine/Binding/BindingContext.cs @@ -21,7 +21,6 @@ internal BindingContext(InvocationContext invocationContext) InvocationContext = invocationContext; ServiceProvider = new ServiceProvider(this); ServiceProvider.AddService(_ => InvocationContext); - ServiceProvider.AddService(_ => InvocationContext.GetCancellationToken()); } internal InvocationContext InvocationContext { get; } diff --git a/src/System.CommandLine/Builder/CommandLineBuilder.cs b/src/System.CommandLine/Builder/CommandLineBuilder.cs index 956663acc4..d45654bbc8 100644 --- a/src/System.CommandLine/Builder/CommandLineBuilder.cs +++ b/src/System.CommandLine/Builder/CommandLineBuilder.cs @@ -41,6 +41,8 @@ public class CommandLineBuilder /// internal Action? ExceptionHandler; + internal TimeSpan? ProcessTerminationTimeout; + // for every generic type with type argument being struct JIT needs to compile a dedicated version // (because each struct is of a different size) // that is why we don't use List for middleware @@ -112,6 +114,7 @@ public Parser Build() => parseErrorReportingExitCode: ParseErrorReportingExitCode, maxLevenshteinDistance: MaxLevenshteinDistance, exceptionHandler: ExceptionHandler, + processTerminationTimeout: ProcessTerminationTimeout, resources: LocalizationResources, middlewarePipeline: _middlewareList is null ? Array.Empty() diff --git a/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs b/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs index 3ee71b811d..9d6a3c9821 100644 --- a/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs +++ b/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs @@ -26,7 +26,7 @@ public static class CommandLineBuilderExtensions /// A command line builder. /// /// Optional timeout for the command to process the exit cancellation. - /// If not passed, or passed null or non-positive timeout (including ), no timeout is enforced. + /// If not passed, a default timeout of 2 seconds is enforced. /// If positive value is passed - command is forcefully terminated after the timeout with exit code 130 (as if was not called). /// Host enforced timeout for ProcessExit event cannot be extended - default is 2 seconds: https://docs.microsoft.com/en-us/dotnet/api/system.appdomain.processexit?view=net-6.0. /// @@ -35,87 +35,7 @@ public static CommandLineBuilder CancelOnProcessTermination( this CommandLineBuilder builder, TimeSpan? timeout = null) { - // https://tldp.org/LDP/abs/html/exitcodes.html - 130 - script terminated by ctrl-c - const int SIGINT_EXIT_CODE = 130; - - if (timeout == null || timeout.Value < TimeSpan.Zero) - { - timeout = Timeout.InfiniteTimeSpan; - } - - builder.AddMiddleware(async (context, next) => - { - ConsoleCancelEventHandler? consoleHandler = null; - EventHandler? processExitHandler = null; - ManualResetEventSlim blockProcessExit = new(initialState: false); - - processExitHandler = (_, _) => - { - // Cancel asynchronously not to block the handler (as then the process might possibly run longer then what was the requested timeout) - Task timeoutTask = Task.Delay(timeout.Value); - Task cancelTask = Task.Factory.StartNew(context.Cancel); - - // The process exits as soon as the event handler returns. - // We provide a return value using Environment.ExitCode - // because Main will not finish executing. - // Wait for the invocation to finish. - if (!blockProcessExit.Wait(timeout > TimeSpan.Zero - ? timeout.Value - : Timeout.InfiniteTimeSpan)) - { - context.ExitCode = SIGINT_EXIT_CODE; - } - // Let's block here (to prevent process bailing out) for the rest of the timeout (if any), for cancellation to finish (if it hasn't yet) - else if (Task.WaitAny(timeoutTask, cancelTask) == 0) - { - // The async cancellation didn't finish in timely manner - context.ExitCode = SIGINT_EXIT_CODE; - } - ExitCode = context.ExitCode; - }; - // Default limit for ProcesExit handler is 2 seconds - // https://docs.microsoft.com/en-us/dotnet/api/system.appdomain.processexit?view=net-6.0 - consoleHandler = (_, args) => - { - // Stop the process from terminating. - // Since the context was cancelled, the invocation should - // finish and Main will return. - args.Cancel = true; - - // If timeout was requested - make sure cancellation processing (or any other activity within the current process) - // doesn't keep the process running after the timeout - if (timeout! > TimeSpan.Zero) - { - Task - .Delay(timeout.Value, default) - .ContinueWith(t => - { - // Prevent our ProcessExit from intervene and block the exit - AppDomain.CurrentDomain.ProcessExit -= processExitHandler; - Environment.Exit(SIGINT_EXIT_CODE); - }, (CancellationToken)default); - } - - // Cancel synchronously here - no need to perform it asynchronously as the timeout is already running (and would kill the process if needed), - // plus we cannot wait only on the cancellation (e.g. via `Task.Factory.StartNew(cts.Cancel).Wait(cancelationProcessingTimeout.Value)`) - // as we need to abort any other possible execution within the process - even outside the context of cancellation processing - context.Cancel(); - }; - - Console.CancelKeyPress += consoleHandler; - AppDomain.CurrentDomain.ProcessExit += processExitHandler; - - try - { - await next(context); - } - finally - { - Console.CancelKeyPress -= consoleHandler; - AppDomain.CurrentDomain.ProcessExit -= processExitHandler; - blockProcessExit?.Set(); - } - }, MiddlewareOrderInternal.Startup); + builder.ProcessTerminationTimeout = timeout ?? TimeSpan.FromSeconds(2); return builder; } @@ -175,7 +95,7 @@ public static CommandLineBuilder EnablePosixBundling( public static CommandLineBuilder RegisterWithDotnetSuggest( this CommandLineBuilder builder) { - builder.AddMiddleware(async (context, next) => + builder.AddMiddleware(async (context, cancellationToken, next) => { var feature = new FeatureRegistration("dotnet-suggest-registration"); @@ -195,7 +115,7 @@ await feature.EnsureRegistered(async () => stdOut: value => stdOut.Append(value), stdErr: value => stdOut.Append(value)); - await dotnetSuggestProcess.CompleteAsync(); + await dotnetSuggestProcess.CompleteAsync(cancellationToken); return $@"{dotnetSuggestProcess.StartInfo.FileName} exited with code {dotnetSuggestProcess.ExitCode} OUT: @@ -215,7 +135,7 @@ await feature.EnsureRegistered(async () => } }); - await next(context); + await next(context, cancellationToken); }, MiddlewareOrderInternal.RegisterWithDotnetSuggest); return builder; @@ -419,10 +339,10 @@ public static CommandLineBuilder AddMiddleware( Action onInvoke, MiddlewareOrder order = MiddlewareOrder.Default) { - builder.AddMiddleware(async (context, next) => + builder.AddMiddleware(async (context, cancellationToken, next) => { onInvoke(context); - await next(context); + await next(context, cancellationToken); }, order); return builder; diff --git a/src/System.CommandLine/CommandExtensions.cs b/src/System.CommandLine/CommandExtensions.cs index fb96cc19f8..b0ecd0af40 100644 --- a/src/System.CommandLine/CommandExtensions.cs +++ b/src/System.CommandLine/CommandExtensions.cs @@ -26,7 +26,9 @@ public static int Invoke( string[] args, IConsole? console = null) { - return GetDefaultInvocationPipeline(command, args).Invoke(console); + ParseResult parseResult = command.GetOrCreateDefaultInvocationParser().Parse(args); + + return InvocationPipeline.Invoke(parseResult, console); } /// @@ -51,13 +53,15 @@ public static int Invoke( /// The console to which output is written during invocation. /// A token that can be used to cancel the invocation. /// The exit code for the invocation. - public static async Task InvokeAsync( + public static Task InvokeAsync( this Command command, string[] args, IConsole? console = null, CancellationToken cancellationToken = default) { - return await GetDefaultInvocationPipeline(command, args).InvokeAsync(console, cancellationToken); + ParseResult parseResult = command.GetOrCreateDefaultInvocationParser().Parse(args); + + return InvocationPipeline.InvokeAsync(parseResult, console, cancellationToken); } /// @@ -76,13 +80,6 @@ public static Task InvokeAsync( CancellationToken cancellationToken = default) => command.InvokeAsync(CommandLineStringSplitter.Instance.Split(commandLine).ToArray(), console, cancellationToken); - private static InvocationPipeline GetDefaultInvocationPipeline(Command command, string[] args) - { - var parseResult = command.GetOrCreateDefaultInvocationParser().Parse(args); - - return new InvocationPipeline(parseResult); - } - /// /// Parses an array strings using the specified command. /// diff --git a/src/System.CommandLine/CommandLineConfiguration.cs b/src/System.CommandLine/CommandLineConfiguration.cs index da668dee3c..3632e2855e 100644 --- a/src/System.CommandLine/CommandLineConfiguration.cs +++ b/src/System.CommandLine/CommandLineConfiguration.cs @@ -47,6 +47,8 @@ public class CommandLineConfiguration /// internal readonly int MaxLevenshteinDistance; + internal readonly TimeSpan? ProcessTerminationTimeout; + internal readonly IReadOnlyList Middleware; private Func? _helpBuilderFactory; @@ -72,7 +74,7 @@ public CommandLineConfiguration( IReadOnlyList? middlewarePipeline = null, Func? helpBuilderFactory = null, TryReplaceToken? tokenReplacer = null) - : this(command, enablePosixBundling, enableDirectives, enableTokenReplacement, false, null, false, null, 0, + : this(command, enablePosixBundling, enableDirectives, enableTokenReplacement, false, null, false, null, 0, null, resources, middlewarePipeline, helpBuilderFactory, tokenReplacer, null) { } @@ -87,6 +89,7 @@ internal CommandLineConfiguration( bool enableSuggestDirective, int? parseErrorReportingExitCode, int maxLevenshteinDistance, + TimeSpan? processTerminationTimeout, LocalizationResources? resources, IReadOnlyList? middlewarePipeline, Func? helpBuilderFactory, @@ -102,6 +105,7 @@ internal CommandLineConfiguration( EnableSuggestDirective = enableSuggestDirective; ParseErrorReportingExitCode = parseErrorReportingExitCode; MaxLevenshteinDistance = maxLevenshteinDistance; + ProcessTerminationTimeout = processTerminationTimeout; LocalizationResources = resources ?? LocalizationResources.Instance; Middleware = middlewarePipeline ?? Array.Empty(); diff --git a/src/System.CommandLine/Handler.Func.cs b/src/System.CommandLine/Handler.Func.cs index 97f3a9f1e4..b791fecc53 100644 --- a/src/System.CommandLine/Handler.Func.cs +++ b/src/System.CommandLine/Handler.Func.cs @@ -3,6 +3,7 @@ using System.CommandLine.Binding; using System.CommandLine.Invocation; +using System.Threading; using System.Threading.Tasks; namespace System.CommandLine; @@ -17,15 +18,15 @@ public static partial class Handler /// public static void SetHandler( this Command command, - Func handle) => - command.Handler = new AnonymousCommandHandler(_ => handle()); + Func handle) => + command.Handler = new AnonymousCommandHandler((ctx, cancellationToken) => handle(cancellationToken)); /// /// Sets a command's handler based on a . /// public static void SetHandler( this Command command, - Func handle) => + Func handle) => command.Handler = new AnonymousCommandHandler(handle); /// @@ -33,14 +34,14 @@ public static void SetHandler( /// public static void SetHandler( this Command command, - Func handle, + Func handle, IValueDescriptor symbol) => command.Handler = new AnonymousCommandHandler( - context => + (context, cancellationToken) => { var value1 = GetValueForHandlerParameter(symbol, context); - return handle(value1!); + return handle(value1!, cancellationToken); }); /// @@ -48,16 +49,16 @@ public static void SetHandler( /// public static void SetHandler( this Command command, - Func handle, + Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2) => command.Handler = new AnonymousCommandHandler( - context => + (context, cancellationToken) => { var value1 = GetValueForHandlerParameter(symbol1, context); var value2 = GetValueForHandlerParameter(symbol2, context); - return handle(value1!, value2!); + return handle(value1!, value2!, cancellationToken); }); /// @@ -65,18 +66,18 @@ public static void SetHandler( /// public static void SetHandler( this Command command, - Func handle, + Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3) => command.Handler = new AnonymousCommandHandler( - context => + (context, cancellationToken) => { var value1 = GetValueForHandlerParameter(symbol1, context); var value2 = GetValueForHandlerParameter(symbol2, context); var value3 = GetValueForHandlerParameter(symbol3, context); - return handle(value1!, value2!, value3!); + return handle(value1!, value2!, value3!, cancellationToken); }); /// @@ -84,20 +85,20 @@ public static void SetHandler( /// public static void SetHandler( this Command command, - Func handle, + Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, IValueDescriptor symbol4) => command.Handler = new AnonymousCommandHandler( - context => + (context, cancellationToken) => { var value1 = GetValueForHandlerParameter(symbol1, context); var value2 = GetValueForHandlerParameter(symbol2, context); var value3 = GetValueForHandlerParameter(symbol3, context); var value4 = GetValueForHandlerParameter(symbol4, context); - return handle(value1!, value2!, value3!, value4!); + return handle(value1!, value2!, value3!, value4!, cancellationToken); }); /// @@ -105,14 +106,14 @@ public static void SetHandler( /// public static void SetHandler( this Command command, - Func handle, + Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, IValueDescriptor symbol4, IValueDescriptor symbol5) => command.Handler = new AnonymousCommandHandler( - context => + (context, cancellationToken) => { var value1 = GetValueForHandlerParameter(symbol1, context); var value2 = GetValueForHandlerParameter(symbol2, context); @@ -120,7 +121,7 @@ public static void SetHandler( var value4 = GetValueForHandlerParameter(symbol4, context); var value5 = GetValueForHandlerParameter(symbol5, context); - return handle(value1!, value2!, value3!, value4!, value5!); + return handle(value1!, value2!, value3!, value4!, value5!, cancellationToken); }); /// @@ -128,7 +129,7 @@ public static void SetHandler( /// public static void SetHandler( this Command command, - Func handle, + Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, @@ -136,7 +137,7 @@ public static void SetHandler( IValueDescriptor symbol5, IValueDescriptor symbol6) => command.Handler = new AnonymousCommandHandler( - context => + (context, cancellationToken) => { var value1 = GetValueForHandlerParameter(symbol1, context); var value2 = GetValueForHandlerParameter(symbol2, context); @@ -145,7 +146,7 @@ public static void SetHandler( var value5 = GetValueForHandlerParameter(symbol5, context); var value6 = GetValueForHandlerParameter(symbol6, context); - return handle(value1!, value2!, value3!, value4!, value5!, value6!); + return handle(value1!, value2!, value3!, value4!, value5!, value6!, cancellationToken); }); /// @@ -153,7 +154,7 @@ public static void SetHandler( /// public static void SetHandler( this Command command, - Func handle, + Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, @@ -162,7 +163,7 @@ public static void SetHandler( IValueDescriptor symbol6, IValueDescriptor symbol7) => command.Handler = new AnonymousCommandHandler( - context => + (context, cancellationToken) => { var value1 = GetValueForHandlerParameter(symbol1, context); var value2 = GetValueForHandlerParameter(symbol2, context); @@ -172,7 +173,7 @@ public static void SetHandler( var value6 = GetValueForHandlerParameter(symbol6, context); var value7 = GetValueForHandlerParameter(symbol7, context); - return handle(value1!, value2!, value3!, value4!, value5!, value6!, value7!); + return handle(value1!, value2!, value3!, value4!, value5!, value6!, value7!, cancellationToken); }); /// @@ -180,7 +181,7 @@ public static void SetHandler( /// public static void SetHandler( this Command command, - Func handle, + Func handle, IValueDescriptor symbol1, IValueDescriptor symbol2, IValueDescriptor symbol3, @@ -190,7 +191,7 @@ public static void SetHandler( IValueDescriptor symbol7, IValueDescriptor symbol8) => command.Handler = new AnonymousCommandHandler( - context => + (context, cancellationToken) => { var value1 = GetValueForHandlerParameter(symbol1, context); var value2 = GetValueForHandlerParameter(symbol2, context); @@ -201,6 +202,6 @@ public static void SetHandler( var value7 = GetValueForHandlerParameter(symbol7, context); var value8 = GetValueForHandlerParameter(symbol8, context); - return handle(value1!, value2!, value3!, value4!, value5!, value6!, value7!, value8!); + return handle(value1!, value2!, value3!, value4!, value5!, value6!, value7!, value8!, cancellationToken); }); } \ No newline at end of file diff --git a/src/System.CommandLine/Help/HelpOption.cs b/src/System.CommandLine/Help/HelpOption.cs index 654afd5647..0fea4b51c9 100644 --- a/src/System.CommandLine/Help/HelpOption.cs +++ b/src/System.CommandLine/Help/HelpOption.cs @@ -44,14 +44,12 @@ internal static void Handler(InvocationContext context) { var output = context.Console.Out.CreateTextWriter(); - var helpContext = new HelpContext(context.BindingContext.HelpBuilder, + var helpContext = new HelpContext(context.HelpBuilder, context.ParseResult.CommandResult.Command, output, context.ParseResult); - context.BindingContext - .HelpBuilder - .Write(helpContext); + context.HelpBuilder.Write(helpContext); } } } \ No newline at end of file diff --git a/src/System.CommandLine/ICommandHandler.cs b/src/System.CommandLine/ICommandHandler.cs index 1f48649742..c5d04dde72 100644 --- a/src/System.CommandLine/ICommandHandler.cs +++ b/src/System.CommandLine/ICommandHandler.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.CommandLine.Invocation; +using System.Threading; using System.Threading.Tasks; namespace System.CommandLine @@ -22,7 +23,8 @@ public interface ICommandHandler /// Performs an action when the associated command is invoked on the command line. /// /// Provides context for the invocation, including parse results and binding support. + /// The token to monitor for cancellation requests. /// A value that can be used as the exit code for the process. - Task InvokeAsync(InvocationContext context); + Task InvokeAsync(InvocationContext context, CancellationToken cancellationToken = default); } } diff --git a/src/System.CommandLine/Invocation/AnonymousCommandHandler.cs b/src/System.CommandLine/Invocation/AnonymousCommandHandler.cs index 962fb97e3e..0a941e3948 100644 --- a/src/System.CommandLine/Invocation/AnonymousCommandHandler.cs +++ b/src/System.CommandLine/Invocation/AnonymousCommandHandler.cs @@ -1,19 +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.Threading; using System.Threading.Tasks; namespace System.CommandLine.Invocation { - internal class AnonymousCommandHandler : ICommandHandler + internal sealed class AnonymousCommandHandler : ICommandHandler { - private readonly Func? _asyncHandle; + private readonly Func? _asyncHandle; private readonly Action? _syncHandle; - public AnonymousCommandHandler(Func handle) + internal AnonymousCommandHandler(Func handle) => _asyncHandle = handle ?? throw new ArgumentNullException(nameof(handle)); - public AnonymousCommandHandler(Action handle) + internal AnonymousCommandHandler(Action handle) => _syncHandle = handle ?? throw new ArgumentNullException(nameof(handle)); public int Invoke(InvocationContext context) @@ -25,39 +26,26 @@ public int Invoke(InvocationContext context) } return SyncUsingAsync(context); // kept in a separate method to avoid JITting - } - private int SyncUsingAsync(InvocationContext context) => InvokeAsync(context).GetAwaiter().GetResult(); + int SyncUsingAsync(InvocationContext context) + => InvokeAsync(context, CancellationToken.None).GetAwaiter().GetResult(); + } - public async Task InvokeAsync(InvocationContext context) + public async Task InvokeAsync(InvocationContext context, CancellationToken cancellationToken) { if (_syncHandle is not null) { return Invoke(context); } - object returnValue = _asyncHandle!(context); - - int ret; - - switch (returnValue) + Task handler = _asyncHandle!(context, cancellationToken); + if (handler is Task intReturning) { - case Task exitCodeTask: - ret = await exitCodeTask; - break; - case Task task: - await task; - ret = context.ExitCode; - break; - case int exitCode: - ret = exitCode; - break; - default: - ret = context.ExitCode; - break; + return await intReturning; } - return ret; + await handler; + return context.ExitCode; } } } \ No newline at end of file diff --git a/src/System.CommandLine/Invocation/InvocationContext.cs b/src/System.CommandLine/Invocation/InvocationContext.cs index 39076d22e8..89c187d307 100644 --- a/src/System.CommandLine/Invocation/InvocationContext.cs +++ b/src/System.CommandLine/Invocation/InvocationContext.cs @@ -1,78 +1,41 @@ // 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; using System.CommandLine.Binding; using System.CommandLine.Help; using System.CommandLine.IO; using System.CommandLine.Parsing; -using System.Threading; namespace System.CommandLine.Invocation { /// /// Supports command invocation by providing access to parse results and other services. /// - public sealed class InvocationContext : IDisposable + public sealed class InvocationContext { private HelpBuilder? _helpBuilder; private BindingContext? _bindingContext; private IConsole? _console; - private readonly CancellationToken _token; - private readonly LinkedList _registrations = new(); - private volatile CancellationTokenSource? _source; /// The result of the current parse operation. /// The console to which output is to be written. - /// A cancellation token that can be used to cancel and invocation. - public InvocationContext( - ParseResult parseResult, - IConsole? console = null, - CancellationToken cancellationToken = default) + public InvocationContext(ParseResult parseResult, IConsole? console = null) { ParseResult = parseResult; _console = console; - - _source = new CancellationTokenSource(); - _token = _source.Token; - if (cancellationToken.CanBeCanceled) - { - LinkToken(cancellationToken); - } } /// /// The binding context for the current invocation. /// - public BindingContext BindingContext - { - get - { - if (_bindingContext is null) - { - _bindingContext = new BindingContext(this); - _bindingContext.ServiceProvider.AddService(_ => GetCancellationToken()); - _bindingContext.ServiceProvider.AddService(_ => this); - } - - return _bindingContext; - } - } + public BindingContext BindingContext => _bindingContext ??= new BindingContext(this); /// /// The console to which output should be written during the current invocation. /// public IConsole Console { - get - { - if (_console is null) - { - _console = new SystemConsole(); - } - - return _console; - } + get => _console ??= new SystemConsole(); set => _console = value; } @@ -107,22 +70,6 @@ public IConsole Console /// As the is passed through the invocation pipeline to the associated with the invoked command, only the last value of this property will be the one applied. public Action? InvocationResult { get; set; } - /// - /// Gets a cancellation token that can be used to check if cancellation has been requested. - /// - public CancellationToken GetCancellationToken() => _token; - - internal void Cancel() - { - using var source = Interlocked.Exchange(ref _source, null); - source?.Cancel(); - } - - public void LinkToken(CancellationToken token) - { - _registrations.AddLast(token.Register(Cancel)); - } - /// public object? GetValue(Option option) => ParseResult.GetValue(option); @@ -138,15 +85,5 @@ public void LinkToken(CancellationToken token) /// public T GetValue(Argument argument) => ParseResult.GetValue(argument); - - /// - void IDisposable.Dispose() - { - Interlocked.Exchange(ref _source, null)?.Dispose(); - foreach (CancellationTokenRegistration registration in _registrations) - { - registration.Dispose(); - } - } } } diff --git a/src/System.CommandLine/Invocation/InvocationMiddleware.cs b/src/System.CommandLine/Invocation/InvocationMiddleware.cs index 7ae994427f..f1b3a92ccf 100644 --- a/src/System.CommandLine/Invocation/InvocationMiddleware.cs +++ b/src/System.CommandLine/Invocation/InvocationMiddleware.cs @@ -1,6 +1,7 @@ // 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; using System.Threading.Tasks; namespace System.CommandLine.Invocation @@ -9,8 +10,10 @@ namespace System.CommandLine.Invocation /// A delegate used for adding command handler invocation middleware. /// /// The context for the current invocation, which will be passed to each middleware and then to the command handler, unless a middleware short circuits it. + /// A token that can be used to cancel the invocation. /// A continuation. Passing the incoming to it will execute the next middleware in the pipeline and, at the end of the pipeline, the command handler. Middleware can short circuit the invocation by not calling this continuation. public delegate Task InvocationMiddleware( InvocationContext context, - Func next); + CancellationToken cancellationToken, + Func next); } diff --git a/src/System.CommandLine/Invocation/InvocationPipeline.cs b/src/System.CommandLine/Invocation/InvocationPipeline.cs index 0251cbb30b..621e93f6f4 100644 --- a/src/System.CommandLine/Invocation/InvocationPipeline.cs +++ b/src/System.CommandLine/Invocation/InvocationPipeline.cs @@ -8,51 +8,66 @@ namespace System.CommandLine.Invocation { - internal class InvocationPipeline + internal static class InvocationPipeline { - private readonly ParseResult _parseResult; + internal static async Task InvokeAsync(ParseResult parseResult, IConsole? console, CancellationToken cancellationToken) + { + InvocationContext context = new (parseResult, console); - internal InvocationPipeline(ParseResult parseResult) - => _parseResult = parseResult ?? throw new ArgumentNullException(nameof(parseResult)); + using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - public async Task InvokeAsync(IConsole? console = null, CancellationToken cancellationToken = default) - { - var context = new InvocationContext(_parseResult, console, cancellationToken); + Task startedInvocation = parseResult.Handler is not null && context.Parser.Configuration.Middleware.Count == 0 + ? parseResult.Handler.InvokeAsync(context, cts.Token) + : InvokeHandlerWithMiddleware(context, cts.Token); + + ProcessTerminationHandler? terminationHandler = parseResult.Parser.Configuration.ProcessTerminationTimeout.HasValue + ? new (cts, startedInvocation, parseResult.Parser.Configuration.ProcessTerminationTimeout.Value) + : null; try { - if (context.Parser.Configuration.Middleware.Count == 0 && _parseResult.Handler is not null) + if (terminationHandler is null) { - return await _parseResult.Handler.InvokeAsync(context); + return await startedInvocation; + } + else + { + // Handlers may not implement cancellation. + // In such cases, when CancelOnProcessTermination is configured and user presses Ctrl+C, + // ProcessTerminationCompletionSource completes first, with the result equal to native exit code for given signal. + Task firstCompletedTask = await Task.WhenAny(startedInvocation, terminationHandler.ProcessTerminationCompletionSource.Task); + return await firstCompletedTask; // return the result or propagate the exception } - - return await InvokeHandlerWithMiddleware(context); } catch (Exception ex) when (context.Parser.Configuration.ExceptionHandler is not null) { context.Parser.Configuration.ExceptionHandler(ex, context); return context.ExitCode; } + finally + { + terminationHandler?.Dispose(); + } - static async Task InvokeHandlerWithMiddleware(InvocationContext context) + static async Task InvokeHandlerWithMiddleware(InvocationContext context, CancellationToken token) { InvocationMiddleware invocationChain = BuildInvocationChain(context, true); - await invocationChain(context, _ => Task.CompletedTask); + await invocationChain(context, token, (_, _) => Task.CompletedTask); return GetExitCode(context); } } - public int Invoke(IConsole? console = null) + internal static int Invoke(ParseResult parseResult, IConsole? console = null) { - var context = new InvocationContext(_parseResult, console); + InvocationContext context = new (parseResult, console); try { - if (context.Parser.Configuration.Middleware.Count == 0 && _parseResult.Handler is not null) + if (context.Parser.Configuration.Middleware.Count == 0 && parseResult.Handler is not null) { - return _parseResult.Handler.Invoke(context); + return parseResult.Handler.Invoke(context); } return InvokeHandlerWithMiddleware(context); // kept in a separate method to avoid JITting @@ -67,7 +82,7 @@ static int InvokeHandlerWithMiddleware(InvocationContext context) { InvocationMiddleware invocationChain = BuildInvocationChain(context, false); - invocationChain(context, static _ => Task.CompletedTask).ConfigureAwait(false).GetAwaiter().GetResult(); + invocationChain(context, CancellationToken.None, static (_, _) => Task.CompletedTask).ConfigureAwait(false).GetAwaiter().GetResult(); return GetExitCode(context); } @@ -78,21 +93,21 @@ private static InvocationMiddleware BuildInvocationChain(InvocationContext conte var invocations = new List(context.Parser.Configuration.Middleware.Count + 1); invocations.AddRange(context.Parser.Configuration.Middleware); - invocations.Add(async (invocationContext, _) => + invocations.Add(async (invocationContext, cancellationToken, _) => { if (invocationContext.ParseResult.Handler is { } handler) { context.ExitCode = invokeAsync - ? await handler.InvokeAsync(invocationContext) + ? await handler.InvokeAsync(invocationContext, cancellationToken) : handler.Invoke(invocationContext); } }); return invocations.Aggregate( (first, second) => - (ctx, next) => - first(ctx, - c => second(c, next))); + (ctx, token, next) => + first(ctx, token, + (c, t) => second(c, t, next))); } private static int GetExitCode(InvocationContext context) diff --git a/src/System.CommandLine/Invocation/MiddlewareOrder.cs b/src/System.CommandLine/Invocation/MiddlewareOrder.cs index 419196a2b5..b4eab4ea83 100644 --- a/src/System.CommandLine/Invocation/MiddlewareOrder.cs +++ b/src/System.CommandLine/Invocation/MiddlewareOrder.cs @@ -31,7 +31,6 @@ public enum MiddlewareOrder internal enum MiddlewareOrderInternal { - Startup = -4000, RegisterWithDotnetSuggest = -2400, } } \ No newline at end of file diff --git a/src/System.CommandLine/Invocation/ProcessTerminationHandler.cs b/src/System.CommandLine/Invocation/ProcessTerminationHandler.cs new file mode 100644 index 0000000000..da83bddad1 --- /dev/null +++ b/src/System.CommandLine/Invocation/ProcessTerminationHandler.cs @@ -0,0 +1,92 @@ +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace System.CommandLine.Invocation; + +internal sealed class ProcessTerminationHandler : IDisposable +{ + private const int SIGINT_EXIT_CODE = 130; + private const int SIGTERM_EXIT_CODE = 143; + + internal readonly TaskCompletionSource ProcessTerminationCompletionSource; + private readonly CancellationTokenSource _handlerCancellationTokenSource; + private readonly Task _startedHandler; + private readonly TimeSpan _processTerminationTimeout; +#if NET7_0_OR_GREATER + private readonly IDisposable? _sigIntRegistration, _sigTermRegistration; +#endif + + internal ProcessTerminationHandler( + CancellationTokenSource handlerCancellationTokenSource, + Task startedHandler, + TimeSpan processTerminationTimeout) + { + ProcessTerminationCompletionSource = new (); + _handlerCancellationTokenSource = handlerCancellationTokenSource; + _startedHandler = startedHandler; + _processTerminationTimeout = processTerminationTimeout; + +#if NET7_0_OR_GREATER // we prefer the new API as they allow for cancelling SIGTERM + if (!OperatingSystem.IsAndroid() + && !OperatingSystem.IsIOS() + && !OperatingSystem.IsTvOS() + && !OperatingSystem.IsBrowser()) + { + _sigIntRegistration = PosixSignalRegistration.Create(PosixSignal.SIGINT, OnPosixSignal); + _sigTermRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, OnPosixSignal); + return; + } +#endif + + Console.CancelKeyPress += OnCancelKeyPress; + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; + } + + public void Dispose() + { +#if NET7_0_OR_GREATER + if (_sigIntRegistration is not null) + { + _sigIntRegistration.Dispose(); + _sigTermRegistration!.Dispose(); + return; + } +#endif + + Console.CancelKeyPress -= OnCancelKeyPress; + AppDomain.CurrentDomain.ProcessExit -= OnProcessExit; + } + +#if NET7_0_OR_GREATER + void OnPosixSignal(PosixSignalContext context) + { + context.Cancel = true; + + Cancel(context.Signal == PosixSignal.SIGINT ? SIGINT_EXIT_CODE : SIGTERM_EXIT_CODE); + } +#endif + + void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e) + { + e.Cancel = true; + + Cancel(SIGINT_EXIT_CODE); + } + + void OnProcessExit(object? sender, EventArgs e) => Cancel(SIGTERM_EXIT_CODE); + + void Cancel(int forcedTerminationExitCode) + { + // request cancellation + _handlerCancellationTokenSource.Cancel(); + + // wait for the configured interval + if (!_startedHandler.Wait(_processTerminationTimeout)) + { + // if the handler does not finish within configured time, + // use the completion source to signal forced completion (preserving native exit code) + ProcessTerminationCompletionSource.SetResult(forcedTerminationExitCode); + } + } +} \ No newline at end of file diff --git a/src/System.CommandLine/Invocation/ServiceProvider.cs b/src/System.CommandLine/Invocation/ServiceProvider.cs index 852a1c9913..6610fcbc3c 100644 --- a/src/System.CommandLine/Invocation/ServiceProvider.cs +++ b/src/System.CommandLine/Invocation/ServiceProvider.cs @@ -8,17 +8,16 @@ namespace System.CommandLine.Invocation { - internal class ServiceProvider : IServiceProvider + internal sealed class ServiceProvider : IServiceProvider { private readonly Dictionary> _services; - public ServiceProvider(BindingContext bindingContext) + internal ServiceProvider(BindingContext bindingContext) { _services = new Dictionary> { [typeof(ParseResult)] = _ => bindingContext.ParseResult, [typeof(IConsole)] = _ => bindingContext.Console, - [typeof(CancellationToken)] = _ => CancellationToken.None, [typeof(HelpBuilder)] = _ => bindingContext.ParseResult.Parser.Configuration.HelpBuilderFactory(bindingContext), [typeof(BindingContext)] = _ => bindingContext }; diff --git a/src/System.CommandLine/Parsing/ParseResultExtensions.cs b/src/System.CommandLine/Parsing/ParseResultExtensions.cs index d4df35a870..89b7a67c02 100644 --- a/src/System.CommandLine/Parsing/ParseResultExtensions.cs +++ b/src/System.CommandLine/Parsing/ParseResultExtensions.cs @@ -24,11 +24,11 @@ public static class ParseResultExtensions /// A console to which output can be written. By default, is used. /// A token that can be used to cancel an invocation. /// A task whose result can be used as a process exit code. - public static async Task InvokeAsync( + public static Task InvokeAsync( this ParseResult parseResult, IConsole? console = null, CancellationToken cancellationToken = default) => - await new InvocationPipeline(parseResult).InvokeAsync(console, cancellationToken); + InvocationPipeline.InvokeAsync(parseResult, console, cancellationToken); /// /// Invokes the appropriate command handler for a parsed command line input. @@ -39,7 +39,7 @@ public static async Task InvokeAsync( public static int Invoke( this ParseResult parseResult, IConsole? console = null) => - new InvocationPipeline(parseResult).Invoke(console); + InvocationPipeline.Invoke(parseResult, console); /// /// Formats a string explaining a parse result. diff --git a/src/System.CommandLine/Parsing/ParserExtensions.cs b/src/System.CommandLine/Parsing/ParserExtensions.cs index 4c90a1783b..85cf6c9d55 100644 --- a/src/System.CommandLine/Parsing/ParserExtensions.cs +++ b/src/System.CommandLine/Parsing/ParserExtensions.cs @@ -49,12 +49,12 @@ public static Task InvokeAsync( /// Parses a command line string array and invokes the handler for the indicated command. /// /// The exit code for the invocation. - public static async Task InvokeAsync( + public static Task InvokeAsync( this Parser parser, string[] args, IConsole? console = null, CancellationToken cancellationToken = default) => - await parser.Parse(args).InvokeAsync(console, cancellationToken); + parser.Parse(args).InvokeAsync(console, cancellationToken); /// /// Parses a command line string.