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

Allow sos <cmd> to be used consistently across all debuggers/hosts #3266

Merged
merged 3 commits into from
Aug 10, 2022
Merged
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
6 changes: 3 additions & 3 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@
</PropertyGroup>
<PropertyGroup>
<!-- Runtime versions to test -->
<MicrosoftNETCoreApp31Version>3.1.25</MicrosoftNETCoreApp31Version>
<MicrosoftNETCoreApp31Version>3.1.28</MicrosoftNETCoreApp31Version>
<MicrosoftAspNetCoreApp31Version>$(MicrosoftNETCoreApp31Version)</MicrosoftAspNetCoreApp31Version>
<MicrosoftNETCoreApp60Version>6.0.6</MicrosoftNETCoreApp60Version>
<MicrosoftNETCoreApp60Version>6.0.8</MicrosoftNETCoreApp60Version>
<MicrosoftAspNetCoreApp60Version>$(MicrosoftNETCoreApp60Version)</MicrosoftAspNetCoreApp60Version>
<!-- The SDK runtime version used to build single-file apps (currently hardcoded) -->
<SingleFileRuntime60Version>6.0.6</SingleFileRuntime60Version>
<SingleFileRuntime60Version>6.0.8</SingleFileRuntime60Version>
<SingleFileRuntimeLatestVersion>7.0.0-rc.1.22403.8</SingleFileRuntimeLatestVersion>
</PropertyGroup>
<PropertyGroup>
Expand Down
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