Command groups to support duplicate command names and better help support (#4285)
authorMike McLaughlin <mikem@microsoft.com>
Wed, 4 Oct 2023 23:17:50 +0000 (16:17 -0700)
committerGitHub <noreply@github.com>
Wed, 4 Oct 2023 23:17:50 +0000 (16:17 -0700)
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.

109 files changed:
diagnostics.sln
src/Microsoft.Diagnostics.DebugServices.Implementation/AssemblyResolver.cs
src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs
src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs
src/Microsoft.Diagnostics.DebugServices.Implementation/Utilities.cs
src/Microsoft.Diagnostics.DebugServices/CommandAttributes.cs
src/Microsoft.Diagnostics.DebugServices/DiagnosticsException.cs
src/Microsoft.Diagnostics.DebugServices/ICommandService.cs
src/Microsoft.Diagnostics.DebugServices/ProviderExportAttribute.cs
src/Microsoft.Diagnostics.DebugServices/ServiceContainer.cs
src/Microsoft.Diagnostics.ExtensionCommands/AnalyzeOOMCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/AssembliesCommand.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.ExtensionCommands/ClrMDHelper.cs
src/Microsoft.Diagnostics.ExtensionCommands/ClrMDHelperCommandBase.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.ExtensionCommands/ClrModulesCommand.cs [deleted file]
src/Microsoft.Diagnostics.ExtensionCommands/ClrRuntimeCommandBase.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.ExtensionCommands/DumpAsyncCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/DumpConcurrentDictionaryCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/DumpConcurrentQueueCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/DumpExceptionsCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/DumpGenCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/DumpHeapCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/DumpObjGCRefsCommand.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.ExtensionCommands/DumpObjGCRefsHelper.cs [deleted file]
src/Microsoft.Diagnostics.ExtensionCommands/DumpRuntimeTypeCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/DumpStackObjectsCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/EEHeapCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/ExtensionCommandBase.cs [deleted file]
src/Microsoft.Diagnostics.ExtensionCommands/ExtensionMethodHelpers.cs
src/Microsoft.Diagnostics.ExtensionCommands/FinalizeQueueCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/FindEphemeralReferencesToLOHCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/FindPointersInCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/FindReferencesToEphemeralCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/GCHeapStatCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/GCRootCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/GCToNativeCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/GCWhereCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/Host/ConsoleLoggingCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/Host/HelpCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/Host/LoggingCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/Host/RegistersCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/Host/SetSymbolServerCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/ListNearObjCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/MAddressCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/NativeAddressHelper.cs
src/Microsoft.Diagnostics.ExtensionCommands/NotReachableInRangeCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/ObjSizeCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacksCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/PathToCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/SimulateGCHeapCorruption.cs
src/Microsoft.Diagnostics.ExtensionCommands/SizeStatsCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/TaskStateCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/ThreadPoolCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/ThreadPoolQueueCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/TimersCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/TraverseHeapCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/VerifyHeapCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/VerifyObjectCommand.cs
src/Microsoft.Diagnostics.Repl/ConsoleService.cs
src/Microsoft.Diagnostics.Repl/ExitCommand.cs
src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs
src/Microsoft.Diagnostics.TestHelpers/TestHost/TestHost.cs
src/SOS/SOS.Extensions/DebuggerServices.cs
src/SOS/SOS.Extensions/HostServices.cs
src/SOS/SOS.Extensions/MemoryRegionServiceFromDebuggerServices.cs
src/SOS/SOS.Extensions/TargetFromFromDebuggerServices.cs
src/SOS/SOS.Hosting/Commands/SOSCommand.cs
src/SOS/SOS.Hosting/SOSHost.cs
src/SOS/SOS.Hosting/SOSLibrary.cs
src/SOS/SOS.UnitTests/ConfigFiles/Unix/Debugger.Tests.Config.txt
src/SOS/SOS.UnitTests/ConfigFiles/Windows/Debugger.Tests.Config.txt
src/SOS/SOS.UnitTests/SOS.UnitTests.csproj
src/SOS/SOS.UnitTests/SOS.cs
src/SOS/SOS.UnitTests/SOSRunner.cs
src/SOS/SOS.UnitTests/Scripts/ConcurrentDictionaries.script
src/SOS/SOS.UnitTests/Scripts/DivZero.script
src/SOS/SOS.UnitTests/Scripts/DumpGen.script
src/SOS/SOS.UnitTests/Scripts/GCTests.script
src/SOS/SOS.UnitTests/Scripts/LineNums.script
src/SOS/SOS.UnitTests/Scripts/NestedExceptionTest.script
src/SOS/SOS.UnitTests/Scripts/Overflow.script
src/SOS/SOS.UnitTests/Scripts/Reflection.script
src/SOS/SOS.UnitTests/Scripts/SimpleThrow.script
src/SOS/SOS.UnitTests/Scripts/StackAndOtherTests.script
src/SOS/SOS.UnitTests/Scripts/TestExtensions.script [new file with mode: 0644]
src/SOS/SOS.UnitTests/Scripts/WebApp.script
src/SOS/Strike/CMakeLists.txt
src/SOS/Strike/Strike.vcxproj
src/SOS/Strike/Strike.vcxproj.filters
src/SOS/Strike/exts.cpp
src/SOS/Strike/exts.h
src/SOS/Strike/gchist.cpp
src/SOS/Strike/managedcommands.cpp [new file with mode: 0644]
src/SOS/Strike/sos.def
src/SOS/Strike/sos_unixexports.src
src/SOS/Strike/sosdocs.txt
src/SOS/Strike/sosdocsunix.txt
src/SOS/Strike/strike.cpp
src/SOS/Strike/util.h
src/SOS/inc/hostservices.h
src/SOS/lldbplugin/soscommand.cpp
src/Tools/dotnet-dump/Analyzer.cs
src/Tools/dotnet-dump/Commands/ReadMemoryCommand.cs
src/Tools/dotnet-dump/Commands/SOSCommand.cs
src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/CommandServiceTests.cs [new file with mode: 0644]
src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/DebugServicesTests.cs
src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/TestCommands.cs [new file with mode: 0644]
src/tests/TestExtension/TestCommands.cs [new file with mode: 0644]
src/tests/TestExtension/TestExtension.csproj [new file with mode: 0644]

index c53cec4f3f9451c8187a54ad98be2123224ac2a3..23ae2ff91a3a2062650dc7032e1b6a16bb7abd47 100644 (file)
@@ -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}
index 95eb3690dc035bcda2f9a13baf568108dbe176d1..626b31082f50f1338cc8b61e4f30429a93de5394 100644 (file)
@@ -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;
         }
 
index 01e4cfdb6156ce29ec13b14d19938e2f3b764e42..2c87c851418213acb0951e4261a9524d1ee25de5 100644 (file)
@@ -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
     /// </summary>
     public class CommandService : ICommandService
     {
-        private Parser _parser;
-        private readonly CommandLineBuilder _rootBuilder;
-        private readonly Dictionary<string, CommandHandler> _commandHandlers = new();
+        private readonly List<CommandGroup> _commandGroups = new();
+        private readonly string _commandPrompt;
 
         /// <summary>
         /// Create an instance of the command processor;
@@ -32,8 +30,10 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
         /// <param name="commandPrompt">command prompted used in help message</param>
         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));
         }
 
         /// <summary>
@@ -41,125 +41,168 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
         /// </summary>
         /// <param name="commandLine">command line text</param>
         /// <param name="services">services for the command</param>
-        /// <returns>true success, false failure</returns>
+        /// <returns>true - found command, false - command not found</returns>
+        /// <exception cref="ArgumentException">empty command line</exception>
+        /// <exception cref="DiagnosticsException">other errors</exception>
+        /// <exception cref="CommandParsingException ">parsing error</exception>
         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);
+        }
+
+        /// <summary>
+        /// Parse and execute the command.
+        /// </summary>
+        /// <param name="commandName">command name</param>
+        /// <param name="commandArguments">command arguments/options</param>
+        /// <param name="services">services for the command</param>
+        /// <returns>true - found command, false - command not found</returns>
+        /// <exception cref="ArgumentException">empty command name or arguments</exception>
+        /// <exception cref="DiagnosticsException">other errors</exception>
+        /// <exception cref="CommandParsingException ">parsing error</exception>
+        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)
+        /// <summary>
+        /// Find, parse and execute the command.
+        /// </summary>
+        /// <param name="commandName">command name</param>
+        /// <param name="commandLineArray">command line</param>
+        /// <param name="services">services for the command</param>
+        /// <returns>true - found command, false - command not found</returns>
+        /// <exception cref="ArgumentException">empty command name</exception>
+        /// <exception cref="DiagnosticsException">other errors</exception>
+        /// <exception cref="CommandParsingException ">parsing error</exception>
+        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<string> 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<ITarget>();
-                        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;
         }
 
         /// <summary>
         /// Displays the help for a command
         /// </summary>
-        /// <param name="commandName">name of the command or alias</param>
         /// <param name="services">service provider</param>
-        /// <returns>true if success, false if command not found</returns>
-        public bool DisplayHelp(string commandName, IServiceProvider services)
+        /// <returns>command invocation and help enumeration</returns>
+        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<Command>().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<ITarget>();
-                    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<ITarget>();
+            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<Command>())
+        /// <summary>
+        /// Displays the detailed help for a command
+        /// </summary>
+        /// <param name="commandName">name of the command or alias</param>
+        /// <param name="services">service provider</param>
+        /// <param name="consoleWidth">the width to format the help or int.MaxValue</param>
+        /// <returns>help text or null if not found</returns>
+        public string GetDetailedHelp(string commandName, IServiceProvider services, int consoleWidth)
+        {
+            if (string.IsNullOrWhiteSpace(commandName))
+            {
+                throw new ArgumentNullException(nameof(commandName));
+            }
+            List<string> 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;
         }
 
         /// <summary>
-        /// Does this command or alias exists?
-        /// </summary>
-        /// <param name="commandName">command or alias name</param>
-        /// <returns>true if command exists</returns>
-        public bool IsCommand(string commandName) => _rootBuilder.Command.Children.Contains(commandName);
-
-        /// <summary>
-        /// Enumerates all the command's name and help
+        /// Enumerates all the command's name, help and aliases
         /// </summary>
-        public IEnumerable<(string name, string help, IEnumerable<string> aliases)> Commands => _commandHandlers.Select((keypair) => (keypair.Value.Name, keypair.Value.Help, keypair.Value.Aliases));
+        public IEnumerable<(string name, string help, IEnumerable<string> aliases)> Commands =>
+            _commandGroups.SelectMany((group) => group.CommandHandlers).Select((handler) => (handler.Name, handler.Help, handler.Aliases));
 
         /// <summary>
         /// 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<IServiceProvider, object> factory)
+        /// <summary>
+        /// This groups like commands that may have the same name as another group or the default one.
+        /// </summary>
+        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<string, CommandHandler> _commandHandlers = new();
 
-            foreach (string alias in commandAttribute.Aliases)
+            /// <summary>
+            /// Create an instance of the command processor;
+            /// </summary>
+            /// <param name="commandPrompt">command prompted used in help message</param>
+            public CommandGroup(string commandPrompt = null)
             {
-                command.AddAlias(alias);
+                _rootBuilder = new CommandLineBuilder(new Command(commandPrompt));
             }
 
-            foreach (PropertyInfo property in type.GetProperties().Where(p => p.CanWrite))
+            /// <summary>
+            /// Parse and execute the command line.
+            /// </summary>
+            /// <param name="commandLine">command line text</param>
+            /// <param name="services">services for the command</param>
+            /// <returns>true if command was found and executed without error</returns>
+            /// <exception cref="DiagnosticsException">parsing error</exception>
+            internal bool Execute(IReadOnlyList<string> 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<IConsoleService>()));
+                            handler.Invoke(context, services);
+                            return true;
+                        }
+                    }
+                }
+                return false;
+            }
+
+            /// <summary>
+            /// Build/return parser
+            /// </summary>
+            internal Parser Parser => _parser ??= _rootBuilder.Build();
+
+            /// <summary>
+            /// Returns all the command handler instances
+            /// </summary>
+            internal IEnumerable<CommandHandler> CommandHandlers => _commandHandlers.Values;
+
+            /// <summary>
+            /// Returns true if command or command alias is found
+            /// </summary>
+            internal bool Contains(string commandName) => _rootBuilder.Command.Children.Contains(commandName);
+
+            /// <summary>
+            /// Returns the command handler for the command or command alias
+            /// </summary>
+            /// <param name="commandName">command or alias</param>
+            /// <param name="handler">handler instance</param>
+            /// <returns>true if found</returns>
+            internal bool TryGetCommandHandler(string commandName, out CommandHandler handler)
+            {
+                handler = null;
+                if (TryGetCommand(commandName, out Command command))
+                {
+                    handler = command.Handler as CommandHandler;
+                }
+                return handler != null;
+            }
+
+            /// <summary>
+            /// Returns the command instance for the command or command alias
+            /// </summary>
+            /// <param name="commandName">command or alias</param>
+            /// <param name="command">command instance</param>
+            /// <returns>true if found</returns>
+            internal bool TryGetCommand(string commandName, out Command command)
+            {
+                command = _rootBuilder.Command.Children.GetByAlias(commandName) as Command;
+                return command != null;
+            }
+
+            /// <summary>
+            /// Add the commands and aliases attributes found in the type.
+            /// </summary>
+            /// <param name="type">Command type to search</param>
+            /// <param name="factory">function to create command instance</param>
+            internal void AddCommands(Type type, Func<IServiceProvider, object> 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<IServiceProvider, object> 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()}";
         }
 
         /// <summary>
@@ -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<IServiceProvider, object> _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<IServiceProvider, object> factory)
             {
                 _commandAttribute = commandAttribute;
                 _arguments = arguments;
-                _properties = properties;
+                _options = options;
                 _factory = factory;
 
-                _methodInfo = type.GetMethods().Where((methodInfo) => methodInfo.GetCustomAttribute<CommandInvokeAttribute>() != 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<CommandInvokeAttribute>() != null)
+                    {
+                        if (_methodInfo != null)
+                        {
+                            throw new ArgumentException($"Multiple CommandInvokeAttribute's found in {type}");
+                        }
+                        _methodInfo = methodInfo;
+                    }
+                    if (methodInfo.GetCustomAttribute<HelpInvokeAttribute>() != 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<FilterInvokeAttribute>();
+                    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<HelpInvokeAttribute>() != null).SingleOrDefault();
+                }
             }
 
             Task<int> ICommandHandler.InvokeAsync(InvocationContext context)
@@ -311,37 +552,25 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
             /// </summary>
             internal string Help => _commandAttribute.Help;
 
+            /// <summary>
+            /// Filter invoke message or null if no attribute or message
+            /// </summary>
+            internal string FilterInvokeMessage => _filterInvokeAttribute?.Message;
+
             /// <summary>
             /// Returns the list of the command's aliases.
             /// </summary>
             internal IEnumerable<string> Aliases => _commandAttribute.Aliases;
 
             /// <summary>
-            /// Returns true if the command should be added.
+            /// Returns the list of arguments
             /// </summary>
-            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<Argument> Arguments => _arguments.Select((a) => a.Argument);
+
+            /// <summary>
+            /// Returns true is the command is supported by the command filter. Calls the FilterInvokeAttribute marked method.
+            /// </summary>
+            internal bool IsCommandSupported(Parser parser, IServiceProvider services) => _methodInfoFilter == null || (bool)Invoke(_methodInfoFilter, context: null, parser, services);
 
             /// <summary>
             /// Execute the command synchronously.
@@ -350,32 +579,56 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
             /// <param name="services">service provider</param>
             internal void Invoke(InvocationContext context, IServiceProvider services) => Invoke(_methodInfo, context, context.Parser, services);
 
+            /// <summary>
+            /// Return the various ways the command can be invoked. For building the help text.
+            /// </summary>
+            internal string HelpInvocation
+            {
+                get
+                {
+                    IEnumerable<string> 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;
+                }
+            }
+
             /// <summary>
             /// Executes the command's help invoke function if exists
             /// </summary>
             /// <param name="parser">parser instance</param>
             /// <param name="services">service provider</param>
             /// <returns>true help called, false no help function</returns>
-            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
         }
 
         /// <summary>
-        /// 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.
         /// </summary>
-        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
         }
 
         /// <summary>
-        /// 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.
         /// </summary>
         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<IConsoleService>();
-                    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<string> _write;
+            #endregion
+        }
 
-                public StandardStreamWriter(Action<string> write) => _write = write;
+        private sealed class StandardStreamWriter : IStandardStreamWriter
+        {
+            private readonly Action<string> _write;
 
-                void IStandardStreamWriter.Write(string value) => _write(value);
-            }
+            public StandardStreamWriter(Action<string> write) => _write = write;
 
-            #endregion
+            void IStandardStreamWriter.Write(string value) => _write(value);
         }
     }
 }
index 090270e28887e30b69533602948c15f386387380..19904d2f103f1f6e81811505c57342a9edd9ac8d 100644 (file)
@@ -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;
             }
         }
index cc69c944aaf452550a9cc11d4abf8ff2b5f923ad..fd231f2270909905a870f968ecea95d44ff92523 100644 (file)
@@ -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
         /// <remarks>
         /// 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!
         /// </remarks>
         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
+    }
 }
index 4b1df4b601e4423d2ced63fdec80e8e391158fee..bd988f0c20003d18288145558a91072f6c04266d 100644 (file)
@@ -6,32 +6,6 @@ using System.Diagnostics;
 
 namespace Microsoft.Diagnostics.DebugServices
 {
-    /// <summary>
-    /// Command flags to filter by OS Platforms, control scope and how the command is registered.
-    /// </summary>
-    [Flags]
-    public enum CommandFlags : byte
-    {
-        Windows = 0x01,
-        Linux = 0x02,
-        OSX = 0x04,
-
-        /// <summary>
-        /// Command is supported when there is no target
-        /// </summary>
-        Global = 0x08,
-
-        /// <summary>
-        /// Command is not added through reflection, but manually with command service API.
-        /// </summary>
-        Manual = 0x10,
-
-        /// <summary>
-        /// Default. All operating system, but target is required
-        /// </summary>
-        Default = Windows | Linux | OSX
-    }
-
     /// <summary>
     /// Marks the class as a Command.
     /// </summary>
@@ -53,11 +27,6 @@ namespace Microsoft.Diagnostics.DebugServices
         /// </summary>
         public string[] Aliases = Array.Empty<string>();
 
-        /// <summary>
-        /// Command flags to filter by OS Platforms, control scope and how the command is registered.
-        /// </summary>
-        public CommandFlags Flags = CommandFlags.Default;
-
         /// <summary>
         /// A string of options that are parsed before the command line options
         /// </summary>
@@ -121,10 +90,24 @@ namespace Microsoft.Diagnostics.DebugServices
     }
 
     /// <summary>
-    /// 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.
     /// </summary>
     [AttributeUsage(AttributeTargets.Method)]
     public class HelpInvokeAttribute : Attribute
     {
     }
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    [AttributeUsage(AttributeTargets.Method)]
+    public class FilterInvokeAttribute : Attribute
+    {
+        /// <summary>
+        /// Message to display if the filter fails
+        /// </summary>
+        public string Message;
+    }
 }
index c6ea751959642718b8f83da301dfaca38a4481f3..fa5d9c85cdcbb91ad01d693b8ea771da16b4f1a7 100644 (file)
@@ -27,23 +27,43 @@ namespace Microsoft.Diagnostics.DebugServices
     }
 
     /// <summary>
-    /// Thrown if a command is not supported on the configuration, platform or runtime
+    /// Thrown if a command is not found.
     /// </summary>
-    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)
         {
         }
+    }
+
+    /// <summary>
+    /// Thrown if a command is not found.
+    /// </summary>
+    public class CommandParsingException : DiagnosticsException
+    {
+        /// <summary>
+        /// The detailed help of the command
+        /// </summary>
+        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;
         }
     }
 }
index 2a4dbe810936314454e8983e135b929554b8246c..64557674498588ae82a8bca300f3fba6a592d270 100644 (file)
@@ -12,7 +12,7 @@ namespace Microsoft.Diagnostics.DebugServices
     public interface ICommandService
     {
         /// <summary>
-        /// Enumerates all the command's name and help
+        /// Enumerates all the command's name, help and aliases
         /// </summary>
         IEnumerable<(string name, string help, IEnumerable<string> aliases)> Commands { get; }
 
@@ -23,11 +23,19 @@ namespace Microsoft.Diagnostics.DebugServices
         void AddCommands(Type type);
 
         /// <summary>
-        /// Displays the help for a command
+        /// Gets help for all of the commands
+        /// </summary>
+        /// <param name="services">service provider</param>
+        /// <returns>command invocation and help enumeration</returns>
+        public IEnumerable<(string Invocation, string Help)> GetAllCommandHelp(IServiceProvider services);
+
+        /// <summary>
+        /// Displays the detailed help for a command
         /// </summary>
         /// <param name="commandName">name of the command or alias</param>
         /// <param name="services">service provider</param>
-        /// <returns>true if success, false if command not found</returns>
-        bool DisplayHelp(string commandName, IServiceProvider services);
+        /// <param name="consoleWidth">the width to format the help or int.MaxValue</param>
+        /// <returns>help text or null if not found</returns>
+        string GetDetailedHelp(string commandName, IServiceProvider services, int consoleWidth);
     }
 }
index 8a70aff3591966597c5c89df0648d08fa82e9ce2..be60004fa177316dd5af5c76acac842de68e6e4a 100644 (file)
@@ -13,7 +13,7 @@ namespace Microsoft.Diagnostics.DebugServices
     {
         /// <summary>
         /// 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.
         /// </summary>
         public Type Type { get; set; }
 
index 9b497c53fea676c7b3836874762552d76670e146..df9aa18d95c9f203d966fc8a46003fa112c9fec7 100644 (file)
@@ -29,9 +29,8 @@ namespace Microsoft.Diagnostics.DebugServices
         /// </summary>
         /// <param name="parent">search this provider if service isn't found in this instance or null</param>
         /// <param name="factories">service factories to initialize provider or null</param>
-        public ServiceContainer(IServiceProvider parent, Dictionary<Type, ServiceFactory> factories)
+        public ServiceContainer(IServiceProvider parent, Dictionary<Type, ServiceFactory> factories = null)
         {
-            Debug.Assert(factories != null);
             _parent = parent;
             _factories = factories;
             _instances = new Dictionary<Type, object>();
@@ -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);
index 35e38607a0242e4cda017b9874e6f54b584c33cb..ad970e05bb6ba35f31cb5ae0f35e697941c088f7 100644 (file)
@@ -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 (file)
index 0000000..ebeda4e
--- /dev/null
@@ -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() ?? "<none>");
+                        Version version = null;
+                        try
+                        {
+                            version = ModuleService.GetModuleFromBaseAddress(module.ImageBase).GetVersionData();
+                        }
+                        catch (DiagnosticsException)
+                        {
+                        }
+                        WriteLine("    Version:         {0}", version?.ToString() ?? "<none>");
+                    }
+                    else
+                    {
+                        WriteLine("{0:X16} {1:X8} {2}{3}", module.ImageBase, module.Size, module.Name, module.IsDynamic ? "(Dynamic)" : "");
+                    }
+                }
+            }
+        }
+    }
+}
index 67e42bac97a85c7e940a62aec9c0eaf89e6e7fcc..1d87e10de319d6c6ea2119634b1248949561d8be 100644 (file)
@@ -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 (file)
index 0000000..b91d528
--- /dev/null
@@ -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
+    {
+        /// <summary>
+        /// Helper bound to the current ClrRuntime that provides high level services on top of ClrMD.
+        /// </summary>
+        [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 (file)
index 6e19a34..0000000
+++ /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() ?? "<none>");
-                        Version version = null;
-                        try
-                        {
-                            version = ModuleService.GetModuleFromBaseAddress(module.ImageBase).GetVersionData();
-                        }
-                        catch (DiagnosticsException)
-                        {
-                        }
-                        WriteLine("    Version:         {0}", version?.ToString() ?? "<none>");
-                    }
-                    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 (file)
index 0000000..2f13912
--- /dev/null
@@ -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;
+    }
+}
index 0acbca362eb2f6f0ae1ea0eaf8b5bdc455d62bf5..954fd6fa614cf1631d4c00b6a30fc64cb0c6fc15 100644 (file)
@@ -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
     {
         /// <summary>The name of the command.</summary>
         private const string CommandName = "dumpasync";
 
         /// <summary>Indent width.</summary>
         private const int TabWidth = 2;
+
         /// <summary>The command invocation syntax when used in Debugger Markup Language (DML) commands.</summary>
         private const string DmlCommandInvoke = $"!{CommandName}";
 
-        /// <summary>The help text to render when asked for help.</summary>
-        private static readonly string s_detailedHelpText =
-            $"Usage: {CommandName} [--stats] [--coalesce] [--address <object address>] [--methodtable <mt address>] [--type <partial type name>] [--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";
-
-        /// <summary>Gets the runtime for the process.  Set by the command framework.</summary>
-        [ServiceImport(Optional = true)]
-        public ClrRuntime? Runtime { get; set; }
-
-
         /// <summary>Gets whether to only show stacks that include the object with the specified address.</summary>
         [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; }
 
         /// <summary>Invokes the command.</summary>
-        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
         }
 
         /// <summary>Gets detailed help for the command.</summary>
-        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
+";
 
         /// <summary>Represents an async object to be used as a frame in an async "stack".</summary>
         private sealed class AsyncObject
index b723a5d01225ea6e9545f0335fed3970f26d6e8f..06bee8bc6ed53a17ca2e4071edd21828d5d73950 100644 (file)
@@ -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<System.Int32, ForDump.DumpStr
 - In case of reference types, the command to dump each object is shown (e.g. dumpobj <[item] address>).
 - For value types, the command to dump each value type is shown (e.g. dumpvc <the Element Methodtable> <[item] address>).
 ";
-        }
 
         private static string Truncate(string str, int nbMaxChars)
         {
index 62c31f97f6a2384c41df179caa7cd35d8ef0044d..9252a6379eb37103e1c53d482b22caaf4ebd0d58 100644 (file)
@@ -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<System.Int32>
+   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<ForDump.ReferenceType>
+   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<System.Int32>" + 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<ForDump.ReferenceType>" + 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 <the Element Methodtable> <[item] address>." + Environment.NewLine +
-    "> dcq 00000202a7933370" + Environment.NewLine +
-    "System.Collections.Concurrent.ConcurrentQueue<ForDump.ValueType>" + 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 <the Element Methodtable> <[item] address>.
+> dcq 00000202a7933370
+System.Collections.Concurrent.ConcurrentQueue<ForDump.ValueType>
+   1 - dumparray 202a79334e0
+   2 - dumparray 202a7938a88
+";
     }
 }
index 4f57cf389763b1752af27def3941c88856717350..b2384cfada879486f0c16d2906e0eb12507348a3 100644 (file)
@@ -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!;
 
index db020e79269f2a7fade5d76590981dcddd0d7cfe..a3ec51e17b16b334533667a5234d27ad2b43fa24 100644 (file)
@@ -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
 ";
-        }
     }
 }
index 5f2faf6549357c87339e3ba4635428dbafae2609..38aa12c77374f0cbcb089331f2b5f6ece81a99cb 100644 (file)
@@ -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 (file)
index 0000000..cdd53f2
--- /dev/null
@@ -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 (file)
index adae249..0000000
+++ /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;
-        }
-    }
-}
index cfc8579e6f78424081b6e4b2ca1d1cf4b2c624f3..8c2de445c6c8a350771185f69c6e13f406f3859f 100644 (file)
@@ -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;
index 131450fe8b1abd6f5f4238e4e8c8ba580d2e63c5..2a211c1eb58a32e5ca78f68d8f36e5ba3c70ad55 100644 (file)
@@ -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()
index c565148650e0be143417f632cc7cea4d00f58224..59fb9fc3cef166fc87a6053c66cca042bebb3acb 100644 (file)
@@ -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 (file)
index 319b45d..0000000
+++ /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
-    {
-        /// <summary>
-        /// Helper bound to the current ClrRuntime that provides high level services on top of ClrMD.
-        /// </summary>
-        [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();
-    }
-}
index 348a23eb84d5d2a86cfad993ae4940254273b8ef..778aab9693365507ca7c5ac5ca3f8cf64b0fbdeb 100644 (file)
@@ -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)
index 9d4efb2ede5efe3da964fe38d76b1e8ae8ec0317..f74aee474a2507ec8409ea6481f0fb7a82a57c66 100644 (file)
@@ -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<ClrObject> EnumerateFinalizableObjects(bool allReady, ulong mt)
         {
             IEnumerable<ClrObject> result = EnumerateValidFinalizableObjectsWithTypeFilter(mt);
index e1715396dab7630634f0aed8f8cfc6689c67c531..221c06f5027c0a054a841641c17b309eb4352ff7 100644 (file)
@@ -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);
index 752bb338c33e24adc3b2ef63119d1a3033d7004c..755328cf4015e10015287447c6925e5eaa227291 100644 (file)
@@ -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---------------
-");
-        }
+";
     }
 }
index 6614438d787479bddb0fa073911e7daef3b0f60e..571a4b97d2c4a572e64ff4eca92718696a2828a3 100644 (file)
@@ -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<ulong> _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<EphemeralRefCount> FindObjectsWithEphemeralReferences()
         {
             foreach (ClrSegment seg in Runtime.Heap.Segments)
index 7a3da019950fa09732f19a0870726103b5f235ec..3552daa85b093155f64716308c7fd3d27381db63 100644 (file)
@@ -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; }
 
index 7260a22eef8bde818f17836e3d2e79c75929aa74..1cf459013a9f0747c1d31b23bdf083ae16f3e839 100644 (file)
@@ -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);
index e5157d4d3119d7f900db2b84638ee14c5deb9ed1..e3e46d6d9f750f1c557be557cbec866b4f99bbb8 100644 (file)
@@ -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
-");
-        }
+";
     }
 }
index 96a61f378357d89b3417a9e360f6573204cab3dd..2ae31938469db59d9271e30c451fa91ddec353dd 100644 (file)
@@ -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; }
 
index f274ba4839786117334f0edf1759160af2dd3f82..38f972e83bebdca4b9eac133e8606b440b134d6f 100644 (file)
@@ -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)]
index f7c6db56adcd7be80d0c3c2b0eb0d93ba91ebec6..2770c53247b636c505b609567a60b7c972cee17e 100644 (file)
@@ -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);
             }
         }
     }
index 18183f6d7e7ce8825c4cb7846c7699c0f82101a3..de53f34e4952be94885264f7fa48205182533561 100644 (file)
@@ -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)]
index 3ce4d83fbe8f9a4b954648a8a5bd160033750c11..78d2c07b0745799a76dd06b502e8a889f5044bf7 100644 (file)
@@ -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.")]
index d94f035d918b3b0405f7057f1135751d6974cf15..fe9ebc59e756fedd968e6713ba62908b72f1babb 100644 (file)
@@ -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.
+";
     }
 }
index 1b4411925f673378c33606d7897d50e526182020..afeffcbfed4fac642b86bcbda2223c74d51c9733 100644 (file)
@@ -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; }
 
index fa221c96ef8ec4d1154dcaf3e7740f2037aad604..9a1774fdc6b9217592454e26bf24281473a17a58 100644 (file)
@@ -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.
-");
-        }
+";
     }
 }
index a8e46ed7c3951f94c0f8c308fe43a93c6892d1ac..7de6a766afd851b5bfa654188b4dc79af658be78 100644 (file)
@@ -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<ClrRuntime>();
-                    RootCacheService rootCache = runtime.Services.GetService<RootCacheService>();
                     if (clrRuntime is not null)
                     {
+                        RootCacheService rootCache = runtime.Services.GetService<RootCacheService>() ?? 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
index 59e399e303152af4251a301155a950c64532d979..27ec0b6fe42b1c64b7d1c3d9083692dc3f2c7e7f 100644 (file)
@@ -14,7 +14,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands
     /// Prints objects and statistics for a range of object pointers.
     /// </summary>
     [Command(Name = "notreachableinrange", Help = "A helper command for !finalizerqueue")]
-    public class NotReachableInRangeCommand : CommandBase
+    public class NotReachableInRangeCommand : ClrRuntimeCommandBase
     {
         private HashSet<ulong> _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; }
 
index 0beb0e98f01f8b421920bab8ebd4190e77260e01..c65f381fa4550a15a043d2ee15a2d464b91f4b51 100644 (file)
@@ -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; }
 
index f61442a9023bddff5cadb128da5c96f55ea2af40..3320f11d34e1ccb650bea082d789b50b16e093cb 100644 (file)
@@ -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
+";
     }
 }
index e63940d3beab218e9118df0dfa286bedde10a5bb..ac0975b85926879fc514d82d44239227b9394725 100644 (file)
@@ -7,12 +7,9 @@ using Microsoft.Diagnostics.Runtime;
 
 namespace Microsoft.Diagnostics.ExtensionCommands
 {
-    [Command(Name ="pathto", Help = "Displays the GC path from <root> to <target>.")]
-    public class PathToCommand : CommandBase
+    [Command(Name ="pathto", Aliases = new[] { "PathTo" }, Help = "Displays the GC path from <root> to <target>.")]
+    public class PathToCommand : ClrRuntimeCommandBase
     {
-        [ServiceImport]
-        public ClrRuntime Runtime { get; set; }
-
         [ServiceImport]
         public RootCacheService RootCache { get; set; }
 
index 2105875b9ddfd27edf28f5b81b62652ac83859d6..e018cf218b6b9f4c23d187882afc4c182a279d5c 100644 (file)
@@ -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<Change> _changes = new();
 
         [ServiceImport]
         public IMemoryService MemoryService { get; set; }
 
-        [ServiceImport]
-        public ClrRuntime Runtime { get; set; }
-
         [Argument]
         public string Command { get; set; }
 
index b8f67a7a5fe4d59c9d07c3567fd8cdf22fa108fb..a1b59b07a0fc5a7cdc8c4cd97c848fead4033f27 100644 (file)
@@ -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);
index 63a7e4536c2c207a4db7abeaa861ea65bbe115cd..716fec09030ac36e42e406f458979dd2c98f3a19 100644 (file)
@@ -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 = "<value> 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 <decimal state value>]
+
+TaskState translates a Task m_stateFlags field value into human readable format.
+It supports hexadecimal address corresponding to a task instance or -v <decimal state value>.
+
+> tks 000001db16cf98f0
+Running
 
-        private readonly string DetailedHelpText =
-            "-------------------------------------------------------------------------------" + Environment.NewLine +
-            "TaskState [hexa address] [-v <decimal state value>]" + 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 <decimal state value>." + Environment.NewLine +
-            Environment.NewLine +
-            "> tks 000001db16cf98f0" + Environment.NewLine +
-            "Running" + Environment.NewLine +
-            Environment.NewLine +
-            "> tks -v 73728" + Environment.NewLine +
-            "WaitingToRun"
-            ;
+> tks -v 73728
+WaitingToRun
+";
     }
 }
index 621b6a501c5358194a086b59a79d7d79a8fbd70e..f78549ffcadf9069081ae285e2e1b22cc16f314b 100644 (file)
@@ -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; }
 
index bee1b08f44b52854c7d22928a3e714d6ba8d0f06..85b5d9b4eddfb5075f108fa90845abb2d4088044 100644 (file)
@@ -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<string, WorkInfo> 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<System.Action>.<ProcessAsyncIfNecessary_Slow>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<System.Action>.<ProcessAsyncIfNecessary_Slow>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<System.Action>.<ProcessAsyncIfNecessary_Slow>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<System.Action>.<ProcessAsyncIfNecessary_Slow>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
         {
index 6a040242a625049d0843a38fd2e9752c64c770ee..2c576c7193f9f5fb681769193ba3c35e989974b2 100644 (file)
@@ -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.<Delay>b__260_1" + Environment.NewLine +
-            "0x000001E09BD09B40 @       1 ms every   ------ ms |  0x000001E09BD09AD8 (System.Threading.Tasks.Task+DelayPromise) -> System.Threading.Tasks.Task+<>c.<Delay>b__260_1" + Environment.NewLine +
-            "0x000001E29BD58C68 @       1 ms every   ------ ms |  0x000001E29BD58C00 (System.Threading.Tasks.Task+DelayPromise) -> System.Threading.Tasks.Task+<>c.<Delay>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.<Delay>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.<Delay>b__260_1
+0x000001E09BD09B40 @       1 ms every   ------ ms |  0x000001E09BD09AD8 (System.Threading.Tasks.Task+DelayPromise) -> System.Threading.Tasks.Task+<>c.<Delay>b__260_1
+0x000001E29BD58C68 @       1 ms every   ------ ms |  0x000001E29BD58C00 (System.Threading.Tasks.Task+DelayPromise) -> System.Threading.Tasks.Task+<>c.<Delay>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.<Delay>b__260_1
+";
+    }
 
     internal sealed class TimerStat
     {
index 40d28205028b60a5792ec6eb41e19589517e15d7..57315180bba1e0edbbce35c3d34d585018a787f5 100644 (file)
@@ -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; }
 
index e5229cbf3239fb937d3e6432886830aa018bd054..c7b7b19578b6a481ff45c66d195f4d82e3751d28 100644 (file)
@@ -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; }
 
index f952aca3615e7edb2762cd3b73bd6600959db8e8..a9779d337b5d5733328e0818da947e7cbc5f6a31 100644 (file)
@@ -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()
index 06aaf7b478e1821ea42386ac8b04ce3059b70a18..4147eabb3d46b2c52d4d962e49258e3d48fa72ed 100644 (file)
@@ -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;
index a8d5bc6bab26abc3d81d299f145ab11d760255fe..f5fff7e1884c88dadc5e81b9036735eeb9d9fd5e 100644 (file)
@@ -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;
index 815ba0cfb304aed02cce693f2bcef551ddb07385..0f24d19275ce831a9f155fd554e2bed7a37cd7ae 100644 (file)
@@ -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);
index c86b121a63bc5e4233fe40f27b3c28f17787a68f..93eea0eec2c1dbe0a2a3d1c509373ca881506cd2 100644 (file)
@@ -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
index 97ea031083ca4b563ff314ea54bd238d30ff8cdf..b5702a7a8280b95d845b3c4825e238179721dbf7 100644 (file)
@@ -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);
index 900f6f43ca2e9040e6580c9cbaf9ff056eaea923..9ee85d194225b4bb1d359bfebe64a95d73e6840e 100644 (file)
@@ -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
     /// <summary>
     /// The extension services Wrapper the native hosts are given
     /// </summary>
-    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<IServiceManager>(_serviceManager);
                 _serviceContainer.AddService<IHost>(this);
+                _serviceContainer.AddService<SOSLibrary.ISOSModule>(this);
+                _serviceContainer.AddService<SOSHost.INativeClient>(DebuggerServices);
                 _serviceContainer.AddService<ICommandService>(_commandService);
                 _serviceContainer.AddService<ISymbolService>(_symbolService);
                 _serviceContainer.AddService<IConsoleService>(fileLoggingConsoleService);
@@ -232,6 +236,10 @@ namespace SOS.Extensions
                 ThreadUnwindServiceFromDebuggerServices threadUnwindService = new(DebuggerServices);
                 _serviceContainer.AddService<IThreadUnwindService>(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<string> 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<IMemoryRegionService>(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<IConsoleService>();
+                // 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);
index 15c729c1dfdbadb82ee6c5a7cde8316154524382..403bcbc8791ca23e53feaf23b8c88e3448130cac 100644 (file)
@@ -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<IMemoryRegion> EnumerateRegions()
index 60aea85883c5c6e2eb3ab1e47007c113226af941..e0c2bee85b63234079439fd4f79491d1628b86de 100644 (file)
@@ -99,6 +99,11 @@ namespace SOS.Extensions
             _serviceContainerFactory.AddServiceFactory<ICrashInfoService>((services) => CreateCrashInfoService(services, debuggerServices));
             OnFlushEvent.Register(() => FlushService<ICrashInfoService>());
 
+            if (debuggerServices.DebugClient is not null)
+            {
+                _serviceContainerFactory.AddServiceFactory<IMemoryRegionService>((services) => new MemoryRegionServiceFromDebuggerServices(debuggerServices.DebugClient));
+            }
+
             Finished();
         }
 
index 1190fd29abeff5ec0bd86c56d47f82edf88c5c58..dfa4a32532811d808d0f29a11cab9cc2c0f014dd 100644 (file)
@@ -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
+    {
+        /// <summary>
+        /// These commands are Windows only.
+        /// </summary>
+        [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
+    {
+        /// <summary>
+        /// Empty service used to prevent native commands from being run
+        /// </summary>
+        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]);
         }
+
+        /// <summary>
+        /// Common native SOS command filter function.
+        /// </summary>
+        /// <param name="managedOnly">not null means to filter out the native C++ SOS commands</param>
+        /// <param name="runtime">runtime instance or null</param>
+        /// <returns></returns>
+        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);
     }
 }
index 03d011d0da3a3528b344c69817d6b87f107577a9..0fe569a0faffd59e5058cb7db02095d40798720c 100644 (file)
@@ -21,6 +21,17 @@ namespace SOS.Hosting
     [ServiceExport(Scope = ServiceScope.Target)]
     public sealed class SOSHost : IDisposable
     {
+       /// <summary>
+       /// Provides the native debugger's debug client instance
+       /// </summary>
+       public interface INativeClient
+       {
+           /// <summary>
+           /// Native debugger client interface
+           /// </summary>
+           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;
 
         /// <summary>
         /// Create an instance of the hosting class. Has the lifetime of the target.
         /// </summary>
-        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);
             }
         }
 
         /// <summary>
         /// Execute a SOS command.
         /// </summary>
-        /// <param name="commandLine">command name and arguments</param>
-        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);
-        }
+        /// <param name="command">just the command name</param>
+        /// <param name="arguments">the command arguments and options</param>
+        public void ExecuteCommand(string command, string arguments) => _sosLibrary.ExecuteCommand(_client, command, arguments);
 
         /// <summary>
-        /// Execute a SOS command.
+        /// Get the detailed help text for a native SOS command.
         /// </summary>
-        /// <param name="command">just the command name</param>
-        /// <param name="arguments">the command arguments and options</param>
-        public void ExecuteCommand(string command, string arguments)
-        {
-            if (_disposed)
-            {
-                throw new ObjectDisposedException("SOSHost instance disposed");
-            }
-            _sosLibrary.ExecuteCommand(_interface, command, arguments);
-        }
+        /// <param name="command">command name</param>
+        /// <returns>help text or null if not found or error</returns>
+        public string GetHelpText(string command) => _sosLibrary.GetHelpText(command);
 
         #region Reverse PInvoke Implementations
 
index d9e4f03da8da9601ff021b896c334b73f580a828..42e1f3e42562ba168457741cee80d2456c01aeec 100644 (file)
@@ -16,6 +16,22 @@ namespace SOS.Hosting
     /// </summary>
     public sealed class SOSLibrary : IDisposable
     {
+       /// <summary>
+       /// Provides the SOS module handle
+       /// </summary>
+       public interface ISOSModule
+       {
+           /// <summary>
+           /// The SOS module path
+           /// </summary>
+           string SOSPath { get; }
+
+           /// <summary>
+           /// The SOS module handle
+           /// </summary>
+           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;
 
         /// <summary>
@@ -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
         /// </summary>
         /// <param name="target">target instance</param>
-        private SOSLibrary(IHost host)
+        /// <param name="host">sos library info or null</param>
+        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
         /// </summary>
         private void Uninitialize()
         {
-            Trace.TraceInformation("SOSHost: Uninitialize");
-            if (_sosLibrary != IntPtr.Zero)
+            Trace.TraceInformation("SOSLibrary: Uninitialize");
+            if (_uninitializeLibrary && _sosLibrary != IntPtr.Zero)
             {
                 SOSUninitializeDelegate uninitializeFunc = SOSHost.GetDelegateFunction<SOSUninitializeDelegate>(_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<SOSCommandDelegate>(_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);
         }
     }
 }
index 5b869886b20df698c09714877d5140e12d767f9a..7a435c20b258c5b7877663c894cefb16b623169e 100644 (file)
       <BuildProjectFramework>net6.0</BuildProjectFramework>
       <RuntimeFrameworkVersion>$(RuntimeVersion60)</RuntimeFrameworkVersion>
     </Option>
+    <!--
+        SOS.TestExtensions
+      -->
+    <Option Condition="'$(RuntimeVersion60)' != ''">
+      <TestName>SOS.TestExtensions</TestName>
+      <BuildProjectFramework>net6.0</BuildProjectFramework>
+      <RuntimeFrameworkVersion>$(RuntimeVersion60)</RuntimeFrameworkVersion>
+      <DotNetDiagnosticExtensions>$(RootBinDir)/bin/TestExtension/$(TargetConfiguration)/netstandard2.0/TestExtension.dll</DotNetDiagnosticExtensions>
+      <SetHostRuntime>$(DotNetRoot)/shared/Microsoft.NETCore.App/$(RuntimeFrameworkVersion)</SetHostRuntime>
+    </Option>
     <!--
         SOS.StackAndOtherTests (cli because tested with embedded, portable PDBs and single-file)
       -->
index 3618f365e9cf6e19abb0f412e7fdca2fb2c3d69d..14898afadcce134673f97015f75728f064715281 100644 (file)
           <BuildProjectFramework>net6.0</BuildProjectFramework>
           <RuntimeFrameworkVersion>$(RuntimeVersion60)</RuntimeFrameworkVersion>
         </Option>
+        <!--
+            SOS.TestExtensions
+          -->
+        <Option Condition="'$(RuntimeVersion60)' != ''">
+          <TestName>SOS.TestExtensions</TestName>
+          <BuildProjectFramework>net6.0</BuildProjectFramework>
+          <RuntimeFrameworkVersion>$(RuntimeVersion60)</RuntimeFrameworkVersion>
+          <DotNetDiagnosticExtensions>$(RootBinDir)\bin\TestExtension\$(TargetConfiguration)\netstandard2.0\TestExtension.dll</DotNetDiagnosticExtensions>
+          <Options>
+            <Option>
+              <SetHostRuntime>$(DotNetRoot)/shared/Microsoft.NETCore.App/$(RuntimeFrameworkVersion)</SetHostRuntime>
+            </Option>
+            <Option>
+              <SetHostRuntime>-netfx</SetHostRuntime>
+            </Option>
+          </Options>
+        </Option>
         <!--
             SOS.StackAndOtherTests (cli because tested with full, embedded and portable PDBs)
           -->
index 3ce4a1bdb52cd91ea9d793e6910771c1987ceae4..db08980770166d00dfcf8ed30c0b84cfb9a9ff70 100644 (file)
@@ -31,6 +31,7 @@
 
   <ItemGroup>
     <ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Diagnostics.TestHelpers\Microsoft.Diagnostics.TestHelpers.csproj" />
+    <ProjectReference Include="$(MSBuildThisFileDirectory)..\..\tests\TestExtension\TestExtension.csproj" />
   </ItemGroup>
     
   <ItemGroup>
index 8849bd4a2204641a322b39073ec98fb91355d186..d7e8f6eb541752cd534f7f4bbd031499eae34d9d 100644 (file)
@@ -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)
     {
index 3d345997a0386fe8d58b82e751a97245250ba415..bee617ec9c6da511e7a48fbc63973c823b460902 100644 (file)
@@ -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"));
index 9d402ecbf39b5365a11dec4ce8e5b9f300b7536d..3a156cb1e8eaa80cb9502cb4775d0f28520461cb 100644 (file)
@@ -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<int, string[]>
index 7d2095a5e13f346e6c8cb0df52644c757c5e042e..456b10790d150c22c397a4af61a5ee511c455f09 100644 (file)
@@ -40,12 +40,7 @@ VERIFY:\s+<HEXVAL>\s+<HEXVAL>\s+[Dd]iv[Zz]ero.*!C\.Main(\(.*\))?\+0x<HEXVAL>\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+<DECVAL>\s+
 VERIFY:\s+UnstartedThread:\s+<DECVAL>\s+
 VERIFY:\s+BackgroundThread:\s+<DECVAL>\s+
index 55bac91815726ded6c22e0eab9b335b421b59ce0..525bfb897dfd953e07cb65c48e65f4ff6fb34730 100644 (file)
@@ -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
 
index 671e4e2e154b999e36bfad6f31b2d7d6df7594e9..a1b172dffb26e9546a97b4c0551f560eba8ed202 100644 (file)
@@ -89,7 +89,7 @@ VERIFY:\s+<HEXVAL>\s+<DECVAL>\s+<DECVAL>\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
index 6f6c11a30e1e3546e86496328643816041db66df..39ec83258bc7fe4b2cd042a9425e5b21a0fabdfb 100644 (file)
@@ -31,12 +31,7 @@ VERIFY:\s+<HEXVAL>\s+<HEXVAL>\s+LineNums.*!LineNums\.Program\.Main.*\+0x<HEXVAL>
 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+<DECVAL>\s+
 VERIFY:\s+UnstartedThread:\s+<DECVAL>\s+
 VERIFY:\s+BackgroundThread:\s+<DECVAL>\s+
index 8c67f036b58874941b38ec7eb80fedf123e8b770..d44f0d7ca91b0351960e88e2625f6a712439a137 100644 (file)
@@ -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+<DECVAL>\s+
 VERIFY:\s+UnstartedThread:\s+<DECVAL>\s+
 VERIFY:\s+BackgroundThread:\s+<DECVAL>\s+
index 3deaf16e462ea87754ac8eed270a7e0cdc38029e..ccc8c8d23221204ce356b7656e760eeea6016be9 100644 (file)
@@ -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
index fe8db12b95d101c6ccdd3b513110d1b09ba186d0..b3f34187c0ad691a8dc51b1c27c2e779e78ffb7b 100644 (file)
@@ -43,12 +43,7 @@ VERIFY:(StackTraceString: <none>\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+<DECVAL>\s+
 VERIFY:\s+UnstartedThread:\s+<DECVAL>\s+
 VERIFY:\s+BackgroundThread:\s+<DECVAL>\s+
index 7740c58804ba95287eafee6ab0212208fce18bcc..8ae602b04b31a54b68c722b779a483624ec1b99b 100644 (file)
@@ -46,12 +46,7 @@ VERIFY:\s+<HEXVAL>\s+<HEXVAL>\s+[Ss]imple[Tt]hrow.*!(\$0_)?Simple\.Main.*\+0x<HE
 VERIFY:\[.*[\\|/]Debuggees[\\|/].*SimpleThrow[\\|/]SimpleThrow\.cs @ 12\s*\]\s*
 
 # Verify that Threads (clrthreads) works
-IFDEF:DOTNETDUMP
 SOSCOMMAND:clrthreads
-ENDIF:DOTNETDUMP
-!IFDEF:DOTNETDUMP
-SOSCOMMAND:Threads
-ENDIF:DOTNETDUMP
 VERIFY:\s*ThreadCount:\s+<DECVAL>\s+
 VERIFY:\s+UnstartedThread:\s+<DECVAL>\s+
 VERIFY:\s+BackgroundThread:\s+<DECVAL>\s+
index 5c31e0ef1dd939d0fce3d75c5066618ed795bb6c..ec9fed65b87623966023d1b55f34092fe1ed7857 100644 (file)
@@ -250,20 +250,20 @@ ENDIF:DESKTOP
 # Verify that "u" works (depends on the IP2MD here)
 SOSCOMMAND:ClrStack
 SOSCOMMAND:IP2MD <POUT>.*\s+(<HEXVAL>)\s+SymbolTestApp\.Program\.Foo4.*\s+<POUT>
-SOSCOMMAND:u <POUT>\s*MethodDesc:\s+(<HEXVAL>)\s*<POUT>
+SOSCOMMAND:clru <POUT>\s*MethodDesc:\s+(<HEXVAL>)\s*<POUT>
 VERIFY:\s*Normal JIT generated code\s+
 VERIFY:\s+SymbolTestApp\.Program\.Foo4\(System\.String\)\s+
 VERIFY:\s+Begin\s+<HEXVAL>,\s+size\s+<HEXVAL>\s+
 VERIFY:\s+(?i:.*[\\|/]SymbolTestApp\.cs) @ (53|57):\s+
 
 # Verify that "u" with no line info works
-SOSCOMMAND:u -n <PREVPOUT>
+SOSCOMMAND:clru -n <PREVPOUT>
 VERIFY:\s*Normal JIT generated code\s+
 VERIFY:\s+SymbolTestApp\.Program\.Foo4\(System\.String\)\s+
 VERIFY:\s+Begin\s+<HEXVAL>,\s+size\s+<HEXVAL>\s+
 
 # Verify that "u" with offsets info works
-SOSCOMMAND:u -o <PREVPOUT>
+SOSCOMMAND:clru -o <PREVPOUT>
 VERIFY:\s*Normal JIT generated code\s+
 VERIFY:\s+SymbolTestApp\.Program\.Foo4\(System\.String\)\s+
 VERIFY:\s+Begin\s+<HEXVAL>,\s+size\s+<HEXVAL>\s+
@@ -279,12 +279,7 @@ VERIFY:\s+Name:\s+SymbolTestApp\.Program\.Foo4\(System\.String\)\s+
 VERIFY:\s+JITTED Code Address:\s+<HEXVAL>\s+
 
 # Verify that Threads (clrthreads) works
-IFDEF:DOTNETDUMP
 SOSCOMMAND:clrthreads
-ENDIF:DOTNETDUMP
-!IFDEF:DOTNETDUMP
-SOSCOMMAND:Threads
-ENDIF:DOTNETDUMP
 VERIFY:\s*ThreadCount:\s+<DECVAL>\s+
 VERIFY:\s+UnstartedThread:\s+<DECVAL>\s+
 VERIFY:\s+BackgroundThread:\s+<DECVAL>\s+
diff --git a/src/SOS/SOS.UnitTests/Scripts/TestExtensions.script b/src/SOS/SOS.UnitTests/Scripts/TestExtensions.script
new file mode 100644 (file)
index 0000000..d18a63a
--- /dev/null
@@ -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+
index b9a0509e0fb58328ec6090c8f9bb09da75326e63..a304a2f0015bf1d12d61e2a883963b54c1d28795 100644 (file)
@@ -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+<DECVAL>\s+
 VERIFY:\s+UnstartedThread:\s+<DECVAL>\s+
 VERIFY:\s+BackgroundThread:\s+<DECVAL>\s+
@@ -138,7 +133,7 @@ VERIFY:\s*STACK <DECVAL>\s*
 VERIFY?:\s*<< Awaiting: <HEXVAL>\s+<HEXVAL>\s+.* >>\s+
 VERIFY:\s*<HEXVAL>\s+<HEXVAL>\s+\((<DECVAL>)?\)\s+.*
 
-SOSCOMMAND:DumpMT --stats <PREVPOUT>
+SOSCOMMAND:DumpMT <PREVPOUT>
 !VERIFY:\s*<HEXVAL> is not a MethodTable
 
 SOSCOMMAND:DumpAsync --coalesce
index 6299c2739143bb511c0d02f3bca35a3e70b85f09..dcd7994672543e3b23aa8188f5c179a6af3a2fbd 100644 (file)
@@ -89,6 +89,7 @@ if(WIN32)
     gchist.cpp
     gcroot.cpp
     symbols.cpp
+    managedcommands.cpp
     metadata.cpp
     sigparser.cpp
     sildasm.cpp
index be63febcfbd333865b7253a1f953390a7aa251ec..29dde66bfac5eb547c37fa2b29e34b48454c45fe 100644 (file)
     <ClCompile Include="exts.cpp" />
     <ClCompile Include="gchist.cpp" />
     <ClCompile Include="gcroot.cpp" />
+    <ClCompile Include="managedcommands.cpp" />
     <ClCompile Include="metadata.cpp" />
     <ClCompile Include="platform\datatarget.cpp" />
     <ClCompile Include="platform\hostimpl.cpp" />
index ec1f21b1e9123578092f6cfe522afef42ae39b5a..e7b14e9eaff37ebe60d9c917c9bf87d9da315c7d 100644 (file)
@@ -32,6 +32,7 @@
     <ClCompile Include="platform\targetimpl.cpp">
       <Filter>platform</Filter>
     </ClCompile>
+    <ClCompile Include="managedcommands.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="data.h" />
@@ -93,4 +94,4 @@
     <Text Include="sosdocs.txt" />
     <Text Include="sosdocsunix.txt" />
   </ItemGroup>
-</Project>
+</Project>
\ No newline at end of file
index 3ebe83fde0e0cf44e83737d269ae4738be07574a..836f38b0d93b8b02f2fe1bf932d6ac2c2d4a1c98 100644 (file)
@@ -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_<arch>_<arch>_<version>.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 <directory>' 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 <directory>' 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,
index cf29ce595b9611bc69e06057a69cfeb4c756da31..96876879a7bbceeb7e0ffc43251f30ac653ef2b7 100644 (file)
@@ -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_<arch>_<arch>_<version>.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 <directory>' 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 <directory>' 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<ISOSDacInterface> 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<ISOSDacInterface> spISD(g_sos);                   \
     ToRelease<IXCLRDataProcess> 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;
 
 //-----------------------------------------------------------------------------------------
index f656cb11837244dbcf9cf17085cd8255891f14cd..82a43a95317101a36e73106f130d675645b83f2f 100644 (file)
@@ -31,8 +31,8 @@
 #include <stdint.h>
 #include <string.h>
 #include <stddef.h>
-
 #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 <valid object pointer>\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 <valid object pointer>\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 <valid object pointer>\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 (file)
index 0000000..2a7b336
--- /dev/null
@@ -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;
+}
+
index 4b9b8fcd7de5c77a15e4ea6fc3bb57d927215eb1..f298a3f9fd1e11f8167360768d1bb45debb75468 100644 (file)
@@ -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
index aa8eaf23d3901726ac65d2e775287e2a20ed680c..cf08981f5de624cf6eefc62e81a5ed8902134e75 100644 (file)
@@ -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
index fa470d2eabfdfbe59cf7f231f660ff1a25733d41..80ec24642f949786344a1a130fa15d2df88e3658 100644 (file)
@@ -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 <functionname>" for detailed info on that function. 
+Type "!soshelp <functionname>" 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 <cache-path>] [-directory <search-directory>] [-timeout <minutes> ] [-pat <token>] [<symbol-server-URL>]
-
--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> - 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.
-\\
index 3310726ca9b29b3a7ea4ebe888d0f7b3b9c52af7..b554a9479aefa163b0fd829ec4502e37eee34db6 100644 (file)
@@ -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 <cache-path>] [-directory <search-directory>] [-timeout <minutes> ] [-pat <token>] [<symbol-server-URL>]
-
--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> - 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.
-\\
index b6f36b5db4cadb0b7ccd312d9bf273631584b530..0d7aad0d49215a2fe1405ecf5d1ae52cebfc8eff 100644 (file)
@@ -70,7 +70,6 @@
 #include <list>
 #endif // !FEATURE_PAL
 #include <wchar.h>
-
 #include "platformspecific.h"
 
 #define NOEXTAPI
@@ -80,7 +79,6 @@
 #undef StackTrace
 
 #include <dbghelp.h>
-
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -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 <sigaddr> <moduleaddr>\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 <sigaddr> <moduleaddr>\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 <delegate object address>\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] <Module Address>\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<char> 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<sos::Exception>("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] <type name>\n");
         ExtOut("                       [<pseudo-register number for result>]\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 <address> <file to save>\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<WCHAR> 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;
index 50467dd8ad41cf96c089103d55d4b53087f66d9f..8eb3f60e52299da9c80ab804ede03c68322aea1c 100644 (file)
@@ -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
 {
index 788f6f353817126feaf3b4da9b36b5fa7d6699d5..74f18156099d061f43fae6aa4608453d3d4cc9f7 100644 (file)
@@ -82,14 +82,6 @@ public:
         PCSTR commandName,
         PCSTR arguments) = 0;
 
-    /// <summary>
-    /// Displays the help for a managed extension command
-    /// </summary>
-    /// <param name="commandName"></param>
-    /// <returns>error code</returns>
-    virtual HRESULT STDMETHODCALLTYPE DisplayHelp( 
-        PCSTR commandName) = 0;
-
     /// <summary>
     /// Uninitialize the extension infrastructure
     /// </summary>
index cdb5c553767b750e927abfffc6bc88f45419b3b3..95847b9bce4b43f94f325575d9f2a18a1a0b9b4c 100644 (file)
@@ -157,6 +157,7 @@ sosCommandInitialize(lldb::SBDebugger debugger)
     g_services->AddCommand("ext", new sosCommand(nullptr), "Executes various coreclr debugging commands. Use the syntax 'sos <command - name> <args>'. 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 <root> to <target>.");
+    g_services->AddManagedCommand("pathto", "Displays the GC path from <root> to <target>.");
     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;
 }
index 6718677d65cfce640cec490cb98d6535f3c90fc2..2a2b5defcff5c2aff7179f80724d23f863587386 100644 (file)
@@ -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<IConsoleFileLoggingService>(_fileLoggingConsoleService);
             _serviceContainer.AddService<IDiagnosticLoggingService>(DiagnosticLoggingService.Instance);
             _serviceContainer.AddService<ICommandService>(_commandService);
+            _serviceContainer.AddService<CommandService>(_commandService);
 
             SymbolService symbolService = new(this);
             _serviceContainer.AddService<ISymbolService>(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)
                     {
                     }
                 }
index f4017f7b479f811c4d3a07194d70548cbacfd1f8..21f6475de8141044bef653b23d4115dc9022414c 100644 (file)
@@ -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.")]
index a3c15fc79e72d0f67a6c264f849dddf78a5b69fb..a2045070c4313a6ca98b1cddb42bdb0e5b7bb543 100644 (file)
@@ -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<SOSHost>();
-                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 (file)
index 0000000..34f0186
--- /dev/null
@@ -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 = "<Pending>")]
+
+namespace Microsoft.Diagnostics.DebugServices.UnitTests
+{
+    public class CommandServiceTests : IDisposable
+    {
+        private const string ListenerName = "CommandServiceTests";
+
+        private static IEnumerable<object[]> _configurations;
+
+        /// <summary>
+        /// Get the first test asset dump. It doesn't matter which one.
+        /// </summary>
+        /// <returns></returns>
+        public static IEnumerable<object[]> 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<IConsoleService>(consoleService);
+
+            CommandService commandService = new();
+            testDump.ServiceContainer.AddService<ICommandService>(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<string> 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);
+        }
+    }
+}
index 040ff1d8aa46923ae78833fdc1f8c2fdb37de96c..fb7ba82b99a5b17eb089510fd1adbcd1ea63f62d 100644 (file)
@@ -30,11 +30,11 @@ namespace Microsoft.Diagnostics.DebugServices.UnitTests
 
         public static IEnumerable<object[]> 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<IRuntimeService>();
             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 (file)
index 0000000..920190d
--- /dev/null
@@ -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 (file)
index 0000000..9e7d6b0
--- /dev/null
@@ -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 (file)
index 0000000..3db8fe9
--- /dev/null
@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Diagnostics.DebugServices\Microsoft.Diagnostics.DebugServices.csproj" />
+  </ItemGroup>
+
+</Project>