Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/support cancellation token #197

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 97 additions & 34 deletions src/CommandLineUtils/CommandLineApplication.Execute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils.Abstractions;
using McMaster.Extensions.CommandLineUtils.Internal;
Expand All @@ -29,25 +30,7 @@ partial class CommandLineApplication
public static int Execute<TApp>(CommandLineContext context)
where TApp : class
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

if (context.Arguments == null)
{
throw new ArgumentNullException(nameof(context) + "." + nameof(context.Arguments));
}

if (context.WorkingDirectory == null)
{
throw new ArgumentNullException(nameof(context) + "." + nameof(context.WorkingDirectory));
}

if (context.Console == null)
{
throw new ArgumentNullException(nameof(context) + "." + nameof(context.Console));
}
VerifyContext(context);

try
{
Expand All @@ -60,16 +43,7 @@ public static int Execute<TApp>(CommandLineContext context)
}
catch (CommandParsingException ex)
{
context.Console.Error.WriteLine(ex.Message);

if (ex is UnrecognizedCommandParsingException uex && uex.NearestMatches.Any())
{
context.Console.Error.WriteLine();
context.Console.Error.WriteLine("Did you mean this?");
context.Console.Error.WriteLine(" " + uex.NearestMatches.First());
}

return ValidationErrorExitCode;
return HandleCommandParsingException(context, ex);
}
}

Expand Down Expand Up @@ -117,8 +91,23 @@ public static int Execute<TApp>(IConsole console, params string[] args)
/// <exception cref="InvalidOperationException">Thrown when attributes are incorrectly configured.</exception>
/// <returns>The process exit code</returns>
public static Task<int> ExecuteAsync<TApp>(params string[] args)
where TApp : class
=> ExecuteAsync<TApp>(PhysicalConsole.Singleton, args);
where TApp : class
=> ExecuteAsync<TApp>(PhysicalConsole.Singleton, default, args);

/// <summary>
/// Creates an instance of <typeparamref name="TApp"/>, matching <paramref name="args"/>
/// to all attributes on the type, and then invoking a method named "OnExecute" or "OnExecuteAsync" if it exists.
/// See <seealso cref="OptionAttribute" />, <seealso cref="ArgumentAttribute" />,
/// <seealso cref="HelpOptionAttribute"/>, and <seealso cref="VersionOptionAttribute"/>.
/// </summary>
/// <param name="cancellationToken">Optional cancellation-token</param>
/// <param name="args">The arguments</param>
/// <typeparam name="TApp">A type that should be bound to the arguments.</typeparam>
/// <exception cref="InvalidOperationException">Thrown when attributes are incorrectly configured.</exception>
/// <returns>The process exit code</returns>
public static Task<int> ExecuteAsync<TApp>(CancellationToken cancellationToken, params string[] args)
where TApp : class
=> ExecuteAsync<TApp>(PhysicalConsole.Singleton, cancellationToken, args);

/// <summary>
/// Creates an instance of <typeparamref name="TApp"/>, matching <paramref name="args"/>
Expand All @@ -133,10 +122,27 @@ public static Task<int> ExecuteAsync<TApp>(params string[] args)
/// <returns>The process exit code</returns>
public static Task<int> ExecuteAsync<TApp>(IConsole console, params string[] args)
where TApp : class
=> ExecuteAsync<TApp>(console, default, args);

/// <summary>
/// Creates an instance of <typeparamref name="TApp"/>, matching <paramref name="args"/>
/// to all attributes on the type, and then invoking a method named "OnExecute" or "OnExecuteAsync" if it exists.
/// See <seealso cref="OptionAttribute" />, <seealso cref="ArgumentAttribute" />,
/// <seealso cref="HelpOptionAttribute"/>, and <seealso cref="VersionOptionAttribute"/>.
/// </summary>
/// <param name="console">The console to use</param>
/// <param name="cancellationToken">Optional cancellation-token</param>
/// <param name="args">The arguments</param>
/// <typeparam name="TApp">A type that should be bound to the arguments.</typeparam>
/// <exception cref="InvalidOperationException">Thrown when attributes are incorrectly configured.</exception>
/// <returns>The process exit code</returns>
public static Task<int> ExecuteAsync<TApp>(IConsole console, CancellationToken cancellationToken,
params string[] args)
where TApp : class
{
args = args ?? new string[0];
var context = new DefaultCommandLineContext(console, Directory.GetCurrentDirectory(), args);
return ExecuteAsync<TApp>(context);
return ExecuteAsync<TApp>(context, cancellationToken);
}

/// <summary>
Expand All @@ -146,11 +152,68 @@ public static Task<int> ExecuteAsync<TApp>(IConsole console, params string[] arg
/// <seealso cref="HelpOptionAttribute"/>, and <seealso cref="VersionOptionAttribute"/>.
/// </summary>
/// <param name="context">The execution context.</param>
/// <param name="cancellationToken">Optional cancellation-token</param>
/// <typeparam name="TApp">A type that should be bound to the arguments.</typeparam>
/// <exception cref="InvalidOperationException">Thrown when attributes are incorrectly configured.</exception>
/// <returns>The process exit code</returns>
public static Task<int> ExecuteAsync<TApp>(CommandLineContext context)
public static async Task<int> ExecuteAsync<TApp>(CommandLineContext context,
CancellationToken cancellationToken = default)
where TApp : class
=> Task.FromResult(Execute<TApp>(context));
{
VerifyContext(context);
cancellationToken.ThrowIfCancellationRequested();

try
{
using (var app = new CommandLineApplication<TApp>())
{
app.SetContext(context);
app.Conventions.UseDefaultConventions();
cancellationToken.ThrowIfCancellationRequested();
return await app.ExecuteAsync(context.Arguments);
}
}
catch (CommandParsingException ex)
{
return HandleCommandParsingException(context, ex);
}
}

private static void VerifyContext(CommandLineContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

if (context.Arguments == null)
{
throw new ArgumentNullException(nameof(context) + "." + nameof(context.Arguments));
}

if (context.WorkingDirectory == null)
{
throw new ArgumentNullException(nameof(context) + "." + nameof(context.WorkingDirectory));
}

if (context.Console == null)
{
throw new ArgumentNullException(nameof(context) + "." + nameof(context.Console));
}
}

private static int HandleCommandParsingException(CommandLineContext context, CommandParsingException ex)
{
context.Console.Error.WriteLine(ex.Message);

if (ex is UnrecognizedCommandParsingException uex && uex.NearestMatches.Any())
{
context.Console.Error.WriteLine();
context.Console.Error.WriteLine("Did you mean this?");
context.Console.Error.WriteLine(" " + uex.NearestMatches.First());
}

return ValidationErrorExitCode;
}
}
}
101 changes: 99 additions & 2 deletions src/CommandLineUtils/CommandLineApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils.Abstractions;
using McMaster.Extensions.CommandLineUtils.Conventions;
Expand Down Expand Up @@ -236,10 +237,36 @@ public CommandOption OptionHelp
/// </summary>
public bool IsShowingInformation { get; protected set; }

private Func<int> _invoke;
private Func<CancellationToken, Task<int>> _invokeAsync;

/// <summary>
/// The action to call when this command is matched and <see cref="IsShowingInformation"/> is <c>false</c>.
/// Specify this or <see cref="InvokeAsync"/>.
/// </summary>
public Func<int> Invoke { get; set; }
public Func<int> Invoke
{
get => _invoke;
set
{
_invoke = value;
_invokeAsync = ct => Task.FromResult(value());
}
}

/// <summary>
/// The async action to call when this command is matched and <see cref="IsShowingInformation"/> is <c>false</c>.
/// Specify this or <see cref="Invoke"/>.
/// </summary>
public Func<CancellationToken, Task<int>> InvokeAsync
{
get => _invokeAsync;
set
{
_invokeAsync = value;
_invoke = () => value(default).GetAwaiter().GetResult();
}
}

/// <summary>
/// The long-form of the version to display in generated help text.
Expand Down Expand Up @@ -632,7 +659,16 @@ public void OnExecute(Func<int> invoke)
/// <param name="invoke"></param>
public void OnExecute(Func<Task<int>> invoke)
{
Invoke = () => invoke().GetAwaiter().GetResult();
InvokeAsync = ct => invoke();
}

/// <summary>
/// Defines an asynchronous callback.
/// </summary>
/// <param name="invoke"></param>
public void OnExecute(Func<CancellationToken, Task<int>> invoke)
{
InvokeAsync = invoke;
}

/// <summary>
Expand Down Expand Up @@ -755,6 +791,67 @@ public int Execute(params string[] args)
return command.Invoke();
}

/// <summary>
/// Parses an array of strings using <see cref="Parse(string[])"/>.
/// <para>
/// If <see cref="OptionHelp"/> was matched, the generated help text is displayed in command line output.
/// </para>
/// <para>
/// If <see cref="OptionVersion"/> was matched, the generated version info is displayed in command line output.
/// </para>
/// <para>
/// If there were any validation errors produced from <see cref="GetValidationResult"/>, <see cref="ValidationErrorHandler"/> is invoked.
/// </para>
/// <para>
/// If the parse result matches this command, <see cref="Invoke"/> will be invoked.
/// </para>
/// </summary>
/// <param name="args"></param>
/// <returns>The return code from <see cref="Invoke"/>.</returns>
public async Task<int> ExecuteAsync(params string[] args)
{
return await ExecuteAsync(default, args);
}

/// <summary>
/// Parses an array of strings using <see cref="Parse(string[])"/>.
/// <para>
/// If <see cref="OptionHelp"/> was matched, the generated help text is displayed in command line output.
/// </para>
/// <para>
/// If <see cref="OptionVersion"/> was matched, the generated version info is displayed in command line output.
/// </para>
/// <para>
/// If there were any validation errors produced from <see cref="GetValidationResult"/>, <see cref="ValidationErrorHandler"/> is invoked.
/// </para>
/// <para>
/// If the parse result matches this command, <see cref="Invoke"/> will be invoked.
/// </para>
/// </summary>
/// <param name="cancellationToken">Optional cancellation-token</param>
/// <param name="args"></param>
/// <returns>The return code from <see cref="Invoke"/>.</returns>
public async Task<int> ExecuteAsync(CancellationToken cancellationToken, params string[] args)
{
var parseResult = Parse(args);
var command = parseResult.SelectedCommand;
cancellationToken.ThrowIfCancellationRequested();

if (command.IsShowingInformation)
{
return HelpExitCode;
}

var validationResult = command.GetValidationResult();
if (validationResult != ValidationResult.Success)
{
return command.ValidationErrorHandler(validationResult);
}

cancellationToken.ThrowIfCancellationRequested();
return await command.InvokeAsync(cancellationToken);
}

/// <summary>
/// Helper method that adds a help option.
/// </summary>
Expand Down
7 changes: 4 additions & 3 deletions src/CommandLineUtils/Conventions/ExecuteMethodConvention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;

namespace McMaster.Extensions.CommandLineUtils.Conventions
Expand All @@ -23,10 +24,10 @@ public virtual void Apply(ConventionContext context)
return;
}

context.Application.OnExecute(async () => await this.OnExecute(context));
context.Application.OnExecute(async cancellationToken => await OnExecute(context, cancellationToken));
}

private async Task<int> OnExecute(ConventionContext context)
private async Task<int> OnExecute(ConventionContext context, CancellationToken cancellationToken)
{
const BindingFlags binding = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;

Expand Down Expand Up @@ -55,7 +56,7 @@ private async Task<int> OnExecute(ConventionContext context)
throw new InvalidOperationException(Strings.NoOnExecuteMethodFound);
}

var arguments = ReflectionHelper.BindParameters(method, context.Application);
var arguments = ReflectionHelper.BindParameters(method, context.Application, cancellationToken);
var model = context.ModelAccessor.GetModel();

if (method.ReturnType == typeof(Task) || method.ReturnType == typeof(Task<int>))
Expand Down
8 changes: 7 additions & 1 deletion src/CommandLineUtils/Internal/ReflectionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Threading;
using McMaster.Extensions.CommandLineUtils.Abstractions;

namespace McMaster.Extensions.CommandLineUtils
Expand Down Expand Up @@ -55,7 +56,8 @@ public static MemberInfo[] GetMembers(Type type)
return type.GetTypeInfo().GetMembers(binding);
}

public static object[] BindParameters(MethodInfo method, CommandLineApplication command)
public static object[] BindParameters(MethodInfo method, CommandLineApplication command,
CancellationToken cancellationToken = default)
{
var methodParams = method.GetParameters();
var arguments = new object[methodParams.Length];
Expand All @@ -80,6 +82,10 @@ public static object[] BindParameters(MethodInfo method, CommandLineApplication
{
arguments[i] = command._context;
}
else if (typeof(CancellationToken).GetTypeInfo().IsAssignableFrom(methodParam.ParameterType))
{
arguments[i] = cancellationToken;
}
else
{
throw new InvalidOperationException(Strings.UnsupportedParameterTypeOnMethod(method.Name, methodParam));
Expand Down
8 changes: 5 additions & 3 deletions src/Hosting.CommandLine/Internal/CommandLineService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace McMaster.Extensions.Hosting.CommandLine.Internal
{
/// <inheritdoc />
/// <inheritdoc cref="ICommandLineService" />
internal class CommandLineService<T> : IDisposable, ICommandLineService where T : class
{
private readonly CommandLineApplication _application;
Expand Down Expand Up @@ -44,10 +44,12 @@ public CommandLineService(ILogger<CommandLineService<T>> logger, CommandLineStat
}

/// <inheritdoc />
public Task<int> RunAsync(CancellationToken cancellationToken)
public async Task<int> RunAsync(CancellationToken cancellationToken)
{
_logger.LogDebug("Running");
return Task.Run(() => _state.ExitCode = _application.Execute(_state.Arguments), cancellationToken);
var exitCode = await _application.ExecuteAsync(cancellationToken, _state.Arguments);
_state.ExitCode = exitCode;
return exitCode;
}

public void Dispose()
Expand Down