Add interactive dump "analyze" dump support to dotnet-dump project.
authorMike McLaughlin <mikem@microsoft.com>
Sun, 17 Feb 2019 20:14:28 +0000 (12:14 -0800)
committerMike McLaughlin <mikem@microsoft.com>
Fri, 22 Feb 2019 23:04:39 +0000 (15:04 -0800)
Use the System.CommandLine CommandProcessor for the new commands.

Add "sos", "exit", "help", native "modules" and "setthread" commands.

12 files changed:
src/Tools/dotnet-dump/AnalyzeContext.cs [new file with mode: 0644]
src/Tools/dotnet-dump/Analyzer.cs [new file with mode: 0644]
src/Tools/dotnet-dump/Commands/ExitCommand.cs [new file with mode: 0644]
src/Tools/dotnet-dump/Commands/HelpCommand.cs [new file with mode: 0644]
src/Tools/dotnet-dump/Commands/ModulesCommand.cs [new file with mode: 0644]
src/Tools/dotnet-dump/Commands/SOSCommand.cs [new file with mode: 0644]
src/Tools/dotnet-dump/Commands/SetThreadCommand.cs [new file with mode: 0644]
src/Tools/dotnet-dump/Dumper.Linux.cs
src/Tools/dotnet-dump/Dumper.Windows.cs
src/Tools/dotnet-dump/Dumper.cs
src/Tools/dotnet-dump/Program.cs
src/Tools/dotnet-dump/dotnet-dump.csproj

diff --git a/src/Tools/dotnet-dump/AnalyzeContext.cs b/src/Tools/dotnet-dump/AnalyzeContext.cs
new file mode 100644 (file)
index 0000000..52315aa
--- /dev/null
@@ -0,0 +1,77 @@
+// --------------------------------------------------------------------
+// 
+// Copyright (c) Microsoft Corporation.  All rights reserved.
+// 
+// --------------------------------------------------------------------
+using Microsoft.Diagnostics.Runtime;
+using SOS;
+using System;
+using System.CommandLine;
+using System.Threading;
+
+namespace Microsoft.Diagnostic.Tools.Dump
+{
+    /// <summary>
+    /// The the common context for analyze commands
+    /// </summary>
+    public class AnalyzeContext: ISOSHostContext
+    {
+        readonly IConsole _console;
+        ClrRuntime _runtime;
+
+        public AnalyzeContext(IConsole console, DataTarget target, Action exit)
+        {
+            _console = console;
+            Target = target;
+            Exit = exit;
+        }
+
+        /// <summary>
+        /// ClrMD data target
+        /// </summary>
+        public DataTarget Target { get; }
+
+        /// <summary>
+        /// ClrMD runtime info
+        /// </summary>
+        public ClrRuntime Runtime
+        {
+            get 
+            {
+                if (_runtime == null)
+                {
+                    if (Target.ClrVersions.Count != 1)
+                    {
+                        throw new InvalidOperationException("More or less than 1 CLR version is present");
+                    }
+                    _runtime = Target.ClrVersions[0].CreateRuntime();
+                }
+                return _runtime;
+            }
+        }
+
+        /// <summary>
+        /// Delegate to invoke to exit repl
+        /// </summary>
+        public Action Exit { get; }
+
+        /// <summary>
+        /// Current OS thread Id
+        /// </summary>
+        public int CurrentThreadId { get; set; }
+
+        /// <summary>
+        /// Cancellation token for current command
+        /// </summary>
+        public CancellationToken CancellationToken { get; set; }
+
+        /// <summary>
+        /// Console write function
+        /// </summary>
+        /// <param name="text"></param>
+        void ISOSHostContext.Write(string text)
+        {
+            _console.Out.Write(text);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Tools/dotnet-dump/Analyzer.cs b/src/Tools/dotnet-dump/Analyzer.cs
new file mode 100644 (file)
index 0000000..480f57b
--- /dev/null
@@ -0,0 +1,72 @@
+using Microsoft.Diagnostic.Repl;
+using Microsoft.Diagnostics.Runtime;
+using System.CommandLine;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Diagnostic.Tools.Dump
+{
+    public class Analyzer
+    {
+        private readonly ConsoleProvider _consoleProvider;
+        private readonly CommandProcessor _commandProcessor;
+
+        public Analyzer()
+        {
+            _consoleProvider = new ConsoleProvider();
+            _commandProcessor = new CommandProcessor(new Assembly[] { typeof(Analyzer).Assembly });
+        }
+
+        public async Task<int> Analyze(FileInfo dump_path, string[] command)
+        {
+            _consoleProvider.Out.WriteLine($"Loading core dump: {dump_path} ...");
+
+            DataTarget target = null;
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
+                target = DataTarget.LoadCoreDump(dump_path.FullName);
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+                target = DataTarget.LoadCrashDump(dump_path.FullName, CrashDumpReader.ClrMD);
+            }
+            else {
+                _consoleProvider.Error.WriteLine($"{RuntimeInformation.OSDescription} not supported");
+                return 1;
+            }
+
+            using (target)
+            {
+                // Create common analyze context for commands
+                var analyzeContext = new AnalyzeContext(_consoleProvider, target, _consoleProvider.Stop) {
+                    CurrentThreadId = unchecked((int)target.DataReader.EnumerateAllThreads().FirstOrDefault())
+                };
+                _commandProcessor.CommandContext = analyzeContext;
+
+                // Automatically enable symbol server support on Linux and MacOS
+                if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+                    await _commandProcessor.Parse("setsymbolserver -ms", _consoleProvider);
+                }
+
+                // Run the commands from the dotnet-dump command line
+                if (command != null)
+                {
+                    foreach (string cmd in command)
+                    {
+                        await _commandProcessor.Parse(cmd, _consoleProvider);
+                    }
+                }
+
+                // Start interactive command line processing
+                await _consoleProvider.Start(async (string commandLine, CancellationToken cancellation) => {
+                    analyzeContext.CancellationToken = cancellation;
+                    await _commandProcessor.Parse(commandLine, _consoleProvider);
+                });
+            }
+
+            return 0;
+        }
+    }
+}
diff --git a/src/Tools/dotnet-dump/Commands/ExitCommand.cs b/src/Tools/dotnet-dump/Commands/ExitCommand.cs
new file mode 100644 (file)
index 0000000..704173d
--- /dev/null
@@ -0,0 +1,19 @@
+
+using Microsoft.Diagnostic.Repl;
+using System.CommandLine;
+using System.Threading.Tasks;
+
+namespace Microsoft.Diagnostic.Tools.Dump
+{
+    [Command(Name = "exit", Help = "Exit interactive mode.")]
+    public class ExitCommand : CommandBase
+    {
+        public AnalyzeContext AnalyzeContext { get; set; }
+
+        public override Task InvokeAsync()
+        {
+            AnalyzeContext.Exit();
+            return Task.CompletedTask;
+        }
+    }
+}
diff --git a/src/Tools/dotnet-dump/Commands/HelpCommand.cs b/src/Tools/dotnet-dump/Commands/HelpCommand.cs
new file mode 100644 (file)
index 0000000..eef9514
--- /dev/null
@@ -0,0 +1,36 @@
+using Microsoft.Diagnostic.Repl;
+using System.CommandLine;
+using System.Threading.Tasks;
+
+namespace Microsoft.Diagnostic.Tools.Dump
+{
+    [Command(Name = "help", Help = "Display help for a command.")]
+    public class HelpCommand : CommandBase
+    {
+        [Argument(Help = "Command to find help.")]
+        public string Command { get; set; }
+
+        public CommandProcessor CommandProcessor { get; set; }
+
+        public override Task InvokeAsync()
+        {
+            return Task.CompletedTask;
+        }
+
+        /// <summary>
+        /// Get help builder interface
+        /// </summary>
+        /// <param name="helpBuilder">help builder</param>
+        public Task InvokeAsync(IHelpBuilder helpBuilder)
+        {
+            Command command = CommandProcessor.GetCommand(Command);
+            if (command != null) {
+                helpBuilder.Write(command);
+            }
+            else {
+                Console.Error.WriteLine($"Help for {Command} not found.");
+            }
+            return Task.CompletedTask;
+        }
+    }
+}
diff --git a/src/Tools/dotnet-dump/Commands/ModulesCommand.cs b/src/Tools/dotnet-dump/Commands/ModulesCommand.cs
new file mode 100644 (file)
index 0000000..e805904
--- /dev/null
@@ -0,0 +1,44 @@
+
+using Microsoft.Diagnostic.Repl;
+using Microsoft.Diagnostics.Runtime;
+using System.CommandLine;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Microsoft.Diagnostic.Tools.Dump
+{
+    [Command(Name = "modules", Help = "Displays the native modules in the process.")]
+    [Command(Name = "lm")]
+    public class ModulesCommand : CommandBase
+    {
+        [Option(Name = "--verbose", Help = "Displays more details.")]
+        [OptionAlias(Name = "-v")]
+        public bool Verbose { get; set; }
+
+        public AnalyzeContext AnalyzeContext { get; set; }
+
+        public override Task InvokeAsync()
+        {
+            foreach (ModuleInfo module in AnalyzeContext.Target.DataReader.EnumerateModules())
+            {
+                if (Verbose)
+                {
+                    WriteLine("{0}", module.FileName);
+                    WriteLine("    Address:   {0:X16}", module.ImageBase);
+                    WriteLine("    FileSize:  {0:X8}", module.FileSize);
+                    WriteLine("    TimeStamp: {0:X8}", module.TimeStamp);
+                    if (module.BuildId != null) {
+                        WriteLine("    BuildId:   {0}", string.Concat(module.BuildId.Select((b) => b.ToString("x2"))));
+                    }
+                    WriteLine("    IsRuntime: {0}", module.IsRuntime);
+                    WriteLine("    IsManaged: {0}", module.IsManaged);
+                }
+                else
+                {
+                    WriteLine("{0:X16} {1:X8} {2}", module.ImageBase, module.FileSize, module.FileName);
+                }
+            }
+            return Task.CompletedTask;
+        }
+    }
+}
diff --git a/src/Tools/dotnet-dump/Commands/SOSCommand.cs b/src/Tools/dotnet-dump/Commands/SOSCommand.cs
new file mode 100644 (file)
index 0000000..fb11171
--- /dev/null
@@ -0,0 +1,70 @@
+using Microsoft.Diagnostic.Repl;
+using SOS;
+using System;
+using System.CommandLine;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Diagnostic.Tools.Dump
+{
+    [Command(Name = "clrstack",         AliasExpansion = "ClrStack",            Help = "Provides a stack trace of managed code only.")]
+    [Command(Name = "clrthreads",       AliasExpansion = "Threads",             Help = "List the managed threads running.")]
+    [Command(Name = "dumpasync",        AliasExpansion = "DumpAsync",           Help = "Displays info about async state machines on the garbage-collected heap.")]
+    [Command(Name = "dumpclass",        AliasExpansion = "DumpClass",           Help = "Displays information about a EE class structure at the specified address.")]
+    [Command(Name = "dumpdelegate",     AliasExpansion = "DumpDelegate",        Help = "Displays information about a delegate.")]
+    [Command(Name = "dumpdomain",       AliasExpansion = "DumpDomain",          Help = "Displays information all the AppDomains and all assemblies within the domains.")]
+    [Command(Name = "dumpheap",         AliasExpansion = "DumpHeap",            Help = "Displays info about the garbage-collected heap and collection statistics about objects.")]
+    [Command(Name = "dumpil",           AliasExpansion = "DumpIL",              Help = "Displays the Microsoft intermediate language (MSIL) that is associated with a managed method.")]
+    [Command(Name = "dumplog",          AliasExpansion = "DumpLog",             Help = "Writes the contents of an in-memory stress log to the specified file.")]
+    [Command(Name = "dumpmd",           AliasExpansion = "DumpMD",              Help = "Displays information about a MethodDesc structure at the specified address.")]
+    [Command(Name = "dumpmodule",       AliasExpansion = "DumpModule",          Help = "Displays information about a EE module structure at the specified address.")]
+    [Command(Name = "dumpmt",           AliasExpansion = "DumpMT",              Help = "Displays information about a method table at the specified address.")]
+    [Command(Name = "dumpobj",          AliasExpansion = "DumpObj",             Help = "Displays info about an object at the specified address.")]
+    [Command(Name = "dumpstack",        AliasExpansion = "DumpStack",           Help = "Displays a native and managed stack trace.")]
+    [Command(Name = "dso",              AliasExpansion = "DumpStackObjects",    Help = "Displays all managed objects found within the bounds of the current stack.")]
+    [Command(Name = "eeheap",           AliasExpansion = "EEHeap",              Help = "Displays info about process memory consumed by internal runtime data structures.")]
+    [Command(Name = "eestack",          AliasExpansion = "EEStack",             Help = "Runs dumpstack on all threads in the process.")]
+    [Command(Name = "finalizequeue",    AliasExpansion = "FinalizeQueue",       Help = "Displays all objects registered for finalization.")]
+    [Command(Name = "gcroot",           AliasExpansion = "GCRoot",              Help = "Displays info about references (or roots) to an object at the specified address.")]
+    [Command(Name = "ip2md",            AliasExpansion = "IP2MD",               Help = "Displays the MethodDesc structure at the specified address in code that has been JIT-compiled.")]
+    [Command(Name = "name2ee",          AliasExpansion = "Name2EE",             Help = "Displays the MethodTable structure and EEClass structure for the specified type or method in the specified module.")]
+    [Command(Name = "pe",               AliasExpansion = "PrintException",      Help = "Displays and formats fields of any object derived from the Exception class at the specified address.")]
+    [Command(Name = "syncblk",          AliasExpansion = "SyncBlk",             Help = "Displays the SyncBlock holder info.")]
+    [Command(Name = "histclear",        AliasExpansion = "HistClear",           Help = "Releases any resources used by the family of Hist commands.")]
+    [Command(Name = "histinit",         AliasExpansion = "HistInit",            Help = "Initializes the SOS structures from the stress log saved in the debuggee.")]
+    [Command(Name = "histobj",          AliasExpansion = "HistObj",             Help = "Examines all stress log relocation records and displays the chain of garbage collection relocations that may have led to the address passed in as an argument.")]
+    [Command(Name = "histobjfind",      AliasExpansion = "HistObjFind",         Help = "Displays all the log entries that reference an object at the specified address.")]
+    [Command(Name = "histroot",         AliasExpansion = "HistRoot",            Help = "Displays information related to both promotions and relocations of the specified root.")]
+    [Command(Name = "setsymbolserver",  AliasExpansion = "SetSymbolServer",     Help = "Enables the symbol server support ")]
+    [Command(Name = "soshelp",          AliasExpansion = "Help",                Help = "Displays all available commands when no parameter is specified, or displays detailed help information about the specified command. soshelp <command>")]
+    public class SOSCommand : CommandBase
+    {
+        [Argument(Name = "arguments", Help = "Arguments to SOS command.")]
+        public string[] Arguments { get; set; }
+
+        public AnalyzeContext AnalyzeContext { get; set; }
+
+        private SOSHost _sosHost;
+
+        public override Task InvokeAsync()
+        {
+            try {
+                if (_sosHost == null) {
+                    _sosHost = new SOSHost(AnalyzeContext.Target.DataReader, AnalyzeContext);
+                }
+                string arguments = null;
+                if (Arguments.Length > 0) {
+                    arguments = string.Concat(Arguments.Select((arg) => arg + " "));
+                }
+                _sosHost.ExecuteCommand(AliasExpansion, arguments);
+            }
+            catch (Exception ex) when (ex is FileNotFoundException || ex is EntryPointNotFoundException || ex is InvalidOperationException) {
+                Console.Error.WriteLine(ex.Message);
+            }
+            return Task.CompletedTask;
+        }
+    }
+}
diff --git a/src/Tools/dotnet-dump/Commands/SetThreadCommand.cs b/src/Tools/dotnet-dump/Commands/SetThreadCommand.cs
new file mode 100644 (file)
index 0000000..35b10da
--- /dev/null
@@ -0,0 +1,35 @@
+
+using Microsoft.Diagnostic.Repl;
+using System.CommandLine;
+using System.Threading.Tasks;
+
+namespace Microsoft.Diagnostic.Tools.Dump
+{
+    [Command(Name = "setthread", Help = "Sets or displays the current thread id for the SOS commands.")]
+    [Command(Name = "threads")]
+    public class SetThreadCommand : CommandBase
+    {
+        [Argument(Help = "The thread id to set, otherwise displays the current id.")]
+        public int? ThreadId { get; set; } = null;
+
+        public AnalyzeContext AnalyzeContext { get; set; }
+
+        public override Task InvokeAsync()
+        {
+            if (ThreadId.HasValue)
+            {
+                AnalyzeContext.CurrentThreadId = ThreadId.Value;
+            }
+            else
+            {
+                int index = 0;
+                foreach (uint threadId in AnalyzeContext.Target.DataReader.EnumerateAllThreads())
+                {
+                    WriteLine("{0}{1} 0x{2:X4} ({2})", threadId == AnalyzeContext.CurrentThreadId ? "*" : " ", index, threadId);
+                    index++;
+                }
+            }
+            return Task.CompletedTask;
+        }
+    }
+}
index 58ad219f2542cb4b9aacba3d4d369cf1ef387ef0..db3f7fd3f93b7aa2d6c728cfe249d0594e224cc6 100644 (file)
@@ -4,39 +4,39 @@ using System.Diagnostics;
 using System.Threading.Tasks;
 using System.IO;
 
-namespace Microsoft.Diagnostics.Tools.Dump
+namespace Microsoft.Diagnostic.Tools.Dump
 {
-    public static partial class Dumper
+    public partial class Dumper
     {
         private static class Linux
         {
             internal static async Task CollectDumpAsync(Process process, string fileName)
             {
                 // We don't work on WSL :(
-                var ostype = await File.ReadAllTextAsync("/proc/sys/kernel/osrelease");
-                if(ostype.Contains("Microsoft"))
+                string ostype = await File.ReadAllTextAsync("/proc/sys/kernel/osrelease");
+                if (ostype.Contains("Microsoft"))
                 {
                     throw new PlatformNotSupportedException("Cannot collect memory dumps from Windows Subsystem for Linux.");
                 }
 
                 // First step is to find the .NET runtime. To do this we look for coreclr.so
-                var coreclr = process.Modules.Cast<ProcessModule>().FirstOrDefault(m => string.Equals(m.ModuleName, "libcoreclr.so"));
-                if(coreclr == null)
+                ProcessModule coreclr = process.Modules.Cast<ProcessModule>().FirstOrDefault(m => string.Equals(m.ModuleName, "libcoreclr.so"));
+                if (coreclr == null)
                 {
                     throw new NotSupportedException("Unable to locate .NET runtime associated with this process!");
                 }
 
                 // Find createdump next to that file
-                var runtimeDirectory = Path.GetDirectoryName(coreclr.FileName);
-                var createDumpPath = Path.Combine(runtimeDirectory, "createdump");
-                if(!File.Exists(createDumpPath))
+                string runtimeDirectory = Path.GetDirectoryName(coreclr.FileName);
+                string createDumpPath = Path.Combine(runtimeDirectory, "createdump");
+                if (!File.Exists(createDumpPath))
                 {
                     throw new NotSupportedException($"Unable to locate 'createdump' tool in '{runtimeDirectory}'");
                 }
 
                 // Create the dump
-                var exitCode = await CreateDumpAsync(createDumpPath, fileName, process.Id);
-                if(exitCode != 0)
+                int exitCode = await CreateDumpAsync(createDumpPath, fileName, process.Id);
+                if (exitCode != 0)
                 {
                     throw new Exception($"createdump exited with non-zero exit code: {exitCode}");
                 }
@@ -50,10 +50,10 @@ namespace Microsoft.Diagnostics.Tools.Dump
                     StartInfo = new ProcessStartInfo()
                     {
                         FileName = exePath,
-                        Arguments = $"-f {fileName} {processId}",
-                        RedirectStandardError = true,
-                        RedirectStandardOutput = true,
-                        RedirectStandardInput = true,
+                        Arguments = $"--diag -f {fileName} {processId}",
+                        //RedirectStandardError = true,
+                        //RedirectStandardOutput = true,
+                        //RedirectStandardInput = true,
                     },
                     EnableRaisingEvents = true,
                 };
index 89350b7c2926359a7f3db9f0d2f40fd45b8d86df..a78626d82606d52e75ca2387f6c6d0017a00c529 100644 (file)
@@ -5,9 +5,9 @@ using System.Runtime.InteropServices;
 using System.Threading.Tasks;
 using Microsoft.Win32.SafeHandles;
 
-namespace Microsoft.Diagnostics.Tools.Dump
+namespace Microsoft.Diagnostic.Tools.Dump
 {
-    public static partial class Dumper
+    public partial class Dumper
     {
         private static class Windows
         {
index fc412e4ff6b3872c42a21116e941739c5cb380a6..cede967d13f78649978a5d8c1ccd0016f5aab8b3 100644 (file)
@@ -1,26 +1,60 @@
 using System;
+using System.CommandLine;
 using System.Diagnostics;
+using System.IO;
 using System.Runtime.InteropServices;
 using System.Threading.Tasks;
 
-namespace Microsoft.Diagnostics.Tools.Dump
+namespace Microsoft.Diagnostic.Tools.Dump
 {
-    public static partial class Dumper
+    public partial class Dumper
     {
-        public static Task CollectDumpAsync(Process process, string fileName)
+        public Dumper()
         {
-            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
-            {
-                return Windows.CollectDumpAsync(process, fileName);
+        }
+
+        public async Task<int> Collect(IConsole console, int processId, string outputDirectory)
+        {
+            if (processId == 0) {
+                console.Error.WriteLine("ProcessId is required.");
+                return 1;
             }
-            else if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+
+            // System.CommandLine has a bug in the default value handling
+            if (outputDirectory == null) {
+                outputDirectory = Directory.GetCurrentDirectory();
+            }
+
+            // Get the process
+            Process process = null;
+            try
             {
-                return Linux.CollectDumpAsync(process, fileName);
+                process = Process.GetProcessById(processId);
             }
-            else
+            catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException)
             {
-                throw new PlatformNotSupportedException("Can't collect a memory dump on this platform.");
+                console.Error.WriteLine($"Invalid process id: {processId}");
+                return 1;
             }
+
+            // Generate the file name
+            string fileName = Path.Combine(outputDirectory, $"{process.ProcessName}-{process.Id}-{DateTime.Now:yyyyMMdd-HHmmss-fff}.dmp");
+
+            console.Out.WriteLine($"Collecting memory dump for {process.ProcessName} (ID: {process.Id}) ...");
+    
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+                await Windows.CollectDumpAsync(process, fileName);
+            }
+            else if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
+                await Linux.CollectDumpAsync(process, fileName);
+            }
+            else {
+                console.Error.WriteLine($"Unsupported operating system {RuntimeInformation.OSDescription}");
+                return 1;
+            }
+
+            console.Out.WriteLine($"Dump saved to {fileName}");
+            return 0;
         }
     }
 }
index cdb386411a0acb46d828e9ca087395696051c64b..8509b5c9c34cc63485636bcadbb3c895a104d66e 100644 (file)
@@ -1,61 +1,59 @@
-using System;
-using System.ComponentModel.DataAnnotations;
-using System.Diagnostics;
+using System.CommandLine;
+using System.CommandLine.Builder;
+using System.CommandLine.Invocation;
 using System.IO;
-using System.Runtime.InteropServices;
 using System.Threading.Tasks;
-using McMaster.Extensions.CommandLineUtils;
-using Microsoft.Internal.Utilities;
 
-namespace Microsoft.Diagnostics.Tools.Dump
+namespace Microsoft.Diagnostic.Tools.Dump
 {
-    [Command(Name = "dotnet-dump", Description = "Captures memory dumps of .NET processes")]
-    internal class Program
+    class Program
     {
-        [Required(ErrorMessage = "You must provide a process ID to be dumped.")]
-        [Option("-p|--process-id <PROCESS_ID>", Description = "The ID of the process to collect a memory dump for")]
-        public int ProcessId { get; set; }
-
-        [Option("-o|--output <OUTPUT_DIRECTORY>", Description = "The directory to write the dump to. Defaults to the current working directory.")]
-        public string OutputDir { get; set; }
-
-        public async Task<int> OnExecute(IConsole console, CommandLineApplication app)
+        public static Task<int> Main(string[] args)
         {
-            if (string.IsNullOrEmpty(OutputDir))
-            {
-                OutputDir = Directory.GetCurrentDirectory();
-            }
-
-            // Get the process
-            var process = Process.GetProcessById(ProcessId);
-
-            // Generate the file name
-            var fileName = Path.Combine(OutputDir, $"{process.ProcessName}-{process.Id}-{DateTime.Now:yyyyMMdd-HHmmss-fff}.dmp");
+            var parser = new CommandLineBuilder()
+                .AddCommand(CollectCommand())
+                .AddCommand(AnalyzeCommand())
+                .UseDefaults()
+                .Build();
 
-            console.WriteLine($"Collecting memory dump for {process.ProcessName} (ID: {process.Id}) ...");
-            await Dumper.CollectDumpAsync(process, fileName);
-            console.WriteLine($"Dump saved to {fileName}");
-
-            return 0;
+            return parser.InvokeAsync(args);
         }
 
-        private static int Main(string[] args)
-        {
-            DebugUtil.WaitForDebuggerIfRequested(ref args);
-
-            try
-            {
-                return CommandLineApplication.Execute<Program>(args);
-            }
-            catch(PlatformNotSupportedException ex)
-            {
-                Console.Error.WriteLine(ex.Message);
-                return 1;
-            }
-            catch (OperationCanceledException)
-            {
-                return 0;
-            }
-        }
+        private static Command CollectCommand() =>
+            new Command(
+                "collect", 
+                "Captures memory dumps of .NET processes.", 
+                new Option[] { ProcessIdOption(), OutputOption() },
+                handler: CommandHandler.Create<IConsole, int, string>(new Dumper().Collect));
+
+        private static Option ProcessIdOption() =>
+            new Option(
+                new[] { "-p", "--process-id" }, 
+                "The ID of the process to collect a memory dump.",
+                new Argument<int> { Name = "processId" });
+
+        private static Option OutputOption() =>
+            new Option(
+                new[] { "-o", "--output" }, 
+                "The directory to write the dump. Defaults to the current working directory.",
+                new Argument<string>(Directory.GetCurrentDirectory()) { Name = "directory" });
+
+        private static Command AnalyzeCommand() =>
+            new Command(
+                "analyze",
+                "Start interactive dump analyze.",
+                new Option[] { RunCommand() }, argument: DumpPath(),
+                handler: CommandHandler.Create<FileInfo, string[]>(new Analyzer().Analyze));
+
+        private static Argument DumpPath() =>
+            new Argument<FileInfo> {
+                Name = "dump_path",
+                Description = "Name of the dump file to analyze." }.ExistingOnly();
+
+        private static Option RunCommand() =>
+            new Option(
+                new[] { "-c", "--command" },
+                "Run the command on start.",
+                new Argument<string[]>() { Name = "command" });
     }
 }
index b8453cade2e67a0ddfd0b4a6a069228624ee15e3..408ce5dd937b121501a15e16ecff5ba20d154c46 100644 (file)
@@ -2,23 +2,83 @@
 
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <!-- Target .NET Core 2.1 so it will run on LTS -->
     <TargetFramework>netcoreapp2.1</TargetFramework>
-    <RootNamespace>Microsoft.Diagnostics.Tools.Dump</RootNamespace>
-
+    <RuntimeFrameworkVersion>2.1.0</RuntimeFrameworkVersion>
     <!-- Don't pack until ship engineering is done. Currently causing the official job to fail.
     <IsPackable>true</IsPackable>
     <PackAsTool>true</PackAsTool>
     -->
+    <PackAsToolShimRuntimeIdentifiers>win-x64;win-x86;osx-x64</PackAsToolShimRuntimeIdentifiers>
+    <!-- The package version needs to be hard coded as a stable version so "dotnet tool install -g dotnet-dump" works -->
+    <Version>$(VersionPrefix)</Version>
+    <PackageVersion>$(VersionPrefix)</PackageVersion>
+    <ToolCommandName>dotnet-dump</ToolCommandName>
+    <RootNamespace>Microsoft.Diagnostic.Tools.Dump</RootNamespace>
+    <Description>Diagnostic dump collect and analyze tool</Description>
+    <PackageTags>Diagnostic</PackageTags>
+    <PackageReleaseNotes>$(Description)</PackageReleaseNotes>
+    <!-- Need to put the shims here to sign -->
+    <PackagedShimOutputRootDirectory>$(OutputPath)</PackagedShimOutputRootDirectory>
+    <SOSNETCoreBinaries>$(ArtifactsBinDir)\SOS.NETCore\$(Configuration)\netstandard2.0\publish\*.dll</SOSNETCoreBinaries>
   </PropertyGroup>
 
   <ItemGroup>
-    <Compile Include="..\Common\ConsoleCancellation.cs" Link="ConsoleCancellation.cs" />
-    <Compile Include="..\Common\DebugUtil.cs" Link="DebugUtil.cs" />
+    <PackageReference Include="Microsoft.Diagnostics.Runtime" Version="$(MicrosoftDiagnosticsRuntimeVersion)" />
   </ItemGroup>
-
+  
   <ItemGroup>
-    <PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.2.5" />
+    <ProjectReference Include="..\..\Microsoft.Diagnostic.Repl\Microsoft.Diagnostic.Repl.csproj" />
+    <ProjectReference Include="..\..\SOS\SOS.Hosting\SOS.Hosting.csproj" />
   </ItemGroup>
 
+  <ItemGroup>
+    <_PackageFiles Include="$(SOSNETCoreBinaries)">
+      <BuildAction>None</BuildAction>
+      <PackagePath>tools/netcoreapp2.1/any/win-x64</PackagePath>
+    </_PackageFiles>
+    <_PackageFiles Include="$(ArtifactsBinDir)\Windows_NT.x64.$(Configuration)\sos.dll">
+      <BuildAction>None</BuildAction>
+      <PackagePath>tools/netcoreapp2.1/any/win-x64</PackagePath>
+    </_PackageFiles>
+    <_PackageFiles Include="$(SOSNETCoreBinaries)">
+      <BuildAction>None</BuildAction>
+      <PackagePath>tools/netcoreapp2.1/any/win-x86</PackagePath>
+    </_PackageFiles>
+    <_PackageFiles Include="$(ArtifactsBinDir)\Windows_NT.x86.$(Configuration)\sos.dll">
+      <BuildAction>None</BuildAction>
+      <PackagePath>tools/netcoreapp2.1/any/win-x86</PackagePath>
+    </_PackageFiles>
+    <_PackageFiles Include="$(SOSNETCoreBinaries)">
+      <BuildAction>None</BuildAction>
+      <PackagePath>tools/netcoreapp2.1/any/linux-x64</PackagePath>
+    </_PackageFiles>
+    <_PackageFiles Include="$(ArtifactsBinDir)\Linux.x64.$(Configuration)\libsosplugin.so">
+      <BuildAction>None</BuildAction>
+      <PackagePath>tools/netcoreapp2.1/any/linux-x64</PackagePath>
+    </_PackageFiles>
+    <_PackageFiles Include="$(ArtifactsBinDir)\Linux.x64.$(Configuration)\libsos.so">
+      <BuildAction>None</BuildAction>
+      <PackagePath>tools/netcoreapp2.1/any/linux-x64</PackagePath>
+    </_PackageFiles>
+    <_PackageFiles Include="$(ArtifactsBinDir)\Linux.x64.$(Configuration)\sosdocsunix.txt">
+      <BuildAction>None</BuildAction>
+      <PackagePath>tools/netcoreapp2.1/any/linux-x64</PackagePath>
+    </_PackageFiles>
+    <_PackageFiles Include="$(SOSNETCoreBinaries)">
+      <BuildAction>None</BuildAction>
+      <PackagePath>tools/netcoreapp2.1/any/osx-x64</PackagePath>
+    </_PackageFiles>
+    <_PackageFiles Include="$(ArtifactsBinDir)\OSX.x64.$(Configuration)\libsosplugin.dylib">
+      <BuildAction>None</BuildAction>
+      <PackagePath>tools/netcoreapp2.1/any/osx-x64</PackagePath>
+    </_PackageFiles>
+    <_PackageFiles Include="$(ArtifactsBinDir)\OSX.x64.$(Configuration)\libsos.dylib">
+      <BuildAction>None</BuildAction>
+      <PackagePath>tools/netcoreapp2.1/any/osx-x64</PackagePath>
+    </_PackageFiles>
+    <_PackageFiles Include="$(ArtifactsBinDir)\OSX.x64.$(Configuration)\sosdocsunix.txt">
+      <BuildAction>None</BuildAction>
+      <PackagePath>tools/netcoreapp2.1/any/osx-x64</PackagePath>
+    </_PackageFiles>
+  </ItemGroup>
 </Project>