From 1a1bf26cfce62d8bafcc79f8562ec6be1dee8f79 Mon Sep 17 00:00:00 2001 From: Mike McLaughlin Date: Mon, 20 May 2019 15:08:23 -0700 Subject: [PATCH] Update System.CommandLine for dotnet-dump (#270) Update System.CommandLine for dotnet-dump Update System.CommandLine to 0.2.0-alpha.19254.1. Fixes issues: https://github.com/dotnet/diagnostics/issues/127 https://github.com/dotnet/diagnostics/issues/234 Remove the separate older System.CommandLine version for the Repl. Replaced the depreciated MethodBinder usage with explicit reflection invoke. Added the CommandInvoke and HelpInvoke attributes to mark the entry points into the command class. Changed the command invoke from async (Task InvokeAsync) to synchronous (void Invoke) because SOS and CLRMD are basically single threaded. Allow the screen width for the repl console to change dynamically. --- eng/Versions.props | 7 +- .../Command/Attributes.cs | 16 ++ .../Command/CommandBase.cs | 10 +- .../Command/CommandProcessor.cs | 157 ++++++++++++++---- .../Microsoft.Diagnostic.Repl.csproj | 2 +- src/Tools/dotnet-dump/Analyzer.cs | 6 +- .../dotnet-dump/Commands/ClrModulesCommand.cs | 3 +- src/Tools/dotnet-dump/Commands/ExitCommand.cs | 3 +- src/Tools/dotnet-dump/Commands/HelpCommand.cs | 14 +- .../dotnet-dump/Commands/ModulesCommand.cs | 3 +- src/Tools/dotnet-dump/Commands/SOSCommand.cs | 10 +- .../dotnet-dump/Commands/SetThreadCommand.cs | 3 +- src/Tools/dotnet-dump/Program.cs | 2 +- 13 files changed, 171 insertions(+), 65 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index 96ec2ee4e..89fd40430 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -25,17 +25,14 @@ 1.0.5 2.0.41 - 0.2.0-alpha.19213.1 - 0.2.0-alpha.19213.1 + 0.2.0-alpha.19254.1 + 0.2.0-alpha.19254.1 2.4.1 2.0.3 1.1.0 - - 0.1.0-alpha-63807-01 - diff --git a/src/Microsoft.Diagnostic.Repl/Command/Attributes.cs b/src/Microsoft.Diagnostic.Repl/Command/Attributes.cs index 678e969d5..06f8df3d2 100644 --- a/src/Microsoft.Diagnostic.Repl/Command/Attributes.cs +++ b/src/Microsoft.Diagnostic.Repl/Command/Attributes.cs @@ -66,4 +66,20 @@ namespace Microsoft.Diagnostic.Repl public class ArgumentAttribute : BaseAttribute { } + + /// + /// Marks the function to invoke to execute the command. + /// + [AttributeUsage(AttributeTargets.Method)] + public class CommandInvokeAttribute : Attribute + { + } + + /// + /// Marks the function to invoke to display alternate help for command. + /// + [AttributeUsage(AttributeTargets.Method)] + public class HelpInvokeAttribute : Attribute + { + } } diff --git a/src/Microsoft.Diagnostic.Repl/Command/CommandBase.cs b/src/Microsoft.Diagnostic.Repl/Command/CommandBase.cs index 7c18f6963..0d8b8cda1 100644 --- a/src/Microsoft.Diagnostic.Repl/Command/CommandBase.cs +++ b/src/Microsoft.Diagnostic.Repl/Command/CommandBase.cs @@ -15,17 +15,16 @@ namespace Microsoft.Diagnostic.Repl /// public abstract class CommandBase { - public const string EntryPointName = nameof(InvokeAsync); - /// - /// Parser invocation context. Contains the ParseResult, CommandResult, etc. + /// Parser invocation context. Contains the ParseResult, CommandResult, etc. Is null when + /// InvokeAdditionalHelp is called. /// public InvocationContext InvocationContext { get; set; } /// /// Console instance /// - public IConsole Console { get { return InvocationContext.Console; } } + public IConsole Console { get; set; } /// /// The AliasExpansion value from the CommandAttribute or null if none. @@ -35,7 +34,8 @@ namespace Microsoft.Diagnostic.Repl /// /// Execute the command /// - public abstract Task InvokeAsync(); + [CommandInvoke] + public abstract void Invoke(); /// /// Display text diff --git a/src/Microsoft.Diagnostic.Repl/Command/CommandProcessor.cs b/src/Microsoft.Diagnostic.Repl/Command/CommandProcessor.cs index 698db9397..3fa9efc46 100644 --- a/src/Microsoft.Diagnostic.Repl/Command/CommandProcessor.cs +++ b/src/Microsoft.Diagnostic.Repl/Command/CommandProcessor.cs @@ -7,8 +7,10 @@ using System; using System.Collections.Generic; using System.CommandLine; +using System.CommandLine.Binding; using System.CommandLine.Builder; using System.CommandLine.Invocation; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -20,16 +22,23 @@ namespace Microsoft.Diagnostic.Repl private readonly Parser _parser; private readonly Command _rootCommand; private readonly Dictionary _services = new Dictionary(); + private readonly Dictionary _commandHandlers = new Dictionary(); /// /// Create an instance of the command processor; /// + /// console instance to use for commands /// The list of assemblies to look for commands - public CommandProcessor(IEnumerable assemblies) + public CommandProcessor(IConsole console, IEnumerable assemblies) { + Debug.Assert(console != null); + Debug.Assert(assemblies != null); _services.Add(typeof(CommandProcessor), this); - var rootBuilder = new CommandLineBuilder(); + _services.Add(typeof(IConsole), console); + _services.Add(typeof(IHelpBuilder), new LocalHelpBuilder(this)); + var rootBuilder = new CommandLineBuilder(new Command(">")); rootBuilder.UseHelp() + .UseHelpBuilder((bindingContext) => GetService()) .UseParseDirective() .UseSuggestDirective() .UseParseErrorReporting() @@ -63,12 +72,11 @@ namespace Microsoft.Diagnostic.Repl /// Parse the command line. /// /// command line txt - /// option console /// exit code - public Task Parse(string commandLine, IConsole console = null) + public Task Parse(string commandLine) { ParseResult result = _parser.Parse(commandLine); - return _parser.InvokeAsync(result, console); + return _parser.InvokeAsync(result, GetService()); } /// @@ -98,9 +106,6 @@ namespace Microsoft.Diagnostic.Repl if (baseAttribute is CommandAttribute commandAttribute) { command = new Command(commandAttribute.Name, commandAttribute.Help); - var builder = new CommandLineBuilder(command); - builder.UseHelp(); - var properties = new List<(PropertyInfo, Option)>(); PropertyInfo argument = null; @@ -112,11 +117,13 @@ namespace Microsoft.Diagnostic.Repl if (argument != null) { throw new ArgumentException($"More than one ArgumentAttribute in command class: {type.Name}"); } + IArgumentArity arity = property.PropertyType.IsArray ? ArgumentArity.ZeroOrMore : ArgumentArity.ZeroOrOne; + command.Argument = new Argument { Name = argumentAttribute.Name ?? property.Name.ToLowerInvariant(), Description = argumentAttribute.Help, ArgumentType = property.PropertyType, - Arity = new ArgumentArity(0, int.MaxValue) + Arity = arity }; argument = property; } @@ -142,7 +149,10 @@ namespace Microsoft.Diagnostic.Repl } } - command.Handler = new Handler(this, commandAttribute.AliasExpansion, argument, properties, type); + var handler = new Handler(this, commandAttribute.AliasExpansion, argument, properties, type); + _commandHandlers.Add(command.Name, handler); + command.Handler = handler; + rootBuilder.AddCommand(command); } @@ -157,10 +167,16 @@ namespace Microsoft.Diagnostic.Repl } } + private T GetService() + { + _services.TryGetValue(typeof(T), out object service); + Debug.Assert(service != null); + return (T)service; + } + private static string BuildAlias(string parameterName) { - if (string.IsNullOrWhiteSpace(parameterName)) - { + if (string.IsNullOrWhiteSpace(parameterName)) { throw new ArgumentException("Value cannot be null or whitespace.", nameof(parameterName)); } return parameterName.Length > 1 ? $"--{parameterName.ToKebabCase()}" : $"-{parameterName.ToLowerInvariant()}"; @@ -175,6 +191,7 @@ namespace Microsoft.Diagnostic.Repl private readonly ConstructorInfo _constructor; private readonly MethodInfo _methodInfo; + private readonly MethodInfo _methodInfoHelp; public Handler(CommandProcessor commandProcessor, string aliasExpansion, PropertyInfo argument, IEnumerable<(PropertyInfo, Option)> properties, Type type) { @@ -186,11 +203,45 @@ namespace Microsoft.Diagnostic.Repl _constructor = type.GetConstructors().SingleOrDefault((info) => info.GetParameters().Length == 0) ?? throw new ArgumentException($"No eligible constructor found in {type}"); - _methodInfo = type.GetMethod(CommandBase.EntryPointName, new Type[] { typeof(IHelpBuilder) }) ?? type.GetMethod(CommandBase.EntryPointName) ?? - throw new ArgumentException($"{CommandBase.EntryPointName} method not found in {type}"); + _methodInfo = type.GetMethods().Where((methodInfo) => methodInfo.GetCustomAttribute() != null).SingleOrDefault() ?? + throw new ArgumentException($"No command invoke method found in {type}"); + + _methodInfoHelp = type.GetMethods().Where((methodInfo) => methodInfo.GetCustomAttribute() != null).SingleOrDefault(); + } + + Task ICommandHandler.InvokeAsync(InvocationContext context) + { + try + { + Invoke(_methodInfo, context); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + return Task.FromResult(context.ResultCode); + } + + /// + /// Executes the command's help invoke function if exists + /// + /// true help called, false no help function + internal bool InvokeHelp() + { + if (_methodInfoHelp == null) + { + return false; + } + // The InvocationContext is null so the options and arguments in the + // command instance created don't get set. The context for the command + // requesting help (either the help command or some other command using + // --help) won't work for the command instance that implements it's own + // help (SOS command). + Invoke(_methodInfoHelp, context: null); + return true; } - public Task InvokeAsync(InvocationContext context) + private void Invoke(MethodInfo methodInfo, InvocationContext context) { try { @@ -198,8 +249,8 @@ namespace Microsoft.Diagnostic.Repl object instance = _constructor.Invoke(new object[0]); SetProperties(context, instance); - var methodBinder = new MethodBinder(_methodInfo, () => instance); - return methodBinder.InvokeAsync(context); + object[] arguments = BuildArguments(methodInfo, context); + methodInfo.Invoke(instance, arguments); } catch (TargetInvocationException ex) { @@ -209,7 +260,7 @@ namespace Microsoft.Diagnostic.Repl private void SetProperties(InvocationContext context, object instance) { - IEnumerable optionResults = context.ParseResult.CommandResult.Children.OfType(); + IEnumerable optionResults = context?.ParseResult.CommandResult.Children.OfType(); foreach ((PropertyInfo Property, Option Option) property in _properties) { @@ -221,16 +272,10 @@ namespace Microsoft.Diagnostic.Repl else { Type propertyType = property.Property.PropertyType; - if (propertyType == typeof(InvocationContext)) { - value = context; - } - else if (propertyType == typeof(IConsole)) { - value = context.Console; - } - else if (_commandProcessor._services.TryGetValue(propertyType, out object service)) { + if (TryGetService(propertyType, context, out object service)) { value = service; } - else if (property.Option != null) + else if (context != null && property.Option != null) { OptionResult optionResult = optionResults.Where((result) => result.Option == property.Option).SingleOrDefault(); if (optionResult != null) { @@ -242,12 +287,68 @@ namespace Microsoft.Diagnostic.Repl property.Property.SetValue(instance, value); } - if (_argument != null) + if (context != null && _argument != null) { - object value = context.ParseResult.CommandResult.GetValueOrDefault(); + object value = null; + ArgumentResult result = context.ParseResult.CommandResult.ArgumentResult; + switch (result) + { + case SuccessfulArgumentResult successful: + value = successful.Value; + break; + case FailedArgumentResult failed: + throw new InvalidOperationException(failed.ErrorMessage); + } _argument.SetValue(instance, value); } } + + private object[] BuildArguments(MethodBase methodBase, InvocationContext context) + { + ParameterInfo[] parameters = methodBase.GetParameters(); + object[] arguments = new object[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) { + Type parameterType = parameters[i].ParameterType; + // Ignoring false: the parameter will passed as null to allow for "optional" + // services. The invoked method needs to check for possible null parameters. + TryGetService(parameterType, context, out arguments[i]); + } + return arguments; + } + + private bool TryGetService(Type type, InvocationContext context, out object service) + { + if (type == typeof(InvocationContext)) { + service = context; + } + else if (!_commandProcessor._services.TryGetValue(type, out service)) { + service = null; + return false; + } + return true; + } + } + + class LocalHelpBuilder : IHelpBuilder + { + private readonly CommandProcessor _commandProcessor; + + public LocalHelpBuilder(CommandProcessor commandProcessor) + { + _commandProcessor = commandProcessor; + } + + void IHelpBuilder.Write(ICommand command) + { + if (_commandProcessor._commandHandlers.TryGetValue(command.Name, out Handler handler)) + { + if (handler.InvokeHelp()) { + return; + } + } + var helpBuilder = new HelpBuilder(_commandProcessor.GetService(), maxWidth: Console.WindowWidth); + helpBuilder.Write(command); + } } } } diff --git a/src/Microsoft.Diagnostic.Repl/Microsoft.Diagnostic.Repl.csproj b/src/Microsoft.Diagnostic.Repl/Microsoft.Diagnostic.Repl.csproj index ec59a2c06..caeb36fa6 100644 --- a/src/Microsoft.Diagnostic.Repl/Microsoft.Diagnostic.Repl.csproj +++ b/src/Microsoft.Diagnostic.Repl/Microsoft.Diagnostic.Repl.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Tools/dotnet-dump/Analyzer.cs b/src/Tools/dotnet-dump/Analyzer.cs index ef80f3661..2f342d852 100644 --- a/src/Tools/dotnet-dump/Analyzer.cs +++ b/src/Tools/dotnet-dump/Analyzer.cs @@ -29,7 +29,7 @@ namespace Microsoft.Diagnostic.Tools.Dump public Analyzer() { _consoleProvider = new ConsoleProvider(); - _commandProcessor = new CommandProcessor(new Assembly[] { typeof(Analyzer).Assembly }); + _commandProcessor = new CommandProcessor(_consoleProvider, new Assembly[] { typeof(Analyzer).Assembly }); _commandProcessor.AddService(_consoleProvider); } @@ -71,14 +71,14 @@ namespace Microsoft.Diagnostic.Tools.Dump if (command != null) { foreach (string cmd in command) { - await _commandProcessor.Parse(cmd, _consoleProvider); + await _commandProcessor.Parse(cmd); } } // Start interactive command line processing await _consoleProvider.Start(async (string commandLine, CancellationToken cancellation) => { analyzeContext.CancellationToken = cancellation; - await _commandProcessor.Parse(commandLine, _consoleProvider); + await _commandProcessor.Parse(commandLine); }); } } diff --git a/src/Tools/dotnet-dump/Commands/ClrModulesCommand.cs b/src/Tools/dotnet-dump/Commands/ClrModulesCommand.cs index 50a24cfa9..1d3fb61af 100644 --- a/src/Tools/dotnet-dump/Commands/ClrModulesCommand.cs +++ b/src/Tools/dotnet-dump/Commands/ClrModulesCommand.cs @@ -11,13 +11,12 @@ namespace Microsoft.Diagnostic.Tools.Dump { public AnalyzeContext AnalyzeContext { get; set; } - public override Task InvokeAsync() + public override void Invoke() { foreach (ClrModule module in AnalyzeContext.Runtime.Modules) { WriteLine("{0:X16} {1}", module.Address, module.FileName); } - return Task.CompletedTask; } } } diff --git a/src/Tools/dotnet-dump/Commands/ExitCommand.cs b/src/Tools/dotnet-dump/Commands/ExitCommand.cs index 716d3e38c..51d35c7df 100644 --- a/src/Tools/dotnet-dump/Commands/ExitCommand.cs +++ b/src/Tools/dotnet-dump/Commands/ExitCommand.cs @@ -11,10 +11,9 @@ namespace Microsoft.Diagnostic.Tools.Dump { public ConsoleProvider ConsoleProvider { get; set; } - public override Task InvokeAsync() + public override void Invoke() { ConsoleProvider.Stop(); - return Task.CompletedTask; } } } diff --git a/src/Tools/dotnet-dump/Commands/HelpCommand.cs b/src/Tools/dotnet-dump/Commands/HelpCommand.cs index eef951401..37b2b3c79 100644 --- a/src/Tools/dotnet-dump/Commands/HelpCommand.cs +++ b/src/Tools/dotnet-dump/Commands/HelpCommand.cs @@ -12,25 +12,17 @@ namespace Microsoft.Diagnostic.Tools.Dump public CommandProcessor CommandProcessor { get; set; } - public override Task InvokeAsync() - { - return Task.CompletedTask; - } + public IHelpBuilder HelpBuilder { get; set; } - /// - /// Get help builder interface - /// - /// help builder - public Task InvokeAsync(IHelpBuilder helpBuilder) + public override void Invoke() { Command command = CommandProcessor.GetCommand(Command); if (command != null) { - helpBuilder.Write(command); + HelpBuilder.Write(command); } else { Console.Error.WriteLine($"Help for {Command} not found."); } - return Task.CompletedTask; } } } diff --git a/src/Tools/dotnet-dump/Commands/ModulesCommand.cs b/src/Tools/dotnet-dump/Commands/ModulesCommand.cs index 8b4091f88..a0e4998c5 100644 --- a/src/Tools/dotnet-dump/Commands/ModulesCommand.cs +++ b/src/Tools/dotnet-dump/Commands/ModulesCommand.cs @@ -17,7 +17,7 @@ namespace Microsoft.Diagnostic.Tools.Dump public AnalyzeContext AnalyzeContext { get; set; } - public override Task InvokeAsync() + public override void Invoke() { foreach (ModuleInfo module in AnalyzeContext.Target.DataReader.EnumerateModules()) { @@ -38,7 +38,6 @@ namespace Microsoft.Diagnostic.Tools.Dump WriteLine("{0:X16} {1:X8} {2}", module.ImageBase, module.FileSize, module.FileName); } } - return Task.CompletedTask; } } } diff --git a/src/Tools/dotnet-dump/Commands/SOSCommand.cs b/src/Tools/dotnet-dump/Commands/SOSCommand.cs index 98a3bcbae..f9bfecee2 100644 --- a/src/Tools/dotnet-dump/Commands/SOSCommand.cs +++ b/src/Tools/dotnet-dump/Commands/SOSCommand.cs @@ -41,7 +41,6 @@ namespace Microsoft.Diagnostic.Tools.Dump [Command(Name = "histobjfind", AliasExpansion = "HistObjFind", Help = "Displays all the log entries that reference an object at the specified address.")] [Command(Name = "histroot", AliasExpansion = "HistRoot", Help = "Displays information related to both promotions and relocations of the specified root.")] [Command(Name = "setsymbolserver", AliasExpansion = "SetSymbolServer", Help = "Enables the symbol server support ")] - [Command(Name = "soshelp", AliasExpansion = "Help", Help = "Displays all available commands when no parameter is specified, or displays detailed help information about the specified command. soshelp ")] public class SOSCommand : CommandBase { [Argument(Name = "arguments", Help = "Arguments to SOS command.")] @@ -49,7 +48,7 @@ namespace Microsoft.Diagnostic.Tools.Dump public AnalyzeContext AnalyzeContext { get; set; } - public override Task InvokeAsync() + public override void Invoke() { try { string arguments = null; @@ -61,7 +60,12 @@ namespace Microsoft.Diagnostic.Tools.Dump catch (Exception ex) when (ex is FileNotFoundException || ex is EntryPointNotFoundException || ex is InvalidOperationException) { Console.Error.WriteLine(ex.Message); } - return Task.CompletedTask; + } + + [HelpInvoke] + public void InvokeHelp() + { + AnalyzeContext.SOSHost.ExecuteCommand("Help", AliasExpansion); } } } diff --git a/src/Tools/dotnet-dump/Commands/SetThreadCommand.cs b/src/Tools/dotnet-dump/Commands/SetThreadCommand.cs index 049ef6cd6..032bacbf5 100644 --- a/src/Tools/dotnet-dump/Commands/SetThreadCommand.cs +++ b/src/Tools/dotnet-dump/Commands/SetThreadCommand.cs @@ -14,7 +14,7 @@ namespace Microsoft.Diagnostic.Tools.Dump public AnalyzeContext AnalyzeContext { get; set; } - public override Task InvokeAsync() + public override void Invoke() { if (ThreadId.HasValue) { @@ -29,7 +29,6 @@ namespace Microsoft.Diagnostic.Tools.Dump index++; } } - return Task.CompletedTask; } } } diff --git a/src/Tools/dotnet-dump/Program.cs b/src/Tools/dotnet-dump/Program.cs index 67cf8cad9..917fb5060 100644 --- a/src/Tools/dotnet-dump/Program.cs +++ b/src/Tools/dotnet-dump/Program.cs @@ -69,6 +69,6 @@ If not specified 'heap' is the default.", new Option( new[] { "-c", "--command" }, "Run the command on start.", - new Argument() { Name = "command" }); + new Argument() { Name = "command", Arity = ArgumentArity.ZeroOrMore }); } } -- 2.34.1