Skip to content

Commit

Permalink
Allow sos <cmd> to be used consistently across all debuggers/hosts
Browse files Browse the repository at this point in the history
Issue #2998

Renamed CommandPlatform to CommandFlags

Add DAC EnumMemoryRegion `enummemory` test command

Don't fail if datatarget's ReadVirtual is passed 0 size
  • Loading branch information
mikem8361 committed Aug 10, 2022
1 parent de671af commit bdc42d4
Show file tree
Hide file tree
Showing 31 changed files with 649 additions and 189 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ public CommandService(string commandPrompt = null)
/// </summary>
/// <param name="commandLine">command line text</param>
/// <param name="services">services for the command</param>
/// <returns>exit code</returns>
public int Execute(string commandLine, IServiceProvider services)
/// <returns>true success, false failure</returns>
public bool Execute(string commandLine, IServiceProvider services)
{
// Parse the command line and invoke the command
ParseResult parseResult = Parser.Parse(commandLine);
Expand All @@ -70,22 +70,35 @@ public int Execute(string commandLine, IServiceProvider services)
{
context.Console.Error.WriteLine($"Command '{command.Name}' needs a target");
}
return 1;
return false;
}
try
{
handler.Invoke(context, services);
}
catch (Exception ex)
{
OnException(ex, context);
if (ex is NullReferenceException ||
ex is ArgumentException ||
ex is ArgumentNullException ||
ex is ArgumentOutOfRangeException ||
ex is NotImplementedException)
{
context.Console.Error.WriteLine(ex.ToString());
}
else
{
context.Console.Error.WriteLine(ex.Message);
}
Trace.TraceError(ex.ToString());
return false;
}
}
}
}

context.InvocationResult?.Apply(context);
return context.ResultCode;
return context.ResultCode == 0;
}

/// <summary>
Expand Down Expand Up @@ -137,6 +150,13 @@ public bool DisplayHelp(string commandName, IServiceProvider services)
return true;
}

/// <summary>
/// Does this command or alias exists?
/// </summary>
/// <param name="commandName">command or alias name</param>
/// <returns>true if command exists</returns>
public bool IsCommand(string commandName) => _rootBuilder.Command.Children.Contains(commandName);

/// <summary>
/// Enumerates all the command's name and help
/// </summary>
Expand All @@ -149,28 +169,31 @@ public bool DisplayHelp(string commandName, IServiceProvider services)
/// <param name="factory">function to create command instance</param>
public void AddCommands(Type type, Func<IServiceProvider, object> factory)
{
for (Type baseType = type; baseType != null; baseType = baseType.BaseType)
if (type.IsClass)
{
if (baseType == typeof(CommandBase)) {
break;
}
var commandAttributes = (CommandAttribute[])baseType.GetCustomAttributes(typeof(CommandAttribute), inherit: false);
foreach (CommandAttribute commandAttribute in commandAttributes)
for (Type baseType = type; baseType != null; baseType = baseType.BaseType)
{
if (factory == null)
if (baseType == typeof(CommandBase))
{
// Assumes zero parameter constructor
ConstructorInfo constructor = type.GetConstructors().SingleOrDefault((info) => info.GetParameters().Length == 0) ??
throw new ArgumentException($"No eligible constructor found in {type}");

factory = (services) => constructor.Invoke(Array.Empty<object>());
break;
}
var commandAttributes = (CommandAttribute[])baseType.GetCustomAttributes(typeof(CommandAttribute), inherit: false);
foreach (CommandAttribute commandAttribute in commandAttributes)
{
if ((commandAttribute.Flags & CommandFlags.Manual) == 0 || factory != null)
{
if (factory == null)
{
factory = (services) => Utilities.InvokeConstructor(type, services, optional: true);
}
CreateCommand(baseType, commandAttribute, factory);
}
}
CreateCommand(baseType, commandAttribute, factory);
}
}

// Build or re-build parser instance after all these commands and aliases are added
FlushParser();
// Build or re-build parser instance after all these commands and aliases are added
FlushParser();
}
}

private void CreateCommand(Type type, CommandAttribute commandAttribute, Func<IServiceProvider, object> factory)
Expand Down Expand Up @@ -234,27 +257,6 @@ private void CreateCommand(Type type, CommandAttribute commandAttribute, Func<IS

private void FlushParser() => _parser = null;

private void OnException(Exception ex, InvocationContext context)
{
if (ex is TargetInvocationException)
{
ex = ex.InnerException;
}
if (ex is NullReferenceException ||
ex is ArgumentException ||
ex is ArgumentNullException ||
ex is ArgumentOutOfRangeException ||
ex is NotImplementedException)
{
context.Console.Error.WriteLine(ex.ToString());
}
else
{
context.Console.Error.WriteLine(ex.Message);
}
Trace.TraceError(ex.ToString());
}

private static string BuildOptionAlias(string parameterName)
{
if (string.IsNullOrWhiteSpace(parameterName)) {
Expand Down Expand Up @@ -319,23 +321,23 @@ Task<int> ICommandHandler.InvokeAsync(InvocationContext context)
/// </summary>
internal bool IsValidPlatform(ITarget target)
{
if ((_commandAttribute.Platform & CommandPlatform.Global) != 0)
if ((_commandAttribute.Flags & CommandFlags.Global) != 0)
{
return true;
}
if (target != null)
{
if (target.OperatingSystem == OSPlatform.Windows)
{
return (_commandAttribute.Platform & CommandPlatform.Windows) != 0;
return (_commandAttribute.Flags & CommandFlags.Windows) != 0;
}
if (target.OperatingSystem == OSPlatform.Linux)
{
return (_commandAttribute.Platform & CommandPlatform.Linux) != 0;
return (_commandAttribute.Flags & CommandFlags.Linux) != 0;
}
if (target.OperatingSystem == OSPlatform.OSX)
{
return (_commandAttribute.Platform & CommandPlatform.OSX) != 0;
return (_commandAttribute.Flags & CommandFlags.OSX) != 0;
}
}
return false;
Expand Down Expand Up @@ -372,9 +374,7 @@ private void Invoke(MethodInfo methodInfo, InvocationContext context, Parser par
{
object instance = _factory(services);
SetProperties(context, parser, services, instance);

object[] arguments = BuildArguments(methodInfo, services);
methodInfo.Invoke(instance, arguments);
Utilities.Invoke(methodInfo, instance, services, optional: true);
}

private void SetProperties(InvocationContext context, Parser parser, IServiceProvider services, object instance)
Expand Down Expand Up @@ -461,21 +461,6 @@ private void SetProperties(InvocationContext context, Parser parser, IServicePro
argument.Property.SetValue(instance, array != null ? array.ToArray() : value);
}
}

private object[] BuildArguments(MethodBase methodBase, IServiceProvider services)
{
ParameterInfo[] parameters = methodBase.GetParameters();
object[] arguments = new object[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
{
Type parameterType = parameters[i].ParameterType;

// The parameter will passed as null to allow for "optional" services. The invoked
// method needs to check for possible null parameters.
arguments[i] = services.GetService(parameterType);
}
return arguments;
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.PortableExecutable;

namespace Microsoft.Diagnostics.DebugServices.Implementation
Expand Down Expand Up @@ -107,5 +108,65 @@ public static Stream TryOpenFile(string path)

return null;
}

/// <summary>
/// Call the constructor of the type and return the instance binding any
/// services in the constructor parameters.
/// </summary>
/// <param name="type">type to create</param>
/// <param name="provider">services</param>
/// <param name="optional">if true, the service is not required</param>
/// <returns>type instance</returns>
public static object InvokeConstructor(Type type, IServiceProvider provider, bool optional)
{
ConstructorInfo constructor = type.GetConstructors().Single();
object[] arguments = BuildArguments(constructor, provider, optional);
try
{
return constructor.Invoke(arguments);
}
catch (TargetInvocationException ex)
{
throw ex.InnerException;
}
}

/// <summary>
/// Call the method and bind any services in the constructor parameters.
/// </summary>
/// <param name="method">method to invoke</param>
/// <param name="instance">class instance or null if static</param>
/// <param name="provider">services</param>
/// <param name="optional">if true, the service is not required</param>
/// <returns>method return value</returns>
public static object Invoke(MethodBase method, object instance, IServiceProvider provider, bool optional)
{
object[] arguments = BuildArguments(method, provider, optional);
try
{
return method.Invoke(instance, arguments);
}
catch (TargetInvocationException ex)
{
throw ex.InnerException;
}
}

private static object[] BuildArguments(MethodBase methodBase, IServiceProvider services, bool optional)
{
ParameterInfo[] parameters = methodBase.GetParameters();
object[] arguments = new object[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
{
// The parameter will passed as null to allow for "optional" services. The invoked
// method needs to check for possible null parameters.
arguments[i] = services.GetService(parameters[i].ParameterType);
if (arguments[i] is null && !optional)
{
throw new DiagnosticsException($"The {parameters[i].ParameterType} service is required by the {parameters[i].Name} parameter");
}
}
return arguments;
}
}
}
13 changes: 9 additions & 4 deletions src/Microsoft.Diagnostics.DebugServices/CommandAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
namespace Microsoft.Diagnostics.DebugServices
{
/// <summary>
/// OS Platforms to add command
/// Command flags to filter by OS Platforms, control scope and how the command is registered.
/// </summary>
[Flags]
public enum CommandPlatform : byte
public enum CommandFlags : byte
{
Windows = 0x01,
Linux = 0x02,
Expand All @@ -21,6 +21,11 @@ public enum CommandPlatform : byte
/// </summary>
Global = 0x08,

/// <summary>
/// Command is not added through reflection, but manually with command service API.
/// </summary>
Manual = 0x10,

/// <summary>
/// Default. All operating system, but target is required
/// </summary>
Expand Down Expand Up @@ -49,9 +54,9 @@ public class CommandAttribute : Attribute
public string[] Aliases = Array.Empty<string>();

/// <summary>
/// Optional OS platform for the command
/// Command flags to filter by OS Platforms, control scope and how the command is registered.
/// </summary>
public CommandPlatform Platform = CommandPlatform.Default;
public CommandFlags Flags = CommandFlags.Default;

/// <summary>
/// A string of options that are parsed before the command line options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

namespace Microsoft.Diagnostics.ExtensionCommands
{
[Command(Name = CommandName, Help = "Displays information about async \"stacks\" on the garbage-collected heap.")]
[Command(Name = CommandName, Aliases = new string[] { "DumpAsync" }, Help = "Displays information about async \"stacks\" on the garbage-collected heap.")]
public sealed class DumpAsyncCommand : ExtensionCommandBase
{
/// <summary>The name of the command.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace Microsoft.Diagnostics.ExtensionCommands
{
[Command(Name = "logging", Help = "Enable/disable internal logging", Platform = CommandPlatform.Global)]
[Command(Name = "logging", Help = "Enable/disable internal logging", Flags = CommandFlags.Global)]
public class LoggingCommand : CommandBase
{
[Option(Name = "enable", Help = "Enable internal logging.")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ namespace Microsoft.Diagnostics.ExtensionCommands
Name = "setsymbolserver",
Aliases = new string[] { "SetSymbolServer" },
Help = "Enable and set symbol server support for symbols and module download",
Platform = CommandPlatform.Global)]
Flags = CommandFlags.Global)]
[Command(
Name = "loadsymbols",
DefaultOptions = "--loadsymbols",
Help = "Load symbols for all modules",
Flags = CommandFlags.Global)]
public class SetSymbolServerCommand : CommandBase
{
public ISymbolService SymbolService { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.Diagnostics.Repl/ExitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace Microsoft.Diagnostics.Repl
{
[Command(Name = "exit", Aliases = new string[] { "quit", "q" }, Help = "Exit interactive mode.", Platform = CommandPlatform.Global)]
[Command(Name = "exit", Aliases = new string[] { "quit", "q" }, Help = "Exit interactive mode.", Flags = CommandFlags.Global | CommandFlags.Manual)]
public class ExitCommand : CommandBase
{
private readonly Action _exit;
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.Diagnostics.Repl/HelpCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace Microsoft.Diagnostics.Repl
{
[Command(Name = "help", Help = "Display help for a command.", Platform = CommandPlatform.Global)]
[Command(Name = "help", Help = "Display help for a command.", Flags = CommandFlags.Global | CommandFlags.Manual)]
public class HelpCommand : CommandBase
{
[Argument(Help = "Command to find help.")]
Expand Down
Loading

0 comments on commit bdc42d4

Please sign in to comment.