From: Mike McLaughlin Date: Wed, 4 Oct 2023 23:17:50 +0000 (-0700) Subject: Command groups to support duplicate command names and better help support (#4285) X-Git-Tag: accepted/tizen/unified/riscv/20231226.055542~35^2~1^2~58 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=bbf3e7b34120a30acf7734a5983d629d72fd4e83;p=platform%2Fcore%2Fdotnet%2Fdiagnostics.git Command groups to support duplicate command names and better help support (#4285) Add extension load testing that includes duplicate command names. Add internal CommandGroup class to CommandService dotnet-dump analyze -c/--command will exit on any parsing errors or exceptions in the command command help not displayed on parsing errors/invalid options Add INIT_API_CHECK_MANAGED native SOS command macro Rename clrmodules command to assemblies and keep clrmodules as an alias Support static HelpInvoke/FilterInvoke methods Add command service testing Better command service help interface. Returns help text instead of printing it on the console directly. ICommandService.DisplayHelp() => GetHelpText(). Better help sorting. Fix some SOS scripts Return error code for command line option errors in native SOS Load next to executing assembly first when hosted under desktop Framework Test using "clrthreads" instead of "Threads" Replace testing "u" with "clru" Add more general command filter mechanism. Add FilterType property to Command attribute. Remove OS filter command flags. NativeAddressHelper service work Move Windows managed command stubs to separate file Add SOS.Hosting services to SOS.Extensions for better native command help. This requires the special ManagedOnlyCommandFilter service to prevent recursion of the C++ commands. On Windows this recursion is from "C++ command" -> "checking if managed version" -> "executing the command in SOS.Hosting" -> "C++ command". Help is now uniform across managed/native, alphabetized and filtered by the current runtime. --- diff --git a/diagnostics.sln b/diagnostics.sln index c53cec4f3..23ae2ff91 100644 --- a/diagnostics.sln +++ b/diagnostics.sln @@ -266,7 +266,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonTestRunner", "src\tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotnetStack.UnitTests", "src\tests\dotnet-stack\DotnetStack.UnitTests.csproj", "{E8F133F8-4D20-475D-9D16-2BA236DAB65F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Diagnostics.WebSocketServer", "src\Microsoft.Diagnostics.WebSocketServer\Microsoft.Diagnostics.WebSocketServer.csproj", "{1043FA82-37CC-4809-80DC-C1EB06A55133}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.WebSocketServer", "src\Microsoft.Diagnostics.WebSocketServer\Microsoft.Diagnostics.WebSocketServer.csproj", "{1043FA82-37CC-4809-80DC-C1EB06A55133}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestExtension", "src\tests\TestExtension\TestExtension.csproj", "{C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -1907,6 +1909,46 @@ Global {1043FA82-37CC-4809-80DC-C1EB06A55133}.RelWithDebInfo|x64.Build.0 = Debug|Any CPU {1043FA82-37CC-4809-80DC-C1EB06A55133}.RelWithDebInfo|x86.ActiveCfg = Debug|Any CPU {1043FA82-37CC-4809-80DC-C1EB06A55133}.RelWithDebInfo|x86.Build.0 = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Checked|Any CPU.Build.0 = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Checked|ARM.ActiveCfg = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Checked|ARM.Build.0 = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Checked|ARM64.ActiveCfg = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Checked|ARM64.Build.0 = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Checked|x64.ActiveCfg = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Checked|x64.Build.0 = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Checked|x86.ActiveCfg = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Checked|x86.Build.0 = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Debug|ARM.ActiveCfg = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Debug|ARM.Build.0 = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Debug|ARM64.Build.0 = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Debug|x64.ActiveCfg = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Debug|x64.Build.0 = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Debug|x86.ActiveCfg = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Debug|x86.Build.0 = Debug|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Release|Any CPU.Build.0 = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Release|ARM.ActiveCfg = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Release|ARM.Build.0 = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Release|ARM64.ActiveCfg = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Release|ARM64.Build.0 = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Release|x64.ActiveCfg = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Release|x64.Build.0 = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Release|x86.ActiveCfg = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.Release|x86.Build.0 = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.RelWithDebInfo|ARM.ActiveCfg = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.RelWithDebInfo|ARM.Build.0 = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.RelWithDebInfo|ARM64.ActiveCfg = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.RelWithDebInfo|ARM64.Build.0 = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.RelWithDebInfo|x64.Build.0 = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E}.RelWithDebInfo|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1966,6 +2008,7 @@ Global {DFF48CB6-4504-41C6-A8F1-F4A3D316D49F} = {03479E19-3F18-49A6-910A-F5041E27E7C0} {E8F133F8-4D20-475D-9D16-2BA236DAB65F} = {03479E19-3F18-49A6-910A-F5041E27E7C0} {1043FA82-37CC-4809-80DC-C1EB06A55133} = {19FAB78C-3351-4911-8F0C-8C6056401740} + {C6EB3C21-FDFF-4CF0-BE3A-3D1A3924408E} = {03479E19-3F18-49A6-910A-F5041E27E7C0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {46465737-C938-44FC-BE1A-4CE139EBB5E0} diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/AssemblyResolver.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/AssemblyResolver.cs index 95eb3690d..626b31082 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/AssemblyResolver.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/AssemblyResolver.cs @@ -37,6 +37,15 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation string probingPath; Assembly assembly; + // Look next to the executing assembly + probingPath = Path.Combine(_defaultAssembliesPath, fileName); + Debug.WriteLine($"Considering {probingPath} based on ExecutingAssembly"); + if (Probe(probingPath, referenceName.Version, out assembly)) + { + Debug.WriteLine($"Matched {probingPath} based on ExecutingAssembly"); + return assembly; + } + // Look next to requesting assembly assemblyPath = args.RequestingAssembly?.Location; if (!string.IsNullOrEmpty(assemblyPath)) @@ -50,15 +59,6 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation } } - // Look next to the executing assembly - probingPath = Path.Combine(_defaultAssembliesPath, fileName); - Debug.WriteLine($"Considering {probingPath} based on ExecutingAssembly"); - if (Probe(probingPath, referenceName.Version, out assembly)) - { - Debug.WriteLine($"Matched {probingPath} based on ExecutingAssembly"); - return assembly; - } - return null; } diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs index 01e4cfdb6..2c87c8514 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs @@ -9,10 +9,9 @@ using System.CommandLine.Help; using System.CommandLine.Invocation; using System.CommandLine.IO; using System.CommandLine.Parsing; -using System.Diagnostics; using System.Linq; using System.Reflection; -using System.Runtime.InteropServices; +using System.Text; using System.Threading.Tasks; namespace Microsoft.Diagnostics.DebugServices.Implementation @@ -22,9 +21,8 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// public class CommandService : ICommandService { - private Parser _parser; - private readonly CommandLineBuilder _rootBuilder; - private readonly Dictionary _commandHandlers = new(); + private readonly List _commandGroups = new(); + private readonly string _commandPrompt; /// /// Create an instance of the command processor; @@ -32,8 +30,10 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// command prompted used in help message public CommandService(string commandPrompt = null) { - _rootBuilder = new CommandLineBuilder(new Command(commandPrompt ?? ">")); - _rootBuilder.UseHelpBuilder((bindingContext) => new LocalHelpBuilder(this, bindingContext.Console, useHelpBuilder: false)); + _commandPrompt = commandPrompt ?? ">"; + + // Create default command group (should always be last in this list) + _commandGroups.Add(new CommandGroup(_commandPrompt)); } /// @@ -41,125 +41,168 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// /// command line text /// services for the command - /// true success, false failure + /// true - found command, false - command not found + /// empty command line + /// other errors + /// parsing error public bool Execute(string commandLine, IServiceProvider services) { - // Parse the command line and invoke the command - ParseResult parseResult = Parser.Parse(commandLine); + string[] commandLineArray = CommandLineStringSplitter.Instance.Split(commandLine).ToArray(); + if (commandLineArray.Length <= 0) + { + throw new ArgumentException("Empty command line", nameof(commandLine)); + } + string commandName = commandLineArray[0].Trim(); + return Execute(commandName, commandLineArray, services); + } + + /// + /// Parse and execute the command. + /// + /// command name + /// command arguments/options + /// services for the command + /// true - found command, false - command not found + /// empty command name or arguments + /// other errors + /// parsing error + public bool Execute(string commandName, string commandArguments, IServiceProvider services) + { + commandName = commandName.Trim(); + string[] commandLineArray = CommandLineStringSplitter.Instance.Split(commandName + " " + (commandArguments ?? "")).ToArray(); + if (commandLineArray.Length <= 0) + { + throw new ArgumentException("Empty command name or arguments", nameof(commandArguments)); + } + return Execute(commandName, commandLineArray, services); + } - InvocationContext context = new(parseResult, new LocalConsole(services)); - if (parseResult.Errors.Count > 0) + /// + /// Find, parse and execute the command. + /// + /// command name + /// command line + /// services for the command + /// true - found command, false - command not found + /// empty command name + /// other errors + /// parsing error + private bool Execute(string commandName, string[] commandLineArray, IServiceProvider services) + { + if (string.IsNullOrEmpty(commandName)) { - context.InvocationResult = new ParseErrorResult(); + throw new ArgumentException("Empty command name", nameof(commandName)); } - else + List messages = new(); + foreach (CommandGroup group in _commandGroups) { - if (parseResult.CommandResult.Command is Command command) + if (group.TryGetCommandHandler(commandName, out CommandHandler handler)) { - if (command.Handler is CommandHandler handler) + try { - ITarget target = services.GetService(); - if (!handler.IsValidPlatform(target)) + if (handler.IsCommandSupported(group.Parser, services)) { - if (target != null) - { - context.Console.Error.WriteLine($"Command '{command.Name}' not supported on this target"); - } - else + if (group.Execute(commandLineArray, services)) { - context.Console.Error.WriteLine($"Command '{command.Name}' needs a target"); + return true; } - return false; - } - try - { - handler.Invoke(context, services); } - catch (Exception ex) + if (handler.FilterInvokeMessage != null) { - if (ex is NullReferenceException or - ArgumentException or - ArgumentNullException or - ArgumentOutOfRangeException or - NotImplementedException) - { - context.Console.Error.WriteLine(ex.ToString()); - } - else - { - context.Console.Error.WriteLine(ex.Message); - } - Trace.TraceError(ex.ToString()); - return false; + messages.Add(handler.FilterInvokeMessage); } } + catch (CommandNotFoundException ex) + { + messages.Add(ex.Message); + } } } - - context.InvocationResult?.Apply(context); - return context.ResultCode == 0; + if (messages.Count > 0) + { + throw new CommandNotFoundException(string.Concat(messages.Select(s => s + Environment.NewLine))); + } + return false; } /// /// Displays the help for a command /// - /// name of the command or alias /// service provider - /// true if success, false if command not found - public bool DisplayHelp(string commandName, IServiceProvider services) + /// command invocation and help enumeration + public IEnumerable<(string Invocation, string Help)> GetAllCommandHelp(IServiceProvider services) { - Command command = null; - if (!string.IsNullOrEmpty(commandName)) + List<(string Invocation, string Help)> help = new(); + foreach (CommandGroup group in _commandGroups) { - command = _rootBuilder.Command.Children.OfType().FirstOrDefault((cmd) => commandName == cmd.Name || cmd.Aliases.Any((alias) => commandName == alias)); - if (command == null) - { - return false; - } - if (command.Handler is CommandHandler handler) + foreach (CommandHandler handler in group.CommandHandlers) { - ITarget target = services.GetService(); - if (!handler.IsValidPlatform(target)) + try + { + if (handler.IsCommandSupported(group.Parser, services)) + { + string invocation = handler.HelpInvocation; + help.Add((invocation, handler.Help)); + } + } + catch (CommandNotFoundException) { - return false; } } } - else - { - ITarget target = services.GetService(); + return help; + } - // Create temporary builder adding only the commands that are valid for the target - CommandLineBuilder builder = new(new Command(_rootBuilder.Command.Name)); - foreach (Command cmd in _rootBuilder.Command.Children.OfType()) + /// + /// Displays the detailed help for a command + /// + /// name of the command or alias + /// service provider + /// the width to format the help or int.MaxValue + /// help text or null if not found + public string GetDetailedHelp(string commandName, IServiceProvider services, int consoleWidth) + { + if (string.IsNullOrWhiteSpace(commandName)) + { + throw new ArgumentNullException(nameof(commandName)); + } + List messages = new(); + foreach (CommandGroup group in _commandGroups) + { + if (group.TryGetCommand(commandName, out Command command)) { - if (cmd.Handler is CommandHandler handler) + if (command.Handler is CommandHandler handler) { - if (handler.IsValidPlatform(target)) + try + { + if (handler.IsCommandSupported(group.Parser, services)) + { + return group.GetDetailedHelp(command, services, consoleWidth); + } + if (handler.FilterInvokeMessage != null) + { + messages.Add(handler.FilterInvokeMessage); + } + } + catch (CommandNotFoundException ex) { - builder.AddCommand(cmd); + messages.Add(ex.Message); } } } - command = builder.Command; } - Debug.Assert(command != null); - IHelpBuilder helpBuilder = new LocalHelpBuilder(this, new LocalConsole(services), useHelpBuilder: true); - helpBuilder.Write(command); - return true; + if (messages.Count > 0) + { + return string.Concat(messages.Select(s => s + Environment.NewLine)); + } + return null; } /// - /// Does this command or alias exists? - /// - /// command or alias name - /// true if command exists - public bool IsCommand(string commandName) => _rootBuilder.Command.Children.Contains(commandName); - - /// - /// Enumerates all the command's name and help + /// Enumerates all the command's name, help and aliases /// - public IEnumerable<(string name, string help, IEnumerable aliases)> Commands => _commandHandlers.Select((keypair) => (keypair.Value.Name, keypair.Value.Help, keypair.Value.Aliases)); + public IEnumerable<(string name, string help, IEnumerable aliases)> Commands => + _commandGroups.SelectMany((group) => group.CommandHandlers).Select((handler) => (handler.Name, handler.Help, handler.Aliases)); /// /// Add the commands and aliases attributes found in the type. @@ -185,84 +228,242 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation CommandAttribute[] commandAttributes = (CommandAttribute[])baseType.GetCustomAttributes(typeof(CommandAttribute), inherit: true); foreach (CommandAttribute commandAttribute in commandAttributes) { - if ((commandAttribute.Flags & CommandFlags.Manual) == 0 || factory != null) + factory ??= (services) => Utilities.CreateInstance(type, services); + + bool dup = true; + foreach (CommandGroup group in _commandGroups) { - factory ??= (services) => Utilities.CreateInstance(type, services); - CreateCommand(baseType, commandAttribute, factory); + // If the group doesn't contain a duplicate command name, add it to that group + if (!group.Contains(commandAttribute.Name)) + { + group.CreateCommand(baseType, commandAttribute, factory); + dup = false; + break; + } + } + // If this is a duplicate command, create a new group and add it to the beginning. The default group must be last. + if (dup) + { + CommandGroup group = new(_commandPrompt); + _commandGroups.Insert(0, group); + group.CreateCommand(baseType, commandAttribute, factory); } } } - - // Build or re-build parser instance after all these commands and aliases are added - FlushParser(); } } - private void CreateCommand(Type type, CommandAttribute commandAttribute, Func factory) + /// + /// This groups like commands that may have the same name as another group or the default one. + /// + private sealed class CommandGroup { - Command command = new(commandAttribute.Name, commandAttribute.Help); - List<(PropertyInfo, Option)> properties = new(); - List<(PropertyInfo, Argument)> arguments = new(); + private Parser _parser; + private readonly CommandLineBuilder _rootBuilder; + private readonly Dictionary _commandHandlers = new(); - foreach (string alias in commandAttribute.Aliases) + /// + /// Create an instance of the command processor; + /// + /// command prompted used in help message + public CommandGroup(string commandPrompt = null) { - command.AddAlias(alias); + _rootBuilder = new CommandLineBuilder(new Command(commandPrompt)); } - foreach (PropertyInfo property in type.GetProperties().Where(p => p.CanWrite)) + /// + /// Parse and execute the command line. + /// + /// command line text + /// services for the command + /// true if command was found and executed without error + /// parsing error + internal bool Execute(IReadOnlyList commandLine, IServiceProvider services) { - ArgumentAttribute argumentAttribute = (ArgumentAttribute)property.GetCustomAttributes(typeof(ArgumentAttribute), inherit: false).SingleOrDefault(); - if (argumentAttribute != null) - { - IArgumentArity arity = property.PropertyType.IsArray ? ArgumentArity.ZeroOrMore : ArgumentArity.ZeroOrOne; + // Parse the command line and invoke the command + ParseResult parseResult = Parser.Parse(commandLine); - Argument argument = new() + if (parseResult.Errors.Count > 0) + { + StringBuilder sb = new(); + foreach (ParseError error in parseResult.Errors) { - Name = argumentAttribute.Name ?? property.Name.ToLowerInvariant(), - Description = argumentAttribute.Help, - ArgumentType = property.PropertyType, - Arity = arity - }; - command.AddArgument(argument); - arguments.Add((property, argument)); + sb.AppendLine(error.Message); + } + string helpText = GetDetailedHelp(parseResult.CommandResult.Command, services, int.MaxValue); + throw new CommandParsingException(sb.ToString(), helpText); } else { - OptionAttribute optionAttribute = (OptionAttribute)property.GetCustomAttributes(typeof(OptionAttribute), inherit: false).SingleOrDefault(); - if (optionAttribute != null) + if (parseResult.CommandResult.Command is Command command) { - Option option = new(optionAttribute.Name ?? BuildOptionAlias(property.Name), optionAttribute.Help) + if (command.Handler is CommandHandler handler) { - Argument = new Argument { ArgumentType = property.PropertyType } - }; - command.AddOption(option); - properties.Add((property, option)); + InvocationContext context = new(parseResult, new LocalConsole(services.GetService())); + handler.Invoke(context, services); + return true; + } + } + } + return false; + } + + /// + /// Build/return parser + /// + internal Parser Parser => _parser ??= _rootBuilder.Build(); + + /// + /// Returns all the command handler instances + /// + internal IEnumerable CommandHandlers => _commandHandlers.Values; + + /// + /// Returns true if command or command alias is found + /// + internal bool Contains(string commandName) => _rootBuilder.Command.Children.Contains(commandName); + + /// + /// Returns the command handler for the command or command alias + /// + /// command or alias + /// handler instance + /// true if found + internal bool TryGetCommandHandler(string commandName, out CommandHandler handler) + { + handler = null; + if (TryGetCommand(commandName, out Command command)) + { + handler = command.Handler as CommandHandler; + } + return handler != null; + } + + /// + /// Returns the command instance for the command or command alias + /// + /// command or alias + /// command instance + /// true if found + internal bool TryGetCommand(string commandName, out Command command) + { + command = _rootBuilder.Command.Children.GetByAlias(commandName) as Command; + return command != null; + } + + /// + /// Add the commands and aliases attributes found in the type. + /// + /// Command type to search + /// function to create command instance + internal void AddCommands(Type type, Func factory) + { + for (Type baseType = type; baseType != null; baseType = baseType.BaseType) + { + if (baseType == typeof(CommandBase)) + { + break; + } + CommandAttribute[] commandAttributes = (CommandAttribute[])baseType.GetCustomAttributes(typeof(CommandAttribute), inherit: false); + foreach (CommandAttribute commandAttribute in commandAttributes) + { + factory ??= (services) => Utilities.CreateInstance(type, services); + CreateCommand(baseType, commandAttribute, factory); + } + } + + // Build or re-build parser instance after all these commands and aliases are added + FlushParser(); + } + + internal void CreateCommand(Type type, CommandAttribute commandAttribute, Func factory) + { + Command command = new(commandAttribute.Name, commandAttribute.Help); + List<(PropertyInfo, Argument)> arguments = new(); + List<(PropertyInfo, Option)> options = new(); - foreach (string alias in optionAttribute.Aliases) + foreach (string alias in commandAttribute.Aliases) + { + command.AddAlias(alias); + } + + foreach (PropertyInfo property in type.GetProperties().Where(p => p.CanWrite)) + { + ArgumentAttribute argumentAttribute = (ArgumentAttribute)property.GetCustomAttributes(typeof(ArgumentAttribute), inherit: false).SingleOrDefault(); + if (argumentAttribute != null) + { + IArgumentArity arity = property.PropertyType.IsArray ? ArgumentArity.ZeroOrMore : ArgumentArity.ZeroOrOne; + + Argument argument = new() + { + Name = argumentAttribute.Name ?? property.Name.ToLowerInvariant(), + Description = argumentAttribute.Help, + ArgumentType = property.PropertyType, + Arity = arity + }; + command.AddArgument(argument); + arguments.Add((property, argument)); + } + else + { + OptionAttribute optionAttribute = (OptionAttribute)property.GetCustomAttributes(typeof(OptionAttribute), inherit: false).SingleOrDefault(); + if (optionAttribute != null) { - option.AddAlias(alias); + Option option = new(optionAttribute.Name ?? BuildOptionAlias(property.Name), optionAttribute.Help) + { + Argument = new Argument { ArgumentType = property.PropertyType } + }; + command.AddOption(option); + options.Add((property, option)); + + foreach (string alias in optionAttribute.Aliases) + { + option.AddAlias(alias); + } } } } + + CommandHandler handler = new(commandAttribute, arguments, options, type, factory); + _commandHandlers.Add(command.Name, handler); + command.Handler = handler; + _rootBuilder.AddCommand(command); + + // Build or re-build parser instance after this command is added + FlushParser(); } - CommandHandler handler = new(commandAttribute, arguments, properties, type, factory); - _commandHandlers.Add(command.Name, handler); - command.Handler = handler; - _rootBuilder.AddCommand(command); - } + internal string GetDetailedHelp(ICommand command, IServiceProvider services, int windowWidth) + { + CaptureConsole console = new(); - private Parser Parser => _parser ??= _rootBuilder.Build(); + // Get the command help + HelpBuilder helpBuilder = new(console, maxWidth: windowWidth); + helpBuilder.Write(command); - private void FlushParser() => _parser = null; + // Get the detailed help if any + if (TryGetCommandHandler(command.Name, out CommandHandler handler)) + { + string helpText = handler.GetDetailedHelp(Parser, services); + if (helpText is not null) + { + console.Out.Write(helpText); + } + } - private static string BuildOptionAlias(string parameterName) - { - if (string.IsNullOrWhiteSpace(parameterName)) + return console.ToString(); + } + + private void FlushParser() => _parser = null; + + private static string BuildOptionAlias(string parameterName) { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(parameterName)); + if (string.IsNullOrWhiteSpace(parameterName)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(parameterName)); + } + return parameterName.Length > 1 ? $"--{parameterName.ToKebabCase()}" : $"-{parameterName.ToLowerInvariant()}"; } - return parameterName.Length > 1 ? $"--{parameterName.ToKebabCase()}" : $"-{parameterName.ToLowerInvariant()}"; } /// @@ -272,28 +473,68 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation { private readonly CommandAttribute _commandAttribute; private readonly IEnumerable<(PropertyInfo Property, Argument Argument)> _arguments; - private readonly IEnumerable<(PropertyInfo Property, Option Option)> _properties; + private readonly IEnumerable<(PropertyInfo Property, Option Option)> _options; private readonly Func _factory; private readonly MethodInfo _methodInfo; private readonly MethodInfo _methodInfoHelp; + private readonly MethodInfo _methodInfoFilter; + private readonly FilterInvokeAttribute _filterInvokeAttribute; public CommandHandler( CommandAttribute commandAttribute, IEnumerable<(PropertyInfo, Argument)> arguments, - IEnumerable<(PropertyInfo, Option)> properties, + IEnumerable<(PropertyInfo, Option)> options, Type type, Func factory) { _commandAttribute = commandAttribute; _arguments = arguments; - _properties = properties; + _options = options; _factory = factory; - _methodInfo = type.GetMethods().Where((methodInfo) => methodInfo.GetCustomAttribute() != null).SingleOrDefault() ?? + // Now search for the command, help and filter attributes in the command type + foreach (MethodInfo methodInfo in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy)) + { + if (methodInfo.GetCustomAttribute() != null) + { + if (_methodInfo != null) + { + throw new ArgumentException($"Multiple CommandInvokeAttribute's found in {type}"); + } + _methodInfo = methodInfo; + } + if (methodInfo.GetCustomAttribute() != null) + { + if (_methodInfoHelp != null) + { + throw new ArgumentException($"Multiple HelpInvokeAttribute's found in {type}"); + } + if (methodInfo.ReturnType != typeof(string)) + { + throw new ArgumentException($"HelpInvokeAttribute doesn't return string in {type}"); + } + _methodInfoHelp = methodInfo; + } + FilterInvokeAttribute filterInvokeAttribute = methodInfo.GetCustomAttribute(); + if (filterInvokeAttribute != null) + { + if (_methodInfoFilter != null) + { + throw new ArgumentException($"Multiple FilterInvokeAttribute's found in {type}"); + } + if (methodInfo.ReturnType != typeof(bool)) + { + throw new ArgumentException($"FilterInvokeAttribute doesn't return bool in {type}"); + } + _filterInvokeAttribute = filterInvokeAttribute; + _methodInfoFilter = methodInfo; + } + } + if (_methodInfo == null) + { throw new ArgumentException($"No command invoke method found in {type}"); - - _methodInfoHelp = type.GetMethods().Where((methodInfo) => methodInfo.GetCustomAttribute() != null).SingleOrDefault(); + } } Task ICommandHandler.InvokeAsync(InvocationContext context) @@ -311,37 +552,25 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// internal string Help => _commandAttribute.Help; + /// + /// Filter invoke message or null if no attribute or message + /// + internal string FilterInvokeMessage => _filterInvokeAttribute?.Message; + /// /// Returns the list of the command's aliases. /// internal IEnumerable Aliases => _commandAttribute.Aliases; /// - /// Returns true if the command should be added. + /// Returns the list of arguments /// - internal bool IsValidPlatform(ITarget target) - { - if ((_commandAttribute.Flags & CommandFlags.Global) != 0) - { - return true; - } - if (target != null) - { - if (target.OperatingSystem == OSPlatform.Windows) - { - return (_commandAttribute.Flags & CommandFlags.Windows) != 0; - } - if (target.OperatingSystem == OSPlatform.Linux) - { - return (_commandAttribute.Flags & CommandFlags.Linux) != 0; - } - if (target.OperatingSystem == OSPlatform.OSX) - { - return (_commandAttribute.Flags & CommandFlags.OSX) != 0; - } - } - return false; - } + internal IEnumerable Arguments => _arguments.Select((a) => a.Argument); + + /// + /// Returns true is the command is supported by the command filter. Calls the FilterInvokeAttribute marked method. + /// + internal bool IsCommandSupported(Parser parser, IServiceProvider services) => _methodInfoFilter == null || (bool)Invoke(_methodInfoFilter, context: null, parser, services); /// /// Execute the command synchronously. @@ -350,32 +579,56 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// service provider internal void Invoke(InvocationContext context, IServiceProvider services) => Invoke(_methodInfo, context, context.Parser, services); + /// + /// Return the various ways the command can be invoked. For building the help text. + /// + internal string HelpInvocation + { + get + { + IEnumerable rawAliases = new string[] { Name }.Concat(Aliases); + string invocation = string.Join(", ", rawAliases); + foreach (Argument argument in Arguments) + { + string argumentDescriptor = argument.Name; + if (!string.IsNullOrWhiteSpace(argumentDescriptor)) + { + invocation = $"{invocation} <{argumentDescriptor}>"; + } + } + return invocation; + } + } + /// /// Executes the command's help invoke function if exists /// /// parser instance /// service provider /// true help called, false no help function - internal bool InvokeHelp(Parser parser, IServiceProvider services) + internal string GetDetailedHelp(Parser parser, IServiceProvider services) { if (_methodInfoHelp == null) { - return false; + return null; } // The InvocationContext is null so the options and arguments in the // command instance created are not 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, parser, services); - return true; + return (string)Invoke(_methodInfoHelp, context: null, parser, services); } - private void Invoke(MethodInfo methodInfo, InvocationContext context, Parser parser, IServiceProvider services) + private object Invoke(MethodInfo methodInfo, InvocationContext context, Parser parser, IServiceProvider services) { - object instance = _factory(services); - SetProperties(context, parser, instance); - Utilities.Invoke(methodInfo, instance, services); + object instance = null; + if (!methodInfo.IsStatic) + { + instance = _factory(services); + SetProperties(context, parser, instance); + } + return Utilities.Invoke(methodInfo, instance, services); } private void SetProperties(InvocationContext context, Parser parser, object instance) @@ -390,31 +643,28 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation } // Now initialize the option and service properties from the default and command line options - foreach ((PropertyInfo Property, Option Option) property in _properties) + foreach ((PropertyInfo Property, Option Option) option in _options) { - object value = property.Property.GetValue(instance); + object value = option.Property.GetValue(instance); - if (property.Option != null) + if (defaultParseResult != null) { - if (defaultParseResult != null) + OptionResult defaultOptionResult = defaultParseResult.FindResultFor(option.Option); + if (defaultOptionResult != null) { - OptionResult defaultOptionResult = defaultParseResult.FindResultFor(property.Option); - if (defaultOptionResult != null) - { - value = defaultOptionResult.GetValueOrDefault(); - } + value = defaultOptionResult.GetValueOrDefault(); } - if (context != null) + } + if (context != null) + { + OptionResult optionResult = context.ParseResult.FindResultFor(option.Option); + if (optionResult != null) { - OptionResult optionResult = context.ParseResult.FindResultFor(property.Option); - if (optionResult != null) - { - value = optionResult.GetValueOrDefault(); - } + value = optionResult.GetValueOrDefault(); } } - property.Property.SetValue(instance, value); + option.Property.SetValue(instance, value); } // Initialize any argument properties from the default and command line arguments @@ -463,66 +713,46 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation } /// - /// Local help builder that allows commands to provide more detailed help - /// text via the "InvokeHelp" function. + /// IConsole implementation that captures all the output into a string. /// - private sealed class LocalHelpBuilder : IHelpBuilder + private sealed class CaptureConsole : IConsole { - private readonly CommandService _commandService; - private readonly LocalConsole _console; - private readonly bool _useHelpBuilder; + private readonly StringBuilder _builder = new(); - public LocalHelpBuilder(CommandService commandService, IConsole console, bool useHelpBuilder) + public CaptureConsole() { - _commandService = commandService; - _console = (LocalConsole)console; - _useHelpBuilder = useHelpBuilder; + Out = Error = new StandardStreamWriter((text) => _builder.Append(text)); } - void IHelpBuilder.Write(ICommand command) - { - bool useHelpBuilder = _useHelpBuilder; - if (_commandService._commandHandlers.TryGetValue(command.Name, out CommandHandler handler)) - { - if (handler.InvokeHelp(_commandService.Parser, _console.Services)) - { - return; - } - useHelpBuilder = true; - } - if (useHelpBuilder) - { - HelpBuilder helpBuilder = new(_console, maxWidth: _console.ConsoleService.WindowWidth); - helpBuilder.Write(command); - } - } + public override string ToString() => _builder.ToString(); + + #region IConsole + + public IStandardStreamWriter Out { get; } + + bool IStandardOut.IsOutputRedirected { get { return false; } } + + public IStandardStreamWriter Error { get; } + + bool IStandardError.IsErrorRedirected { get { return false; } } + + bool IStandardIn.IsInputRedirected { get { return false; } } + + #endregion } /// - /// This class does two things: wraps the IConsoleService and provides the IConsole interface and - /// pipes through the System.CommandLine parsing allowing per command invocation data (service - /// provider and raw command line) to be passed through. + /// This class wraps the IConsoleService and provides the IConsole interface for System.CommandLine. /// private sealed class LocalConsole : IConsole { - private IConsoleService _console; + private readonly IConsoleService _consoleService; - public LocalConsole(IServiceProvider services) + public LocalConsole(IConsoleService consoleService) { - Services = services; - Out = new StandardStreamWriter(ConsoleService.Write); - Error = new StandardStreamWriter(ConsoleService.WriteError); - } - - internal readonly IServiceProvider Services; - - internal IConsoleService ConsoleService - { - get - { - _console ??= Services.GetService(); - return _console; - } + _consoleService = consoleService; + Out = new StandardStreamWriter(_consoleService.Write); + Error = new StandardStreamWriter(_consoleService.WriteError); } #region IConsole @@ -537,16 +767,16 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation bool IStandardIn.IsInputRedirected { get { return false; } } - private sealed class StandardStreamWriter : IStandardStreamWriter - { - private readonly Action _write; + #endregion + } - public StandardStreamWriter(Action write) => _write = write; + private sealed class StandardStreamWriter : IStandardStreamWriter + { + private readonly Action _write; - void IStandardStreamWriter.Write(string value) => _write(value); - } + public StandardStreamWriter(Action write) => _write = write; - #endregion + void IStandardStreamWriter.Write(string value) => _write(value); } } } diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs index 090270e28..19904d2f1 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs @@ -363,6 +363,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation public ExtensionLoadContext(string extensionPath) { + Trace.TraceInformation($"ExtensionLoadContext: {extensionPath}"); _extensionPath = extensionPath; } @@ -387,12 +388,15 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation { throw new InvalidOperationException($"Extension assembly reference version not supported for {assemblyName.Name} {assemblyName.Version}"); } + Trace.TraceInformation($"ExtensionLoadContext: loading SOS assembly {assembly.CodeBase}"); return assembly; } else if (_extensionPaths.TryGetValue(assemblyName.Name, out string path)) { + Trace.TraceInformation($"ExtensionLoadContext: loading from extension path {path}"); return LoadFromAssemblyPath(path); } + Trace.TraceInformation($"ExtensionLoadContext: returning null {assemblyName}"); return null; } } diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Utilities.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Utilities.cs index cc69c944a..fd231f227 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Utilities.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Utilities.cs @@ -9,6 +9,8 @@ using System.Linq; using System.Reflection; using System.Reflection.PortableExecutable; using System.Runtime.InteropServices; +using System.Text; +using System.Threading; using Microsoft.FileFormats; using Microsoft.FileFormats.ELF; using Microsoft.FileFormats.MachO; @@ -34,7 +36,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// /// This function is neither commutative nor associative; the hash codes must be combined in /// a deterministic order. Do not use this when hashing collections whose contents are - /// nondeterministically ordered! + /// non-deterministically ordered! /// public static int CombineHashCodes(int hashCode0, int hashCode1) { @@ -412,4 +414,46 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation return arguments; } } + + public class CaptureConsoleService : IConsoleService + { + private readonly StringBuilder _builder = new(); + + public CaptureConsoleService() + { + } + + public void Clear() => _builder.Clear(); + + public override string ToString() => _builder.ToString(); + + #region IConsoleService + + public void Write(string text) + { + _builder.Append(text); + } + + public void WriteWarning(string text) + { + _builder.Append(text); + } + + public void WriteError(string text) + { + _builder.Append(text); + } + + public bool SupportsDml => false; + + public void WriteDml(string text) => throw new NotSupportedException(); + + public void WriteDmlExec(string text, string _) => throw new NotSupportedException(); + + public CancellationToken CancellationToken { get; set; } = CancellationToken.None; + + int IConsoleService.WindowWidth => int.MaxValue; + + #endregion + } } diff --git a/src/Microsoft.Diagnostics.DebugServices/CommandAttributes.cs b/src/Microsoft.Diagnostics.DebugServices/CommandAttributes.cs index 4b1df4b60..bd988f0c2 100644 --- a/src/Microsoft.Diagnostics.DebugServices/CommandAttributes.cs +++ b/src/Microsoft.Diagnostics.DebugServices/CommandAttributes.cs @@ -6,32 +6,6 @@ using System.Diagnostics; namespace Microsoft.Diagnostics.DebugServices { - /// - /// Command flags to filter by OS Platforms, control scope and how the command is registered. - /// - [Flags] - public enum CommandFlags : byte - { - Windows = 0x01, - Linux = 0x02, - OSX = 0x04, - - /// - /// Command is supported when there is no target - /// - Global = 0x08, - - /// - /// Command is not added through reflection, but manually with command service API. - /// - Manual = 0x10, - - /// - /// Default. All operating system, but target is required - /// - Default = Windows | Linux | OSX - } - /// /// Marks the class as a Command. /// @@ -53,11 +27,6 @@ namespace Microsoft.Diagnostics.DebugServices /// public string[] Aliases = Array.Empty(); - /// - /// Command flags to filter by OS Platforms, control scope and how the command is registered. - /// - public CommandFlags Flags = CommandFlags.Default; - /// /// A string of options that are parsed before the command line options /// @@ -121,10 +90,24 @@ namespace Microsoft.Diagnostics.DebugServices } /// - /// Marks the function to invoke to display alternate help for command. + /// Marks the function to invoke to return the alternate help for command. The function returns + /// a string. The Argument and Option properties of the command are not set. /// [AttributeUsage(AttributeTargets.Method)] public class HelpInvokeAttribute : Attribute { } + + /// + /// Marks the function to invoke to filter a command. The function returns a bool; true if + /// the command is supported. The Argument and Option properties of the command are not set. + /// + [AttributeUsage(AttributeTargets.Method)] + public class FilterInvokeAttribute : Attribute + { + /// + /// Message to display if the filter fails + /// + public string Message; + } } diff --git a/src/Microsoft.Diagnostics.DebugServices/DiagnosticsException.cs b/src/Microsoft.Diagnostics.DebugServices/DiagnosticsException.cs index c6ea75195..fa5d9c85c 100644 --- a/src/Microsoft.Diagnostics.DebugServices/DiagnosticsException.cs +++ b/src/Microsoft.Diagnostics.DebugServices/DiagnosticsException.cs @@ -27,23 +27,43 @@ namespace Microsoft.Diagnostics.DebugServices } /// - /// Thrown if a command is not supported on the configuration, platform or runtime + /// Thrown if a command is not found. /// - public class CommandNotSupportedException : DiagnosticsException + public class CommandNotFoundException : DiagnosticsException { - public CommandNotSupportedException() - : base() + public const string NotFoundMessage = $"Unrecognized SOS command"; + + public CommandNotFoundException(string message) + : base(message) + { + } + + public CommandNotFoundException(string message, Exception innerException) + : base(message, innerException) { } + } + + /// + /// Thrown if a command is not found. + /// + public class CommandParsingException : DiagnosticsException + { + /// + /// The detailed help of the command + /// + public string DetailedHelp { get; } - public CommandNotSupportedException(string message) + public CommandParsingException(string message, string detailedHelp) : base(message) { + DetailedHelp = detailedHelp; } - public CommandNotSupportedException(string message, Exception innerException) + public CommandParsingException(string message, string detailedHelp, Exception innerException) : base(message, innerException) { + DetailedHelp = detailedHelp; } } } diff --git a/src/Microsoft.Diagnostics.DebugServices/ICommandService.cs b/src/Microsoft.Diagnostics.DebugServices/ICommandService.cs index 2a4dbe810..645576744 100644 --- a/src/Microsoft.Diagnostics.DebugServices/ICommandService.cs +++ b/src/Microsoft.Diagnostics.DebugServices/ICommandService.cs @@ -12,7 +12,7 @@ namespace Microsoft.Diagnostics.DebugServices public interface ICommandService { /// - /// Enumerates all the command's name and help + /// Enumerates all the command's name, help and aliases /// IEnumerable<(string name, string help, IEnumerable aliases)> Commands { get; } @@ -23,11 +23,19 @@ namespace Microsoft.Diagnostics.DebugServices void AddCommands(Type type); /// - /// Displays the help for a command + /// Gets help for all of the commands + /// + /// service provider + /// command invocation and help enumeration + public IEnumerable<(string Invocation, string Help)> GetAllCommandHelp(IServiceProvider services); + + /// + /// Displays the detailed help for a command /// /// name of the command or alias /// service provider - /// true if success, false if command not found - bool DisplayHelp(string commandName, IServiceProvider services); + /// the width to format the help or int.MaxValue + /// help text or null if not found + string GetDetailedHelp(string commandName, IServiceProvider services, int consoleWidth); } } diff --git a/src/Microsoft.Diagnostics.DebugServices/ProviderExportAttribute.cs b/src/Microsoft.Diagnostics.DebugServices/ProviderExportAttribute.cs index 8a70aff35..be60004fa 100644 --- a/src/Microsoft.Diagnostics.DebugServices/ProviderExportAttribute.cs +++ b/src/Microsoft.Diagnostics.DebugServices/ProviderExportAttribute.cs @@ -13,7 +13,7 @@ namespace Microsoft.Diagnostics.DebugServices { /// /// The interface or type to register the provider. If null, the provider type registered will be - /// he class itself or the return type of the method. + /// the class itself or the return type of the method. /// public Type Type { get; set; } diff --git a/src/Microsoft.Diagnostics.DebugServices/ServiceContainer.cs b/src/Microsoft.Diagnostics.DebugServices/ServiceContainer.cs index 9b497c53f..df9aa18d9 100644 --- a/src/Microsoft.Diagnostics.DebugServices/ServiceContainer.cs +++ b/src/Microsoft.Diagnostics.DebugServices/ServiceContainer.cs @@ -29,9 +29,8 @@ namespace Microsoft.Diagnostics.DebugServices /// /// search this provider if service isn't found in this instance or null /// service factories to initialize provider or null - public ServiceContainer(IServiceProvider parent, Dictionary factories) + public ServiceContainer(IServiceProvider parent, Dictionary factories = null) { - Debug.Assert(factories != null); _parent = parent; _factories = factories; _instances = new Dictionary(); @@ -88,7 +87,7 @@ namespace Microsoft.Diagnostics.DebugServices { return service; } - if (_factories.TryGetValue(type, out ServiceFactory factory)) + if (_factories != null && _factories.TryGetValue(type, out ServiceFactory factory)) { service = factory(this); _instances.Add(type, service); diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/AnalyzeOOMCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/AnalyzeOOMCommand.cs index 35e38607a..ad970e05b 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/AnalyzeOOMCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/AnalyzeOOMCommand.cs @@ -7,12 +7,9 @@ using Microsoft.Diagnostics.Runtime; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "analyzeoom", Help = "Displays the info of the last OOM that occurred on an allocation request to the GC heap.")] - public class AnalyzeOOMCommand : CommandBase + [Command(Name = "analyzeoom", Aliases = new[] { "AnalyzeOOM" }, Help = "Displays the info of the last OOM that occurred on an allocation request to the GC heap.")] + public class AnalyzeOOMCommand : ClrRuntimeCommandBase { - [ServiceImport] - public ClrRuntime Runtime { get; set; } - public override void Invoke() { bool foundOne = false; diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/AssembliesCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/AssembliesCommand.cs new file mode 100644 index 000000000..ebeda4ed1 --- /dev/null +++ b/src/Microsoft.Diagnostics.ExtensionCommands/AssembliesCommand.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Text.RegularExpressions; +using Microsoft.Diagnostics.DebugServices; +using Microsoft.Diagnostics.Runtime; + +namespace Microsoft.Diagnostics.ExtensionCommands +{ + [Command(Name = "assemblies", Aliases = new[] { "clrmodules" }, Help = "Lists the managed assemblies in the process.")] + public class AssembliesCommand : ClrRuntimeCommandBase + { + [ServiceImport] + public IModuleService ModuleService { get; set; } + + [Option(Name = "--name", Aliases = new string[] { "-n" }, Help = "RegEx filter on assembly name (path not included).")] + public string AssemblyName { get; set; } + + [Option(Name = "--verbose", Aliases = new string[] { "-v" }, Help = "Displays detailed information about the assemblies.")] + public bool Verbose { get; set; } + + public override void Invoke() + { + Regex regex = AssemblyName is not null ? new Regex(AssemblyName, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant) : null; + foreach (ClrModule module in Runtime.EnumerateModules()) + { + if (regex is null || !string.IsNullOrEmpty(module.Name) && regex.IsMatch(Path.GetFileName(module.Name))) + { + if (Verbose) + { + WriteLine("{0}{1}", module.Name, module.IsDynamic ? "(Dynamic)" : ""); + WriteLine(" AssemblyName: {0}", module.AssemblyName); + WriteLine(" ImageBase: {0:X16}", module.ImageBase); + WriteLine(" Size: {0:X8}", module.Size); + WriteLine(" ModuleAddress: {0:X16}", module.Address); + WriteLine(" AssemblyAddress: {0:X16}", module.AssemblyAddress); + WriteLine(" IsPEFile: {0}", module.IsPEFile); + WriteLine(" Layout: {0}", module.Layout); + WriteLine(" IsDynamic: {0}", module.IsDynamic); + WriteLine(" MetadataAddress: {0:X16}", module.MetadataAddress); + WriteLine(" MetadataSize: {0:X16}", module.MetadataLength); + WriteLine(" PdbInfo: {0}", module.Pdb?.ToString() ?? ""); + Version version = null; + try + { + version = ModuleService.GetModuleFromBaseAddress(module.ImageBase).GetVersionData(); + } + catch (DiagnosticsException) + { + } + WriteLine(" Version: {0}", version?.ToString() ?? ""); + } + else + { + WriteLine("{0:X16} {1:X8} {2}{3}", module.ImageBase, module.Size, module.Name, module.IsDynamic ? "(Dynamic)" : ""); + } + } + } + } + } +} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ClrMDHelper.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ClrMDHelper.cs index 67e42bac9..1d87e10de 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/ClrMDHelper.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ClrMDHelper.cs @@ -17,7 +17,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands private readonly ClrHeap _heap; [ServiceExport(Scope = ServiceScope.Runtime)] - public static ClrMDHelper Create([ServiceImport(Optional = true)] ClrRuntime clrRuntime) + public static ClrMDHelper TryCreate([ServiceImport(Optional = true)] ClrRuntime clrRuntime) { return clrRuntime != null ? new ClrMDHelper(clrRuntime) : null; } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ClrMDHelperCommandBase.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ClrMDHelperCommandBase.cs new file mode 100644 index 000000000..b91d528e6 --- /dev/null +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ClrMDHelperCommandBase.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DebugServices; + +namespace Microsoft.Diagnostics.ExtensionCommands +{ + public abstract class ClrMDHelperCommandBase : CommandBase + { + /// + /// Helper bound to the current ClrRuntime that provides high level services on top of ClrMD. + /// + [ServiceImport(Optional = true)] + public ClrMDHelper Helper { get; set; } + + [FilterInvoke(Message = ClrRuntimeCommandBase.RuntimeNotFoundMessage)] + public static bool FilterInvoke([ServiceImport(Optional = true)] ClrMDHelper helper) => helper != null; + } +} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ClrModulesCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ClrModulesCommand.cs deleted file mode 100644 index 6e19a346e..000000000 --- a/src/Microsoft.Diagnostics.ExtensionCommands/ClrModulesCommand.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.IO; -using System.Text.RegularExpressions; -using Microsoft.Diagnostics.DebugServices; -using Microsoft.Diagnostics.Runtime; - -namespace Microsoft.Diagnostics.ExtensionCommands -{ - [Command(Name = "clrmodules", Help = "Lists the managed modules in the process.")] - public class ClrModulesCommand : CommandBase - { - [ServiceImport(Optional = true)] - public ClrRuntime Runtime { get; set; } - - [ServiceImport] - public IModuleService ModuleService { get; set; } - - [Option(Name = "--name", Aliases = new string[] { "-n" }, Help = "RegEx filter on module name (path not included).")] - public string ModuleName { get; set; } - - [Option(Name = "--verbose", Aliases = new string[] { "-v" }, Help = "Displays detailed information about the modules.")] - public bool Verbose { get; set; } - - public override void Invoke() - { - if (Runtime == null) - { - throw new DiagnosticsException("No CLR runtime set"); - } - Regex regex = ModuleName is not null ? new Regex(ModuleName, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant) : null; - foreach (ClrModule module in Runtime.EnumerateModules()) - { - if (regex is null || !string.IsNullOrEmpty(module.Name) && regex.IsMatch(Path.GetFileName(module.Name))) - { - if (Verbose) - { - WriteLine("{0}{1}", module.Name, module.IsDynamic ? "(Dynamic)" : ""); - WriteLine(" AssemblyName: {0}", module.AssemblyName); - WriteLine(" ImageBase: {0:X16}", module.ImageBase); - WriteLine(" Size: {0:X8}", module.Size); - WriteLine(" ModuleAddress: {0:X16}", module.Address); - WriteLine(" AssemblyAddress: {0:X16}", module.AssemblyAddress); - WriteLine(" IsPEFile: {0}", module.IsPEFile); - WriteLine(" Layout: {0}", module.Layout); - WriteLine(" IsDynamic: {0}", module.IsDynamic); - WriteLine(" MetadataAddress: {0:X16}", module.MetadataAddress); - WriteLine(" MetadataSize: {0:X16}", module.MetadataLength); - WriteLine(" PdbInfo: {0}", module.Pdb?.ToString() ?? ""); - Version version = null; - try - { - version = ModuleService.GetModuleFromBaseAddress(module.ImageBase).GetVersionData(); - } - catch (DiagnosticsException) - { - } - WriteLine(" Version: {0}", version?.ToString() ?? ""); - } - else - { - WriteLine("{0:X16} {1:X8} {2}{3}", module.ImageBase, module.Size, module.Name, module.IsDynamic ? "(Dynamic)" : ""); - } - } - } - } - } -} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ClrRuntimeCommandBase.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ClrRuntimeCommandBase.cs new file mode 100644 index 000000000..2f139129d --- /dev/null +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ClrRuntimeCommandBase.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DebugServices; +using Microsoft.Diagnostics.Runtime; + +namespace Microsoft.Diagnostics.ExtensionCommands +{ + public abstract class ClrRuntimeCommandBase : CommandBase + { + public const string RuntimeNotFoundMessage = "No CLR runtime found. This means that a .NET runtime module or the DAC for the runtime can not be found or downloaded."; + + [ServiceImport(Optional = true)] + public ClrRuntime Runtime { get; set; } + + [FilterInvoke(Message = RuntimeNotFoundMessage)] + public static bool FilterInvoke([ServiceImport(Optional = true)] ClrRuntime runtime) => runtime != null; + } +} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpAsyncCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpAsyncCommand.cs index 0acbca362..954fd6fa6 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/DumpAsyncCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/DumpAsyncCommand.cs @@ -14,45 +14,17 @@ using Microsoft.Diagnostics.Runtime.Interfaces; namespace Microsoft.Diagnostics.ExtensionCommands { [Command(Name = CommandName, Aliases = new string[] { "DumpAsync" }, Help = "Displays information about async \"stacks\" on the garbage-collected heap.")] - public sealed class DumpAsyncCommand : ExtensionCommandBase + public sealed class DumpAsyncCommand : ClrRuntimeCommandBase { /// The name of the command. private const string CommandName = "dumpasync"; /// Indent width. private const int TabWidth = 2; + /// The command invocation syntax when used in Debugger Markup Language (DML) commands. private const string DmlCommandInvoke = $"!{CommandName}"; - /// The help text to render when asked for help. - private static readonly string s_detailedHelpText = - $"Usage: {CommandName} [--stats] [--coalesce] [--address ] [--methodtable ] [--type ] [--tasks] [--completed] [--fields]" + Environment.NewLine + - Environment.NewLine + - "Displays information about async \"stacks\" on the garbage-collected heap. Stacks" + Environment.NewLine + - "are synthesized by finding all task objects (including async state machine box" + Environment.NewLine + - "objects) on the GC heap and chaining them together based on continuations." + Environment.NewLine + - Environment.NewLine + - "Options:" + Environment.NewLine + - " --stats Summarize all async frames found rather than showing detailed stacks." + Environment.NewLine + - " --coalesce Coalesce stacks and portions of stacks that are the same." + Environment.NewLine + - " --address Only show stacks that include the object with the specified address." + Environment.NewLine + - " --methodtable Only show stacks that include objects with the specified method table." + Environment.NewLine + - " --type Only show stacks that include objects whose type includes the specified name in its name." + Environment.NewLine + - " --tasks Include stacks that contain only non-state machine task objects." + Environment.NewLine + - " --completed Include completed tasks in stacks." + Environment.NewLine + - " --fields Show fields for each async stack frame." + Environment.NewLine + - Environment.NewLine + - "Examples:" + Environment.NewLine + - $"Summarize all async frames associated with a specific method table address: !{CommandName} --stats --methodtable 0x00007ffbcfbe0970" + Environment.NewLine + - $"Show all stacks coalesced by common frames: !{CommandName} --coalesce" + Environment.NewLine + - $"Show each stack that includes \"ReadAsync\": !{CommandName} --type ReadAsync" + Environment.NewLine + - $"Show each stack that includes an object at a specific address, and include fields: !{CommandName} --address 0x000001264adce778 --fields"; - - /// Gets the runtime for the process. Set by the command framework. - [ServiceImport(Optional = true)] - public ClrRuntime? Runtime { get; set; } - - /// Gets whether to only show stacks that include the object with the specified address. [Option(Name = "--address", Aliases = new string[] { "-addr" }, Help = "Only show stacks that include the object with the specified address.")] public string? ObjectAddress @@ -96,27 +68,19 @@ namespace Microsoft.Diagnostics.ExtensionCommands public bool CoalesceStacks { get; set; } /// Invokes the command. - public override void ExtensionInvoke() + public override void Invoke() { - ClrRuntime? runtime = Runtime; - if (runtime is null) - { - WriteLineError("Unable to access runtime."); - return; - } - + ClrRuntime runtime = Runtime; ClrHeap heap = runtime.Heap; if (!heap.CanWalkHeap) { - WriteLineError("Unable to examine the heap."); - return; + throw new DiagnosticsException("Unable to examine the heap."); } ClrType? taskType = runtime.BaseClassLibrary.GetTypeByName("System.Threading.Tasks.Task"); if (taskType is null) { - WriteLineError("Unable to find required type."); - return; + throw new DiagnosticsException("Unable to find required type."); } ClrStaticField? taskCompletionSentinelType = taskType.GetStaticFieldByName("s_taskCompletionSentinel"); @@ -1191,7 +1155,18 @@ namespace Microsoft.Diagnostics.ExtensionCommands } /// Gets detailed help for the command. - protected override string GetDetailedHelp() => s_detailedHelpText; + [HelpInvoke] + public static string GetDetailedHelp() => +@"Displays information about async ""stacks"" on the garbage-collected heap. Stacks +are synthesized by finding all task objects (including async state machine box +objects) on the GC heap and chaining them together based on continuations. + +Examples: + Summarize all async frames associated with a specific method table address: dumpasync --stats --methodtable 0x00007ffbcfbe0970 + Show all stacks coalesced by common frames: dumpasync --coalesce + Show each stack that includes ""ReadAsync"": dumpasync --type ReadAsync + Show each stack that includes an object at a specific address, and include fields: dumpasync --address 0x000001264adce778 --fields +"; /// Represents an async object to be used as a frame in an async "stack". private sealed class AsyncObject diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpConcurrentDictionaryCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpConcurrentDictionaryCommand.cs index b723a5d01..06bee8bc6 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/DumpConcurrentDictionaryCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/DumpConcurrentDictionaryCommand.cs @@ -9,40 +9,36 @@ using Microsoft.Diagnostics.Runtime; namespace Microsoft.Diagnostics.ExtensionCommands { [Command(Name = "dumpconcurrentdictionary", Aliases = new string[] { "dcd" }, Help = "Displays concurrent dictionary content.")] - public class DumpConcurrentDictionaryCommand : ExtensionCommandBase + public class DumpConcurrentDictionaryCommand : ClrMDHelperCommandBase { [Argument(Help = "The address of a ConcurrentDictionary object.")] public string Address { get; set; } - [ServiceImport] + [ServiceImport(Optional = true)] public ClrRuntime Runtime { get; set; } - public override void ExtensionInvoke() + public override void Invoke() { if (string.IsNullOrEmpty(Address)) { - WriteLine("Missing ConcurrentDictionary address..."); - return; + throw new DiagnosticsException("Missing ConcurrentDictionary address..."); } if (!TryParseAddress(Address, out ulong address)) { - WriteLine("Hexadecimal address expected..."); - return; + throw new DiagnosticsException("Hexadecimal address expected..."); } ClrHeap heap = Runtime.Heap; ClrType type = heap.GetObjectType(address); if (type?.Name is null) { - WriteLine($"{Address:x16} is not referencing an object..."); - return; + throw new DiagnosticsException($"{Address:x16} is not referencing an object..."); } if (!type.Name.StartsWith("System.Collections.Concurrent.ConcurrentDictionary<")) { - WriteLine($"{Address:x16} is not a ConcurrentDictionary but an instance of {type.Name}..."); - return; + throw new DiagnosticsException($"{Address:x16} is not a ConcurrentDictionary but an instance of {type.Name}..."); } WriteLine($"{type.Name}"); @@ -67,9 +63,8 @@ namespace Microsoft.Diagnostics.ExtensionCommands WriteLine(string.Empty); } - protected override string GetDetailedHelp() - { - return + [HelpInvoke] + public static string GetDetailedHelp() => @"------------------------------------------------------------------------------- DumpConcurrentDictionary Lists all items (key/value pairs) in the given concurrent dictionary. @@ -89,7 +84,6 @@ System.Collections.Concurrent.ConcurrentDictionary). - For value types, the command to dump each value type is shown (e.g. dumpvc <[item] address>). "; - } private static string Truncate(string str, int nbMaxChars) { diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpConcurrentQueueCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpConcurrentQueueCommand.cs index 62c31f97f..9252a6379 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/DumpConcurrentQueueCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/DumpConcurrentQueueCommand.cs @@ -8,41 +8,36 @@ using Microsoft.Diagnostics.Runtime; namespace Microsoft.Diagnostics.ExtensionCommands { [Command(Name = "dumpconcurrentqueue", Aliases = new string[] { "dcq" }, Help = "Displays concurrent queue content.")] - public class DumpConcurrentQueueCommand : ExtensionCommandBase + public class DumpConcurrentQueueCommand : ClrMDHelperCommandBase { [Argument(Help = "The address of a ConcurrentQueue object.")] public string Address { get; set; } - [ServiceImport] + [ServiceImport(Optional = true)] public ClrRuntime Runtime { get; set; } - public override void ExtensionInvoke() + public override void Invoke() { if (string.IsNullOrEmpty(Address)) { - WriteLine("Missing ConcurrentQueue address..."); - return; + throw new DiagnosticsException("Missing ConcurrentQueue address..."); } if (!TryParseAddress(Address, out ulong address)) { - WriteLine("Hexadecimal address expected..."); - return; + throw new DiagnosticsException("Hexadecimal address expected..."); } ClrHeap heap = Runtime.Heap; ClrType type = heap.GetObjectType(address); if (type == null) { - WriteLine($"{Address:x16} is not referencing an object..."); - return; + throw new DiagnosticsException($"{Address:x16} is not referencing an object..."); } - if (!type.Name.StartsWith("System.Collections.Concurrent.ConcurrentQueue<")) { - WriteLine($"{Address:x16} is not a ConcurrentQueue but an instance of {type.Name}..."); - return; + throw new DiagnosticsException($"{Address:x16} is not a ConcurrentQueue but an instance of {type.Name}..."); } WriteLine($"{type.Name}"); @@ -64,39 +59,33 @@ namespace Microsoft.Diagnostics.ExtensionCommands WriteLine(""); } - protected override string GetDetailedHelp() - { - return DetailedHelpText; - } + [HelpInvoke] + public static string GetDetailedHelp() => +@"------------------------------------------------------------------------------- +DumpConcurrentQueue + +Lists all items in the given concurrent queue. + +For simple types such as numbers, boolean and string, values are shown. +> dcq 00000202a79320e8 +System.Collections.Concurrent.ConcurrentQueue + 1 - 0 + 2 - 1 + 3 - 2 + +In case of reference types, the command to dump each object is shown. +> dcq 00000202a79337f8 +System.Collections.Concurrent.ConcurrentQueue + 1 - dumpobj 0x202a7934e38 + 2 - dumpobj 0x202a7934fd0 + 3 - dumpobj 0x202a7935078 - private readonly string DetailedHelpText = - "-------------------------------------------------------------------------------" + Environment.NewLine + - "DumpConcurrentQueue" + Environment.NewLine + - Environment.NewLine + - "Lists all items in the given concurrent queue." + Environment.NewLine + - Environment.NewLine + - "For simple types such as numbers, boolean and string, values are shown." + Environment.NewLine + - "> dcq 00000202a79320e8" + Environment.NewLine + - "System.Collections.Concurrent.ConcurrentQueue" + Environment.NewLine + - " 1 - 0" + Environment.NewLine + - " 2 - 1" + Environment.NewLine + - " 3 - 2" + Environment.NewLine + - Environment.NewLine + - "In case of reference types, the command to dump each object is shown." + Environment.NewLine + - "> dcq 00000202a79337f8" + Environment.NewLine + - "System.Collections.Concurrent.ConcurrentQueue" + Environment.NewLine + - " 1 - dumpobj 0x202a7934e38" + Environment.NewLine + - " 2 - dumpobj 0x202a7934fd0" + Environment.NewLine + - " 3 - dumpobj 0x202a7935078" + Environment.NewLine + - Environment.NewLine + - "For value types, the command to dump each array segment is shown." + Environment.NewLine + - "The next step is to manually dump each element with dumpvc <[item] address>." + Environment.NewLine + - "> dcq 00000202a7933370" + Environment.NewLine + - "System.Collections.Concurrent.ConcurrentQueue" + Environment.NewLine + - " 1 - dumparray 202a79334e0" + Environment.NewLine + - " 2 - dumparray 202a7938a88" + Environment.NewLine + - Environment.NewLine + - "" - ; +For value types, the command to dump each array segment is shown. +The next step is to manually dump each element with dumpvc <[item] address>. +> dcq 00000202a7933370 +System.Collections.Concurrent.ConcurrentQueue + 1 - dumparray 202a79334e0 + 2 - dumparray 202a7938a88 +"; } } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpExceptionsCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpExceptionsCommand.cs index 4f57cf389..b2384cfad 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/DumpExceptionsCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/DumpExceptionsCommand.cs @@ -14,11 +14,8 @@ using Microsoft.Diagnostics.Runtime; namespace Microsoft.Diagnostics.ExtensionCommands { [Command(Name = "dumpexceptions", Help = "Displays a list of all managed exceptions.")] - public class DumpExceptionsCommand : CommandBase + public class DumpExceptionsCommand : ClrRuntimeCommandBase { - [ServiceImport] - public ClrRuntime Runtime { get; set; } = null!; - [ServiceImport] public LiveObjectService LiveObjects { get; set; } = null!; diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpGenCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpGenCommand.cs index db020e792..a3ec51e17 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/DumpGenCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/DumpGenCommand.cs @@ -8,7 +8,7 @@ using Microsoft.Diagnostics.Runtime; namespace Microsoft.Diagnostics.ExtensionCommands { [Command(Name = "dumpgen", Aliases = new string[] { "dg" }, Help = "Displays heap content for the specified generation.")] - public class DumpGenCommand : ExtensionCommandBase + public class DumpGenCommand : ClrMDHelperCommandBase { private const string statsHeader32bits = " MT Count TotalSize Class Name"; private const string statsHeader64bits = " MT Count TotalSize Class Name"; @@ -24,7 +24,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands [Option(Name = "-mt", Help = "The address pointing on a Method table.")] public string MethodTableAddress { get; set; } - public override void ExtensionInvoke() + public override void Invoke() { GCGeneration generation = ParseGenerationArgument(Generation); if (generation != GCGeneration.NotSet) @@ -43,7 +43,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands } else { - WriteLine("Hexadecimal address expected for -mt option"); + throw new DiagnosticsException("Hexadecimal address expected for -mt option"); } } WriteLine(string.Empty); @@ -88,12 +88,11 @@ namespace Microsoft.Diagnostics.ExtensionCommands WriteLine($"Total {objectsCount} objects"); } - private GCGeneration ParseGenerationArgument(string generation) + private static GCGeneration ParseGenerationArgument(string generation) { if (string.IsNullOrEmpty(generation)) { - WriteLine("Generation argument is missing"); - return GCGeneration.NotSet; + throw new DiagnosticsException("Generation argument is missing"); } string lowerString = generation.ToLowerInvariant(); GCGeneration result = lowerString switch @@ -106,17 +105,16 @@ namespace Microsoft.Diagnostics.ExtensionCommands "foh" => GCGeneration.FrozenObjectHeap, _ => GCGeneration.NotSet, }; + if (result == GCGeneration.NotSet) { - WriteLine($"{generation} is not a supported generation (gen0, gen1, gen2, loh, poh, foh)"); + throw new DiagnosticsException($"{generation} is not a supported generation (gen0, gen1, gen2, loh, poh, foh)"); } return result; } - - protected override string GetDetailedHelp() - { - return + [HelpInvoke] + public static string GetDetailedHelp() => @"------------------------------------------------------------------------------- DumpGen This command can be used for 2 use cases: @@ -160,6 +158,5 @@ Total 46 objects 00000184aa23e918 00007ff9ea6e75b8 40 Total 3 objects "; - } } } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpHeapCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpHeapCommand.cs index 5f2faf654..38aa12c77 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/DumpHeapCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/DumpHeapCommand.cs @@ -9,15 +9,12 @@ using Microsoft.Diagnostics.Runtime; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "dumpheap", Help = "Displays a list of all managed objects.")] - public class DumpHeapCommand : CommandBase + [Command(Name = "dumpheap", Aliases = new[] { "DumpHeap" }, Help = "Displays a list of all managed objects.")] + public class DumpHeapCommand : ClrRuntimeCommandBase { [ServiceImport] public IMemoryService MemoryService { get; set; } - [ServiceImport] - public ClrRuntime Runtime { get; set; } - [ServiceImport] public LiveObjectService LiveObjects { get; set; } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpObjGCRefsCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpObjGCRefsCommand.cs new file mode 100644 index 000000000..cdd53f260 --- /dev/null +++ b/src/Microsoft.Diagnostics.ExtensionCommands/DumpObjGCRefsCommand.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Text; +using Microsoft.Diagnostics.DebugServices; +using Microsoft.Diagnostics.ExtensionCommands.Output; +using Microsoft.Diagnostics.Runtime; + +namespace Microsoft.Diagnostics.ExtensionCommands +{ + [Command(Name = "dumpobjgcrefs", Help = "A helper command to implement !dumpobj -refs")] + public sealed class DumpObjGCRefsCommand : ClrRuntimeCommandBase + { + private readonly StringBuilderPool _stringBuilderPool = new(260); + + [Argument(Name = "object")] + public string ObjectAddress { get; set; } + + public override void Invoke() + { + if (!TryParseAddress(ObjectAddress, out ulong objAddress)) + { + throw new ArgumentException($"Invalid object address: '{ObjectAddress}'", nameof(ObjectAddress)); + } + + ClrObject obj = Runtime.Heap.GetObject(objAddress); + if (!obj.IsValid) + { + Console.WriteLine($"Unable to walk object references, invalid object."); + return; + } + + ClrReference[] refs = obj.EnumerateReferencesWithFields(carefully: false, considerDependantHandles: false).ToArray(); + if (refs.Length == 0) + { + Console.WriteLine("GC Refs: none"); + return; + } + + Console.WriteLine("GC Refs:"); + + Column fieldNameColumn = ColumnKind.Text.GetAppropriateWidth(refs.Select(r => GetFieldName(r))); + Column offsetName = ColumnKind.HexOffset.GetAppropriateWidth(refs.Select(r => r.Offset)); + + Table output = new(Console, fieldNameColumn, offsetName, ColumnKind.DumpObj, ColumnKind.TypeName); + output.WriteHeader("Field", "Offset", "Object", "Type"); + foreach (ClrReference objRef in refs) + { + output.WriteRow(GetFieldName(objRef), objRef.Offset, objRef.Object, objRef.Object.Type); + } + } + + private string GetFieldName(ClrReference objRef) + { + Console.CancellationToken.ThrowIfCancellationRequested(); + + if (objRef.Field is null) + { + return null; + } + + if (objRef.InnerField is null) + { + return objRef.Field?.Name; + } + + StringBuilder sb = _stringBuilderPool.Rent(); + bool foundOneFieldName = false; + + for (ClrReference? curr = objRef; curr.HasValue; curr = curr.Value.InnerField) + { + if (sb.Length > 0) + { + sb.Append('.'); + } + + string fieldName = curr.Value.Field?.Name; + if (string.IsNullOrWhiteSpace(fieldName)) + { + sb.Append("???"); + } + else + { + sb.Append(fieldName); + foundOneFieldName = true; + } + } + + // Make sure we don't just return "???.???.???" + string result = foundOneFieldName ? sb.ToString() : null; + _stringBuilderPool.Return(sb); + return result; + } + } +} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpObjGCRefsHelper.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpObjGCRefsHelper.cs deleted file mode 100644 index adae249ab..000000000 --- a/src/Microsoft.Diagnostics.ExtensionCommands/DumpObjGCRefsHelper.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Linq; -using System.Text; -using Microsoft.Diagnostics.DebugServices; -using Microsoft.Diagnostics.ExtensionCommands.Output; -using Microsoft.Diagnostics.Runtime; - -namespace Microsoft.Diagnostics.ExtensionCommands -{ - [Command(Name = "dumpobjgcrefs", Help = "A helper command to implement !dumpobj -refs")] - public sealed class DumpObjGCRefsHelper : CommandBase - { - private readonly StringBuilderPool _stringBuilderPool = new(260); - - [ServiceImport] - public ClrRuntime Runtime { get; set; } - - [Argument(Name = "object")] - public string ObjectAddress { get; set; } - - public override void Invoke() - { - if (!TryParseAddress(ObjectAddress, out ulong objAddress)) - { - throw new ArgumentException($"Invalid object address: '{ObjectAddress}'", nameof(ObjectAddress)); - } - - ClrObject obj = Runtime.Heap.GetObject(objAddress); - if (!obj.IsValid) - { - Console.WriteLine($"Unable to walk object references, invalid object."); - return; - } - - ClrReference[] refs = obj.EnumerateReferencesWithFields(carefully: false, considerDependantHandles: false).ToArray(); - if (refs.Length == 0) - { - Console.WriteLine("GC Refs: none"); - return; - } - - Console.WriteLine("GC Refs:"); - - Column fieldNameColumn = ColumnKind.Text.GetAppropriateWidth(refs.Select(r => GetFieldName(r))); - Column offsetName = ColumnKind.HexOffset.GetAppropriateWidth(refs.Select(r => r.Offset)); - - Table output = new(Console, fieldNameColumn, offsetName, ColumnKind.DumpObj, ColumnKind.TypeName); - output.WriteHeader("Field", "Offset", "Object", "Type"); - foreach (ClrReference objRef in refs) - { - output.WriteRow(GetFieldName(objRef), objRef.Offset, objRef.Object, objRef.Object.Type); - } - } - - private string GetFieldName(ClrReference objRef) - { - Console.CancellationToken.ThrowIfCancellationRequested(); - - if (objRef.Field is null) - { - return null; - } - - if (objRef.InnerField is null) - { - return objRef.Field?.Name; - } - - StringBuilder sb = _stringBuilderPool.Rent(); - bool foundOneFieldName = false; - - for (ClrReference? curr = objRef; curr.HasValue; curr = curr.Value.InnerField) - { - if (sb.Length > 0) - { - sb.Append('.'); - } - - string fieldName = curr.Value.Field?.Name; - if (string.IsNullOrWhiteSpace(fieldName)) - { - sb.Append("???"); - } - else - { - sb.Append(fieldName); - foundOneFieldName = true; - } - } - - // Make sure we don't just return "???.???.???" - string result = foundOneFieldName ? sb.ToString() : null; - _stringBuilderPool.Return(sb); - return result; - } - } -} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpRuntimeTypeCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpRuntimeTypeCommand.cs index cfc8579e6..8c2de445c 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/DumpRuntimeTypeCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/DumpRuntimeTypeCommand.cs @@ -8,12 +8,9 @@ using static Microsoft.Diagnostics.ExtensionCommands.Output.ColumnKind; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "dumpruntimetypes", Help = "Finds all System.RuntimeType objects in the GC heap and prints the type name and MethodTable they refer too.")] - public sealed class DumpRuntimeTypeCommand : CommandBase + [Command(Name = "dumpruntimetypes", Aliases = new[] { "DumpRuntimeTypes" }, Help = "Finds all System.RuntimeType objects in the GC heap and prints the type name and MethodTable they refer too.")] + public sealed class DumpRuntimeTypeCommand : ClrRuntimeCommandBase { - [ServiceImport] - public ClrRuntime Runtime { get; set; } - public override void Invoke() { Table output = null; diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpStackObjectsCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpStackObjectsCommand.cs index 131450fe8..2a211c1eb 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/DumpStackObjectsCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/DumpStackObjectsCommand.cs @@ -15,8 +15,8 @@ using static Microsoft.Diagnostics.ExtensionCommands.Output.ColumnKind; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "dumpstackobjects", Aliases = new string[] { "dso" }, Help = "Displays all managed objects found within the bounds of the current stack.")] - public class DumpStackObjectsCommand : CommandBase + [Command(Name = "dumpstackobjects", Aliases = new string[] { "dso", "DumpStackObjects" }, Help = "Displays all managed objects found within the bounds of the current stack.")] + public class DumpStackObjectsCommand : ClrRuntimeCommandBase { [ServiceImport] public IMemoryService MemoryService { get; set; } @@ -27,13 +27,10 @@ namespace Microsoft.Diagnostics.ExtensionCommands [ServiceImport] public IThreadService ThreadService { get; set; } - [ServiceImport] - public ClrRuntime Runtime { get; set; } - [Option(Name = "-verify", Help = "Verify each object and only print ones that are valid objects.")] public bool Verify { get; set; } - [Argument(Name = "StackBounds", Help = "The top and bottom of the stack (in hex).")] + [Argument(Name = "stackbounds", Help = "The top and bottom of the stack (in hex).")] public string[] Bounds { get; set; } public override void Invoke() diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/EEHeapCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/EEHeapCommand.cs index c56514865..59fb9fc3c 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/EEHeapCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/EEHeapCommand.cs @@ -13,8 +13,8 @@ using static Microsoft.Diagnostics.ExtensionCommands.Output.ColumnKind; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = CommandName, Help = "Displays information about native memory that CLR has allocated.")] - public class EEHeapCommand : CommandBase + [Command(Name = CommandName, Aliases = new[] { "EEHeap" }, Help = "Displays information about native memory that CLR has allocated.")] + public class EEHeapCommand : ClrRuntimeCommandBase { private const string CommandName = "eeheap"; @@ -23,9 +23,6 @@ namespace Microsoft.Diagnostics.ExtensionCommands // Don't use the word "Total" if we have filtered out entries private string TotalString => HeapWithFilters.HasFilters ? "Partial" : "Total"; - [ServiceImport] - public ClrRuntime Runtime { get; set; } - [ServiceImport] public IMemoryService MemoryService { get; set; } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ExtensionCommandBase.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ExtensionCommandBase.cs deleted file mode 100644 index 319b45db8..000000000 --- a/src/Microsoft.Diagnostics.ExtensionCommands/ExtensionCommandBase.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Diagnostics.DebugServices; - -namespace Microsoft.Diagnostics.ExtensionCommands -{ - public abstract class ExtensionCommandBase : CommandBase - { - /// - /// Helper bound to the current ClrRuntime that provides high level services on top of ClrMD. - /// - [ServiceImport(Optional = true)] - public ClrMDHelper Helper { get; set; } - - public override void Invoke() - { - if (Helper == null) - { - throw new DiagnosticsException("No CLR runtime set"); - } - ExtensionInvoke(); - } - - public abstract void ExtensionInvoke(); - - [HelpInvoke] - public void InvokeHelp() - { - WriteLine(GetDetailedHelp()); - } - - protected abstract string GetDetailedHelp(); - } -} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ExtensionMethodHelpers.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ExtensionMethodHelpers.cs index 348a23eb8..778aab969 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/ExtensionMethodHelpers.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ExtensionMethodHelpers.cs @@ -11,6 +11,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands internal static class ExtensionMethodHelpers { public static string ConvertToHumanReadable(this ulong totalBytes) => ConvertToHumanReadable((double)totalBytes); + public static string ConvertToHumanReadable(this long totalBytes) => ConvertToHumanReadable((double)totalBytes); public static string ConvertToHumanReadable(this double totalBytes) diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/FinalizeQueueCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/FinalizeQueueCommand.cs index 9d4efb2ed..f74aee474 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/FinalizeQueueCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/FinalizeQueueCommand.cs @@ -11,8 +11,8 @@ using static Microsoft.Diagnostics.ExtensionCommands.Output.ColumnKind; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "finalizequeue", Help = "Displays all objects registered for finalization.")] - public class FinalizeQueueCommand : CommandBase + [Command(Name = "finalizequeue", Aliases = new[] { "fq", "FinalizeQueue" }, Help = "Displays all objects registered for finalization.")] + public class FinalizeQueueCommand : ClrRuntimeCommandBase { [Option(Name = "-detail", Help = "Will display extra information on any SyncBlocks that need to be cleaned up, and on any RuntimeCallableWrappers (RCWs) that await cleanup. Both of these data structures are cached and cleaned up by the finalizer thread when it gets a chance to run.")] public bool Detail { get; set; } @@ -38,9 +38,6 @@ namespace Microsoft.Diagnostics.ExtensionCommands [ServiceImport] public DumpHeapService DumpHeap { get; set; } - [ServiceImport] - public ClrRuntime Runtime { get; set; } - public override void Invoke() { ulong mt = 0; @@ -87,6 +84,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands DumpHeap.PrintHeap(objects, displayKind, Stat, printFragmentation: false); } + private IEnumerable EnumerateFinalizableObjects(bool allReady, ulong mt) { IEnumerable result = EnumerateValidFinalizableObjectsWithTypeFilter(mt); diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/FindEphemeralReferencesToLOHCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/FindEphemeralReferencesToLOHCommand.cs index e1715396d..221c06f50 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/FindEphemeralReferencesToLOHCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/FindEphemeralReferencesToLOHCommand.cs @@ -12,14 +12,11 @@ using static Microsoft.Diagnostics.ExtensionCommands.Output.ColumnKind; namespace Microsoft.Diagnostics.ExtensionCommands { [Command(Name = "ephtoloh", Help = "Finds ephemeral objects which reference the large object heap.")] - public class FindEphemeralReferencesToLOHCommand : CommandBase + public class FindEphemeralReferencesToLOHCommand : ClrRuntimeCommandBase { // IComparer for binary search private readonly IComparer<(ClrObject, ClrObject)> _firstObjectComparer = Comparer<(ClrObject, ClrObject)>.Create((x, y) => x.Item1.Address.CompareTo(y.Item1.Address)); - [ServiceImport] - public ClrRuntime Runtime { get; set; } - public override void Invoke() { int segments = Runtime.Heap.Segments.Count(seg => seg.Kind is not GCSegmentKind.Frozen or GCSegmentKind.Pinned); diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/FindPointersInCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/FindPointersInCommand.cs index 752bb338c..755328cf4 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/FindPointersInCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/FindPointersInCommand.cs @@ -62,6 +62,9 @@ namespace Microsoft.Diagnostics.ExtensionCommands PrintPointers(!ShowAllObjects, Regions); } + [FilterInvoke(Message = "The memory region service does not exists. This command is only supported under windbg/cdb debuggers.")] + public static bool FilterInvoke([ServiceImport(Optional = true)] ClrRuntime runtime, [ServiceImport(Optional = true)] NativeAddressHelper helper) => runtime != null && helper != null; + private void PrintPointers(bool pinnedOnly, params string[] memTypes) { DescribedRegion[] allRegions = AddressHelper.EnumerateAddressSpace(tagClrMemoryRanges: true, includeReserveMemory: false, tagReserveMemoryHeuristically: false, includeHandleTableIfSlow: false).ToArray(); @@ -496,9 +499,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands } [HelpInvoke] - public void HelpInvoke() - { - WriteLine( + public static string GetDetailedHelp() => @"------------------------------------------------------------------------------- The findpointersin command will search the regions of memory given by MADDRESS_TYPE_LIST to find all pointers to other memory regions and display them. By default, pointers @@ -508,15 +509,15 @@ pointer, or a stray pointer that should be ignored.) If --all is set, then this command print out ALL objects that are pointed to instead of collapsing them into one entry. -usage: !findpointersin [--all] MADDRESS_TYPE_LIST +usage: findpointersin [--all] MADDRESS_TYPE_LIST -Note: The MADDRESS_TYPE_LIST must be a memory type as printed by !maddress. +Note: The MADDRESS_TYPE_LIST must be a memory type as printed by maddress. -Example: ""!findpointersin PAGE_READWRITE"" will only search for regions of memory that +Example: ""findpointersin PAGE_READWRITE"" will only search for regions of memory that !maddress marks as ""PAGE_READWRITE"" and not every page of memory that's marked with PAGE_READWRITE protection. -Example: Running the command ""!findpointersin Stack PAGE_READWRITE"" will find all pointers +Example: Running the command ""findpointersin Stack PAGE_READWRITE"" will find all pointers on any ""Stack"" and ""PAGE_READWRITE"" memory segments and summarize those contents into three tables: One table for pointers to the GC heap, one table for pointers where symbols could be resolved, and one table of pointers where we couldn't resolve symbols. @@ -549,7 +550,6 @@ Sample Output: ... --------------------------------------------------------- [ TOTALS ] ---------33,360---------72,029--------------- -"); - } +"; } } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/FindReferencesToEphemeralCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/FindReferencesToEphemeralCommand.cs index 6614438d7..571a4b97d 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/FindReferencesToEphemeralCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/FindReferencesToEphemeralCommand.cs @@ -11,11 +11,8 @@ using static Microsoft.Diagnostics.ExtensionCommands.Output.ColumnKind; namespace Microsoft.Diagnostics.ExtensionCommands { [Command(Name = "ephrefs", Help = "Finds older generation objects which reference objects in the ephemeral segment.")] - public class FindReferencesToEphemeralCommand : CommandBase + public class FindReferencesToEphemeralCommand : ClrRuntimeCommandBase { - [ServiceImport] - public ClrRuntime Runtime { get; set; } - private readonly HashSet _referenced = new(); private ulong _referencedSize; @@ -71,7 +68,6 @@ namespace Microsoft.Diagnostics.ExtensionCommands Console.WriteLine($"{objCount:n0} older generation objects referenced {_referenced.Count:n0} younger objects ({_referencedSize:n0} bytes)"); } - private IEnumerable FindObjectsWithEphemeralReferences() { foreach (ClrSegment seg in Runtime.Heap.Segments) diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/GCHeapStatCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/GCHeapStatCommand.cs index 7a3da0199..3552daa85 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/GCHeapStatCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/GCHeapStatCommand.cs @@ -11,12 +11,9 @@ using static Microsoft.Diagnostics.ExtensionCommands.Output.ColumnKind; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "gcheapstat", DefaultOptions = "GCHeapStat", Help = "Displays various GC heap stats.")] - public class GCHeapStatCommand : CommandBase + [Command(Name = "gcheapstat", Aliases = new[] { "GCHeapStat" }, Help = "Displays various GC heap stats.")] + public class GCHeapStatCommand : ClrRuntimeCommandBase { - [ServiceImport] - public ClrRuntime Runtime { get; set; } - [ServiceImport] public LiveObjectService LiveObjects { get; set; } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/GCRootCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/GCRootCommand.cs index 7260a22ee..1cf459013 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/GCRootCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/GCRootCommand.cs @@ -11,8 +11,8 @@ using static Microsoft.Diagnostics.ExtensionCommands.Output.ColumnKind; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "gcroot", Help = "Displays info about references (or roots) to an object at the specified address.")] - public class GCRootCommand : CommandBase + [Command(Name = "gcroot", Aliases = new[] { "GCRoot" }, Help = "Displays info about references (or roots) to an object at the specified address.")] + public class GCRootCommand : ClrRuntimeCommandBase { private StringBuilder _lineBuilder = new(64); private ClrRoot _lastRoot; @@ -20,9 +20,6 @@ namespace Microsoft.Diagnostics.ExtensionCommands [ServiceImport] public IMemoryService Memory { get; set; } - [ServiceImport] - public ClrRuntime Runtime { get; set; } - [ServiceImport] public RootCacheService RootCache { get; set; } @@ -68,8 +65,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands ClrSegment seg = Runtime.Heap.GetSegmentByAddress(address); if (seg is null) { - Console.WriteLineError($"Address {address:x} is not in the managed heap."); - return; + throw new DiagnosticsException($"Address {address:x} is not in the managed heap."); } Generation objectGen = seg.GetGeneration(address); diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/GCToNativeCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/GCToNativeCommand.cs index e5157d4d3..e3e46d6d9 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/GCToNativeCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/GCToNativeCommand.cs @@ -24,10 +24,10 @@ namespace Microsoft.Diagnostics.ExtensionCommands public bool ShowAll { get; set; } [ServiceImport] - public ClrRuntime Runtime { get; set; } + public NativeAddressHelper AddressHelper { get; set; } [ServiceImport] - public NativeAddressHelper AddressHelper { get; set; } + public ClrRuntime Runtime { get; set; } private int Width { @@ -58,6 +58,9 @@ namespace Microsoft.Diagnostics.ExtensionCommands PrintGCPointersToMemory(ShowAll, MemoryTypes); } + [FilterInvoke(Message = "The memory region service does not exists. This command is only supported under windbg/cdb debuggers.")] + public static bool FilterInvoke([ServiceImport(Optional = true)] ClrRuntime runtime, [ServiceImport(Optional = true)] NativeAddressHelper helper) => runtime != null && helper != null; + public void PrintGCPointersToMemory(bool showAll, params string[] memoryTypes) { // Strategy: @@ -556,24 +559,22 @@ namespace Microsoft.Diagnostics.ExtensionCommands } [HelpInvoke] - public void HelpInvoke() - { - WriteLine( + public static string GetDetailedHelp() => @"------------------------------------------------------------------------------- -!gctonative searches the GC heap for pointers to native memory. This is used +gctonative searches the GC heap for pointers to native memory. This is used to help locate regions of native memory that are referenced (or possibly held alive) by objects on the GC heap. -usage: !gctonative [--all] MADDRESS_TYPE_LIST +usage: gctonative [--all] MADDRESS_TYPE_LIST -Note: The MADDRESS_TYPE_LIST must be a memory type as printed by !maddress. +Note: The MADDRESS_TYPE_LIST must be a memory type as printed by maddress. If --all is set, a full list of every pointer from the GC heap to the specified memory will be displayed instead of just a summary table. Sample Output: - 0:000> !gctonative PAGE_READWRITE + 0:000> gctonative PAGE_READWRITE Walking GC heap to find pointers... Resolving object names... ================================================ PAGE_READWRITE Regions ================================================ @@ -618,7 +619,6 @@ Sample Output: System.Net.Sockets.SocketAsyncEngine | 1 | 7f059800edd0 Microsoft.Extensions.Caching.Memory.CacheEntry | 1 | 7f05241e0000 System.Runtime.CompilerServices.AsyncTaskMethodBuilder<...>+AsyncStateMachine... | 1 | 7f0500000004 -"); - } +"; } } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/GCWhereCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/GCWhereCommand.cs index 96a61f378..2ae319384 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/GCWhereCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/GCWhereCommand.cs @@ -11,12 +11,9 @@ using static Microsoft.Diagnostics.ExtensionCommands.Output.ColumnKind; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "gcwhere", Help = "Displays the location in the GC heap of the specified address.")] - public class GCWhereCommand : CommandBase + [Command(Name = "gcwhere", Aliases = new[] { "GCWhere" }, Help = "Displays the location in the GC heap of the specified address.")] + public class GCWhereCommand : ClrRuntimeCommandBase { - [ServiceImport] - public ClrRuntime Runtime { get; set; } - [ServiceImport] public IMemoryService MemoryService { get; set; } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/ConsoleLoggingCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/ConsoleLoggingCommand.cs index f274ba483..38f972e83 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/ConsoleLoggingCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/ConsoleLoggingCommand.cs @@ -5,8 +5,8 @@ using Microsoft.Diagnostics.DebugServices; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "logopen", Help = "Enables console file logging.", Flags = CommandFlags.Global)] - [Command(Name = "logclose", DefaultOptions = "--disable", Help = "Disables console file logging.", Flags = CommandFlags.Global)] + [Command(Name = "logopen", Help = "Enables console file logging.")] + [Command(Name = "logclose", DefaultOptions = "--disable", Help = "Disables console file logging.")] public class ConsoleLoggingCommand : CommandBase { [ServiceImport(Optional = true)] diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/HelpCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/HelpCommand.cs index f7c6db56a..2770c5324 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/HelpCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/HelpCommand.cs @@ -2,11 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.Diagnostics.DebugServices; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "help", Help = "Displays help for a command.", Flags = CommandFlags.Global)] + [Command(Name = "help", Aliases = new string[] { "soshelp" }, Help = "Displays help for a command.")] public class HelpCommand : CommandBase { [Argument(Help = "Command to find help.")] @@ -20,9 +22,21 @@ namespace Microsoft.Diagnostics.ExtensionCommands public override void Invoke() { - if (!CommandService.DisplayHelp(Command, Services)) + if (string.IsNullOrWhiteSpace(Command)) { - throw new NotSupportedException($"Help for {Command} not found"); + IEnumerable<(string Invocation, string Help)> commands = CommandService.GetAllCommandHelp(Services); + int invocationWidth = commands.Max((item) => item.Invocation.Length) + 4; + + Write(string.Concat(commands. + OrderBy(item => item.Invocation, StringComparer.OrdinalIgnoreCase). + Select((item) => $"{FormatInvocation(item.Invocation)}{item.Help}{Environment.NewLine}"))); + + string FormatInvocation(string invocation) => invocation + new string(' ', invocationWidth - invocation.Length); + } + else + { + string helpText = CommandService.GetDetailedHelp(Command, Services, Console.WindowWidth) ?? throw new DiagnosticsException($"Help for {Command} not found"); + Write(helpText); } } } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/LoggingCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/LoggingCommand.cs index 18183f6d7..de53f34e4 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/LoggingCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/LoggingCommand.cs @@ -5,7 +5,7 @@ using Microsoft.Diagnostics.DebugServices; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "logging", Help = "Enables/disables internal diagnostic logging.", Flags = CommandFlags.Global)] + [Command(Name = "logging", Help = "Enables/disables internal diagnostic logging.")] public class LoggingCommand : CommandBase { [ServiceImport(Optional = true)] diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/RegistersCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/RegistersCommand.cs index 3ce4d83fb..78d2c07b0 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/RegistersCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/RegistersCommand.cs @@ -12,7 +12,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands [ServiceImport] public IThreadService ThreadService { get; set; } - [ServiceImport] + [ServiceImport(Optional = true)] public IThread CurrentThread { get; set; } [Option(Name = "--verbose", Aliases = new string[] { "-v" }, Help = "Displays more details.")] diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/SetSymbolServerCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/SetSymbolServerCommand.cs index d94f035d9..fe9ebc59e 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/SetSymbolServerCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/SetSymbolServerCommand.cs @@ -5,16 +5,8 @@ using Microsoft.Diagnostics.DebugServices; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command( - Name = "setsymbolserver", - Aliases = new string[] { "SetSymbolServer" }, - Help = "Enables and sets symbol server support for symbols and module download.", - Flags = CommandFlags.Global)] - [Command( - Name = "loadsymbols", - DefaultOptions = "--loadsymbols", - Help = "Loads symbols for all modules.", - Flags = CommandFlags.Global)] + [Command(Name = "setsymbolserver", Aliases = new string[] { "SetSymbolServer" }, Help = "Enables and sets symbol server support for symbols and module download.")] + [Command(Name = "loadsymbols", DefaultOptions = "--loadsymbols", Help = "Loads symbols for all modules.")] public class SetSymbolServerCommand : CommandBase { [ServiceImport] @@ -107,5 +99,92 @@ namespace Microsoft.Diagnostics.ExtensionCommands Write(SymbolService.ToString()); } } + + [HelpInvoke] + public static string GetDetailedHelp(IHost host) + { + switch (host.HostType) + { + case HostType.DbgEng: + return s_detailedHelpTextDbgEng; + case HostType.Lldb: + return s_detailedHelpTextLLDB; + case HostType.DotnetDump: + return s_detailedHelpTextDotNetDump; + } + return null; + } + + private const string s_detailedHelpTextDbgEng = + @" +This commands enables symbol server support for portable PDBs for managed assemblies and +.NET Core native modules files (like the DAC) in SOS. If the .sympath is set, the symbol +server supported is automatically set and this command isn't necessary. +"; + + private const string s_detailedHelpTextLLDB = + @" +This commands enables symbol server support in SOS. The portable PDBs for managed assemblies +and .NET Core native symbol and module (like the DAC) files are downloaded. + +To enable downloading symbols from the Microsoft symbol server: + + (lldb) setsymbolserver -ms + +This command may take some time without any output while it attempts to download the symbol files. + +To disable downloading or clear the current SOS symbol settings allowing new symbol paths to be set: + + (lldb) setsymbolserver -disable + +To add a directory to search for symbols: + + (lldb) setsymbolserver -directory /home/mikem/symbols + +This command can be used so the module/symbol file structure does not have to match the machine +file structure that the core dump was generated. + +To clear the default cache run ""rm -r $HOME/.dotnet/symbolcache"" in a command shell. + +If you receive an error like the one below on a core dump, you need to set the .NET Core +runtime with the ""sethostruntime"" command. Type ""soshelp sethostruntime"" for more details. + + (lldb) setsymbolserver -ms + Error: Fail to initialize CoreCLR 80004005 + SetSymbolServer -ms failed + +The ""-loadsymbols"" option and the ""loadsymbol"" command alias attempts to download the native .NET +Core symbol files. It is only useful for live sessions and not core dumps. This command needs to +be run before the lldb ""bt"" (stack trace) or the ""clrstack -f"" (dumps both managed and native +stack frames). + + (lldb) loadsymbols + (lldb) bt +"; + + private const string s_detailedHelpTextDotNetDump = + @" +This commands enables symbol server support in SOS. The portable PDBs for managed assemblies +and .NET Core native module (like the DAC) files are downloaded. + +To enable downloading symbols from the Microsoft symbol server: + + > setsymbolserver -ms + +This command may take some time without any output while it attempts to download the symbol files. + +To disable downloading or clear the current SOS symbol settings allowing new symbol paths to be set: + + > setsymbolserver -disable + +To add a directory to search for symbols: + + > setsymbolserver -directory /home/mikem/symbols + +This command can be used so the module/symbol file structure does not have to match the machine +file structure that the core dump was generated. + +To clear the default cache run ""rm -r $HOME/.dotnet/symbolcache"" in a command shell. +"; } } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ListNearObjCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ListNearObjCommand.cs index 1b4411925..afeffcbfe 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/ListNearObjCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ListNearObjCommand.cs @@ -11,12 +11,9 @@ using static Microsoft.Diagnostics.ExtensionCommands.Output.ColumnKind; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "listnearobj", Help = "Displays the object preceding and succeeding the specified address.")] - public class ListNearObjCommand : CommandBase + [Command(Name = "listnearobj", Aliases = new[] { "lno", "ListNearObj" }, Help = "Displays the object preceding and succeeding the specified address.")] + public class ListNearObjCommand : ClrRuntimeCommandBase { - [ServiceImport] - public ClrRuntime Runtime { get; set; } - [ServiceImport] public IMemoryService MemoryService { get; set; } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/MAddressCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/MAddressCommand.cs index fa221c96e..9a1774fdc 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/MAddressCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/MAddressCommand.cs @@ -196,15 +196,16 @@ namespace Microsoft.Diagnostics.ExtensionCommands } } + [FilterInvoke(Message = "The memory region service does not exists. This command is only supported under windbg/cdb debuggers.")] + public static bool FilterInvoke([ServiceImport(Optional = true)] NativeAddressHelper helper) => helper != null; + [HelpInvoke] - public void HelpInvoke() - { - WriteLine( + public static string GetDetailedHelp() => $@"------------------------------------------------------------------------------- -maddress is a managed version of !address, which attempts to annotate all memory +!maddress is a managed version of !address, which attempts to annotate all memory with information about CLR's heaps. -usage: !sos maddress [{SummaryFlag}] [{ImagesFlag}] [{ForceHandleTableFlag}] [{ReserveFlag} [{ReserveHeuristicFlag}]] +usage: !maddress [{SummaryFlag}] [{ImagesFlag}] [{ForceHandleTableFlag}] [{ReserveFlag} [{ReserveHeuristicFlag}]] Flags: {SummaryFlag} @@ -238,7 +239,6 @@ Flags: {BySizeFlag} Order the list of memory blocks by size (descending) when printing the list of all memory blocks instead of by address. -"); - } +"; } } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/NativeAddressHelper.cs b/src/Microsoft.Diagnostics.ExtensionCommands/NativeAddressHelper.cs index a8e46ed7c..7de6a766a 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/NativeAddressHelper.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/NativeAddressHelper.cs @@ -12,22 +12,29 @@ using Microsoft.Diagnostics.Runtime; namespace Microsoft.Diagnostics.ExtensionCommands { - [ServiceExport(Scope = ServiceScope.Target)] public sealed class NativeAddressHelper : IDisposable { private readonly IDisposable _onFlushEvent; private ((bool, bool, bool, bool) Key, DescribedRegion[] Result) _previous; - public NativeAddressHelper(ITarget target) + [ServiceExport(Scope = ServiceScope.Target)] + public static NativeAddressHelper TryCreate(ITarget target, [ServiceImport(Optional = true)] IMemoryRegionService memoryRegionService) + { + return memoryRegionService != null ? new NativeAddressHelper(target, memoryRegionService) : null; + } + + private NativeAddressHelper(ITarget target, IMemoryRegionService memoryRegionService) { Target = target; + MemoryRegionService = memoryRegionService; _onFlushEvent = target.OnFlushEvent.Register(() => _previous = default); } public void Dispose() => _onFlushEvent.Dispose(); - [ServiceImport] - public ITarget Target { get; set; } + public ITarget Target { get; } + + public IMemoryRegionService MemoryRegionService { get; } [ServiceImport] public IMemoryService MemoryService { get; set; } @@ -41,9 +48,6 @@ namespace Microsoft.Diagnostics.ExtensionCommands [ServiceImport] public IModuleService ModuleService { get; set; } - [ServiceImport] - public IMemoryRegionService MemoryRegionService { get; set; } - [ServiceImport] public IConsoleService Console { get; set; } @@ -107,9 +111,9 @@ namespace Microsoft.Diagnostics.ExtensionCommands foreach (IRuntime runtime in RuntimeService.EnumerateRuntimes()) { ClrRuntime clrRuntime = runtime.Services.GetService(); - RootCacheService rootCache = runtime.Services.GetService(); if (clrRuntime is not null) { + RootCacheService rootCache = runtime.Services.GetService() ?? throw new DiagnosticsException("NativeAddressHelper: RootCacheService not found"); foreach ((ulong Address, ulong Size, ClrMemoryKind Kind) mem in EnumerateClrMemoryAddresses(clrRuntime, rootCache, includeHandleTableIfSlow)) { // The GCBookkeeping range is a large region of memory that the GC reserved. We'll simply mark every diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/NotReachableInRangeCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/NotReachableInRangeCommand.cs index 59e399e30..27ec0b6fe 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/NotReachableInRangeCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/NotReachableInRangeCommand.cs @@ -14,7 +14,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands /// Prints objects and statistics for a range of object pointers. /// [Command(Name = "notreachableinrange", Help = "A helper command for !finalizerqueue")] - public class NotReachableInRangeCommand : CommandBase + public class NotReachableInRangeCommand : ClrRuntimeCommandBase { private HashSet _nonFQLiveObjects; @@ -30,9 +30,6 @@ namespace Microsoft.Diagnostics.ExtensionCommands [ServiceImport] public IMemoryService Memory { get; set; } - [ServiceImport] - public ClrRuntime Runtime { get; set; } - [Option(Name = "-short")] public bool Short { get; set; } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ObjSizeCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ObjSizeCommand.cs index 0beb0e98f..c65f381fa 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/ObjSizeCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ObjSizeCommand.cs @@ -8,12 +8,9 @@ using Microsoft.Diagnostics.Runtime; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "objsize", Help = "Lists the sizes of the all the objects found on managed threads.")] - public class ObjSizeCommand : CommandBase + [Command(Name = "objsize", Aliases = new[] { "ObjSize" }, Help = "Lists the sizes of the all the objects found on managed threads.")] + public class ObjSizeCommand : ClrRuntimeCommandBase { - [ServiceImport] - public ClrRuntime Runtime { get; set; } - [ServiceImport] public DumpHeapService DumpHeap { get; set; } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacksCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacksCommand.cs index f61442a90..3320f11d3 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacksCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacksCommand.cs @@ -9,17 +9,17 @@ using ParallelStacks.Runtime; namespace Microsoft.Diagnostics.ExtensionCommands { [Command(Name = "parallelstacks", Aliases = new string[] { "pstacks" }, Help = "Displays the merged threads stack similarly to the Visual Studio 'Parallel Stacks' panel.")] - public class ParallelStacksCommand : ExtensionCommandBase + public class ParallelStacksCommand : ClrMDHelperCommandBase { - [ServiceImport] + [ServiceImport(Optional = true)] public ClrRuntime Runtime { get; set; } [Option(Name = "--allthreads", Aliases = new string[] { "-a" }, Help = "Displays all threads per group instead of at most 4 by default.")] public bool AllThreads { get; set; } - public override void ExtensionInvoke() + public override void Invoke() { - ParallelStack ps = ParallelStacks.Runtime.ParallelStack.Build(Runtime); + ParallelStack ps = ParallelStack.Build(Runtime); if (ps == null) { return; @@ -41,47 +41,41 @@ namespace Microsoft.Diagnostics.ExtensionCommands WriteLine($"==> {ps.ThreadIds.Count} threads with {ps.Stacks.Count} roots{Environment.NewLine}"); } - protected override string GetDetailedHelp() - { - return DetailedHelpText; - } + [HelpInvoke] + public static string GetDetailedHelp() => +@"------------------------------------------------------------------------------- +ParallelStacks + +pstacks groups the callstack of all running threads and shows a merged display a la Visual Studio 'Parallel Stacks' panel +By default, only 4 threads ID per frame group are listed. Use --allThreads/-a to list all threads ID. + +> pstacks +________________________________________________ +~~~~ 8f8c + 1 (dynamicClass).IL_STUB_PInvoke(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr) + ... + 1 System.Console.ReadLine() + 1 NetCoreConsoleApp.Program.Main(String[]) + +________________________________________________ + ~~~~ 7034 + 1 System.Threading.Monitor.Wait(Object, Int32, Boolean) + ... + 1 System.Threading.Tasks.Task.Wait() + 1 NetCoreConsoleApp.Program+c.b__1_4(Object) + ~~~~ 9c6c,4020 + 2 System.Threading.Monitor.Wait(Object, Int32, Boolean) + ... + 2 NetCoreConsoleApp.Program+c__DisplayClass1_0.b__7() + 3 System.Threading.Tasks.Task.InnerInvoke() + 4 System.Threading.Tasks.Task+c.cctor>b__278_1(Object) + ... + 4 System.Threading.Tasks.Task.ExecuteEntryUnsafe() + 4 System.Threading.Tasks.Task.ExecuteWorkItem() + 7 System.Threading.ThreadPoolWorkQueue.Dispatch() + 7 System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() - private readonly string DetailedHelpText = - "-------------------------------------------------------------------------------" + Environment.NewLine + - "ParallelStacks" + Environment.NewLine + - Environment.NewLine + - "pstacks groups the callstack of all running threads and shows a merged display a la Visual Studio 'Parallel Stacks' panel" + Environment.NewLine + - "By default, only 4 threads ID per frame group are listed. Use --allThreads/-a to list all threads ID." + Environment.NewLine + - Environment.NewLine + - "> pstacks" + Environment.NewLine + - "________________________________________________" + Environment.NewLine + - "~~~~ 8f8c" + Environment.NewLine + - " 1 (dynamicClass).IL_STUB_PInvoke(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr)" + Environment.NewLine + - " ..." + Environment.NewLine + - " 1 System.Console.ReadLine()" + Environment.NewLine + - " 1 NetCoreConsoleApp.Program.Main(String[])" + Environment.NewLine + - Environment.NewLine + - "________________________________________________" + Environment.NewLine + - " ~~~~ 7034" + Environment.NewLine + - " 1 System.Threading.Monitor.Wait(Object, Int32, Boolean)" + Environment.NewLine + - " ..." + Environment.NewLine + - " 1 System.Threading.Tasks.Task.Wait()" + Environment.NewLine + - " 1 NetCoreConsoleApp.Program+c.b__1_4(Object)" + Environment.NewLine + - " ~~~~ 9c6c,4020" + Environment.NewLine + - " 2 System.Threading.Monitor.Wait(Object, Int32, Boolean)" + Environment.NewLine + - " ..." + Environment.NewLine + - " 2 NetCoreConsoleApp.Program+c__DisplayClass1_0.b__7()" + Environment.NewLine + - " 3 System.Threading.Tasks.Task.InnerInvoke()" + Environment.NewLine + - " 4 System.Threading.Tasks.Task+c.cctor>b__278_1(Object)" + Environment.NewLine + - " ..." + Environment.NewLine + - " 4 System.Threading.Tasks.Task.ExecuteEntryUnsafe()" + Environment.NewLine + - " 4 System.Threading.Tasks.Task.ExecuteWorkItem()" + Environment.NewLine + - " 7 System.Threading.ThreadPoolWorkQueue.Dispatch()" + Environment.NewLine + - " 7 System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()" + Environment.NewLine + - Environment.NewLine + - "==> 8 threads with 2 roots" + Environment.NewLine + - Environment.NewLine + - "" - ; +==> 8 threads with 2 roots +"; } } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/PathToCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/PathToCommand.cs index e63940d3b..ac0975b85 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/PathToCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/PathToCommand.cs @@ -7,12 +7,9 @@ using Microsoft.Diagnostics.Runtime; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name ="pathto", Help = "Displays the GC path from to .")] - public class PathToCommand : CommandBase + [Command(Name ="pathto", Aliases = new[] { "PathTo" }, Help = "Displays the GC path from to .")] + public class PathToCommand : ClrRuntimeCommandBase { - [ServiceImport] - public ClrRuntime Runtime { get; set; } - [ServiceImport] public RootCacheService RootCache { get; set; } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/SimulateGCHeapCorruption.cs b/src/Microsoft.Diagnostics.ExtensionCommands/SimulateGCHeapCorruption.cs index 2105875b9..e018cf218 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/SimulateGCHeapCorruption.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/SimulateGCHeapCorruption.cs @@ -13,16 +13,13 @@ using static Microsoft.Diagnostics.ExtensionCommands.Output.ColumnKind; namespace Microsoft.Diagnostics.ExtensionCommands { [DebugCommand(Name=nameof(SimulateGCHeapCorruption), Help = "Writes values to the GC heap in strategic places to simulate heap corruption.")] - public class SimulateGCHeapCorruption : CommandBase + public class SimulateGCHeapCorruption : ClrRuntimeCommandBase { private static readonly List _changes = new(); [ServiceImport] public IMemoryService MemoryService { get; set; } - [ServiceImport] - public ClrRuntime Runtime { get; set; } - [Argument] public string Command { get; set; } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/SizeStatsCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/SizeStatsCommand.cs index b8f67a7a5..a1b59b07a 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/SizeStatsCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/SizeStatsCommand.cs @@ -10,11 +10,8 @@ using Microsoft.Diagnostics.Runtime; namespace Microsoft.Diagnostics.ExtensionCommands { [Command(Name = "sizestats", Help = "Size statistics for the GC heap.")] - public sealed class SizeStatsCommand : CommandBase + public sealed class SizeStatsCommand : ClrRuntimeCommandBase { - [ServiceImport] - public ClrRuntime Runtime { get; set; } - public override void Invoke() { SizeStats(Generation.Generation0, isFree: false); diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/TaskStateCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/TaskStateCommand.cs index 63a7e4536..716fec090 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/TaskStateCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/TaskStateCommand.cs @@ -7,7 +7,7 @@ using Microsoft.Diagnostics.DebugServices; namespace Microsoft.Diagnostics.ExtensionCommands { [Command(Name = "taskstate", Aliases = new string[] { "tks" }, Help = "Displays a Task state in a human readable format.")] - public class TaskStateCommand : ExtensionCommandBase + public class TaskStateCommand : ClrMDHelperCommandBase { [Argument(Help = "The Task instance address.")] public string Address { get; set; } @@ -15,7 +15,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands [Option(Name = "--value", Aliases = new string[] { "-v" }, Help = " is the value of a Task m_stateFlags field.")] public ulong? Value { get; set; } - public override void ExtensionInvoke() + public override void Invoke() { if (string.IsNullOrEmpty(Address) && !Value.HasValue) { @@ -57,23 +57,19 @@ namespace Microsoft.Diagnostics.ExtensionCommands } - protected override string GetDetailedHelp() - { - return DetailedHelpText; - } + [HelpInvoke] + public static string GetDetailedHelp() => +@"------------------------------------------------------------------------------- +TaskState [hexa address] [-v ] + +TaskState translates a Task m_stateFlags field value into human readable format. +It supports hexadecimal address corresponding to a task instance or -v . + +> tks 000001db16cf98f0 +Running - private readonly string DetailedHelpText = - "-------------------------------------------------------------------------------" + Environment.NewLine + - "TaskState [hexa address] [-v ]" + Environment.NewLine + - Environment.NewLine + - "TaskState translates a Task m_stateFlags field value into human readable format." + Environment.NewLine + - "It supports hexadecimal address corresponding to a task instance or -v ." + Environment.NewLine + - Environment.NewLine + - "> tks 000001db16cf98f0" + Environment.NewLine + - "Running" + Environment.NewLine + - Environment.NewLine + - "> tks -v 73728" + Environment.NewLine + - "WaitingToRun" - ; +> tks -v 73728 +WaitingToRun +"; } } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ThreadPoolCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ThreadPoolCommand.cs index 621b6a501..f78549ffc 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/ThreadPoolCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ThreadPoolCommand.cs @@ -11,12 +11,9 @@ using static Microsoft.Diagnostics.ExtensionCommands.Output.ColumnKind; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "threadpool", Help = "Displays info about the runtime thread pool.")] - public sealed class ThreadPoolCommand : CommandBase + [Command(Name = "threadpool", Aliases = new[] { "ThreadPool" }, Help = "Displays info about the runtime thread pool.")] + public sealed class ThreadPoolCommand : ClrRuntimeCommandBase { - [ServiceImport] - public ClrRuntime Runtime { get; set; } - [Option(Name = "-ti", Help = "Print the hill climbing log.", Aliases = new string[] { "-hc" })] public bool PrintHillClimbingLog { get; set; } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ThreadPoolQueueCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ThreadPoolQueueCommand.cs index bee1b08f4..85b5d9b4e 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/ThreadPoolQueueCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ThreadPoolQueueCommand.cs @@ -9,9 +9,9 @@ using Microsoft.Diagnostics.DebugServices; namespace Microsoft.Diagnostics.ExtensionCommands { [Command(Name = "threadpoolqueue", Aliases = new string[] { "tpq" }, Help = "Displays queued ThreadPool work items.")] - public class ThreadPoolQueueCommand : ExtensionCommandBase + public class ThreadPoolQueueCommand : ClrMDHelperCommandBase { - public override void ExtensionInvoke() + public override void Invoke() { Dictionary workItems = new(); int workItemCount = 0; @@ -102,42 +102,36 @@ namespace Microsoft.Diagnostics.ExtensionCommands wi.Count++; } - protected override string GetDetailedHelp() - { - return DetailedHelpText; - } + [HelpInvoke] + public static string GetDetailedHelp() => +@"------------------------------------------------------------------------------- +ThreadPoolQueue + +ThreadPoolQueue lists the enqueued work items in the Clr Thread Pool followed by a summary of the different tasks/work items. +The global queue is first iterated before local per-thread queues. +The name of the method to be called (on which instance if any) is also provided when available. + +> tpq + +global work item queue________________________________ +0x000002AC3C1DDBB0 Work | (ASP.global_asax)System.Web.HttpApplication.ResumeStepsWaitCallback + ... +0x000002AABEC19148 Task | System.Threading.Tasks.Dataflow.Internal.TargetCore.b__3 + +local per thread work items_____________________________________ +0x000002AE79D80A00 System.Threading.Tasks.ContinuationTaskFromTask + ... +0x000002AB7CBB84A0 Task | System.Net.Http.HttpClientHandler.StartRequest + + 7 Task System.Threading.Tasks.Dataflow.Internal.TargetCore.b__3 + ... + 84 Task System.Net.Http.HttpClientHandler.StartRequest +---- +6039 - private readonly string DetailedHelpText = - "-------------------------------------------------------------------------------" + Environment.NewLine + - "ThreadPoolQueue" + Environment.NewLine + - Environment.NewLine + - "ThreadPoolQueue lists the enqueued work items in the Clr Thread Pool followed by a summary of the different tasks/work items." + Environment.NewLine + - "The global queue is first iterated before local per-thread queues." + Environment.NewLine + - "The name of the method to be called (on which instance if any) is also provided when available." + Environment.NewLine + - Environment.NewLine + - "> tpq" + Environment.NewLine + - Environment.NewLine + - "global work item queue________________________________" + Environment.NewLine + - "0x000002AC3C1DDBB0 Work | (ASP.global_asax)System.Web.HttpApplication.ResumeStepsWaitCallback" + Environment.NewLine + - " ..." + Environment.NewLine + - "0x000002AABEC19148 Task | System.Threading.Tasks.Dataflow.Internal.TargetCore.b__3" + Environment.NewLine + - "" + Environment.NewLine + - "local per thread work items_____________________________________" + Environment.NewLine + - "0x000002AE79D80A00 System.Threading.Tasks.ContinuationTaskFromTask" + Environment.NewLine + - " ..." + Environment.NewLine + - "0x000002AB7CBB84A0 Task | System.Net.Http.HttpClientHandler.StartRequest" + Environment.NewLine + - "" + Environment.NewLine + - " 7 Task System.Threading.Tasks.Dataflow.Internal.TargetCore.b__3" + Environment.NewLine + - " ..." + Environment.NewLine + - " 84 Task System.Net.Http.HttpClientHandler.StartRequest" + Environment.NewLine + - "----" + Environment.NewLine + - "6039" + Environment.NewLine + - "" + Environment.NewLine + - "1810 Work (ASP.global_asax) System.Web.HttpApplication.ResumeStepsWaitCallback" + Environment.NewLine + - "----" + Environment.NewLine + - "1810" + Environment.NewLine + - "" - ; +1810 Work (ASP.global_asax) System.Web.HttpApplication.ResumeStepsWaitCallback +---- +1810"; private sealed class WorkInfo { diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/TimersCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/TimersCommand.cs index 6a040242a..2c576c719 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/TimersCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/TimersCommand.cs @@ -9,9 +9,9 @@ using Microsoft.Diagnostics.DebugServices; namespace Microsoft.Diagnostics.ExtensionCommands { [Command(Name = "timerinfo", Aliases = new string[] { "ti" }, Help = "Displays information about running timers.")] - public class TimersCommand : ExtensionCommandBase + public class TimersCommand : ClrMDHelperCommandBase { - public override void ExtensionInvoke() + public override void Invoke() { try { @@ -104,33 +104,28 @@ namespace Microsoft.Diagnostics.ExtensionCommands } - protected override string GetDetailedHelp() - { - return DetailedHelpText; - } + [HelpInvoke] + public static string GetDetailedHelp() => +@"------------------------------------------------------------------------------- +TimerInfo - private readonly string DetailedHelpText = - "-------------------------------------------------------------------------------" + Environment.NewLine + - "TimerInfo" + Environment.NewLine + - Environment.NewLine + - "TimerInfo lists all the running timers followed by a summary of the different items." + Environment.NewLine + - "The name of the method to be called (on which instance if any) is also provided when available." + Environment.NewLine + - Environment.NewLine + - "> ti" + Environment.NewLine + - "0x000001E29BD45848 @ 964 ms every 1000 ms | 0x000001E29BD0C828 (Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.Heartbeat) ->" + Environment.NewLine + - "0x000001E19BD0F868 @ 1 ms every ------ ms | 0x000001E19BD0F800 (System.Threading.Tasks.Task+DelayPromise) -> System.Threading.Tasks.Task+<>c.b__260_1" + Environment.NewLine + - "0x000001E09BD09B40 @ 1 ms every ------ ms | 0x000001E09BD09AD8 (System.Threading.Tasks.Task+DelayPromise) -> System.Threading.Tasks.Task+<>c.b__260_1" + Environment.NewLine + - "0x000001E29BD58C68 @ 1 ms every ------ ms | 0x000001E29BD58C00 (System.Threading.Tasks.Task+DelayPromise) -> System.Threading.Tasks.Task+<>c.b__260_1" + Environment.NewLine + - "0x000001E29BCB1398 @ 5000 ms every ------ ms | 0x0000000000000000 () -> System.Diagnostics.Tracing.EventPipeController.PollForTracingCommand" + Environment.NewLine + - Environment.NewLine + - " 5 timers" + Environment.NewLine + - "-----------------------------------------------" + Environment.NewLine + - " 1 | @ 964 ms every 1000 ms | (Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.Heartbeat) ->" + Environment.NewLine + - " 1 | @ 5000 ms every ------ ms | () -> System.Diagnostics.Tracing.EventPipeController.PollForTracingCommand" + Environment.NewLine + - " 3 | @ 1 ms every ------ ms | (System.Threading.Tasks.Task+DelayPromise) -> System.Threading.Tasks.Task+<>c.b__260_1" - ; - } +TimerInfo lists all the running timers followed by a summary of the different items. +The name of the method to be called (on which instance if any) is also provided when available. +> ti +0x000001E29BD45848 @ 964 ms every 1000 ms | 0x000001E29BD0C828 (Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.Heartbeat) -> +0x000001E19BD0F868 @ 1 ms every ------ ms | 0x000001E19BD0F800 (System.Threading.Tasks.Task+DelayPromise) -> System.Threading.Tasks.Task+<>c.b__260_1 +0x000001E09BD09B40 @ 1 ms every ------ ms | 0x000001E09BD09AD8 (System.Threading.Tasks.Task+DelayPromise) -> System.Threading.Tasks.Task+<>c.b__260_1 +0x000001E29BD58C68 @ 1 ms every ------ ms | 0x000001E29BD58C00 (System.Threading.Tasks.Task+DelayPromise) -> System.Threading.Tasks.Task+<>c.b__260_1 +0x000001E29BCB1398 @ 5000 ms every ------ ms | 0x0000000000000000 () -> System.Diagnostics.Tracing.EventPipeController.PollForTracingCommand + + 5 timers +----------------------------------------------- + 1 | @ 964 ms every 1000 ms | (Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.Heartbeat) -> + 1 | @ 5000 ms every ------ ms | () -> System.Diagnostics.Tracing.EventPipeController.PollForTracingCommand + 3 | @ 1 ms every ------ ms | (System.Threading.Tasks.Task+DelayPromise) -> System.Threading.Tasks.Task+<>c.b__260_1 +"; + } internal sealed class TimerStat { diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/TraverseHeapCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/TraverseHeapCommand.cs index 40d282050..57315180b 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/TraverseHeapCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/TraverseHeapCommand.cs @@ -12,12 +12,9 @@ using Microsoft.Diagnostics.Runtime; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "traverseheap", Help = "Writes out heap information to a file in a format understood by the CLR Profiler.")] - public class TraverseHeapCommand : CommandBase + [Command(Name = "traverseheap", Aliases = new[] { "TraverseHeap" }, Help = "Writes out heap information to a file in a format understood by the CLR Profiler.")] + public class TraverseHeapCommand : ClrRuntimeCommandBase { - [ServiceImport] - public ClrRuntime Runtime { get; set; } - [ServiceImport] public RootCacheService RootCache { get; set; } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/VerifyHeapCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/VerifyHeapCommand.cs index e5229cbf3..c7b7b1957 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/VerifyHeapCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/VerifyHeapCommand.cs @@ -9,16 +9,13 @@ using static Microsoft.Diagnostics.ExtensionCommands.Output.ColumnKind; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = CommandName, Help = "Searches the managed heap for memory corruption..")] - public class VerifyHeapCommand : CommandBase + [Command(Name = CommandName, Aliases = new[] { "VerifyHeap" }, Help = "Searches the managed heap for memory corruption..")] + public class VerifyHeapCommand : ClrRuntimeCommandBase { private const string CommandName = "verifyheap"; private int _totalObjects; - [ServiceImport] - public ClrRuntime Runtime { get; set; } - [ServiceImport] public IMemoryService MemoryService { get; set; } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/VerifyObjectCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/VerifyObjectCommand.cs index f952aca36..a9779d337 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/VerifyObjectCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/VerifyObjectCommand.cs @@ -11,16 +11,13 @@ using static Microsoft.Diagnostics.ExtensionCommands.Output.ColumnKind; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "verifyobj", Help = "Checks the given object for signs of corruption.")] - public sealed class VerifyObjectCommand : CommandBase + [Command(Name = "verifyobj", Aliases = new[] { "VerifyObj" }, Help = "Checks the given object for signs of corruption.")] + public sealed class VerifyObjectCommand : ClrRuntimeCommandBase { - [ServiceImport] - public ClrRuntime Runtime { get; set; } - [ServiceImport] public IMemoryService Memory { get; set; } - [Argument(Name = "ObjectAddress", Help = "The object to verify.")] + [Argument(Name = "objectaddress", Help = "The object to verify.")] public string ObjectAddress { get; set; } public override void Invoke() diff --git a/src/Microsoft.Diagnostics.Repl/ConsoleService.cs b/src/Microsoft.Diagnostics.Repl/ConsoleService.cs index 06aaf7b47..4147eabb3 100644 --- a/src/Microsoft.Diagnostics.Repl/ConsoleService.cs +++ b/src/Microsoft.Diagnostics.Repl/ConsoleService.cs @@ -508,8 +508,14 @@ namespace Microsoft.Diagnostics.Repl } catch (Exception ex) { - // Most exceptions should not excape the command dispatch, but just in case - WriteLine(OutputType.Error, "ERROR: {0}", ex.Message); + if (!string.IsNullOrEmpty(ex.Message)) + { + WriteLine(OutputType.Error, "ERROR: {0}", ex.Message); + } + if (ex is CommandParsingException parsingException) + { + WriteLine(OutputType.Normal, parsingException.DetailedHelp); + } Trace.TraceError(ex.ToString()); m_lastCommandLine = null; result = false; diff --git a/src/Microsoft.Diagnostics.Repl/ExitCommand.cs b/src/Microsoft.Diagnostics.Repl/ExitCommand.cs index a8d5bc6ba..f5fff7e18 100644 --- a/src/Microsoft.Diagnostics.Repl/ExitCommand.cs +++ b/src/Microsoft.Diagnostics.Repl/ExitCommand.cs @@ -6,7 +6,7 @@ using Microsoft.Diagnostics.DebugServices; namespace Microsoft.Diagnostics.Repl { - [Command(Name = "exit", Aliases = new string[] { "quit", "q" }, Help = "Exits interactive mode.", Flags = CommandFlags.Global | CommandFlags.Manual)] + [Command(Name = "exit", Aliases = new string[] { "quit", "q" }, Help = "Exits interactive mode.")] public class ExitCommand : CommandBase { private readonly Action _exit; diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs b/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs index 815ba0cfb..0f24d1927 100644 --- a/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs +++ b/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs @@ -45,6 +45,17 @@ namespace Microsoft.Diagnostics.TestHelpers _symbolService.AddCachePath(_symbolService.DefaultSymbolCache); } + public override void Dispose() + { + base.Dispose(); + _dataTarget?.Dispose(); + _dataTarget = null; + } + + public ServiceManager ServiceManager => _serviceManager; + + public ServiceContainer ServiceContainer => _serviceContainer; + protected override ITarget GetTarget() { _dataTarget = DataTarget.LoadDump(DumpFile); diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestHost.cs b/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestHost.cs index c86b121a6..93eea0eec 100644 --- a/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestHost.cs +++ b/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestHost.cs @@ -1,11 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using Microsoft.Diagnostics.DebugServices; namespace Microsoft.Diagnostics.TestHelpers { - public abstract class TestHost + public abstract class TestHost : IDisposable { private TestDataReader _testData; private ITarget _target; @@ -17,6 +18,12 @@ namespace Microsoft.Diagnostics.TestHelpers Config = config; } + public virtual void Dispose() + { + _target?.Destroy(); + _target = null; + } + public TestDataReader TestData { get diff --git a/src/SOS/SOS.Extensions/DebuggerServices.cs b/src/SOS/SOS.Extensions/DebuggerServices.cs index 97ea03108..b5702a7a8 100644 --- a/src/SOS/SOS.Extensions/DebuggerServices.cs +++ b/src/SOS/SOS.Extensions/DebuggerServices.cs @@ -11,11 +11,12 @@ using System.Text; using Microsoft.Diagnostics.DebugServices; using Microsoft.Diagnostics.Runtime; using Microsoft.Diagnostics.Runtime.Utilities; +using SOS.Hosting; using SOS.Hosting.DbgEng.Interop; namespace SOS.Extensions { - internal sealed unsafe class DebuggerServices : CallableCOMWrapper + internal sealed unsafe class DebuggerServices : CallableCOMWrapper, SOSHost.INativeClient { internal enum OperatingSystem { @@ -40,6 +41,7 @@ namespace SOS.Extensions : base(new RefCountedFreeLibrary(IntPtr.Zero), IID_IDebuggerServices, punk) { _hostType = hostType; + Client = punk; // This uses COM marshalling code, so we also check that the OSPlatform is Windows. if (hostType == HostType.DbgEng && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -52,6 +54,12 @@ namespace SOS.Extensions } } + #region INativeClient + + public IntPtr Client { get; } + + #endregion + public HResult GetOperatingSystem(out OperatingSystem operatingSystem) { return VTable.GetOperatingSystem(Self, out operatingSystem); diff --git a/src/SOS/SOS.Extensions/HostServices.cs b/src/SOS/SOS.Extensions/HostServices.cs index 900f6f43c..9ee85d194 100644 --- a/src/SOS/SOS.Extensions/HostServices.cs +++ b/src/SOS/SOS.Extensions/HostServices.cs @@ -4,9 +4,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Reflection; using System.Runtime.InteropServices; -using System.Text; using Microsoft.Diagnostics.DebugServices; using Microsoft.Diagnostics.DebugServices.Implementation; using Microsoft.Diagnostics.ExtensionCommands; @@ -20,7 +20,7 @@ namespace SOS.Extensions /// /// The extension services Wrapper the native hosts are given /// - public sealed unsafe class HostServices : COMCallableIUnknown, IHost + public sealed unsafe class HostServices : COMCallableIUnknown, IHost, SOSLibrary.ISOSModule { private static readonly Guid IID_IHostServices = new("27B2CB8D-BDEE-4CBD-B6EF-75880D76D46F"); @@ -42,6 +42,7 @@ namespace SOS.Extensions private readonly SymbolService _symbolService; private readonly HostWrapper _hostWrapper; private ServiceContainer _serviceContainer; + private ServiceContainer _servicesWithManagedOnlyFilter; private ContextServiceFromDebuggerServices _contextService; private int _targetIdFactory; private ITarget _target; @@ -101,14 +102,17 @@ namespace SOS.Extensions return HResult.E_FAIL; } Debug.Assert(Instance == null); - Instance = new HostServices(); + Instance = new HostServices(extensionPath, extensionLibrary); return initialializeCallback(Instance.IHostServices); } - private HostServices() + private HostServices(string extensionPath, IntPtr extensionsLibrary) { + SOSPath = Path.GetDirectoryName(extensionPath); + SOSHandle = extensionsLibrary; + _serviceManager = new ServiceManager(); - _commandService = new CommandService(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ">!ext" : null); + _commandService = new CommandService(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ">!sos" : null); _serviceManager.NotifyExtensionLoad.Register(_commandService.AddCommands); _symbolService = new SymbolService(this) @@ -128,7 +132,6 @@ namespace SOS.Extensions builder.AddMethod(new FlushTargetDelegate(FlushTarget)); builder.AddMethod(new DestroyTargetDelegate(DestroyTarget)); builder.AddMethod(new DispatchCommandDelegate(DispatchCommand)); - builder.AddMethod(new DisplayHelpDelegate(DisplayHelp)); builder.AddMethod(new UninitializeDelegate(Uninitialize)); IHostServices = builder.Complete(); @@ -193,16 +196,15 @@ namespace SOS.Extensions FileLoggingConsoleService fileLoggingConsoleService = new(consoleService); DiagnosticLoggingService.Instance.SetConsole(consoleService, fileLoggingConsoleService); - // Don't register everything in the SOSHost assembly; just the wrappers - _serviceManager.RegisterExportedServices(typeof(TargetWrapper)); - _serviceManager.RegisterExportedServices(typeof(RuntimeWrapper)); - // Register all the services and commands in the Microsoft.Diagnostics.DebugServices.Implementation assembly _serviceManager.RegisterAssembly(typeof(Target).Assembly); // Register all the services and commands in the SOS.Extensions (this) assembly _serviceManager.RegisterAssembly(typeof(HostServices).Assembly); + // Register all the services and commands in the SOS.Hosting assembly + _serviceManager.RegisterAssembly(typeof(SOSHost).Assembly); + // Register all the services and commands in the Microsoft.Diagnostics.ExtensionCommands assembly _serviceManager.RegisterAssembly(typeof(ClrMDHelper).Assembly); @@ -220,6 +222,8 @@ namespace SOS.Extensions _serviceContainer = _serviceManager.CreateServiceContainer(ServiceScope.Global, parent: null); _serviceContainer.AddService(_serviceManager); _serviceContainer.AddService(this); + _serviceContainer.AddService(this); + _serviceContainer.AddService(DebuggerServices); _serviceContainer.AddService(_commandService); _serviceContainer.AddService(_symbolService); _serviceContainer.AddService(fileLoggingConsoleService); @@ -232,6 +236,10 @@ namespace SOS.Extensions ThreadUnwindServiceFromDebuggerServices threadUnwindService = new(DebuggerServices); _serviceContainer.AddService(threadUnwindService); + // Used to invoke only managed commands + _servicesWithManagedOnlyFilter = new(_contextService.Services); + _servicesWithManagedOnlyFilter.AddService(new SOSCommandBase.ManagedOnlyCommandFilter()); + // Add each extension command to the native debugger foreach ((string name, string help, IEnumerable aliases) in _commandService.Commands) { @@ -241,12 +249,6 @@ namespace SOS.Extensions Trace.TraceWarning($"Cannot add extension command {hr:X8} {name} - {help}"); } } - - if (DebuggerServices.DebugClient is IDebugControl5 control) - { - MemoryRegionServiceFromDebuggerServices memRegions = new(DebuggerServices.DebugClient, control); - _serviceContainer.AddService(memRegions); - } } catch (Exception ex) { @@ -349,56 +351,27 @@ namespace SOS.Extensions { return HResult.E_INVALIDARG; } - if (!_commandService.IsCommand(commandName)) - { - return HResult.E_NOTIMPL; - } try { - StringBuilder sb = new(); - sb.Append(commandName); - if (!string.IsNullOrWhiteSpace(commandArguments)) - { - sb.Append(' '); - sb.Append(commandArguments); - } - if (_commandService.Execute(sb.ToString(), _contextService.Services)) + if (_commandService.Execute(commandName, commandArguments, commandName == "help" ? _contextService.Services : _servicesWithManagedOnlyFilter)) { return HResult.S_OK; } - } - catch (CommandNotSupportedException) - { - return HResult.E_NOTIMPL; - } - catch (Exception ex) - { - Trace.TraceError(ex.ToString()); - } - return HResult.E_FAIL; - } - - private int DisplayHelp( - IntPtr self, - string commandName) - { - try - { - if (!_commandService.DisplayHelp(commandName, _contextService.Services)) + else { - return HResult.E_INVALIDARG; + // The command was not found or supported + return HResult.E_NOTIMPL; } } - catch (CommandNotSupportedException) - { - return HResult.E_NOTIMPL; - } catch (Exception ex) { Trace.TraceError(ex.ToString()); - return HResult.E_FAIL; + IConsoleService consoleService = Services.GetService(); + // TODO: when we can figure out how to deal with error messages in the scripts that are displayed on STDERROR under lldb + //consoleService.WriteLineError(ex.Message); + consoleService.WriteLine(ex.Message); } - return HResult.S_OK; + return HResult.E_FAIL; } private void Uninitialize( @@ -436,6 +409,14 @@ namespace SOS.Extensions #endregion + #region SOSLibrary.ISOSModule + + public string SOSPath { get; } + + public IntPtr SOSHandle { get; } + + #endregion + #region IHostServices delegates [UnmanagedFunctionPointer(CallingConvention.Winapi)] @@ -471,11 +452,6 @@ namespace SOS.Extensions [In, MarshalAs(UnmanagedType.LPStr)] string commandName, [In, MarshalAs(UnmanagedType.LPStr)] string commandArguments); - [UnmanagedFunctionPointer(CallingConvention.Winapi)] - private delegate int DisplayHelpDelegate( - [In] IntPtr self, - [In, MarshalAs(UnmanagedType.LPStr)] string commandName); - [UnmanagedFunctionPointer(CallingConvention.Winapi)] private delegate void UninitializeDelegate( [In] IntPtr self); diff --git a/src/SOS/SOS.Extensions/MemoryRegionServiceFromDebuggerServices.cs b/src/SOS/SOS.Extensions/MemoryRegionServiceFromDebuggerServices.cs index 15c729c1d..403bcbc87 100644 --- a/src/SOS/SOS.Extensions/MemoryRegionServiceFromDebuggerServices.cs +++ b/src/SOS/SOS.Extensions/MemoryRegionServiceFromDebuggerServices.cs @@ -15,10 +15,10 @@ namespace SOS.Extensions private readonly IDebugClient5 _client; private readonly IDebugControl5 _control; - public MemoryRegionServiceFromDebuggerServices(IDebugClient5 client, IDebugControl5 control) + public MemoryRegionServiceFromDebuggerServices(IDebugClient5 client) { _client = client; - _control = control; + _control = (IDebugControl5)client; } public IEnumerable EnumerateRegions() diff --git a/src/SOS/SOS.Extensions/TargetFromFromDebuggerServices.cs b/src/SOS/SOS.Extensions/TargetFromFromDebuggerServices.cs index 60aea8588..e0c2bee85 100644 --- a/src/SOS/SOS.Extensions/TargetFromFromDebuggerServices.cs +++ b/src/SOS/SOS.Extensions/TargetFromFromDebuggerServices.cs @@ -99,6 +99,11 @@ namespace SOS.Extensions _serviceContainerFactory.AddServiceFactory((services) => CreateCrashInfoService(services, debuggerServices)); OnFlushEvent.Register(() => FlushService()); + if (debuggerServices.DebugClient is not null) + { + _serviceContainerFactory.AddServiceFactory((services) => new MemoryRegionServiceFromDebuggerServices(debuggerServices.DebugClient)); + } + Finished(); } diff --git a/src/SOS/SOS.Hosting/Commands/SOSCommand.cs b/src/SOS/SOS.Hosting/Commands/SOSCommand.cs index 1190fd29a..dfa4a3253 100644 --- a/src/SOS/SOS.Hosting/Commands/SOSCommand.cs +++ b/src/SOS/SOS.Hosting/Commands/SOSCommand.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Diagnostics; -using System.IO; using System.Linq; +using System.Runtime.InteropServices; using Microsoft.Diagnostics.DebugServices; namespace SOS.Hosting @@ -43,17 +42,45 @@ namespace SOS.Hosting [Command(Name = "ip2md", DefaultOptions = "IP2MD", Help = "Displays the MethodDesc structure at the specified address in code that has been JIT-compiled.")] [Command(Name = "name2ee", DefaultOptions = "Name2EE", Help = "Displays the MethodTable structure and EEClass structure for the specified type or method in the specified module.")] [Command(Name = "printexception", DefaultOptions = "PrintException", Aliases = new string[] { "pe" }, Help = "Displays and formats fields of any object derived from the Exception class at the specified address.")] - [Command(Name = "soshelp", DefaultOptions = "Help", Help = "Displays help for a specific SOS command.")] [Command(Name = "syncblk", DefaultOptions = "SyncBlk", Help = "Displays the SyncBlock holder info.")] [Command(Name = "threadstate", DefaultOptions = "ThreadState", Help = "Pretty prints the meaning of a threads state.")] - [Command(Name = "comstate", DefaultOptions = "COMState", Flags = CommandFlags.Windows, Help = "Lists the COM apartment model for each thread.")] - [Command(Name = "dumprcw", DefaultOptions = "DumpRCW", Flags = CommandFlags.Windows, Help = "Displays information about a Runtime Callable Wrapper.")] - [Command(Name = "dumpccw", DefaultOptions = "DumpCCW", Flags = CommandFlags.Windows, Help = "Displays information about a COM Callable Wrapper.")] - [Command(Name = "dumppermissionset", DefaultOptions = "DumpPermissionSet", Flags = CommandFlags.Windows, Help = "Displays a PermissionSet object (debug build only).")] - [Command(Name = "gchandleleaks", DefaultOptions = "GCHandleLeaks", Flags = CommandFlags.Windows, Help = "Helps in tracking down GCHandle leaks")] - [Command(Name = "watsonbuckets", DefaultOptions = "WatsonBuckets", Flags = CommandFlags.Windows, Help = "Displays the Watson buckets.")] - public class SOSCommand : CommandBase + public class SOSCommand : SOSCommandBase { + [FilterInvoke] + public static bool FilterInvoke( + [ServiceImport(Optional = true)] ManagedOnlyCommandFilter managedOnly, + [ServiceImport(Optional = true)] IRuntime runtime) => + SOSCommandBase.Filter(managedOnly, runtime); + } + + [Command(Name = "comstate", DefaultOptions = "COMState", Help = "Lists the COM apartment model for each thread.")] + [Command(Name = "dumprcw", DefaultOptions = "DumpRCW", Help = "Displays information about a Runtime Callable Wrapper.")] + [Command(Name = "dumpccw", DefaultOptions = "DumpCCW", Help = "Displays information about a COM Callable Wrapper.")] + [Command(Name = "dumppermissionset", DefaultOptions = "DumpPermissionSet", Help = "Displays a PermissionSet object (debug build only).")] + [Command(Name = "gchandleleaks", DefaultOptions = "GCHandleLeaks", Help = "Helps in tracking down GCHandle leaks.")] + [Command(Name = "watsonbuckets", DefaultOptions = "WatsonBuckets", Help = "Displays the Watson buckets.")] + public class WindowsSOSCommand : SOSCommandBase + { + /// + /// These commands are Windows only. + /// + [FilterInvoke] + public static bool FilterInvoke( + [ServiceImport(Optional = true)] ITarget target, + [ServiceImport(Optional = true)] ManagedOnlyCommandFilter managedOnly, + [ServiceImport(Optional = true)] IRuntime runtime) => + target != null && target.OperatingSystem == OSPlatform.Windows && SOSCommandBase.Filter(managedOnly, runtime); + } + + public class SOSCommandBase : CommandBase + { + /// + /// Empty service used to prevent native commands from being run + /// + public class ManagedOnlyCommandFilter + { + } + [Argument(Name = "arguments", Help = "Arguments to SOS command.")] public string[] Arguments { get; set; } @@ -62,22 +89,30 @@ namespace SOS.Hosting public override void Invoke() { - try - { - Debug.Assert(Arguments != null && Arguments.Length > 0); - string arguments = string.Concat(Arguments.Skip(1).Select((arg) => arg + " ")).Trim(); - SOSHost.ExecuteCommand(Arguments[0], arguments); - } - catch (Exception ex) when (ex is FileNotFoundException or EntryPointNotFoundException or InvalidOperationException) - { - WriteLineError(ex.Message); - } + Debug.Assert(Arguments != null && Arguments.Length > 0); + string arguments = string.Concat(Arguments.Skip(1).Select((arg) => arg + " ")).Trim(); + SOSHost.ExecuteCommand(Arguments[0], arguments); } [HelpInvoke] - public void InvokeHelp() + public string GetDetailedHelp() { - SOSHost.ExecuteCommand("Help", Arguments[0]); + return SOSHost.GetHelpText(Arguments[0]); } + + /// + /// Common native SOS command filter function. + /// + /// not null means to filter out the native C++ SOS commands + /// runtime instance or null + /// + public static bool Filter(ManagedOnlyCommandFilter managedOnly, IRuntime runtime) => + // This filters out these native C++ commands if requested by host (in this case SOS.Extensions) to prevent recursion. + managedOnly == null && + // This commands require a .NET Core, Desktop Framework or .NET Core single file runtime (not a Native AOT runtime) + runtime != null && + (runtime.RuntimeType == RuntimeType.NetCore || + runtime.RuntimeType == RuntimeType.Desktop || + runtime.RuntimeType == RuntimeType.SingleFile); } } diff --git a/src/SOS/SOS.Hosting/SOSHost.cs b/src/SOS/SOS.Hosting/SOSHost.cs index 03d011d0d..0fe569a0f 100644 --- a/src/SOS/SOS.Hosting/SOSHost.cs +++ b/src/SOS/SOS.Hosting/SOSHost.cs @@ -21,6 +21,17 @@ namespace SOS.Hosting [ServiceExport(Scope = ServiceScope.Target)] public sealed class SOSHost : IDisposable { + /// + /// Provides the native debugger's debug client instance + /// + public interface INativeClient + { + /// + /// Native debugger client interface + /// + IntPtr Client { get; } + } + // This is what dbgeng/IDebuggerServices returns for non-PE modules that don't have a timestamp internal const uint InvalidTimeStamp = 0xFFFFFFFE; internal const uint InvalidChecksum = 0xFFFFFFFF; @@ -45,72 +56,62 @@ namespace SOS.Hosting private readonly SOSLibrary _sosLibrary; #pragma warning restore - private readonly IntPtr _interface; + private readonly IntPtr _client; private readonly ulong _ignoreAddressBitsMask; - private bool _disposed; + private readonly bool _releaseClient; /// /// Create an instance of the hosting class. Has the lifetime of the target. /// - public SOSHost(ITarget target, IMemoryService memoryService) + public SOSHost(ITarget target, IMemoryService memoryService, [ServiceImport(Optional = true)] INativeClient client) { - Target = target ?? throw new DiagnosticsException("No target"); + Target = target; MemoryService = memoryService; _ignoreAddressBitsMask = memoryService.SignExtensionMask(); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + // If running under a native debugger, use the client instance supplied by the debugger for commands + if (client != null) { - DebugClient debugClient = new(this); - _interface = debugClient.IDebugClient; + _client = client.Client; } else { - LLDBServices lldbServices = new(this); - _interface = lldbServices.ILLDBServices; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + DebugClient debugClient = new(this); + _client = debugClient.IDebugClient; + } + else + { + LLDBServices lldbServices = new(this); + _client = lldbServices.ILLDBServices; + } + _releaseClient = true; } } void IDisposable.Dispose() { - Trace.TraceInformation($"SOSHost.Dispose {_disposed}"); - if (!_disposed) + Trace.TraceInformation($"SOSHost.Dispose"); + if (_releaseClient) { - _disposed = true; - ComWrapper.ReleaseWithCheck(_interface); + ComWrapper.ReleaseWithCheck(_client); } } /// /// Execute a SOS command. /// - /// command name and arguments - public void ExecuteCommand(string commandLine) - { - string command = "Help"; - string arguments = null; - - if (commandLine != null) - { - int firstSpace = commandLine.IndexOf(' '); - command = firstSpace == -1 ? commandLine : commandLine.Substring(0, firstSpace); - arguments = firstSpace == -1 ? null : commandLine.Substring(firstSpace); - } - ExecuteCommand(command, arguments); - } + /// just the command name + /// the command arguments and options + public void ExecuteCommand(string command, string arguments) => _sosLibrary.ExecuteCommand(_client, command, arguments); /// - /// Execute a SOS command. + /// Get the detailed help text for a native SOS command. /// - /// just the command name - /// the command arguments and options - public void ExecuteCommand(string command, string arguments) - { - if (_disposed) - { - throw new ObjectDisposedException("SOSHost instance disposed"); - } - _sosLibrary.ExecuteCommand(_interface, command, arguments); - } + /// command name + /// help text or null if not found or error + public string GetHelpText(string command) => _sosLibrary.GetHelpText(command); #region Reverse PInvoke Implementations diff --git a/src/SOS/SOS.Hosting/SOSLibrary.cs b/src/SOS/SOS.Hosting/SOSLibrary.cs index d9e4f03da..42e1f3e42 100644 --- a/src/SOS/SOS.Hosting/SOSLibrary.cs +++ b/src/SOS/SOS.Hosting/SOSLibrary.cs @@ -16,6 +16,22 @@ namespace SOS.Hosting /// public sealed class SOSLibrary : IDisposable { + /// + /// Provides the SOS module handle + /// + public interface ISOSModule + { + /// + /// The SOS module path + /// + string SOSPath { get; } + + /// + /// The SOS module handle + /// + IntPtr SOSHandle { get; } + } + [UnmanagedFunctionPointer(CallingConvention.Winapi)] private delegate int SOSCommandDelegate( IntPtr ILLDBServices, @@ -29,10 +45,20 @@ namespace SOS.Hosting [UnmanagedFunctionPointer(CallingConvention.Winapi)] private delegate void SOSUninitializeDelegate(); + [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true, EntryPoint = "FindResourceA")] + public static extern IntPtr FindResource(IntPtr hModule, string name, string type); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr LoadResource(IntPtr hModule, IntPtr hResource); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr LockResource(IntPtr hResource); + private const string SOSInitialize = "SOSInitializeByHost"; private const string SOSUninitialize = "SOSUninitializeByHost"; private readonly HostWrapper _hostWrapper; + private readonly bool _uninitializeLibrary; private IntPtr _sosLibrary = IntPtr.Zero; /// @@ -41,12 +67,12 @@ namespace SOS.Hosting public string SOSPath { get; set; } [ServiceExport(Scope = ServiceScope.Global)] - public static SOSLibrary Create(IHost host) + public static SOSLibrary TryCreate(IHost host, [ServiceImport(Optional = true)] ISOSModule sosModule) { SOSLibrary sosLibrary = null; try { - sosLibrary = new SOSLibrary(host); + sosLibrary = new SOSLibrary(host, sosModule); sosLibrary.Initialize(); } catch @@ -61,10 +87,20 @@ namespace SOS.Hosting /// Create an instance of the hosting class /// /// target instance - private SOSLibrary(IHost host) + /// sos library info or null + private SOSLibrary(IHost host, ISOSModule sosModule) { - string rid = InstallHelper.GetRid(); - SOSPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), rid); + if (sosModule is not null) + { + SOSPath = sosModule.SOSPath; + _sosLibrary = sosModule.SOSHandle; + } + else + { + string rid = InstallHelper.GetRid(); + SOSPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), rid); + _uninitializeLibrary = true; + } _hostWrapper = new HostWrapper(host); } @@ -131,15 +167,15 @@ namespace SOS.Hosting /// private void Uninitialize() { - Trace.TraceInformation("SOSHost: Uninitialize"); - if (_sosLibrary != IntPtr.Zero) + Trace.TraceInformation("SOSLibrary: Uninitialize"); + if (_uninitializeLibrary && _sosLibrary != IntPtr.Zero) { SOSUninitializeDelegate uninitializeFunc = SOSHost.GetDelegateFunction(_sosLibrary, SOSUninitialize); uninitializeFunc?.Invoke(); Microsoft.Diagnostics.Runtime.DataTarget.PlatformFunctions.FreeLibrary(_sosLibrary); - _sosLibrary = IntPtr.Zero; } + _sosLibrary = IntPtr.Zero; _hostWrapper.ReleaseWithCheck(); } @@ -156,17 +192,85 @@ namespace SOS.Hosting SOSCommandDelegate commandFunc = SOSHost.GetDelegateFunction(_sosLibrary, command); if (commandFunc == null) { - throw new DiagnosticsException($"SOS command not found: {command}"); + throw new CommandNotFoundException($"{CommandNotFoundException.NotFoundMessage} '{command}'"); } int result = commandFunc(client, arguments ?? ""); if (result == HResult.E_NOTIMPL) { - throw new CommandNotSupportedException($"SOS command not found: {command}"); + throw new CommandNotFoundException($"{CommandNotFoundException.NotFoundMessage} '{command}'"); } if (result != HResult.S_OK) { Trace.TraceError($"SOS command FAILED 0x{result:X8}"); + throw new DiagnosticsException(string.Empty); + } + } + + public string GetHelpText(string command) + { + string helpText; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + IntPtr hResInfo = FindResource(_sosLibrary, "DOCUMENTATION", "TEXT"); + if (hResInfo == IntPtr.Zero) + { + throw new DiagnosticsException("Can not SOS help text"); + } + IntPtr hResource = LoadResource(_sosLibrary, hResInfo); + if (hResource == IntPtr.Zero) + { + throw new DiagnosticsException("Can not SOS help text"); + } + IntPtr helpTextPtr = LockResource(hResource); + if (helpTextPtr == IntPtr.Zero) + { + throw new DiagnosticsException("Can not SOS help text"); + } + helpText = Marshal.PtrToStringAnsi(helpTextPtr); + } + else + { + string helpTextFile = Path.Combine(SOSPath, "sosdocsunix.txt"); + helpText = File.ReadAllText(helpTextFile); + } + command = command.ToLowerInvariant(); + string searchString = $"COMMAND: {command}."; + + // Search for command in help text file + int start = helpText.IndexOf(searchString); + if (start == -1) + { + throw new DiagnosticsException($"Documentation for {command} not found"); + } + + // Go to end of line + start = helpText.IndexOf('\n', start); + if (start == -1) + { + throw new DiagnosticsException($"No newline in documentation resource or file"); + } + + // Find the first occurrence of \\ followed by an \r or an \n on a line by itself. + int end = start++; + while (true) + { + end = helpText.IndexOf("\\\\", end + 1); + if (end == -1) + { + break; + } + char c = helpText[end - 1]; + if (c is '\r' or '\n') + { + break; + } + c = helpText[end + 3]; + if (c is '\r' or '\n') + { + break; + } } + return end == -1 ? helpText.Substring(start) : helpText.Substring(start, end - start); } } } diff --git a/src/SOS/SOS.UnitTests/ConfigFiles/Unix/Debugger.Tests.Config.txt b/src/SOS/SOS.UnitTests/ConfigFiles/Unix/Debugger.Tests.Config.txt index 5b869886b..7a435c20b 100644 --- a/src/SOS/SOS.UnitTests/ConfigFiles/Unix/Debugger.Tests.Config.txt +++ b/src/SOS/SOS.UnitTests/ConfigFiles/Unix/Debugger.Tests.Config.txt @@ -91,6 +91,16 @@ net6.0 $(RuntimeVersion60) + + diff --git a/src/SOS/SOS.UnitTests/ConfigFiles/Windows/Debugger.Tests.Config.txt b/src/SOS/SOS.UnitTests/ConfigFiles/Windows/Debugger.Tests.Config.txt index 3618f365e..14898afad 100644 --- a/src/SOS/SOS.UnitTests/ConfigFiles/Windows/Debugger.Tests.Config.txt +++ b/src/SOS/SOS.UnitTests/ConfigFiles/Windows/Debugger.Tests.Config.txt @@ -107,6 +107,23 @@ net6.0 $(RuntimeVersion60) + + diff --git a/src/SOS/SOS.UnitTests/SOS.UnitTests.csproj b/src/SOS/SOS.UnitTests/SOS.UnitTests.csproj index 3ce4a1bdb..db0898077 100644 --- a/src/SOS/SOS.UnitTests/SOS.UnitTests.csproj +++ b/src/SOS/SOS.UnitTests/SOS.UnitTests.csproj @@ -31,6 +31,7 @@ + diff --git a/src/SOS/SOS.UnitTests/SOS.cs b/src/SOS/SOS.UnitTests/SOS.cs index 8849bd4a2..d7e8f6eb5 100644 --- a/src/SOS/SOS.UnitTests/SOS.cs +++ b/src/SOS/SOS.UnitTests/SOS.cs @@ -225,7 +225,6 @@ public class SOS { throw new SkipTestException("This test validates POH behavior, which was introduced in .net 5"); } - await SOSTestHelpers.RunTest( config, debuggeeName: "GCPOH", @@ -311,6 +310,17 @@ public class SOS testName: "SOS.StackTests"); } + [SkippableTheory, MemberData(nameof(SOSTestHelpers.GetConfigurations), "TestName", "SOS.TestExtensions", MemberType = typeof(SOSTestHelpers))] + public async Task TestExtensions(TestConfiguration config) + { + await SOSTestHelpers.RunTest( + config, + debuggeeName: "LineNums", + scriptName: "TestExtensions.script", + Output, + testName: "SOS.TestExtensions"); + } + [SkippableTheory, MemberData(nameof(Configurations))] public async Task OtherCommands(TestConfiguration config) { diff --git a/src/SOS/SOS.UnitTests/SOSRunner.cs b/src/SOS/SOS.UnitTests/SOSRunner.cs index 3d345997a..bee617ec9 100644 --- a/src/SOS/SOS.UnitTests/SOSRunner.cs +++ b/src/SOS/SOS.UnitTests/SOSRunner.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.IO; using System.IO.Pipes; using System.Linq; +using System.Reflection.Metadata.Ecma335; using System.Text; using System.Text.RegularExpressions; using System.Threading; @@ -696,6 +697,13 @@ public class SOSRunner : IDisposable // Issue: https://github.com/dotnet/diagnostics/issues/3126 processRunner.WithRuntimeConfiguration("EnableWriteXorExecute", "0"); + // Setup the extension environment variable + string extensions = config.DotNetDiagnosticExtensions(); + if (!string.IsNullOrEmpty(extensions)) + { + processRunner.WithEnvironmentVariable("DOTNET_DIAGNOSTIC_EXTENSIONS", extensions); + } + DumpType? dumpType = null; if (action is DebuggerAction.LoadDump or DebuggerAction.LoadDumpWithDotNetDump) { @@ -793,6 +801,7 @@ public class SOSRunner : IDisposable { await ContinueExecution(); } + // Adds the "!" prefix under dbgeng, nothing under lldb. Meant for SOS (native) commands. else if (line.StartsWith("SOSCOMMAND:")) { string input = line.Substring("SOSCOMMAND:".Length).TrimStart(); @@ -801,6 +810,19 @@ public class SOSRunner : IDisposable throw new Exception($"SOS command FAILED: {input}"); } } + else if (line.StartsWith("SOSCOMMAND_FAIL:")) + { + string input = line.Substring("SOSCOMMAND_FAIL:".Length).TrimStart(); + if (await RunSosCommand(input)) + { + // The cdb runcommand extension doesn't get the execute command failures (limitation in dbgeng). + if (Debugger != NativeDebugger.Cdb) + { + throw new Exception($"SOS command did not fail: {input}"); + } + } + } + // Adds the "!sos" prefix under dbgeng, "sos " under lldb. Meant for extensions (managed) commands else if (line.StartsWith("EXTCOMMAND:")) { string input = line.Substring("EXTCOMMAND:".Length).TrimStart(); @@ -809,6 +831,19 @@ public class SOSRunner : IDisposable throw new Exception($"Extension command FAILED: {input}"); } } + else if (line.StartsWith("EXTCOMMAND_FAIL:")) + { + string input = line.Substring("EXTCOMMAND_FAIL:".Length).TrimStart(); + if (await RunSosCommand(input, extensionCommand: true)) + { + // The cdb runcommand extension doesn't get the execute command failures (limitation in dbgeng). + if (Debugger != NativeDebugger.Cdb) + { + throw new Exception($"Extension command did not fail: {input}"); + } + } + } + // Never adds any prefix. Meant for native debugger commands. else if (line.StartsWith("COMMAND:")) { string input = line.Substring("COMMAND:".Length).TrimStart(); @@ -817,6 +852,18 @@ public class SOSRunner : IDisposable throw new Exception($"Debugger command FAILED: {input}"); } } + else if (line.StartsWith("COMMAND_FAIL:")) + { + string input = line.Substring("COMMAND_FAIL:".Length).TrimStart(); + if (await RunCommand(input)) + { + // The cdb runcommand extension doesn't get the execute command failures (limitation in dbgeng). + if (Debugger != NativeDebugger.Cdb) + { + throw new Exception($"Debugger command did not fail: {input}"); + } + } + } else if (line.StartsWith("VERIFY:")) { string verifyLine = line.Substring("VERIFY:".Length); @@ -1060,8 +1107,6 @@ public class SOSRunner : IDisposable } break; case NativeDebugger.Lldb: - command = "sos " + command; - break; case NativeDebugger.DotNetDump: if (extensionCommand) { @@ -1681,6 +1726,11 @@ public static class TestConfigurationExtensions return TestConfiguration.MakeCanonicalPath(dotnetDumpPath); } + public static string DotNetDiagnosticExtensions(this TestConfiguration config) + { + return TestConfiguration.MakeCanonicalPath(config.GetValue("DotNetDiagnosticExtensions")); + } + public static string SOSPath(this TestConfiguration config) { return TestConfiguration.MakeCanonicalPath(config.GetValue("SOSPath")); diff --git a/src/SOS/SOS.UnitTests/Scripts/ConcurrentDictionaries.script b/src/SOS/SOS.UnitTests/Scripts/ConcurrentDictionaries.script index 9d402ecbf..3a156cb1e 100644 --- a/src/SOS/SOS.UnitTests/Scripts/ConcurrentDictionaries.script +++ b/src/SOS/SOS.UnitTests/Scripts/ConcurrentDictionaries.script @@ -9,13 +9,13 @@ IFDEF:NETCORE_OR_DOTNETDUMP # Load SOS even though it doesn't actually load the sos module on dotnet-dump but it runs some initial settings/commands. LOADSOS -EXTCOMMAND: dcd +EXTCOMMAND_FAIL: dcd VERIFY: Missing ConcurrentDictionary address -EXTCOMMAND: dcd abcdefgh +EXTCOMMAND_FAIL: dcd abcdefgh VERIFY: Hexadecimal address expected -EXTCOMMAND: dcd 0000000000000001 +EXTCOMMAND_FAIL: dcd 0000000000000001 VERIFY: is not referencing an object # Checks on ConcurrentDictionary diff --git a/src/SOS/SOS.UnitTests/Scripts/DivZero.script b/src/SOS/SOS.UnitTests/Scripts/DivZero.script index 7d2095a5e..456b10790 100644 --- a/src/SOS/SOS.UnitTests/Scripts/DivZero.script +++ b/src/SOS/SOS.UnitTests/Scripts/DivZero.script @@ -40,12 +40,7 @@ VERIFY:\s+\s+\s+[Dd]iv[Zz]ero.*!C\.Main(\(.*\))?\+0x\s+ VERIFY:\[.*[\\|/]Debuggees[\\|/].*DivZero[\\|/]DivZero\.cs @ (57|56)\s*\]\s* # Verify that Threads (clrthreads) works -IFDEF:DOTNETDUMP SOSCOMMAND:clrthreads -ENDIF:DOTNETDUMP -!IFDEF:DOTNETDUMP -SOSCOMMAND:Threads -ENDIF:DOTNETDUMP VERIFY:\s*ThreadCount:\s+\s+ VERIFY:\s+UnstartedThread:\s+\s+ VERIFY:\s+BackgroundThread:\s+\s+ diff --git a/src/SOS/SOS.UnitTests/Scripts/DumpGen.script b/src/SOS/SOS.UnitTests/Scripts/DumpGen.script index 55bac9181..525bfb897 100644 --- a/src/SOS/SOS.UnitTests/Scripts/DumpGen.script +++ b/src/SOS/SOS.UnitTests/Scripts/DumpGen.script @@ -9,17 +9,17 @@ IFDEF:NETCORE_OR_DOTNETDUMP # Load SOS even though it doesn't actually load the sos module on dotnet-dump but it runs some initial settings/commands. LOADSOS -EXTCOMMAND: dumpgen +EXTCOMMAND_FAIL: dumpgen VERIFY: Generation argument is missing -EXTCOMMAND: dumpgen invalid +EXTCOMMAND_FAIL: dumpgen invalid VERIFY: invalid is not a supported generation !IFDEF:LLDB -EXTCOMMAND: dumpgen gen0 -mt +EXTCOMMAND_FAIL: dumpgen gen0 -mt VERIFY: Required argument missing for option: -mt -EXTCOMMAND: dumpgen gen1 -mt zzzzz +EXTCOMMAND_FAIL: dumpgen gen1 -mt zzzzz VERIFY: Hexadecimal address expected for -mt option ENDIF:LLDB diff --git a/src/SOS/SOS.UnitTests/Scripts/GCTests.script b/src/SOS/SOS.UnitTests/Scripts/GCTests.script index 671e4e2e1..a1b172dff 100644 --- a/src/SOS/SOS.UnitTests/Scripts/GCTests.script +++ b/src/SOS/SOS.UnitTests/Scripts/GCTests.script @@ -89,7 +89,7 @@ VERIFY:\s+\s+\s+\s+GCWhere\s+ IFDEF:WINDOWS SOSCOMMAND:DumpHeap -stat -gen xxx -VERIFY:\s*System\.ArgumentException: Unknown generation: xxx\. Only gen0, gen1, gen2, loh \(large\), poh \(pinned\) and foh \(frozen\) are supported\s+ +VERIFY:\s*Unknown generation: xxx\. Only gen0, gen1, gen2, loh \(large\), poh \(pinned\) and foh \(frozen\) are supported\s+ ENDIF:WINDOWS IFDEF:WINDOWS diff --git a/src/SOS/SOS.UnitTests/Scripts/LineNums.script b/src/SOS/SOS.UnitTests/Scripts/LineNums.script index 6f6c11a30..39ec83258 100644 --- a/src/SOS/SOS.UnitTests/Scripts/LineNums.script +++ b/src/SOS/SOS.UnitTests/Scripts/LineNums.script @@ -31,12 +31,7 @@ VERIFY:\s+\s+\s+LineNums.*!LineNums\.Program\.Main.*\+0x VERIFY:\[.*[\\|/]Debuggees[\\|/].*LineNums[\\|/]Program\.cs @ 13\s*\]\s* # Verify that Threads (clrthreads) works -IFDEF:DOTNETDUMP SOSCOMMAND:clrthreads -ENDIF:DOTNETDUMP -!IFDEF:DOTNETDUMP -SOSCOMMAND:Threads -ENDIF:DOTNETDUMP VERIFY:\s*ThreadCount:\s+\s+ VERIFY:\s+UnstartedThread:\s+\s+ VERIFY:\s+BackgroundThread:\s+\s+ diff --git a/src/SOS/SOS.UnitTests/Scripts/NestedExceptionTest.script b/src/SOS/SOS.UnitTests/Scripts/NestedExceptionTest.script index 8c67f036b..d44f0d7ca 100644 --- a/src/SOS/SOS.UnitTests/Scripts/NestedExceptionTest.script +++ b/src/SOS/SOS.UnitTests/Scripts/NestedExceptionTest.script @@ -99,12 +99,7 @@ VERIFY:HResult:\s+80131509\s+ VERIFY:There are nested exceptions on this thread. Run with -nested for details # Verify that Threads (clrthreads) works -IFDEF:DOTNETDUMP SOSCOMMAND:clrthreads -ENDIF:DOTNETDUMP -!IFDEF:DOTNETDUMP -SOSCOMMAND:Threads -ENDIF:DOTNETDUMP VERIFY:\s*ThreadCount:\s+\s+ VERIFY:\s+UnstartedThread:\s+\s+ VERIFY:\s+BackgroundThread:\s+\s+ diff --git a/src/SOS/SOS.UnitTests/Scripts/Overflow.script b/src/SOS/SOS.UnitTests/Scripts/Overflow.script index 3deaf16e4..ccc8c8d23 100644 --- a/src/SOS/SOS.UnitTests/Scripts/Overflow.script +++ b/src/SOS/SOS.UnitTests/Scripts/Overflow.script @@ -9,12 +9,7 @@ CONTINUE LOADSOS -IFDEF:DOTNETDUMP SOSCOMMAND:clrthreads -managedexception -ENDIF:DOTNETDUMP -!IFDEF:DOTNETDUMP -SOSCOMMAND:Threads -managedexception -ENDIF:DOTNETDUMP # 5) Verifying that PrintException gives us the right exception in the format above. SOSCOMMAND:PrintException diff --git a/src/SOS/SOS.UnitTests/Scripts/Reflection.script b/src/SOS/SOS.UnitTests/Scripts/Reflection.script index fe8db12b9..b3f34187c 100644 --- a/src/SOS/SOS.UnitTests/Scripts/Reflection.script +++ b/src/SOS/SOS.UnitTests/Scripts/Reflection.script @@ -43,12 +43,7 @@ VERIFY:(StackTraceString: \s+)? VERIFY:HResult:\s+80131604 # Verify that Threads (clrthreads) works -IFDEF:DOTNETDUMP SOSCOMMAND:clrthreads -ENDIF:DOTNETDUMP -!IFDEF:DOTNETDUMP -SOSCOMMAND:Threads -ENDIF:DOTNETDUMP VERIFY:\s*ThreadCount:\s+\s+ VERIFY:\s+UnstartedThread:\s+\s+ VERIFY:\s+BackgroundThread:\s+\s+ diff --git a/src/SOS/SOS.UnitTests/Scripts/SimpleThrow.script b/src/SOS/SOS.UnitTests/Scripts/SimpleThrow.script index 7740c5880..8ae602b04 100644 --- a/src/SOS/SOS.UnitTests/Scripts/SimpleThrow.script +++ b/src/SOS/SOS.UnitTests/Scripts/SimpleThrow.script @@ -46,12 +46,7 @@ VERIFY:\s+\s+\s+[Ss]imple[Tt]hrow.*!(\$0_)?Simple\.Main.*\+0x\s+ VERIFY:\s+UnstartedThread:\s+\s+ VERIFY:\s+BackgroundThread:\s+\s+ diff --git a/src/SOS/SOS.UnitTests/Scripts/StackAndOtherTests.script b/src/SOS/SOS.UnitTests/Scripts/StackAndOtherTests.script index 5c31e0ef1..ec9fed65b 100644 --- a/src/SOS/SOS.UnitTests/Scripts/StackAndOtherTests.script +++ b/src/SOS/SOS.UnitTests/Scripts/StackAndOtherTests.script @@ -250,20 +250,20 @@ ENDIF:DESKTOP # Verify that "u" works (depends on the IP2MD here) SOSCOMMAND:ClrStack SOSCOMMAND:IP2MD .*\s+()\s+SymbolTestApp\.Program\.Foo4.*\s+ -SOSCOMMAND:u \s*MethodDesc:\s+()\s* +SOSCOMMAND:clru \s*MethodDesc:\s+()\s* VERIFY:\s*Normal JIT generated code\s+ VERIFY:\s+SymbolTestApp\.Program\.Foo4\(System\.String\)\s+ VERIFY:\s+Begin\s+,\s+size\s+\s+ VERIFY:\s+(?i:.*[\\|/]SymbolTestApp\.cs) @ (53|57):\s+ # Verify that "u" with no line info works -SOSCOMMAND:u -n +SOSCOMMAND:clru -n VERIFY:\s*Normal JIT generated code\s+ VERIFY:\s+SymbolTestApp\.Program\.Foo4\(System\.String\)\s+ VERIFY:\s+Begin\s+,\s+size\s+\s+ # Verify that "u" with offsets info works -SOSCOMMAND:u -o +SOSCOMMAND:clru -o VERIFY:\s*Normal JIT generated code\s+ VERIFY:\s+SymbolTestApp\.Program\.Foo4\(System\.String\)\s+ VERIFY:\s+Begin\s+,\s+size\s+\s+ @@ -279,12 +279,7 @@ VERIFY:\s+Name:\s+SymbolTestApp\.Program\.Foo4\(System\.String\)\s+ VERIFY:\s+JITTED Code Address:\s+\s+ # Verify that Threads (clrthreads) works -IFDEF:DOTNETDUMP SOSCOMMAND:clrthreads -ENDIF:DOTNETDUMP -!IFDEF:DOTNETDUMP -SOSCOMMAND:Threads -ENDIF:DOTNETDUMP VERIFY:\s*ThreadCount:\s+\s+ VERIFY:\s+UnstartedThread:\s+\s+ VERIFY:\s+BackgroundThread:\s+\s+ diff --git a/src/SOS/SOS.UnitTests/Scripts/TestExtensions.script b/src/SOS/SOS.UnitTests/Scripts/TestExtensions.script new file mode 100644 index 000000000..d18a63acd --- /dev/null +++ b/src/SOS/SOS.UnitTests/Scripts/TestExtensions.script @@ -0,0 +1,15 @@ +CONTINUE + +LOADSOS + +SOSCOMMAND:clrstack +VERIFY:Test command #1 invoked\s+ + +SOSCOMMAND:dumpheap +VERIFY:Test command #2 invoked\s+ + +SOSCOMMAND:assemblies +VERIFY:Test command #4 invoked\s+ + +SOSCOMMAND_FAIL:ip2md 0 +!VERIFY:Test command #5 invoked\s+ diff --git a/src/SOS/SOS.UnitTests/Scripts/WebApp.script b/src/SOS/SOS.UnitTests/Scripts/WebApp.script index b9a0509e0..a304a2f00 100644 --- a/src/SOS/SOS.UnitTests/Scripts/WebApp.script +++ b/src/SOS/SOS.UnitTests/Scripts/WebApp.script @@ -101,12 +101,7 @@ VERIFY:.*\s+Child\s+SP\s+IP\s+Call Site\s+ VERIFY:.*\s+Stack walk complete.\s+ # Verify that Threads (clrthreads) works -IFDEF:DOTNETDUMP SOSCOMMAND:clrthreads -ENDIF:DOTNETDUMP -!IFDEF:DOTNETDUMP -SOSCOMMAND:Threads -ENDIF:DOTNETDUMP VERIFY:\s*ThreadCount:\s+\s+ VERIFY:\s+UnstartedThread:\s+\s+ VERIFY:\s+BackgroundThread:\s+\s+ @@ -138,7 +133,7 @@ VERIFY:\s*STACK \s* VERIFY?:\s*<< Awaiting: \s+\s+.* >>\s+ VERIFY:\s*\s+\s+\(()?\)\s+.* -SOSCOMMAND:DumpMT --stats +SOSCOMMAND:DumpMT !VERIFY:\s* is not a MethodTable SOSCOMMAND:DumpAsync --coalesce diff --git a/src/SOS/Strike/CMakeLists.txt b/src/SOS/Strike/CMakeLists.txt index 6299c2739..dcd799467 100644 --- a/src/SOS/Strike/CMakeLists.txt +++ b/src/SOS/Strike/CMakeLists.txt @@ -89,6 +89,7 @@ if(WIN32) gchist.cpp gcroot.cpp symbols.cpp + managedcommands.cpp metadata.cpp sigparser.cpp sildasm.cpp diff --git a/src/SOS/Strike/Strike.vcxproj b/src/SOS/Strike/Strike.vcxproj index be63febcf..29dde66bf 100644 --- a/src/SOS/Strike/Strike.vcxproj +++ b/src/SOS/Strike/Strike.vcxproj @@ -393,6 +393,7 @@ + diff --git a/src/SOS/Strike/Strike.vcxproj.filters b/src/SOS/Strike/Strike.vcxproj.filters index ec1f21b1e..e7b14e9ea 100644 --- a/src/SOS/Strike/Strike.vcxproj.filters +++ b/src/SOS/Strike/Strike.vcxproj.filters @@ -32,6 +32,7 @@ platform + @@ -93,4 +94,4 @@ - + \ No newline at end of file diff --git a/src/SOS/Strike/exts.cpp b/src/SOS/Strike/exts.cpp index 3ebe83fde..836f38b0d 100644 --- a/src/SOS/Strike/exts.cpp +++ b/src/SOS/Strike/exts.cpp @@ -65,14 +65,9 @@ ExtQuery(PDEBUG_CLIENT client) HRESULT ExtQuery(ILLDBServices* services) { - // Initialize the PAL and extension suppport in one place and only once. - if (!g_palInitialized) + if (!InitializePAL()) { - if (PAL_InitializeDLL() != 0) - { - return E_FAIL; - } - g_palInitialized = true; + return E_FAIL; } g_ExtServices = services; @@ -196,6 +191,74 @@ ExtRelease(void) ReleaseTarget(); } +// Executes managed extension commands. Returns E_NOTIMPL if the command doesn't exists. +HRESULT +ExecuteCommand(PCSTR commandName, PCSTR args) +{ + if (commandName != nullptr && strlen(commandName) > 0) + { + IHostServices* hostServices = GetHostServices(); + if (hostServices != nullptr) + { + return hostServices->DispatchCommand(commandName, args); + } + } + return E_NOTIMPL; +} + +void +EENotLoadedMessage(HRESULT Status) +{ +#ifdef FEATURE_PAL + ExtOut("Failed to find runtime module (%s), 0x%08x\n", GetRuntimeDllName(IRuntime::Core), Status); +#else + ExtOut("Failed to find runtime module (%s or %s or %s), 0x%08x\n", GetRuntimeDllName(IRuntime::Core), GetRuntimeDllName(IRuntime::WindowsDesktop), GetRuntimeDllName(IRuntime::UnixCore), Status); +#endif + ExtOut("Extension commands need it in order to have something to do.\n"); + ExtOut("For more information see https://go.microsoft.com/fwlink/?linkid=2135652\n"); +} + +void +DACMessage(HRESULT Status) +{ + ExtOut("Failed to load data access module, 0x%08x\n", Status); + if (GetHost()->GetHostType() == IHost::HostType::DbgEng) + { + ExtOut("Verify that 1) you have a recent build of the debugger (10.0.18317.1001 or newer)\n"); + ExtOut(" 2) the file %s that matches your version of %s is\n", GetDacDllName(), GetRuntimeDllName()); + ExtOut(" in the version directory or on the symbol path\n"); + ExtOut(" 3) or, if you are debugging a dump file, verify that the file\n"); + ExtOut(" %s___.dll is on your symbol path.\n", GetDacModuleName()); + ExtOut(" 4) you are debugging on a platform and architecture that supports this\n"); + ExtOut(" the dump file. For example, an ARM dump file must be debugged\n"); + ExtOut(" on an X86 or an ARM machine; an AMD64 dump file must be\n"); + ExtOut(" debugged on an AMD64 machine.\n"); + ExtOut("\n"); + ExtOut("You can run the command '!setclrpath ' to control the load path of %s.\n", GetDacDllName()); + ExtOut("\n"); + ExtOut("Or you can also run the debugger command .cordll to control the debugger's\n"); + ExtOut("load of %s. .cordll -ve -u -l will do a verbose reload.\n", GetDacDllName()); + ExtOut("If that succeeds, the SOS command should work on retry.\n"); + ExtOut("\n"); + ExtOut("If you are debugging a minidump, you need to make sure that your executable\n"); + ExtOut("path is pointing to %s as well.\n", GetRuntimeDllName()); + } + else + { + if (Status == CORDBG_E_MISSING_DEBUGGER_EXPORTS) + { + ExtOut("You can run the debugger command 'setclrpath ' to control the load of %s.\n", GetDacDllName()); + ExtOut("If that succeeds, the SOS command should work on retry.\n"); + } + else + { + ExtOut("Can not load or initialize %s. The target runtime may not be initialized.\n", GetDacDllName()); + } + } + ExtOut("\n"); + ExtOut("For more information see https://go.microsoft.com/fwlink/?linkid=2135652\n"); +} + #ifndef FEATURE_PAL BOOL IsMiniDumpFileNODAC(); @@ -321,6 +384,21 @@ DllMain(HANDLE hInstance, DWORD dwReason, LPVOID lpReserved) #else // FEATURE_PAL +BOOL +InitializePAL() +{ + // Initialize the PAL only once + if (!g_palInitialized) + { + if (PAL_InitializeDLL() != 0) + { + return false; + } + g_palInitialized = true; + } + return true; +} + HRESULT DebugClient::QueryInterface( REFIID InterfaceId, diff --git a/src/SOS/Strike/exts.h b/src/SOS/Strike/exts.h index cf29ce595..96876879a 100644 --- a/src/SOS/Strike/exts.h +++ b/src/SOS/Strike/exts.h @@ -223,6 +223,7 @@ IsInitializedByDbgEng(); extern ILLDBServices* g_ExtServices; extern ILLDBServices2* g_ExtServices2; +extern BOOL InitializePAL(); #define IsInitializedByDbgEng() false @@ -237,6 +238,15 @@ ArchQuery(void); void ExtRelease(void); +HRESULT +ExecuteCommand(PCSTR commandName, PCSTR args); + +void +EENotLoadedMessage(HRESULT Status); + +void +DACMessage(HRESULT Status); + extern BOOL ControlC; inline BOOL IsInterrupt() @@ -264,57 +274,6 @@ public: ~__ExtensionCleanUp(){ExtRelease();} }; -inline void EENotLoadedMessage(HRESULT Status) -{ -#ifdef FEATURE_PAL - ExtOut("Failed to find runtime module (%s), 0x%08x\n", GetRuntimeDllName(IRuntime::Core), Status); -#else - ExtOut("Failed to find runtime module (%s or %s or %s), 0x%08x\n", GetRuntimeDllName(IRuntime::Core), GetRuntimeDllName(IRuntime::WindowsDesktop), GetRuntimeDllName(IRuntime::UnixCore), Status); -#endif - ExtOut("Extension commands need it in order to have something to do.\n"); - ExtOut("For more information see https://go.microsoft.com/fwlink/?linkid=2135652\n"); -} - -inline void DACMessage(HRESULT Status) -{ - ExtOut("Failed to load data access module, 0x%08x\n", Status); - if (GetHost()->GetHostType() == IHost::HostType::DbgEng) - { - ExtOut("Verify that 1) you have a recent build of the debugger (10.0.18317.1001 or newer)\n"); - ExtOut(" 2) the file %s that matches your version of %s is\n", GetDacDllName(), GetRuntimeDllName()); - ExtOut(" in the version directory or on the symbol path\n"); - ExtOut(" 3) or, if you are debugging a dump file, verify that the file\n"); - ExtOut(" %s___.dll is on your symbol path.\n", GetDacModuleName()); - ExtOut(" 4) you are debugging on a platform and architecture that supports this\n"); - ExtOut(" the dump file. For example, an ARM dump file must be debugged\n"); - ExtOut(" on an X86 or an ARM machine; an AMD64 dump file must be\n"); - ExtOut(" debugged on an AMD64 machine.\n"); - ExtOut("\n"); - ExtOut("You can run the command '!setclrpath ' to control the load path of %s.\n", GetDacDllName()); - ExtOut("\n"); - ExtOut("Or you can also run the debugger command .cordll to control the debugger's\n"); - ExtOut("load of %s. .cordll -ve -u -l will do a verbose reload.\n", GetDacDllName()); - ExtOut("If that succeeds, the SOS command should work on retry.\n"); - ExtOut("\n"); - ExtOut("If you are debugging a minidump, you need to make sure that your executable\n"); - ExtOut("path is pointing to %s as well.\n", GetRuntimeDllName()); - } - else - { - if (Status == CORDBG_E_MISSING_DEBUGGER_EXPORTS) - { - ExtOut("You can run the debugger command 'setclrpath ' to control the load of %s.\n", GetDacDllName()); - ExtOut("If that succeeds, the SOS command should work on retry.\n"); - } - else - { - ExtOut("Can not load or initialize %s. The target runtime may not be initialized.\n", GetDacDllName()); - } - } - ExtOut("\n"); - ExtOut("For more information see https://go.microsoft.com/fwlink/?linkid=2135652\n"); -} - // The minimum initialization for a command #define INIT_API_EXT() \ HRESULT Status; \ @@ -331,6 +290,11 @@ inline void DACMessage(HRESULT Status) INIT_API_EXT() \ if ((Status = ArchQuery()) != S_OK) return Status; +#define INIT_API_NOEE_PROBE_MANAGED(name) \ + INIT_API_EXT() \ + if ((Status = ExecuteCommand(name, args)) != E_NOTIMPL) return Status; \ + if ((Status = ArchQuery()) != S_OK) return Status; + #define INIT_API_EE() \ if ((Status = GetRuntime(&g_pRuntime)) != S_OK) \ { \ @@ -342,6 +306,10 @@ inline void DACMessage(HRESULT Status) INIT_API_NOEE() \ INIT_API_EE() +#define INIT_API_NODAC_PROBE_MANAGED(name) \ + INIT_API_NOEE_PROBE_MANAGED(name) \ + INIT_API_EE() + #define INIT_API_DAC() \ if ((Status = LoadClrDebugDll()) != S_OK) \ { \ @@ -355,19 +323,27 @@ inline void DACMessage(HRESULT Status) ToRelease spISD(g_sos); \ ResetGlobals(); +#define INIT_API_PROBE_MANAGED(name) \ + INIT_API_NODAC_PROBE_MANAGED(name) \ + INIT_API_DAC() + #define INIT_API() \ INIT_API_NODAC() \ INIT_API_DAC() +#define INIT_API_EFN() \ + INIT_API_NODAC() \ + INIT_API_DAC() + // Attempt to initialize DAC and SOS globals, but do not "return" on failure. // Instead, mark the failure to initialize the DAC by setting g_bDacBroken to TRUE. // This should be used from extension commands that should work OK even when no // runtime is loaded in the debuggee, e.g. DumpLog, DumpStack. These extensions // and functions they call should test g_bDacBroken before calling any DAC enabled // feature. -#define INIT_API_NO_RET_ON_FAILURE() \ - INIT_API_NODAC() \ - if ((Status = LoadClrDebugDll()) != S_OK) \ +#define INIT_API_NO_RET_ON_FAILURE(name) \ + INIT_API_NODAC_PROBE_MANAGED(name) \ + if ((Status = LoadClrDebugDll()) != S_OK) \ { \ ExtOut("Failed to load data access module (%s), 0x%08x\n", GetDacDllName(), Status); \ ExtOut("Some functionality may be impaired\n"); \ @@ -382,6 +358,30 @@ inline void DACMessage(HRESULT Status) ToRelease spISD(g_sos); \ ToRelease spIDP(g_clrData); +#ifdef FEATURE_PAL + +#define MINIDUMP_NOT_SUPPORTED() +#define ONLY_SUPPORTED_ON_WINDOWS_TARGET() + +#else // !FEATURE_PAL + +#define MINIDUMP_NOT_SUPPORTED() \ + if (IsMiniDumpFile()) \ + { \ + ExtOut("This command is not supported in a minidump without full memory\n"); \ + ExtOut("To try the command anyway, run !MinidumpMode 0\n"); \ + return Status; \ + } + +#define ONLY_SUPPORTED_ON_WINDOWS_TARGET() \ + if (!IsWindowsTarget()) \ + { \ + ExtOut("This command is only supported for Windows targets\n"); \ + return Status; \ + } + +#endif // FEATURE_PAL + extern BOOL g_bDacBroken; //----------------------------------------------------------------------------------------- diff --git a/src/SOS/Strike/gchist.cpp b/src/SOS/Strike/gchist.cpp index f656cb118..82a43a953 100644 --- a/src/SOS/Strike/gchist.cpp +++ b/src/SOS/Strike/gchist.cpp @@ -31,8 +31,8 @@ #include #include #include - #include "strike.h" + // We need to define the target address type. This will be used in the // functions that read directly from the debuggee address space, vs. using // the DAC tgo read the DAC-ized data structures. @@ -258,7 +258,7 @@ void GcHistAddLog(LPCSTR msg, StressMsg* stressMsg) DECLARE_API(HistStats) { INIT_API(); - + ExtOut ("%8s %8s %8s\n", "GCCount", "Promotes", "Relocs"); ExtOut ("-----------------------------------\n"); @@ -348,12 +348,13 @@ DECLARE_API(HistRoot) }; if (!GetCMDOption(args, NULL, 0, arg, ARRAY_SIZE(arg), &nArg)) - return Status; - + { + return E_INVALIDARG; + } if (nArg != 1) { ExtOut ("!Root \n"); - return Status; + return E_INVALIDARG; } size_t Root = (size_t) GetExpression(rootstr.data); @@ -463,12 +464,13 @@ DECLARE_API(HistObjFind) }; if (!GetCMDOption(args, NULL, 0, arg, ARRAY_SIZE(arg), &nArg)) - return Status; - + { + return E_INVALIDARG; + } if (nArg != 1) { ExtOut ("!ObjSearch \n"); - return Status; + return E_INVALIDARG; } size_t object = (size_t) GetExpression(objstr.data); @@ -542,12 +544,13 @@ DECLARE_API(HistObj) }; if (!GetCMDOption(args, NULL, 0, arg, ARRAY_SIZE(arg), &nArg)) - return Status; - + { + return E_INVALIDARG; + } if (nArg != 1) { ExtOut ("!object \n"); - return Status; + return E_INVALIDARG; } size_t object = (size_t) GetExpression(objstr.data); diff --git a/src/SOS/Strike/managedcommands.cpp b/src/SOS/Strike/managedcommands.cpp new file mode 100644 index 000000000..2a7b33615 --- /dev/null +++ b/src/SOS/Strike/managedcommands.cpp @@ -0,0 +1,222 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "exts.h" + +// Windows host only managed command stubs + +HRESULT ExecuteManagedOnlyCommand(PCSTR commandName, PCSTR args) +{ + HRESULT hr = ExecuteCommand(commandName, args); + if (hr == E_NOTIMPL) + { + ExtErr("Unrecognized command '%s'\n", commandName); + } + return hr; +} + +DECLARE_API(DumpStackObjects) +{ + INIT_API_EXT(); + MINIDUMP_NOT_SUPPORTED(); + return ExecuteManagedOnlyCommand("dumpstackobjects", args); +} + +DECLARE_API(EEHeap) +{ + INIT_API_EXT(); + return ExecuteManagedOnlyCommand("eeheap", args); +} + +DECLARE_API(TraverseHeap) +{ + INIT_API_EXT(); + MINIDUMP_NOT_SUPPORTED(); + return ExecuteManagedOnlyCommand("traverseheap", args); +} + +DECLARE_API(DumpRuntimeTypes) +{ + INIT_API_EXT(); + MINIDUMP_NOT_SUPPORTED(); + return ExecuteManagedOnlyCommand("dumpruntimetypes", args); +} + +DECLARE_API(DumpHeap) +{ + INIT_API_EXT(); + MINIDUMP_NOT_SUPPORTED(); + return ExecuteManagedOnlyCommand("dumpheap", args); +} + +DECLARE_API(VerifyHeap) +{ + INIT_API_EXT(); + MINIDUMP_NOT_SUPPORTED(); + return ExecuteManagedOnlyCommand("verifyheap", args); +} + +DECLARE_API(AnalyzeOOM) +{ + INIT_API_EXT(); + MINIDUMP_NOT_SUPPORTED(); + return ExecuteManagedOnlyCommand("analyzeoom", args); +} + +DECLARE_API(VerifyObj) +{ + INIT_API_EXT(); + MINIDUMP_NOT_SUPPORTED(); + return ExecuteManagedOnlyCommand("verifyobj", args); +} + +DECLARE_API(ListNearObj) +{ + INIT_API_EXT(); + MINIDUMP_NOT_SUPPORTED(); + return ExecuteManagedOnlyCommand("listnearobj", args); +} + +DECLARE_API(GCHeapStat) +{ + INIT_API_EXT(); + MINIDUMP_NOT_SUPPORTED(); + return ExecuteManagedOnlyCommand("gcheapstat", args); +} + +DECLARE_API(FinalizeQueue) +{ + INIT_API_EXT(); + MINIDUMP_NOT_SUPPORTED(); + return ExecuteManagedOnlyCommand("finalizequeue", args); +} + +DECLARE_API(ThreadPool) +{ + INIT_API_EXT(); + MINIDUMP_NOT_SUPPORTED(); + return ExecuteManagedOnlyCommand("threadpool", args); +} + +DECLARE_API(PathTo) +{ + INIT_API_EXT(); + MINIDUMP_NOT_SUPPORTED(); + return ExecuteManagedOnlyCommand("pathto", args); +} + +DECLARE_API(GCRoot) +{ + INIT_API_EXT(); + MINIDUMP_NOT_SUPPORTED(); + return ExecuteManagedOnlyCommand("gcroot", args); +} + +DECLARE_API(GCWhere) +{ + INIT_API_EXT(); + MINIDUMP_NOT_SUPPORTED(); + return ExecuteManagedOnlyCommand("gcwhere", args); +} + +DECLARE_API(ObjSize) +{ + INIT_API_EXT(); + MINIDUMP_NOT_SUPPORTED(); + return ExecuteManagedOnlyCommand("objsize", args); +} + +DECLARE_API(SetSymbolServer) +{ + INIT_API_EXT(); + return ExecuteManagedOnlyCommand("setsymbolserver", args); +} + +DECLARE_API(assemblies) +{ + INIT_API_EXT(); + return ExecuteManagedOnlyCommand("assemblies", args); +} + +DECLARE_API(crashinfo) +{ + INIT_API_EXT(); + return ExecuteManagedOnlyCommand("crashinfo", args); +} + +DECLARE_API(DumpAsync) +{ + INIT_API_EXT(); + return ExecuteManagedOnlyCommand("dumpasync", args); +} + +DECLARE_API(logging) +{ + INIT_API_EXT(); + return ExecuteManagedOnlyCommand("logging", args); +} + +DECLARE_API(maddress) +{ + INIT_API_EXT(); + return ExecuteManagedOnlyCommand("maddress", args); +} + +DECLARE_API(dumpexceptions) +{ + INIT_API_EXT(); + return ExecuteManagedOnlyCommand("dumpexceptions", args); +} + +DECLARE_API(dumpgen) +{ + INIT_API_EXT(); + return ExecuteManagedOnlyCommand("dumpgen", args); +} + +DECLARE_API(sizestats) +{ + INIT_API_EXT(); + return ExecuteManagedOnlyCommand("sizestats", args); +} + +typedef HRESULT (*PFN_COMMAND)(PDEBUG_CLIENT client, PCSTR args); + +// +// Executes managed extension commands (i.e. !sos) +// +DECLARE_API(ext) +{ + INIT_API_EXT(); + + if (args == nullptr || strlen(args) <= 0) + { + args = "Help"; + } + std::string arguments(args); + size_t pos = arguments.find(' '); + std::string commandName = arguments.substr(0, pos); + if (pos != std::string::npos) + { + arguments = arguments.substr(pos + 1); + } + else + { + arguments.clear(); + } + Status = ExecuteCommand(commandName.c_str(), arguments.c_str()); + if (Status == E_NOTIMPL) + { + PFN_COMMAND commandFunc = (PFN_COMMAND)GetProcAddress(g_hInstance, commandName.c_str()); + if (commandFunc != nullptr) + { + Status = (*commandFunc)(client, arguments.c_str()); + } + else + { + ExtErr("Unrecognized command '%s'\n", commandName.c_str()); + } + } + return Status; +} + diff --git a/src/SOS/Strike/sos.def b/src/SOS/Strike/sos.def index 4b9b8fcd7..f298a3f9f 100644 --- a/src/SOS/Strike/sos.def +++ b/src/SOS/Strike/sos.def @@ -7,7 +7,8 @@ EXPORTS AnalyzeOOM analyzeoom=AnalyzeOOM ao=AnalyzeOOM - clrmodules + assemblies + clrmodules=assemblies ClrStack clrstack=ClrStack CLRStack=ClrStack @@ -27,6 +28,7 @@ EXPORTS dumpdelegate=DumpDelegate DumpDomain dumpdomain=DumpDomain + dumpexceptions #ifdef TRACE_GC DumpGCLog dumpgclog=DumpGCLog @@ -38,6 +40,8 @@ EXPORTS DumpGCConfigLog dumpgcconfiglog=DumpGCConfigLog dclog=DumpGCConfigLog + dumpgen + dg=dumpgen DumpHeap dumpheap=DumpHeap DumpIL @@ -121,6 +125,7 @@ EXPORTS ListNearObj listnearobj=ListNearObj lno=ListNearObj + maddress Name2EE name2ee=Name2EE ObjSize @@ -137,6 +142,7 @@ EXPORTS setsymbolserver=SetSymbolServer SetClrPath setclrpath=SetClrPath + sizestats SOSFlush sosflush=SOSFlush StopOnException @@ -161,6 +167,7 @@ EXPORTS Traverseheap=TraverseHeap u U=u + clru=u VerifyHeap verifyheap=VerifyHeap Verifyheap=VerifyHeap diff --git a/src/SOS/Strike/sos_unixexports.src b/src/SOS/Strike/sos_unixexports.src index aa8eaf23d..cf08981f5 100644 --- a/src/SOS/Strike/sos_unixexports.src +++ b/src/SOS/Strike/sos_unixexports.src @@ -2,7 +2,6 @@ ; The .NET Foundation licenses this file to you under the MIT license. ; See the LICENSE file in the project root for more information. -AnalyzeOOM bpmd ClrStack dbgout @@ -13,32 +12,24 @@ DumpClass DumpDelegate DumpDomain DumpGCData -DumpHeap DumpIL DumpLog DumpMD DumpModule DumpMT DumpObj -DumpRuntimeTypes DumpSig DumpSigElem DumpStack -DumpStackObjects DumpVC -EEHeap EEVersion EEStack EHInfo enummem -FinalizeQueue FindAppDomain FindRoots GCHandles -GCHeapStat GCInfo -GCRoot -GCWhere Help HistClear HistInit @@ -47,11 +38,8 @@ HistObjFind HistRoot HistStats IP2MD -ListNearObj Name2EE -ObjSize PrintException -PathTo runtimes StopOnCatch SetClrPath @@ -61,13 +49,9 @@ runtimes SuppressJitOptimization SyncBlk Threads -ThreadPool ThreadState Token2EE -TraverseHeap u -VerifyHeap -VerifyObj SOSInitializeByHost SOSUninitializeByHost diff --git a/src/SOS/Strike/sosdocs.txt b/src/SOS/Strike/sosdocs.txt index fa470d2ea..80ec24642 100644 --- a/src/SOS/Strike/sosdocs.txt +++ b/src/SOS/Strike/sosdocs.txt @@ -19,7 +19,7 @@ COMMAND: contents. SOS is a debugger extension DLL designed to aid in the debugging of managed programs. Functions are listed by category, then roughly in order of importance. Shortcut names for popular functions are listed in parenthesis. -Type "!help " for detailed info on that function. +Type "!soshelp " for detailed info on that function. Object Inspection Examining code and stacks ----------------------------- ----------------------------- @@ -2638,26 +2638,6 @@ You can use the "dotnet --info" in a command shell to find the path of an instal .NET Core runtime. \\ -COMMAND: setsymbolserver. -!SetSymbolServer [-ms] [-mi] [-disable] [-log] [-cache ] [-directory ] [-timeout ] [-pat ] [] - --ms - Use the public Microsoft symbol server. --mi - Use the internal symweb symbol server. --disable - Disable symbol download support. --directory - Directory to search for symbols. Can be more than one. --timeout - Specify the symbol server timeout in minutes --pat - Access token to the authenticated server. --cache - Specific a symbol cache directory. The default is %%TEMP%%\SymbolCache if not specified. - - Symbol server URL. - -This commands enables symbol server support for portable PDBs in SOS. If the .sympath is set, this -symbol server support is automatically enabled. - -To disable downloading or clear the current SOS symbol settings allowing new symbol paths to be set: - - 0:000> !setsymbolserver -disable -\\ - COMMAND: sosstatus. !SOSStatus [-reset] @@ -2699,8 +2679,3 @@ runtimes [-netfx] [-netcore] List and select the .NET runtimes in the target process. \\ -COMMAND: logging. -logging [enable] [disable] - -Enables or disables the internal trace logging. -\\ diff --git a/src/SOS/Strike/sosdocsunix.txt b/src/SOS/Strike/sosdocsunix.txt index 3310726ca..b554a9479 100644 --- a/src/SOS/Strike/sosdocsunix.txt +++ b/src/SOS/Strike/sosdocsunix.txt @@ -2268,57 +2268,6 @@ You can use the "dotnet --info" in a command shell to find the path of an instal .NET Core runtime. \\ -COMMAND: setsymbolserver. -COMMAND: loadsymbols. -SetSymbolServer [-ms] [-disable] [-log] [-loadsymbols] [-cache ] [-directory ] [-timeout ] [-pat ] [] - --ms - Use the public Microsoft symbol server. --disable - Disable symbol download support. --directory - Directory to search for symbols. Can be more than one. --timeout - Specify the symbol server timeout in minutes --pat - Access token to the authenticated server. --cache - Specific a symbol cache directory. The default is $HOME/.dotnet/symbolcache if not specified. --loadsymbols - Attempts to download the native .NET Core symbols for the runtime - - Symbol server URL. - -This commands enables symbol server support in SOS. The portable PDBs for managed assemblies -and .NET Core native symbol files are downloaded. - -To enable downloading symbols from the Microsoft symbol server: - - (lldb) setsymbolserver -ms - -This command may take some time without any output while it attempts to download the symbol files. - -To disable downloading or clear the current SOS symbol settings allowing new symbol paths to be set: - - (lldb) setsymbolserver -disable - -To add a directory to search for symbols: - - (lldb) setsymbolserver -directory /home/mikem/symbols - -This command can be used so the module/symbol file structure does not have to match the machine -file structure that the core dump was generated. - -To clear the default cache run "rm -r $HOME/.dotnet/symbolcache" in a command shell. - -If you receive an error like the one below on a core dump, you need to set the .NET Core -runtime with the "sethostruntime" command. Type "soshelp sethostruntime" for more details. - - (lldb) setsymbolserver -ms - Error: Fail to initialize CoreCLR 80004005 - SetSymbolServer -ms failed - -The "-loadsymbols" option and the "loadsymbol" command alias attempts to download the native .NET -Core symbol files. It is only useful for live sessions and not core dumps. This command needs to -be run before the lldb "bt" (stack trace) or the "clrstack -f" (dumps both managed and native -stack frames). - - (lldb) loadsymbols - (lldb) bt -\\ - COMMAND: sosstatus. SOSStatus [-reset] @@ -2348,8 +2297,3 @@ runtimes List the .NET runtimes in the target process. \\ -COMMAND: logging. -logging [enable] [disable] - -Enables or disables the internal trace logging. -\\ diff --git a/src/SOS/Strike/strike.cpp b/src/SOS/Strike/strike.cpp index b6f36b5db..0d7aad0d4 100644 --- a/src/SOS/Strike/strike.cpp +++ b/src/SOS/Strike/strike.cpp @@ -70,7 +70,6 @@ #include #endif // !FEATURE_PAL #include - #include "platformspecific.h" #define NOEXTAPI @@ -80,7 +79,6 @@ #undef StackTrace #include - #include #include #include @@ -222,7 +220,7 @@ extern const char* g_sosPrefix; DECLARE_API (MinidumpMode) { - INIT_API (); + INIT_API(); ONLY_SUPPORTED_ON_WINDOWS_TARGET(); DWORD_PTR Value=0; @@ -234,7 +232,7 @@ DECLARE_API (MinidumpMode) size_t nArg; if (!GetCMDOption(args, NULL, 0, arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } if (nArg == 0) { @@ -269,7 +267,7 @@ DECLARE_API (MinidumpMode) \**********************************************************************/ DECLARE_API(IP2MD) { - INIT_API(); + INIT_API_PROBE_MANAGED("ip2md"); MINIDUMP_NOT_SUPPORTED(); BOOL dml = FALSE; @@ -286,14 +284,14 @@ DECLARE_API(IP2MD) if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); if (IP == 0) { ExtOut("%s is not IP\n", args); - return Status; + return E_INVALIDARG; } CLRDATA_ADDRESS cdaStart = TO_CDADDR(IP); @@ -376,25 +374,6 @@ GetContextStackTrace(ULONG osThreadId, PULONG pnumFrames) return hr; } - -// -// Executes managed extension commands -// -HRESULT ExecuteCommand(PCSTR commandName, PCSTR args) -{ - IHostServices* hostServices = GetHostServices(); - if (hostServices != nullptr) - { - if (commandName != nullptr && strlen(commandName) > 0) - { - return hostServices->DispatchCommand(commandName, args); - } - } - ExtErr("Unrecognized command %s\n", commandName); - return E_NOTIMPL; -} - - /**********************************************************************\ * Routine Description: * * * @@ -457,7 +436,7 @@ void DumpStackInternal(DumpStackFlag *pDSFlag) DECLARE_API(DumpStack) { - INIT_API_NO_RET_ON_FAILURE(); + INIT_API_NO_RET_ON_FAILURE("dumpstack"); MINIDUMP_NOT_SUPPORTED(); @@ -483,7 +462,9 @@ DECLARE_API(DumpStack) }; size_t nArg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) - return Status; + { + return E_INVALIDARG; + } // symlines will be non-zero only if SYMOPT_LOAD_LINES was set in the symbol options ULONG symlines = 0; @@ -537,7 +518,7 @@ DECLARE_API (EEStack) if (!GetCMDOption(args, option, ARRAY_SIZE(option), NULL, 0, NULL)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder enableDML(dml); @@ -617,21 +598,6 @@ DECLARE_API (EEStack) return Status; } -/**********************************************************************\ -* Routine Description: * -* * -* This function is called to dump the address and name of all * -* Managed Objects on the stack. * -* * -\**********************************************************************/ -DECLARE_API(DumpStackObjects) -{ - INIT_API_EXT(); - MINIDUMP_NOT_SUPPORTED(); - - return ExecuteCommand("dumpstackobjects", args); -} - /**********************************************************************\ * Routine Description: * * * @@ -641,7 +607,7 @@ DECLARE_API(DumpStackObjects) \**********************************************************************/ DECLARE_API(DumpMD) { - INIT_API(); + INIT_API_PROBE_MANAGED("dumpmd"); MINIDUMP_NOT_SUPPORTED(); DWORD_PTR dwStartAddr = NULL; @@ -659,7 +625,7 @@ DECLARE_API(DumpMD) if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -757,7 +723,7 @@ GetILAddressResult GetILAddress(const DacpMethodDescData& MethodDescData); \**********************************************************************/ DECLARE_API(DumpIL) { - INIT_API(); + INIT_API_PROBE_MANAGED("dumpil"); MINIDUMP_NOT_SUPPORTED(); DWORD_PTR dwStartAddr = NULL; DWORD_PTR dwDynamicMethodObj = NULL; @@ -778,7 +744,7 @@ DECLARE_API(DumpIL) if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -973,12 +939,12 @@ DECLARE_API(DumpSig) size_t nArg; if (!GetCMDOption(args, NULL, 0, arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } if (nArg != 2) { ExtOut("%sdumpsig \n", SOSPrefix); - return Status; + return E_INVALIDARG; } DWORD_PTR dwSigAddr = GetExpression(sigExpr.data); @@ -1020,13 +986,13 @@ DECLARE_API(DumpSigElem) size_t nArg; if (!GetCMDOption(args, NULL, 0, arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } if (nArg != 2) { ExtOut("%sdumpsigelem \n", SOSPrefix); - return Status; + return E_INVALIDARG; } DWORD_PTR dwSigAddr = GetExpression(sigExpr.data); @@ -1035,7 +1001,7 @@ DECLARE_API(DumpSigElem) if (dwSigAddr == 0 || dwModuleAddr == 0) { ExtOut("Invalid parameters %s %s\n", sigExpr.data, moduleExpr.data); - return Status; + return E_INVALIDARG; } DumpSigWorker(dwSigAddr, dwModuleAddr, FALSE); @@ -1051,7 +1017,7 @@ DECLARE_API(DumpSigElem) \**********************************************************************/ DECLARE_API(DumpClass) { - INIT_API(); + INIT_API_PROBE_MANAGED("dumpclass"); MINIDUMP_NOT_SUPPORTED(); DWORD_PTR dwStartAddr = 0; @@ -1069,13 +1035,13 @@ DECLARE_API(DumpClass) size_t nArg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } if (nArg == 0) { ExtOut("Missing EEClass address\n"); - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -1170,7 +1136,7 @@ DECLARE_API(DumpMT) DWORD_PTR dwStartAddr=0; DWORD_PTR dwOriginalAddr; - INIT_API(); + INIT_API_PROBE_MANAGED("dumpmt"); MINIDUMP_NOT_SUPPORTED(); @@ -1189,7 +1155,7 @@ DECLARE_API(DumpMT) size_t nArg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -1198,7 +1164,7 @@ DECLARE_API(DumpMT) if (nArg == 0) { Print("Missing MethodTable address\n"); - return Status; + return E_INVALIDARG; } dwOriginalAddr = dwStartAddr; @@ -1207,7 +1173,7 @@ DECLARE_API(DumpMT) if (!IsMethodTable(dwStartAddr)) { Print(dwOriginalAddr, " is not a MethodTable\n"); - return Status; + return E_INVALIDARG; } DacpMethodTableData vMethTable; @@ -1216,7 +1182,7 @@ DECLARE_API(DumpMT) if (vMethTable.bIsFree) { Print("Free MethodTable\n"); - return Status; + return E_INVALIDARG; } DacpMethodTableCollectibleData vMethTableCollectible; @@ -1834,7 +1800,7 @@ HRESULT PrintPermissionSet (TADDR p_PermSet) \**********************************************************************/ DECLARE_API(DumpArray) { - INIT_API(); + INIT_API_PROBE_MANAGED("dumparray"); DumpArrayFlags flags; @@ -1857,7 +1823,7 @@ DECLARE_API(DumpArray) size_t nArg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -1865,7 +1831,7 @@ DECLARE_API(DumpArray) if (p_Object == 0) { ExtOut("Invalid parameter %s\n", flags.strObject); - return Status; + return E_INVALIDARG; } if (!sos::IsObject(p_Object, true)) @@ -2052,7 +2018,7 @@ HRESULT PrintArray(DacpObjectData& objData, DumpArrayFlags& flags, BOOL isPermSe \**********************************************************************/ DECLARE_API(DumpObj) { - INIT_API(); + INIT_API_PROBE_MANAGED("dumpobj"); MINIDUMP_NOT_SUPPORTED(); @@ -2073,7 +2039,7 @@ DECLARE_API(DumpObj) size_t nArg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } DWORD_PTR p_Object = GetExpression(str_Object.data); @@ -2081,7 +2047,7 @@ DECLARE_API(DumpObj) if (p_Object == 0) { ExtOut("Invalid parameter %s\n", args); - return Status; + return E_INVALIDARG; } try { @@ -2129,7 +2095,7 @@ DECLARE_API(DumpALC) size_t nArg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } DWORD_PTR p_Object = GetExpression(str_Object.data); @@ -2137,7 +2103,7 @@ DECLARE_API(DumpALC) if (p_Object == 0) { ExtOut("Invalid parameter %s\n", args); - return Status; + return E_INVALIDARG; } try @@ -2163,7 +2129,7 @@ DECLARE_API(DumpALC) DECLARE_API(DumpDelegate) { - INIT_API(); + INIT_API_PROBE_MANAGED("dumpdelegate"); MINIDUMP_NOT_SUPPORTED(); try @@ -2182,12 +2148,12 @@ DECLARE_API(DumpDelegate) size_t nArg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } if (nArg != 1) { ExtOut("Usage: %sdumpdelegate \n", SOSPrefix); - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -2879,7 +2845,7 @@ HRESULT FormatException(CLRDATA_ADDRESS taObj, BOOL bLineNumbers = FALSE) DECLARE_API(PrintException) { - INIT_API(); + INIT_API_PROBE_MANAGED("printexception"); BOOL dml = FALSE; BOOL bShowNested = FALSE; @@ -2901,7 +2867,7 @@ DECLARE_API(PrintException) size_t nArg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } CheckBreakingRuntimeChange(); @@ -2969,7 +2935,7 @@ DECLARE_API(PrintException) { ExtOut("Invalid exception object %s\n", args); } - return Status; + return E_INVALIDARG; } if (bCCW) @@ -2996,7 +2962,7 @@ DECLARE_API(PrintException) if ((threadAddr == NULL) || (Thread.Request(g_sos, threadAddr) != S_OK)) { ExtOut("The current thread is unmanaged\n"); - return Status; + return E_INVALIDARG; } if (Thread.firstNestedException) @@ -3048,7 +3014,7 @@ DECLARE_API(PrintException) \**********************************************************************/ DECLARE_API(DumpVC) { - INIT_API(); + INIT_API_PROBE_MANAGED("dumpvc"); MINIDUMP_NOT_SUPPORTED(); DWORD_PTR p_MT = NULL; @@ -3067,7 +3033,7 @@ DECLARE_API(DumpVC) size_t nArg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -3109,7 +3075,7 @@ DECLARE_API(DumpRCW) size_t nArg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -3237,7 +3203,7 @@ DECLARE_API(DumpCCW) size_t nArg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -3493,7 +3459,7 @@ DECLARE_API(DumpPermissionSet) size_t nArg; if (!GetCMDOption(args, NULL, 0, arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } if (nArg!=1) { @@ -3508,18 +3474,6 @@ DECLARE_API(DumpPermissionSet) #endif // _DEBUG #endif // FEATURE_PAL -/**********************************************************************\ -* Routine Description: * -* * -* This function dumps GC heap size. * -* * -\**********************************************************************/ -DECLARE_API(EEHeap) -{ - INIT_API_EXT(); - return ExecuteCommand("eeheap", args); -} - void PrintGCStat(HeapStat *inStat, const char* label=NULL) { if (inStat) @@ -3540,20 +3494,6 @@ void PrintGCStat(HeapStat *inStat, const char* label=NULL) } } -DECLARE_API(TraverseHeap) -{ - INIT_API_EXT(); - MINIDUMP_NOT_SUPPORTED(); - return ExecuteCommand("traverseheap", args); -} - -DECLARE_API(DumpRuntimeTypes) -{ - INIT_API_EXT(); - MINIDUMP_NOT_SUPPORTED(); - return ExecuteCommand("dumpruntimetypes", args); -} - namespace sos { class FragmentationBlock @@ -3591,52 +3531,6 @@ namespace sos }; } -DECLARE_API(DumpHeap) -{ - INIT_API_EXT(); - MINIDUMP_NOT_SUPPORTED(); - return ExecuteCommand("dumpheap", args); -} - -DECLARE_API(VerifyHeap) -{ - INIT_API_EXT(); - MINIDUMP_NOT_SUPPORTED(); - return ExecuteCommand("verifyheap", args); -} - -DECLARE_API(AnalyzeOOM) -{ - INIT_API_EXT(); - MINIDUMP_NOT_SUPPORTED(); - - return ExecuteCommand("analyzeoom", args); -} - -DECLARE_API(VerifyObj) -{ - INIT_API_EXT(); - MINIDUMP_NOT_SUPPORTED(); - - return ExecuteCommand("verifyobj", args); -} - -DECLARE_API(ListNearObj) -{ - INIT_API_EXT(); - MINIDUMP_NOT_SUPPORTED(); - - return ExecuteCommand("listnearobj", args); -} - -DECLARE_API(GCHeapStat) -{ - INIT_API_EXT(); - MINIDUMP_NOT_SUPPORTED(); - - return ExecuteCommand("gcheapstat", args); -} - /**********************************************************************\ * Routine Description: * * * @@ -3665,7 +3559,7 @@ DECLARE_API(SyncBlk) size_t nArg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -3915,21 +3809,6 @@ DECLARE_API(RCWCleanupList) } #endif // FEATURE_COMINTEROP -/**********************************************************************\ -* Routine Description: * -* * -* This function is called to dump the contents of the finalizer * -* queue. * -* * -\**********************************************************************/ -DECLARE_API(FinalizeQueue) -{ - INIT_API_EXT(); - MINIDUMP_NOT_SUPPORTED(); - - return ExecuteCommand("finalizequeue", args); -} - enum { // These are the values set in m_dwTransientFlags. // Note that none of these flags survive a prejit save/restore. @@ -4011,12 +3890,12 @@ DECLARE_API(DumpModule) size_t nArg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } if (nArg != 1) { ExtOut("Usage: DumpModule [-mt] \n"); - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -4182,7 +4061,7 @@ DECLARE_API(DumpDomain) if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -4303,7 +4182,7 @@ DECLARE_API(DumpAssembly) if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -4849,7 +4728,7 @@ DECLARE_API(ThreadState) DECLARE_API(Threads) { - INIT_API(); + INIT_API_PROBE_MANAGED("clrthreads"); BOOL bPrintSpecialThreads = FALSE; BOOL bPrintLiveThreadsOnly = FALSE; @@ -4865,7 +4744,7 @@ DECLARE_API(Threads) }; if (!GetCMDOption(args, option, ARRAY_SIZE(option), NULL, 0, NULL)) { - return Status; + return E_INVALIDARG; } if (bSwitchToManagedExceptionThread) @@ -5961,7 +5840,7 @@ DECLARE_API(SOSHandleCLRN) HRESULT HandleRuntimeLoadedNotification(IDebugClient* client) { - INIT_API(); + INIT_API_EFN(); EnableModuleLoadUnloadCallbacks(); return g_ExtControl->Execute(DEBUG_EXECUTE_NOT_LOGGED, "sxe -c \"!SOSHandleCLRN\" clrn", 0); } @@ -5970,13 +5849,13 @@ HRESULT HandleRuntimeLoadedNotification(IDebugClient* client) HRESULT HandleExceptionNotification(ILLDBServices *client) { - INIT_API(); + INIT_API_EFN(); return HandleCLRNotificationEvent(); } HRESULT HandleRuntimeLoadedNotification(ILLDBServices *client) { - INIT_API(); + INIT_API_EFN(); EnableModuleLoadUnloadCallbacks(); return g_ExtServices->SetExceptionCallback(HandleExceptionNotification); } @@ -6027,7 +5906,7 @@ DECLARE_API(bpmd) size_t nArg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } bool fBadParam = false; @@ -6359,20 +6238,6 @@ DECLARE_API(bpmd) return Status; } -/**********************************************************************\ -* Routine Description: * -* * -* This function is called to dump the managed threadpool * -* * -\**********************************************************************/ -DECLARE_API(ThreadPool) -{ - INIT_API_EXT(); - MINIDUMP_NOT_SUPPORTED(); - - return ExecuteCommand("threadpool", args); -} - DECLARE_API(FindAppDomain) { INIT_API(); @@ -6393,7 +6258,7 @@ DECLARE_API(FindAppDomain) if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -6643,7 +6508,7 @@ BOOL traverseEh(UINT clauseIndex,UINT totalClauses,DACEHInfo *pEHInfo,LPVOID tok DECLARE_API(EHInfo) { - INIT_API(); + INIT_API_PROBE_MANAGED("ehinfo"); MINIDUMP_NOT_SUPPORTED(); DWORD_PTR dwStartAddr = NULL; @@ -6662,7 +6527,7 @@ DECLARE_API(EHInfo) size_t nArg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg) || (0 == nArg)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -6724,7 +6589,7 @@ DECLARE_API(EHInfo) \**********************************************************************/ DECLARE_API(GCInfo) { - INIT_API(); + INIT_API_PROBE_MANAGED("gcinfo"); MINIDUMP_NOT_SUPPORTED(); TADDR taStartAddr = NULL; @@ -6742,7 +6607,7 @@ DECLARE_API(GCInfo) size_t nArg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg) || (0 == nArg)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -7049,7 +6914,7 @@ DECLARE_API(u) }; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg) || (nArg < 1)) { - return Status; + return E_INVALIDARG; } // symlines will be non-zero only if SYMOPT_LOAD_LINES was set in the symbol options ULONG symlines = 0; @@ -7553,10 +7418,8 @@ HRESULT GetIntermediateLangMap(BOOL bIL, const DacpCodeHeaderData& codeHeaderDat \**********************************************************************/ DECLARE_API(DumpLog) { - INIT_API_NO_RET_ON_FAILURE(); - + INIT_API_NO_RET_ON_FAILURE("dumplog"); MINIDUMP_NOT_SUPPORTED(); - _ASSERTE(g_pRuntime != nullptr); // Not supported on desktop runtime @@ -7584,7 +7447,7 @@ DECLARE_API(DumpLog) size_t nArg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } if (nArg > 0 && sFileName.data != NULL) { @@ -8048,7 +7911,7 @@ extern char sccsid[]; \**********************************************************************/ DECLARE_API(EEVersion) { - INIT_API_NO_RET_ON_FAILURE(); + INIT_API_NO_RET_ON_FAILURE("eeversion"); static const int fileVersionBufferSize = 1024; ArrayHolder fileVersionBuffer = new char[fileVersionBufferSize]; @@ -8139,39 +8002,31 @@ DECLARE_API(EEVersion) \**********************************************************************/ DECLARE_API(SOSStatus) { - INIT_API_NOEE(); + INIT_API_NOEE_PROBE_MANAGED("sosstatus"); - IHostServices* hostServices = GetHostServices(); - if (hostServices != nullptr) + BOOL bReset = FALSE; + CMDOption option[] = + { // name, vptr, type, hasValue + {"-reset", &bReset, COBOOL, FALSE}, + {"--reset", &bReset, COBOOL, FALSE}, + {"-r", &bReset, COBOOL, FALSE}, + }; + if (!GetCMDOption(args, option, ARRAY_SIZE(option), NULL, 0, NULL)) { - Status = hostServices->DispatchCommand("sosstatus", args); + return E_INVALIDARG; } - else + if (bReset) { - BOOL bReset = FALSE; - CMDOption option[] = - { // name, vptr, type, hasValue - {"-reset", &bReset, COBOOL, FALSE}, - {"--reset", &bReset, COBOOL, FALSE}, - {"-r", &bReset, COBOOL, FALSE}, - }; - if (!GetCMDOption(args, option, ARRAY_SIZE(option), NULL, 0, NULL)) - { - return Status; - } - if (bReset) + ITarget* target = GetTarget(); + if (target != nullptr) { - ITarget* target = GetTarget(); - if (target != nullptr) - { - target->Flush(); - } - ExtOut("Internal cached state reset\n"); - return S_OK; + target->Flush(); } - Target::DisplayStatus(); + ExtOut("Internal cached state reset\n"); + return S_OK; } - return Status; + Target::DisplayStatus(); + return S_OK; } #ifndef FEATURE_PAL @@ -8485,13 +8340,13 @@ DECLARE_API(Token2EE) size_t nArg; if (!GetCMDOption(args,option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } if (nArg!=2) { ExtOut("Usage: %stoken2ee module_name mdToken\n", SOSPrefix); ExtOut(" You can pass * for module_name to search all modules.\n"); - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -8557,7 +8412,7 @@ DECLARE_API(Token2EE) \**********************************************************************/ DECLARE_API(Name2EE) { - INIT_API(); + INIT_API_PROBE_MANAGED("name2ee"); MINIDUMP_NOT_SUPPORTED(); StringHolder DllName, TypeName; @@ -8577,7 +8432,7 @@ DECLARE_API(Name2EE) if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -8620,7 +8475,7 @@ DECLARE_API(Name2EE) ExtOut(" use * for module_name to search all loaded modules\n"); ExtOut("Examples: %sname2ee mscorlib.dll System.String.ToString\n", SOSPrefix); ExtOut(" %sname2ee *!System.String\n", SOSPrefix); - return Status; + return E_INVALIDARG; } int numModule; @@ -8675,43 +8530,9 @@ DECLARE_API(Name2EE) return Status; } - -DECLARE_API(PathTo) -{ - INIT_API_EXT(); - MINIDUMP_NOT_SUPPORTED(); - - return ExecuteCommand("pathto", args); -} - - -/**********************************************************************\ -* Routine Description: * -* * -* This function finds all roots (on stack or in handles) for a * -* given object. * -* * -\**********************************************************************/ -DECLARE_API(GCRoot) -{ - INIT_API_EXT(); - MINIDUMP_NOT_SUPPORTED(); - - return ExecuteCommand("gcroot", args); -} - -DECLARE_API(GCWhere) -{ - INIT_API_EXT(); - MINIDUMP_NOT_SUPPORTED(); - - return ExecuteCommand("gcwhere", args); -} - - DECLARE_API(FindRoots) { - INIT_API_EXT(); + INIT_API(); MINIDUMP_NOT_SUPPORTED(); if (IsDumpFile()) @@ -8737,7 +8558,7 @@ DECLARE_API(FindRoots) }; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -8939,8 +8760,10 @@ public: {"/d", &mDML, COBOOL, FALSE}, }; - if (!GetCMDOption(args,option,ARRAY_SIZE(option),NULL,0,NULL)) + if (!GetCMDOption(args, option, ARRAY_SIZE(option), NULL, 0, NULL)) + { sos::Throw("Failed to parse command line arguments."); + } if (type != NULL) { if (_stricmp(type, "Pinned") == 0) @@ -9326,7 +9149,7 @@ DECLARE_API(GetCodeTypeFlags) size_t nArg; if (!GetCMDOption(args, NULL, 0, arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } size_t preg = 1; // by default @@ -9336,7 +9159,7 @@ DECLARE_API(GetCodeTypeFlags) if (preg > 19) { ExtOut("Pseudo-register number must be between 0 and 19\n"); - return Status; + return E_INVALIDARG; } } @@ -9434,19 +9257,19 @@ DECLARE_API(StopOnException) size_t nArg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } if (IsDumpFile()) { ExtOut("Live debugging session required\n"); - return Status; + return E_INVALIDARG; } if (nArg < 1 || nArg > 2) { ExtOut("usage: StopOnException [-derived] [-create | -create2] \n"); ExtOut(" []\n"); ExtOut("ex: StopOnException -create System.OutOfMemoryException 1\n"); - return Status; + return E_INVALIDARG; } size_t preg = 1; // by default @@ -9456,7 +9279,7 @@ DECLARE_API(StopOnException) if (preg > 19) { ExtOut("Pseudo-register number must be between 0 and 19\n"); - return Status; + return E_INVALIDARG; } } @@ -9541,20 +9364,6 @@ DECLARE_API(StopOnException) return Status; } -/**********************************************************************\ -* Routine Description: * -* * -* This function finds the size of an object or all roots. * -* * -\**********************************************************************/ -DECLARE_API(ObjSize) -{ - INIT_API_EXT(); - MINIDUMP_NOT_SUPPORTED(); - - return ExecuteCommand("objsize", args); -} - #ifndef FEATURE_PAL // For FEATURE_PAL, MEMORY_BASIC_INFORMATION64 doesn't exist yet. TODO? DECLARE_API(GCHandleLeaks) @@ -9580,7 +9389,7 @@ DECLARE_API(GCHandleLeaks) if (!GetCMDOption(args, option, ARRAY_SIZE(option), NULL, 0, NULL)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -11582,7 +11391,7 @@ DECLARE_API(Watch) }; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } if(addExpression.data != NULL || aExpression.data != NULL) @@ -11671,7 +11480,7 @@ DECLARE_API(Watch) DECLARE_API(ClrStack) { - INIT_API(); + INIT_API_PROBE_MANAGED("clrstack"); BOOL bAll = FALSE; BOOL bParams = FALSE; @@ -11715,7 +11524,7 @@ DECLARE_API(ClrStack) }; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } EnableDMLHolder dmlHolder(dml); @@ -11785,7 +11594,7 @@ BOOL IsMemoryInfoAvailable() return TRUE; } -DECLARE_API( VMMap ) +DECLARE_API(VMMap) { INIT_API(); @@ -11799,30 +11608,21 @@ DECLARE_API( VMMap ) } return Status; -} // DECLARE_API( vmmap ) +} #endif // FEATURE_PAL DECLARE_API(SOSFlush) { - INIT_API_NOEE(); + INIT_API_NOEE_PROBE_MANAGED("sosflush"); - IHostServices* hostServices = GetHostServices(); - if (hostServices != nullptr) + ITarget* target = GetTarget(); + if (target != nullptr) { - Status = hostServices->DispatchCommand("sosflush", args); + target->Flush(); } - else - { - ITarget* target = GetTarget(); - if (target != nullptr) - { - target->Flush(); - } - ExtOut("Internal cached state reset\n"); - return S_OK; - } - return Status; + ExtOut("Internal cached state reset\n"); + return S_OK; } #ifndef FEATURE_PAL @@ -11867,16 +11667,16 @@ DECLARE_API(SaveModule) size_t nArg; if (!GetCMDOption(args, NULL, 0, arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } if (nArg != 2) { ExtOut("Usage: SaveModule
\n"); - return Status; + return E_INVALIDARG; } if (moduleAddr == 0) { ExtOut ("Invalid arg\n"); - return Status; + return E_INVALIDARG; } char* ptr = Location.data; @@ -12057,7 +11857,7 @@ DECLARE_API(dbgout) if (!GetCMDOption(args, option, ARRAY_SIZE(option), NULL, 0, NULL)) { - return Status; + return E_INVALIDARG; } Output::SetDebugOutputEnabled(!bOff); @@ -12532,7 +12332,7 @@ HRESULT CALLBACK _EFN_StackTrace( size_t uiSizeOfContext, DWORD Flags) { - INIT_API(); + INIT_API_EFN(); Status = ImplementEFNStackTraceTry(client, wszTextOut, puiTextLength, pTransitionContexts, puiTransitionContextCount, @@ -12830,7 +12630,7 @@ DECLARE_API(VerifyStackTrace) if (!GetCMDOption(args, option, ARRAY_SIZE(option), NULL,0,NULL)) { - return Status; + return E_INVALIDARG; } if (bVerifyManagedExcepStack) @@ -13036,7 +12836,7 @@ DECLARE_API(SaveState) size_t nArg; if (!GetCMDOption(args, NULL, 0, arg, ARRAY_SIZE(arg), &nArg)) { - return E_FAIL; + return E_INVALIDARG; } if(nArg == 0) @@ -13076,7 +12876,7 @@ DECLARE_API(SuppressJitOptimization) size_t nArg; if (!GetCMDOption(args, NULL, 0, arg, ARRAY_SIZE(arg), &nArg)) { - return E_FAIL; + return E_INVALIDARG; } if (nArg == 1 && (_stricmp(onOff.data, "On") == 0)) @@ -13405,7 +13205,7 @@ _EFN_GetManagedExcepStack( ULONG cbString ) { - INIT_API(); + INIT_API_EFN(); ArrayHolder tmpStr = new NOTHROW WCHAR[cbString]; if (tmpStr == NULL) @@ -13436,7 +13236,7 @@ _EFN_GetManagedExcepStackW( ULONG cchString ) { - INIT_API(); + INIT_API_EFN(); return ImplementEFNGetManagedExcepStack(StackObjAddr, wszStackString, cchString); } @@ -13452,7 +13252,7 @@ _EFN_GetManagedObjectName( ULONG cbName ) { - INIT_API (); + INIT_API_EFN(); if (!sos::IsObject(objAddr, false)) { @@ -13481,7 +13281,7 @@ _EFN_GetManagedObjectFieldInfo( PULONG pOffset ) { - INIT_API(); + INIT_API_EFN(); DacpObjectData objData; LPWSTR fieldName = (LPWSTR)alloca(mdNameLen * sizeof(WCHAR)); @@ -13540,7 +13340,7 @@ DECLARE_API(VerifyGMT) if (!GetCMDOption(args, NULL, 0, arg, ARRAY_SIZE(arg), &nArg)) { - return Status; + return E_INVALIDARG; } } ULONG64 managedThread; @@ -13565,7 +13365,7 @@ _EFN_GetManagedThread( ULONG osThreadId, PULONG64 pManagedThread) { - INIT_API(); + INIT_API_EFN(); _ASSERTE(pManagedThread != nullptr); *pManagedThread = 0; @@ -13623,7 +13423,7 @@ DECLARE_API(SetHostRuntime) size_t narg; if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &narg)) { - return E_FAIL; + return E_INVALIDARG; } if (narg > 0 || bNetCore || bNetFx || bNone) { @@ -13689,41 +13489,31 @@ exit: // DECLARE_API(SetClrPath) { - INIT_API_NOEE(); + INIT_API_NODAC_PROBE_MANAGED("setclrpath"); - IHostServices* hostServices = GetHostServices(); - if (hostServices != nullptr) + StringHolder runtimeModulePath; + CMDValue arg[] = + { + {&runtimeModulePath.data, COSTRING}, + }; + size_t narg; + if (!GetCMDOption(args, nullptr, 0, arg, ARRAY_SIZE(arg), &narg)) { - return hostServices->DispatchCommand("setclrpath", args); + return E_FAIL; } - else + if (narg > 0) { - INIT_API_EE(); - - StringHolder runtimeModulePath; - CMDValue arg[] = - { - {&runtimeModulePath.data, COSTRING}, - }; - size_t narg; - if (!GetCMDOption(args, nullptr, 0, arg, ARRAY_SIZE(arg), &narg)) + std::string fullPath; + if (!GetAbsolutePath(runtimeModulePath.data, fullPath)) { + ExtErr("Invalid runtime directory %s\n", fullPath.c_str()); return E_FAIL; } - if (narg > 0) - { - std::string fullPath; - if (!GetAbsolutePath(runtimeModulePath.data, fullPath)) - { - ExtErr("Invalid runtime directory %s\n", fullPath.c_str()); - return E_FAIL; - } - g_pRuntime->SetRuntimeDirectory(fullPath.c_str()); - } - const char* runtimeDirectory = g_pRuntime->GetRuntimeDirectory(); - if (runtimeDirectory != nullptr) { - ExtOut("Runtime module directory: %s\n", runtimeDirectory); - } + g_pRuntime->SetRuntimeDirectory(fullPath.c_str()); + } + const char* runtimeDirectory = g_pRuntime->GetRuntimeDirectory(); + if (runtimeDirectory != nullptr) { + ExtOut("Runtime module directory: %s\n", runtimeDirectory); } return S_OK; } @@ -13733,138 +13523,46 @@ DECLARE_API(SetClrPath) // DECLARE_API(runtimes) { - INIT_API_NOEE(); + INIT_API_NOEE_PROBE_MANAGED("runtimes"); - IHostServices* hostServices = GetHostServices(); - if (hostServices != nullptr) + BOOL bNetFx = FALSE; + BOOL bNetCore = FALSE; + CMDOption option[] = + { // name, vptr, type, hasValue + {"-netfx", &bNetFx, COBOOL, FALSE}, + {"-netcore", &bNetCore, COBOOL, FALSE}, + }; + if (!GetCMDOption(args, option, ARRAY_SIZE(option), NULL, 0, NULL)) { - Status = hostServices->DispatchCommand("runtimes", args); + return E_INVALIDARG; } - else + if (bNetCore || bNetFx) { - BOOL bNetFx = FALSE; - BOOL bNetCore = FALSE; - CMDOption option[] = - { // name, vptr, type, hasValue - {"-netfx", &bNetFx, COBOOL, FALSE}, - {"-netcore", &bNetCore, COBOOL, FALSE}, - }; - if (!GetCMDOption(args, option, ARRAY_SIZE(option), NULL, 0, NULL)) - { - return Status; - } - if (bNetCore || bNetFx) - { #ifndef FEATURE_PAL - if (IsWindowsTarget()) - { - PCSTR name = bNetFx ? "desktop .NET Framework" : ".NET Core"; - if (!Target::SwitchRuntime(bNetFx)) - { - ExtErr("The %s runtime is not loaded\n", name); - return E_FAIL; - } - ExtOut("Switched to %s runtime successfully\n", name); - } - else -#endif + if (IsWindowsTarget()) + { + PCSTR name = bNetFx ? "desktop .NET Framework" : ".NET Core"; + if (!Target::SwitchRuntime(bNetFx)) { - ExtErr("The '-netfx' and '-netcore' options are only supported on Windows targets\n"); - return E_FAIL; + ExtErr("The %s runtime is not loaded\n", name); + return E_INVALIDARG; } + ExtOut("Switched to %s runtime successfully\n", name); } else +#endif { - Target::DisplayStatus(); + ExtErr("The '-netfx' and '-netcore' options are only supported on Windows targets\n"); + return E_INVALIDARG; } } - return Status; -} - -#ifdef HOST_WINDOWS -// -// Sets the symbol server path. -// -DECLARE_API(SetSymbolServer) -{ - INIT_API_EXT(); - return ExecuteCommand("setsymbolserver", args); -} - -// -// Dumps the managed assemblies -// -DECLARE_API(clrmodules) -{ - INIT_API_EXT(); - return ExecuteCommand("clrmodules", args); -} - -// -// Dumps the Native AOT crash info -// -DECLARE_API(crashinfo) -{ - INIT_API_EXT(); - return ExecuteCommand("crashinfo", args); -} - -// -// Dumps async stacks -// -DECLARE_API(DumpAsync) -{ - INIT_API_EXT(); - return ExecuteCommand("dumpasync", args); -} - -// -// Enables and disables managed extension logging -// -DECLARE_API(logging) -{ - INIT_API_EXT(); - return ExecuteCommand("logging", args); -} - -typedef HRESULT (*PFN_COMMAND)(PDEBUG_CLIENT client, PCSTR args); - -// -// Executes managed extension commands -// -DECLARE_API(ext) -{ - INIT_API_EXT(); - - if (args == nullptr || strlen(args) <= 0) - { - args = "Help"; - } - std::string arguments(args); - size_t pos = arguments.find(' '); - std::string commandName = arguments.substr(0, pos); - if (pos != std::string::npos) - { - arguments = arguments.substr(pos + 1); - } else { - arguments.clear(); - } - Status = ExecuteCommand(commandName.c_str(), arguments.c_str()); - if (Status == E_NOTIMPL) - { - PFN_COMMAND commandFunc = (PFN_COMMAND)GetProcAddress(g_hInstance, commandName.c_str()); - if (commandFunc != nullptr) - { - Status = (*commandFunc)(client, arguments.c_str()); - } + Target::DisplayStatus(); } return Status; } -#endif // HOST_WINDOWS - void PrintHelp (__in_z LPCSTR pszCmdName) { static LPSTR pText = NULL; @@ -13969,7 +13667,7 @@ void PrintHelp (__in_z LPCSTR pszCmdName) \**********************************************************************/ DECLARE_API(Help) { - INIT_API_EXT(); + INIT_API_NOEE_PROBE_MANAGED("help"); StringHolder commandName; CMDValue arg[] = @@ -13986,15 +13684,6 @@ DECLARE_API(Help) if (nArg == 1) { - IHostServices* hostServices = GetHostServices(); - if (hostServices != nullptr) - { - if (hostServices->DisplayHelp(commandName.data) == S_OK) - { - return S_OK; - } - } - // Convert commandName to lower-case LPSTR curChar = commandName.data; while (*curChar != '\0') @@ -14016,12 +13705,6 @@ DECLARE_API(Help) else { PrintHelp ("contents"); - IHostServices* hostServices = GetHostServices(); - if (hostServices != nullptr) - { - ExtOut("\n"); - hostServices->DisplayHelp(nullptr); - } } return S_OK; diff --git a/src/SOS/Strike/util.h b/src/SOS/Strike/util.h index 50467dd8a..8eb3f60e5 100644 --- a/src/SOS/Strike/util.h +++ b/src/SOS/Strike/util.h @@ -1778,8 +1778,6 @@ CLRDATA_ADDRESS GetAppDomain(CLRDATA_ADDRESS objPtr); BOOL IsMTForFreeObj(DWORD_PTR pMT); -HRESULT ExecuteCommand(PCSTR commandName, PCSTR args); - enum ARGTYPE {COBOOL,COSIZE_T,COHEX,COSTRING}; struct CMDOption { diff --git a/src/SOS/inc/hostservices.h b/src/SOS/inc/hostservices.h index 788f6f353..74f181560 100644 --- a/src/SOS/inc/hostservices.h +++ b/src/SOS/inc/hostservices.h @@ -82,14 +82,6 @@ public: PCSTR commandName, PCSTR arguments) = 0; - /// - /// Displays the help for a managed extension command - /// - /// - /// error code - virtual HRESULT STDMETHODCALLTYPE DisplayHelp( - PCSTR commandName) = 0; - /// /// Uninitialize the extension infrastructure /// diff --git a/src/SOS/lldbplugin/soscommand.cpp b/src/SOS/lldbplugin/soscommand.cpp index cdb5c5537..95847b9bc 100644 --- a/src/SOS/lldbplugin/soscommand.cpp +++ b/src/SOS/lldbplugin/soscommand.cpp @@ -157,6 +157,7 @@ sosCommandInitialize(lldb::SBDebugger debugger) g_services->AddCommand("ext", new sosCommand(nullptr), "Executes various coreclr debugging commands. Use the syntax 'sos '. For more information, see 'soshelp'."); g_services->AddManagedCommand("analyzeoom", "Provides a stack trace of managed code only."); g_services->AddCommand("bpmd", new sosCommand("bpmd"), "Creates a breakpoint at the specified managed method in the specified module."); + g_services->AddManagedCommand("assemblies", "Lists the managed modules in the process."); g_services->AddManagedCommand("clrmodules", "Lists the managed modules in the process."); g_services->AddCommand("clrstack", new sosCommand("ClrStack"), "Provides a stack trace of managed code only."); g_services->AddCommand("clrthreads", new sosCommand("Threads"), "Lists the managed threads running."); @@ -204,12 +205,12 @@ sosCommandInitialize(lldb::SBDebugger debugger) g_services->AddCommand("histroot", new sosCommand("HistRoot"), "Displays information related to both promotions and relocations of the specified root."); g_services->AddCommand("histstats", new sosCommand("HistStats"), "Displays stress log stats."); g_services->AddCommand("ip2md", new sosCommand("IP2MD"), "Displays the MethodDesc structure at the specified address in code that has been JIT-compiled."); - g_services->AddCommand("listnearobj", new sosCommand("ListNearObj"), "Displays the object preceding and succeeding the specified address."); + g_services->AddManagedCommand("listnearobj", "Displays the object preceding and succeeding the specified address."); g_services->AddManagedCommand("loadsymbols", "Loads the .NET Core native module symbols."); g_services->AddManagedCommand("logging", "Enables/disables internal SOS logging."); g_services->AddCommand("name2ee", new sosCommand("Name2EE"), "Displays the MethodTable structure and EEClass structure for the specified type or method in the specified module."); g_services->AddManagedCommand("objsize", "Displays the size of the specified object."); - g_services->AddCommand("pathto", new sosCommand("PathTo"), "Displays the GC path from to ."); + g_services->AddManagedCommand("pathto", "Displays the GC path from to ."); g_services->AddCommand("pe", new sosCommand("PrintException"), "Displays and formats fields of any object derived from the Exception class at the specified address."); g_services->AddCommand("printexception", new sosCommand("PrintException"), "Displays and formats fields of any object derived from the Exception class at the specified address."); g_services->AddCommand("runtimes", new sosCommand("runtimes"), "Lists the runtimes in the target or change the default runtime."); @@ -225,5 +226,6 @@ sosCommandInitialize(lldb::SBDebugger debugger) g_services->AddCommand("token2ee", new sosCommand("token2ee"), "Displays the MethodTable structure and MethodDesc structure for the specified token and module."); g_services->AddManagedCommand("verifyheap", "Checks the GC heap for signs of corruption."); g_services->AddManagedCommand("verifyobj", "Checks the object that is passed as an argument for signs of corruption."); + g_services->AddManagedCommand("traverseheap", "Writes out heap information to a file in a format understood by the CLR Profiler."); return true; } diff --git a/src/Tools/dotnet-dump/Analyzer.cs b/src/Tools/dotnet-dump/Analyzer.cs index 6718677d6..2a2b5defc 100644 --- a/src/Tools/dotnet-dump/Analyzer.cs +++ b/src/Tools/dotnet-dump/Analyzer.cs @@ -63,11 +63,11 @@ namespace Microsoft.Diagnostics.Tools.Dump _consoleService.AddCommandHistory(history); } catch (Exception ex) when - (ex is IOException or - ArgumentNullException or - UnauthorizedAccessException or - NotSupportedException or - SecurityException) + (ex is IOException + or ArgumentNullException + or UnauthorizedAccessException + or NotSupportedException + or SecurityException) { } @@ -86,9 +86,6 @@ namespace Microsoft.Diagnostics.Tools.Dump // Add the specially handled exit command _commandService.AddCommands(typeof(ExitCommand), (services) => new ExitCommand(_consoleService.Stop)); - // Add "sos" command manually - _commandService.AddCommands(typeof(SOSCommand), (services) => new SOSCommand(_commandService, services)); - // Display any extension assembly loads on console _serviceManager.NotifyExtensionLoad.Register((Assembly assembly) => _fileLoggingConsoleService.WriteLine($"Loading extension {assembly.Location}")); _serviceManager.NotifyExtensionLoadFailure.Register((Exception ex) => _fileLoggingConsoleService.WriteLine(ex.Message)); @@ -107,6 +104,7 @@ namespace Microsoft.Diagnostics.Tools.Dump _serviceContainer.AddService(_fileLoggingConsoleService); _serviceContainer.AddService(DiagnosticLoggingService.Instance); _serviceContainer.AddService(_commandService); + _serviceContainer.AddService(_commandService); SymbolService symbolService = new(this); _serviceContainer.AddService(symbolService); @@ -133,18 +131,24 @@ namespace Microsoft.Diagnostics.Tools.Dump symbolService.AddCachePath(symbolService.DefaultSymbolCache); symbolService.AddDirectoryPath(Path.GetDirectoryName(dump_path.FullName)); - // Run the commands from the dotnet-dump command line + // Run the commands from the dotnet-dump command line. Any errors/exceptions from the + // command execution will be displayed and dotnet-dump exited. if (command != null) { - foreach (string cmd in command) + foreach (string commandLine in command) { - _commandService.Execute(cmd, contextService.Services); + if (!_commandService.Execute(commandLine, contextService.Services)) + { + throw new CommandNotFoundException($"{CommandNotFoundException.NotFoundMessage} '{commandLine}'"); + } if (_consoleService.Shutdown) { break; } } } + + // Now start the REPL command loop if the console isn't redirected if (!_consoleService.Shutdown && (!Console.IsOutputRedirected || Console.IsInputRedirected)) { // Start interactive command line processing @@ -153,21 +157,25 @@ namespace Microsoft.Diagnostics.Tools.Dump _consoleService.Start((string prompt, string commandLine, CancellationToken cancellation) => { _fileLoggingConsoleService.WriteLine("{0}{1}", prompt, commandLine); - _commandService.Execute(commandLine, contextService.Services); + if (!_commandService.Execute(commandLine, contextService.Services)) + { + throw new CommandNotFoundException($"{CommandNotFoundException.NotFoundMessage} '{commandLine}'"); + } }); } } catch (Exception ex) when - (ex is ClrDiagnosticsException or - FileNotFoundException or - DirectoryNotFoundException or - UnauthorizedAccessException or - PlatformNotSupportedException or - InvalidDataException or - InvalidOperationException or - NotSupportedException) + (ex is ClrDiagnosticsException + or DiagnosticsException + or FileNotFoundException + or DirectoryNotFoundException + or UnauthorizedAccessException + or PlatformNotSupportedException + or InvalidDataException + or InvalidOperationException + or NotSupportedException) { - _fileLoggingConsoleService.WriteError($"{ex.Message}"); + _fileLoggingConsoleService.WriteLineError($"{ex.Message}"); return Task.FromResult(1); } finally @@ -186,10 +194,10 @@ namespace Microsoft.Diagnostics.Tools.Dump File.WriteAllLines(historyFileName, _consoleService.GetCommandHistory()); } catch (Exception ex) when - (ex is IOException or - UnauthorizedAccessException or - NotSupportedException or - SecurityException) + (ex is IOException + or UnauthorizedAccessException + or NotSupportedException + or SecurityException) { } } diff --git a/src/Tools/dotnet-dump/Commands/ReadMemoryCommand.cs b/src/Tools/dotnet-dump/Commands/ReadMemoryCommand.cs index f4017f7b4..21f6475de 100644 --- a/src/Tools/dotnet-dump/Commands/ReadMemoryCommand.cs +++ b/src/Tools/dotnet-dump/Commands/ReadMemoryCommand.cs @@ -7,7 +7,7 @@ using Microsoft.Diagnostics.DebugServices; namespace Microsoft.Diagnostics.Tools.Dump { - [Command(Name = "readmemory", Aliases = new string[] { "d" }, Help = "Dumps memory contents.")] + [Command(Name = "d", Aliases = new string[] { "readmemory" }, Help = "Dumps memory contents.")] [Command(Name = "db", DefaultOptions = "--ascii:true --unicode:false --ascii-string:false --unicode-string:false -c:128 -l:1 -w:16", Help = "Dumps memory as bytes.")] [Command(Name = "dc", DefaultOptions = "--ascii:false --unicode:true --ascii-string:false --unicode-string:false -c:64 -l:2 -w:8", Help = "Dumps memory as chars.")] [Command(Name = "da", DefaultOptions = "--ascii:false --unicode:false --ascii-string:true --unicode-string:false -c:128 -l:1 -w:0", Help = "Dumps memory as zero-terminated byte strings.")] diff --git a/src/Tools/dotnet-dump/Commands/SOSCommand.cs b/src/Tools/dotnet-dump/Commands/SOSCommand.cs index a3c15fc79..a2045070c 100644 --- a/src/Tools/dotnet-dump/Commands/SOSCommand.cs +++ b/src/Tools/dotnet-dump/Commands/SOSCommand.cs @@ -9,55 +9,48 @@ using SOS.Hosting; namespace Microsoft.Diagnostics.Tools.Dump { - [Command(Name = "sos", Aliases = new string[] { "ext" }, Help = "Executes various SOS debugging commands.", Flags = CommandFlags.Global | CommandFlags.Manual)] + [Command(Name = "sos", Aliases = new string[] { "ext" }, Help = "Executes various SOS debugging commands.")] public class SOSCommand : CommandBase { - private readonly CommandService _commandService; - private readonly IServiceProvider _services; - private SOSHost _sosHost; + [ServiceImport] + public CommandService CommandService { get; set; } - [Argument(Name = "arguments", Help = "SOS command and arguments.")] + [ServiceImport] + public IServiceProvider Services { get; set; } + + [ServiceImport(Optional = true)] + public SOSHost SOSHost { get; set; } + + [Argument(Name = "command_and_arguments", Help = "SOS command and arguments.")] public string[] Arguments { get; set; } - public SOSCommand(CommandService commandService, IServiceProvider services) + public SOSCommand() { - _commandService = commandService; - _services = services; } public override void Invoke() { - string commandLine; - string commandName; + string command; + string arguments; if (Arguments != null && Arguments.Length > 0) { - commandLine = string.Concat(Arguments.Select((arg) => arg + " ")).Trim(); - commandName = Arguments[0]; + command = Arguments[0]; + arguments = string.Concat(Arguments.Skip(1).Select((arg) => arg + " ")).Trim(); } else { - commandLine = commandName = "help"; + command = "help"; + arguments = null; } - if (_commandService.IsCommand(commandName)) + if (CommandService.Execute(command, arguments, Services)) { - try - { - _commandService.Execute(commandLine, _services); - return; - } - catch (CommandNotSupportedException) - { - } + return; } - if (_sosHost is null) + if (SOSHost is null) { - _sosHost = _services.GetService(); - if (_sosHost is null) - { - throw new DiagnosticsException($"'{commandName}' command not found"); - } + throw new CommandNotFoundException($"{CommandNotFoundException.NotFoundMessage} '{command}'"); } - _sosHost.ExecuteCommand(commandLine); + SOSHost.ExecuteCommand(command, arguments); } } } diff --git a/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/CommandServiceTests.cs b/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/CommandServiceTests.cs new file mode 100644 index 000000000..34f018693 --- /dev/null +++ b/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/CommandServiceTests.cs @@ -0,0 +1,132 @@ +using Microsoft.Diagnostics.DebugServices.Implementation; +using Microsoft.Diagnostics.TestHelpers; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Xunit; +using Xunit.Abstractions; +using Xunit.Extensions; + +[assembly: SuppressMessage("Performance", "CA1825:Avoid zero-length array allocations.", Justification = "")] + +namespace Microsoft.Diagnostics.DebugServices.UnitTests +{ + public class CommandServiceTests : IDisposable + { + private const string ListenerName = "CommandServiceTests"; + + private static IEnumerable _configurations; + + /// + /// Get the first test asset dump. It doesn't matter which one. + /// + /// + public static IEnumerable GetConfiguration() + { + return _configurations ??= TestRunConfiguration.Instance.Configurations + .Where((config) => config.AllSettings.ContainsKey("DumpFile")) + .Take(1) + .Select(c => new[] { c }) + .ToImmutableArray(); + } + + ITestOutputHelper Output { get; set; } + + public CommandServiceTests(ITestOutputHelper output) + { + Output = output; + LoggingListener.EnableListener(output, ListenerName); + } + + void IDisposable.Dispose() => Trace.Listeners.Remove(ListenerName); + + [SkippableTheory, MemberData(nameof(GetConfiguration))] + public void CommandServiceTest1(TestConfiguration config) + { + using TestDump testDump = new(config); + + CaptureConsoleService consoleService = new(); + testDump.ServiceContainer.AddService(consoleService); + + CommandService commandService = new(); + testDump.ServiceContainer.AddService(commandService); + + // Add all the test commands + commandService.AddCommands(typeof(TestCommand1).Assembly); + + // See if the test commands exists + Assert.Contains(commandService.Commands, ((string name, string help, IEnumerable aliases) cmd) => cmd.name == "testcommand"); + + // Invoke only TestCommand1 + TestCommand1.FilterValue = true; + TestCommand1.Invoked = false; + TestCommand2.FilterValue = false; + TestCommand2.Invoked = false; + TestCommand3.FilterValue = false; + TestCommand3.Invoked = false; + Assert.True(commandService.Execute("testcommand", testDump.Target.Services)); + Assert.True(TestCommand1.Invoked); + Assert.False(TestCommand2.Invoked); + Assert.False(TestCommand3.Invoked); + + // Check for TestCommand1 help + string help1 = commandService.GetDetailedHelp("testcommand", testDump.Target.Services, consoleWidth: int.MaxValue); + Assert.NotNull(help1); + Output.WriteLine(help1); + Assert.Contains("Test command #1", help1); + + // Invoke only TestCommand2 + TestCommand1.FilterValue = false; + TestCommand1.Invoked = false; + TestCommand2.FilterValue = true; + TestCommand2.Invoked = false; + TestCommand3.FilterValue = false; + TestCommand3.Invoked = false; + Assert.True(commandService.Execute("testcommand", testDump.Target.Services)); + Assert.False(TestCommand1.Invoked); + Assert.True(TestCommand2.Invoked); + Assert.False(TestCommand3.Invoked); + + // Invoke only TestCommand3 + + TestCommand1.FilterValue = false; + TestCommand1.Invoked = false; + TestCommand2.FilterValue = false; + TestCommand2.Invoked = false; + TestCommand3.FilterValue = true; + TestCommand3.Invoked = false; + Assert.True(commandService.Execute("testcommand", "--foo 23", testDump.Target.Services)); + Assert.False(TestCommand1.Invoked); + Assert.False(TestCommand2.Invoked); + Assert.True(TestCommand3.Invoked); + + // Check for TestCommand3 help + string help3 = commandService.GetDetailedHelp("testcommand", testDump.Target.Services, consoleWidth: int.MaxValue); + Assert.NotNull(help3); + Output.WriteLine(help3); + Assert.Contains("Test command #3", help3); + + // Invoke none of the test commands + TestCommand1.FilterValue = false; + TestCommand1.Invoked = false; + TestCommand2.FilterValue = false; + TestCommand2.Invoked = false; + TestCommand3.FilterValue = false; + TestCommand3.Invoked = false; + try + { + Assert.False(commandService.Execute("testcommand", testDump.Target.Services)); + } + catch (DiagnosticsException ex) + { + Assert.Matches("Test command #2 filter", ex.Message); + } + Assert.False(TestCommand1.Invoked); + Assert.False(TestCommand2.Invoked); + Assert.False(TestCommand3.Invoked); + } + } +} diff --git a/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/DebugServicesTests.cs b/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/DebugServicesTests.cs index 040ff1d8a..fb7ba82b9 100644 --- a/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/DebugServicesTests.cs +++ b/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/DebugServicesTests.cs @@ -30,11 +30,11 @@ namespace Microsoft.Diagnostics.DebugServices.UnitTests public static IEnumerable GetConfigurations() { - _configurations ??= TestRunConfiguration.Instance.Configurations + return _configurations ??= TestRunConfiguration.Instance.Configurations .Where((config) => config.AllSettings.ContainsKey("DumpFile")) - .Select((config) => CreateHost(config)) - .Select((host) => new[] { host }).ToImmutableArray(); - return _configurations; + .Select(CreateHost) + .Select((host) => new[] { host }) + .ToImmutableArray(); } private static TestHost CreateHost(TestConfiguration config) @@ -116,6 +116,15 @@ namespace Microsoft.Diagnostics.DebugServices.UnitTests } Assert.NotNull(module); + if (OS.Kind != OSKind.Windows) + { + // Skip managed modules when running on Linux/OSX because of the 6.0 injection activation issue in the DAC + if (moduleData.TryGetValue("IsManaged", out bool isManaged) && isManaged) + { + continue; + } + } + if (host.Target.Host.HostType != HostType.Lldb) { // Check that the resulting module matches the test data @@ -264,6 +273,11 @@ namespace Microsoft.Diagnostics.DebugServices.UnitTests { throw new SkipTestException("Not supported on Alpine Linux"); } + // Disable running on Linux/OSX because of the 6.0 injection activation issue in the DAC + if (OS.Kind != OSKind.Windows) + { + throw new SkipTestException("Not supported on Linux"); + } IRuntimeService runtimeService = host.Target.Services.GetService(); Assert.NotNull(runtimeService); diff --git a/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/TestCommands.cs b/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/TestCommands.cs new file mode 100644 index 000000000..920190d72 --- /dev/null +++ b/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/TestCommands.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Xunit; + +namespace Microsoft.Diagnostics.DebugServices.UnitTests +{ + [Command(Name = "testcommand", Help = "Test command #1")] + public class TestCommand1 : CommandBase + { + public static bool FilterValue; + public static bool Invoked; + + [ServiceImport] + public ITarget Target { get; set; } + + [Argument(Name = "FileName", Help = "Test argument.")] + public string FileName { get; set; } + + public override void Invoke() + { + Assert.NotNull(Target); + Invoked = true; + } + + [FilterInvoke] + public bool FilterInvoke() => FilterValue; + } + + [Command(Name = "testcommand", Help = "Test command #2")] + public class TestCommand2 : CommandBase + { + public static bool FilterValue; + public static bool Invoked; + + [ServiceImport] + public ITarget Target { get; set; } + + [Option(Name = "--foo", Help = "Test option.")] + public int Foo { get; set; } + + public override void Invoke() + { + Assert.NotNull(Target); + Invoked = true; + } + + [FilterInvoke(Message = "Test command #2 filter")] + public bool FilterInvoke() => FilterValue; + } + + [Command(Name = "testcommand", Help = "Test command #3")] + public class TestCommand3 : CommandBase + { + public static bool FilterValue; + public static bool Invoked; + + [ServiceImport] + public ITarget Target { get; set; } + + [Option(Name = "--foo", Help = "Test option.")] + public int Foo { get; set; } + + public override void Invoke() + { + Assert.NotNull(Target); + Invoked = true; + } + + [FilterInvoke] + public static bool FilterInvoke() => FilterValue; + } +} diff --git a/src/tests/TestExtension/TestCommands.cs b/src/tests/TestExtension/TestCommands.cs new file mode 100644 index 000000000..9e7d6b0a4 --- /dev/null +++ b/src/tests/TestExtension/TestCommands.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Diagnostics.DebugServices; + +namespace TestExtension +{ + [Command(Name = "clrstack", Help = "Test command #1")] + public class TestCommand1 : CommandBase + { + [ServiceImport] + public ITarget Target { get; set; } + + [Argument(Name = "FileName", Help = "Test argument.")] + public string FileName { get; set; } + + public override void Invoke() + { + if (Target is null) + { + throw new ArgumentNullException(nameof(Target)); + } + WriteLine("Test command #1 invoked"); + } + + [FilterInvoke] + public bool FilterInvoke() => true; + } + + [Command(Name = "dumpheap", Help = "Test command #2")] + public class TestCommand2 : CommandBase + { + [ServiceImport] + public ITarget Target { get; set; } + + [Option(Name = "--foo", Help = "Test option.")] + public int Foo { get; set; } + + public override void Invoke() + { + if (Target is null) + { + throw new ArgumentNullException(nameof(Target)); + } + WriteLine("Test command #2 invoked"); + } + + [FilterInvoke] + public bool FilterInvoke() => true; + } + + [Command(Name = "dumpheap", Help = "Test command #3")] + public class TestCommand3 : CommandBase + { + [ServiceImport] + public ITarget Target { get; set; } + + [Option(Name = "--foo", Help = "Test option.")] + public int Foo { get; set; } + + public override void Invoke() + { + if (Target is null) + { + throw new ArgumentNullException(nameof(Target)); + } + WriteLine("Test command #3 invoked"); + } + + [FilterInvoke] + public bool FilterInvoke() => false; + } + + [Command(Name = "assemblies", Help = "Test command #4")] + public class TestCommand4 : CommandBase + { + [ServiceImport] + public ITarget Target { get; set; } + + [Option(Name = "--foo", Help = "Test option.")] + public int Foo { get; set; } + + public override void Invoke() + { + if (Target is null) + { + throw new ArgumentNullException(nameof(Target)); + } + WriteLine("Test command #4 invoked"); + } + + [FilterInvoke] + public static bool FilterInvoke() => true; + } + + [Command(Name = "ip2md", Help = "Test command #5")] + public class TestCommand5 : CommandBase + { + [ServiceImport] + public ITarget Target { get; set; } + + [Option(Name = "--bar", Help = "Test option #5.")] + public int Foo { get; set; } + + public override void Invoke() + { + if (Target is null) + { + throw new ArgumentNullException(nameof(Target)); + } + WriteLine("Test command #5 invoked"); + } + + [FilterInvoke] + public static bool FilterInvoke() => false; + } +} diff --git a/src/tests/TestExtension/TestExtension.csproj b/src/tests/TestExtension/TestExtension.csproj new file mode 100644 index 000000000..3db8fe9c1 --- /dev/null +++ b/src/tests/TestExtension/TestExtension.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + +