Extend dotnet-gcdump support for mobile and dotnet-dsrouter scenarios. (#4081)
authorJohan Lorensson <lateralusx.github@gmail.com>
Tue, 29 Aug 2023 10:52:09 +0000 (12:52 +0200)
committerGitHub <noreply@github.com>
Tue, 29 Aug 2023 10:52:09 +0000 (12:52 +0200)
.net8 adds support for dotnet-gcdump on Mono, meaning dotnet-gcdump will
be used targeting mobile platforms. Currently dotnet-gcdump doesn't have
support needed since it can only connect using pid or process name and
has logic to make sure the process connected to has the same pid as the
-p argument passed to dotnet-gcdump.

On mobile platforms, runtime is running on device and communicates using
TCP/IP (over loopback interface, using Android adb port forwarding or
usbmux on iOS). All logic related to communicate with devices on
different platforms is handled by dotnet-dsrouter, that expose the same
IPC channels as a normal runtime would do, meaning most diagnostic tools
can connect to dotnet-dsrouter that will route all communication to/from
device. Tools like dotnet-trace can leverage
--diagnostic-port=<ipc>,connect instead of pid to connect to a named IPC
channel on dotnet-dsrouter (routed to a TCP/IP port on device). It is
also possible to connect directly towards dotnet-dsrouters pid since it
will masquerade like a regular runtime, but it will not alter the pid
passed in EventPipe messages, meaning that dotnet.gcdump's pid checks
currently breaks that scenario.

This PR extends diagnostic tooling support for mobile and
dotnet-dsrouter scenarios.

* Add support for --diagnostic-port argument in dotnet-gcdump, but only
support connect mode, since listen mode (reverse diagnostic server) is
mainly for startup tracing where GC dump is not full supported.
* Add support for new command, convert, in dotnet-gcdump that can take a
nettrace file as input and convert it into a gcdump file. Can be useful
if GC dumps have been captured by tools like dotnet-trace, meaning that
dotnet-gcdump will be able to convert it into a gcdump.
* Add ability to tell diagnostic tools currently supporting
--diagnostic-port command that process in -p is a dsrouter process. This
will make the tools construct a default IPC channel used by dsrouter
based on the -p parameter, simplify the connect scenarios against
dsrouter from tools a lot, since it can use the default IPC channel
setup by dsrouter.
* Always setup default IPC server channel in dsrouter if no specific
ipcs argument has been set.
* Fix dsrouter ctrl-c shutdown issue + additional error logging for
diagnostic.

14 files changed:
src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcTransport.cs
src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs
src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj
src/Tools/Common/Commands/Utils.cs
src/Tools/dotnet-counters/CounterMonitor.cs
src/Tools/dotnet-counters/Program.cs
src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs
src/Tools/dotnet-dsrouter/USBMuxTcpClientRouterFactory.cs
src/Tools/dotnet-gcdump/CommandLine/CollectCommandHandler.cs
src/Tools/dotnet-gcdump/CommandLine/ConvertCommandHandler.cs [new file with mode: 0644]
src/Tools/dotnet-gcdump/CommandLine/ReportCommandHandler.cs
src/Tools/dotnet-gcdump/DotNetHeapDump/EventPipeDotNetHeapDumper.cs
src/Tools/dotnet-gcdump/Program.cs
src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs

index d49b24d007fc52fd956b6cfc5573cfc3be8da43e..6789b0aedfe1f535b0a7188f636093b174e8b770 100644 (file)
@@ -262,25 +262,7 @@ namespace Microsoft.Diagnostics.NETCore.Client
 
         private string GetDefaultAddress()
         {
-            try
-            {
-                Process process = Process.GetProcessById(_pid);
-            }
-            catch (ArgumentException)
-            {
-                throw new ServerNotAvailableException($"Process {_pid} is not running.");
-            }
-            catch (InvalidOperationException)
-            {
-                throw new ServerNotAvailableException($"Process {_pid} seems to be elevated.");
-            }
-
-            if (!TryGetDefaultAddress(_pid, out string transportName))
-            {
-                throw new ServerNotAvailableException($"Process {_pid} not running compatible .NET runtime.");
-            }
-
-            return transportName;
+            return GetDefaultAddress(_pid);
         }
 
         private static bool TryGetDefaultAddress(int pid, out string defaultAddress)
@@ -290,6 +272,16 @@ namespace Microsoft.Diagnostics.NETCore.Client
             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
             {
                 defaultAddress = $"dotnet-diagnostic-{pid}";
+
+                try
+                {
+                    string dsrouterAddress = Directory.GetFiles(IpcRootPath, $"dotnet-diagnostic-dsrouter-{pid}").FirstOrDefault();
+                    if (!string.IsNullOrEmpty(dsrouterAddress))
+                    {
+                        defaultAddress = dsrouterAddress;
+                    }
+                }
+                catch { }
             }
             else
             {
@@ -298,15 +290,62 @@ namespace Microsoft.Diagnostics.NETCore.Client
                     defaultAddress = Directory.GetFiles(IpcRootPath, $"dotnet-diagnostic-{pid}-*-socket") // Try best match.
                         .OrderByDescending(f => new FileInfo(f).LastWriteTime)
                         .FirstOrDefault();
+
+                    string dsrouterAddress = Directory.GetFiles(IpcRootPath, $"dotnet-diagnostic-dsrouter-{pid}-*-socket") // Try best match.
+                        .OrderByDescending(f => new FileInfo(f).LastWriteTime)
+                        .FirstOrDefault();
+
+                    if (!string.IsNullOrEmpty(dsrouterAddress) && !string.IsNullOrEmpty(defaultAddress))
+                    {
+                        FileInfo defaultFile = new(defaultAddress);
+                        FileInfo dsrouterFile = new(dsrouterAddress);
+
+                        if (dsrouterFile.LastWriteTime >= defaultFile.LastWriteTime)
+                        {
+                            defaultAddress = dsrouterAddress;
+                        }
+                    }
                 }
-                catch (InvalidOperationException)
-                {
-                }
+                catch { }
             }
 
             return !string.IsNullOrEmpty(defaultAddress);
         }
 
+        public static string GetDefaultAddress(int pid)
+        {
+            try
+            {
+                Process process = Process.GetProcessById(pid);
+            }
+            catch (ArgumentException)
+            {
+                throw new ServerNotAvailableException($"Process {pid} is not running.");
+            }
+            catch (InvalidOperationException)
+            {
+                throw new ServerNotAvailableException($"Process {pid} seems to be elevated.");
+            }
+
+            if (!TryGetDefaultAddress(pid, out string defaultAddress))
+            {
+                throw new ServerNotAvailableException($"Process {pid} not running compatible .NET runtime.");
+            }
+
+            return defaultAddress;
+        }
+
+        public static bool IsDefaultAddressDSRouter(int pid, string address)
+        {
+            if (address.StartsWith(IpcRootPath, StringComparison.OrdinalIgnoreCase))
+            {
+                address = address.Substring(IpcRootPath.Length);
+            }
+
+            string dsrouterAddress = $"dotnet-diagnostic-dsrouter-{pid}";
+            return address.StartsWith(dsrouterAddress, StringComparison.OrdinalIgnoreCase);
+        }
+
         public override bool Equals(object obj)
         {
             return Equals(obj as PidIpcEndpoint);
index 19cd302023c7ffc7154606e254e43a6e24931784..80abfd44c327a4b1d69b5684192b20c659c2a75a 100644 (file)
@@ -137,6 +137,8 @@ namespace Microsoft.Diagnostics.NETCore.Client
                             routerFactory.Logger?.LogInformation("Starting automatic shutdown.");
                             throw;
                         }
+
+                        routerFactory.Logger?.LogTrace($"runRouter continues after exception: {ex.Message}");
                     }
                 }
             }
index 094b309355e707c9efed2fedcb404f440c1bab11..c4fe96c3e8a09cb1e887454a76b55f0a8aec3bda 100644 (file)
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <OutputType>Library</OutputType>
     <TargetFrameworks Condition="'$(DotNetBuildFromSource)' != 'true'">netstandard2.0;net6.0</TargetFrameworks>
@@ -31,6 +31,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <InternalsVisibleTo Include="dotnet-gcdump" />
     <InternalsVisibleTo Include="dotnet-counters" />
     <InternalsVisibleTo Include="dotnet-dsrouter" />
     <InternalsVisibleTo Include="dotnet-monitor" />
index 2d4951e9b5221904f618440edf99c13f21856998..823bedaf24e5447ed75d5bf81fbd2d1a8c3695af 100644 (file)
@@ -2,9 +2,8 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
-using System.Collections.Generic;
 using System.Diagnostics;
-
+using System.Collections.Generic;
 using Microsoft.Diagnostics.NETCore.Client;
 
 namespace Microsoft.Internal.Common.Utils
@@ -72,7 +71,7 @@ namespace Microsoft.Internal.Common.Utils
         public static bool ValidateArgumentsForAttach(int processId, string name, string port, out int resolvedProcessId)
         {
             resolvedProcessId = -1;
-            if (processId == 0 && name == null && string.IsNullOrEmpty(port))
+            if (processId == 0 && string.IsNullOrEmpty(name) && string.IsNullOrEmpty(port))
             {
                 Console.WriteLine("Must specify either --process-id, --name, or --diagnostic-port.");
                 return false;
@@ -82,24 +81,24 @@ namespace Microsoft.Internal.Common.Utils
                 Console.WriteLine($"{processId} is not a valid process ID");
                 return false;
             }
-            else if (processId != 0 && name != null && !string.IsNullOrEmpty(port))
+            else if (processId != 0 && !string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(port))
             {
                 Console.WriteLine("Only one of the --name, --process-id, or --diagnostic-port options may be specified.");
                 return false;
             }
-            else if (processId != 0 && name != null)
+            else if (processId != 0 && !string.IsNullOrEmpty(name))
             {
-                Console.WriteLine("Can only one of specify --name or --process-id.");
+                Console.WriteLine("Only one of the --name or --process-id options may be specified.");
                 return false;
             }
             else if (processId != 0 && !string.IsNullOrEmpty(port))
             {
-                Console.WriteLine("Can only one of specify --process-id or --diagnostic-port.");
+                Console.WriteLine("Only one of the --process-id or --diagnostic-port options may be specified.");
                 return false;
             }
-            else if (name != null && !string.IsNullOrEmpty(port))
+            else if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(port))
             {
-                Console.WriteLine("Can only one of specify --name or --diagnostic-port.");
+                Console.WriteLine("Only one of the --name or --diagnostic-port options may be specified.");
                 return false;
             }
             // If we got this far it means only one of --name/--diagnostic-port/--process-id was specified
@@ -108,7 +107,7 @@ namespace Microsoft.Internal.Common.Utils
                 return true;
             }
             // Resolve name option
-            else if (name != null)
+            else if (!string.IsNullOrEmpty(name))
             {
                 processId = CommandUtils.FindProcessIdWithName(name);
                 if (processId < 0)
index 5ec240d0523ddfa5c4e06a60e897cbfa1c9050c5..a2a623d0a6ede1123074585c0dff74532b95fb09 100644 (file)
@@ -613,7 +613,6 @@ namespace Microsoft.Diagnostics.Tools.Counters
                 {
                     return (int)ReturnCode.ArgumentError;
                 }
-
                 ct.Register(() => _shouldExit.TrySetResult((int)ReturnCode.Ok));
 
                 DiagnosticsClientBuilder builder = new("dotnet-counters", 10);
index 826b8373b69ef30224b313a5519d96cb29cec180..ec79a2e6efd89a44d245cc4e55125a9b55752d70 100644 (file)
@@ -167,7 +167,7 @@ namespace Microsoft.Diagnostics.Tools.Counters
 
         private static Option DiagnosticPortOption() =>
             new(
-                alias: "--diagnostic-port",
+                aliases: new[] { "--dport", "--diagnostic-port" },
                 description: "The path to diagnostic port to be used.")
             {
                 Argument = new Argument<string>(name: "diagnosticPort", getDefaultValue: () => "")
index f7c37f11776bb5e124d212bd8510037c0cd51d2a..59692a9e103c53a4d24a05b6fdc028077ff52994 100644 (file)
@@ -105,6 +105,8 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
 
                 ILogger logger = loggerFactory.CreateLogger("dotnet-dsrouter");
 
+                logger.LogInformation($"Starting dotnet-dsrouter using pid={Process.GetCurrentProcess().Id}");
+
                 Task<int> routerTask = createRouterTask(logger, Launcher, linkedCancelToken);
 
                 while (!linkedCancelToken.IsCancellationRequested)
@@ -127,7 +129,19 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
                         }
                     }
                 }
-                return routerTask.Result;
+
+                if (!routerTask.IsCompleted)
+                {
+                    cancelRouterTask.Cancel();
+                }
+
+                await Task.WhenAny(routerTask, Task.Delay(1000, CancellationToken.None)).ConfigureAwait(false);
+                if (routerTask.IsCompleted)
+                {
+                    return routerTask.Result;
+                }
+
+                return 0;
             }
         }
 
@@ -335,19 +349,12 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
 
         private static string GetDefaultIpcServerPath(ILogger logger)
         {
+            string path = string.Empty;
             int processId = Process.GetCurrentProcess().Id;
+
             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
             {
-                string path = Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}");
-                if (File.Exists(path))
-                {
-                    logger?.LogWarning($"Default IPC server path, {path}, already in use. To disable default diagnostics for dotnet-dsrouter, set DOTNET_EnableDiagnostics=0 and re-run.");
-
-                    path = Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-dsrouter-{processId}");
-                    logger?.LogWarning($"Fallback using none default IPC server path, {path}.");
-                }
-
-                return path.Substring(PidIpcEndpoint.IpcRootPath.Length);
+                path = $"dotnet-diagnostic-dsrouter-{processId}";
             }
             else
             {
@@ -358,19 +365,13 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
                 unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
 #endif
                 TimeSpan diff = Process.GetCurrentProcess().StartTime.ToUniversalTime() - unixEpoch;
-
-                string path = Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}-{(long)diff.TotalSeconds}-socket");
-                if (Directory.GetFiles(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}-*-socket").Length != 0)
-                {
-                    logger?.LogWarning($"Default IPC server path, {Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}-*-socket")}, already in use. To disable default diagnostics for dotnet-dsrouter, set DOTNET_EnableDiagnostics=0 and re-run.");
-
-                    path = Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-dsrouter-{processId}-{(long)diff.TotalSeconds}-socket");
-                    logger?.LogWarning($"Fallback using none default IPC server path, {path}.");
-                }
-
-                return path;
+                path = Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-dsrouter-{processId}-{(long)diff.TotalSeconds}-socket");
             }
 
+            logger?.LogDebug($"Using default IPC server path, {path}.");
+            logger?.LogDebug($"Attach to default dotnet-dsrouter IPC server using --process-id {processId} diagnostic tooling argument.");
+
+            return path;
         }
 
         private static TcpClientRouterFactory.CreateInstanceDelegate ChooseTcpClientRouterFactory(string forwardPort, ILogger logger)
index 95cce47a61d79ea61843aa9036d3baa67cd2122b..1bea8efc788572d6cee629bdb8c01e1feba891ae 100644 (file)
@@ -362,6 +362,7 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
             {
                 if (_deviceConnectionID == 0)
                 {
+                    _logger.LogError($"Failed to connect device over USB, no device currently connected.");
                     throw new Exception($"Failed to connect device over USB, no device currently connected.");
                 }
 
@@ -370,6 +371,7 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
 
             if (result != 0)
             {
+                _logger?.LogError($"Failed USBMuxConnectByPort: device = {_deviceConnectionID}, port = {_port}, result = {result}.");
                 throw new Exception($"Failed to connect device over USB using connection {_deviceConnectionID} and port {_port}.");
             }
 
index ee8723815ee1c8922f445d485b01eaf83d83817e..63f8912ecc9711d4063bbc53bff57d42aab0d262 100644 (file)
@@ -8,6 +8,7 @@ using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
 using Graphs;
+using Microsoft.Diagnostics.NETCore.Client;
 using Microsoft.Internal.Common.Utils;
 using Microsoft.Tools.Common;
 
@@ -15,7 +16,7 @@ namespace Microsoft.Diagnostics.Tools.GCDump
 {
     internal static class CollectCommandHandler
     {
-        private delegate Task<int> CollectDelegate(CancellationToken ct, IConsole console, int processId, string output, int timeout, bool verbose, string name);
+        private delegate Task<int> CollectDelegate(CancellationToken ct, IConsole console, int processId, string output, int timeout, bool verbose, string name, string diagnosticPort);
 
         /// <summary>
         /// Collects a gcdump from a currently running process.
@@ -24,37 +25,42 @@ namespace Microsoft.Diagnostics.Tools.GCDump
         /// <param name="console"></param>
         /// <param name="processId">The process to collect the gcdump from.</param>
         /// <param name="output">The output path for the collected gcdump.</param>
+        /// <param name="timeout">The timeout for the collected gcdump.</param>
+        /// <param name="verbose">Enable verbose logging.</param>
+        /// <param name="name">The process name to collect the gcdump from.</param>
+        /// <param name="diagnosticPort">The diagnostic IPC channel to collect the gcdump from.</param>
         /// <returns></returns>
-        private static async Task<int> Collect(CancellationToken ct, IConsole console, int processId, string output, int timeout, bool verbose, string name)
+        private static async Task<int> Collect(CancellationToken ct, IConsole console, int processId, string output, int timeout, bool verbose, string name, string diagnosticPort)
         {
-            if (name != null)
+            if (!CommandUtils.ValidateArgumentsForAttach(processId, name, diagnosticPort, out int resolvedProcessId))
             {
-                if (processId != 0)
-                {
-                    Console.WriteLine("Can only specify either --name or --process-id option.");
-                    return -1;
-                }
-                processId = CommandUtils.FindProcessIdWithName(name);
-                if (processId < 0)
-                {
-                    return -1;
-                }
+                return -1;
             }
 
-            try
+            processId = resolvedProcessId;
+
+            if (!string.IsNullOrEmpty(diagnosticPort))
             {
-                if (processId < 0)
+                try
                 {
-                    Console.Out.WriteLine($"The PID cannot be negative: {processId}");
-                    return -1;
+                    IpcEndpointConfig config = IpcEndpointConfig.Parse(diagnosticPort);
+                    if (!config.IsConnectConfig)
+                    {
+                        Console.Error.WriteLine("--diagnostic-port is only supporting connect mode.");
+                        return -1;
+                    }
                 }
-
-                if (processId == 0)
+                catch (Exception ex)
                 {
-                    Console.Out.WriteLine("-p|--process-id is required");
+                    Console.Error.WriteLine($"--diagnostic-port argument error: {ex.Message}");
                     return -1;
                 }
 
+                processId = 0;
+            }
+
+            try
+            {
                 output = string.IsNullOrEmpty(output)
                     ? $"{DateTime.Now:yyyyMMdd\\_HHmmss}_{processId}.gcdump"
                     : output;
@@ -74,7 +80,7 @@ namespace Microsoft.Diagnostics.Tools.GCDump
                 Console.Out.WriteLine($"Writing gcdump to '{outputFileInfo.FullName}'...");
 
                 Task<bool> dumpTask = Task.Run(() => {
-                    if (TryCollectMemoryGraph(ct, processId, timeout, verbose, out MemoryGraph memoryGraph))
+                    if (TryCollectMemoryGraph(ct, processId, diagnosticPort, timeout, verbose, out MemoryGraph memoryGraph))
                     {
                         GCHeapDump.WriteMemoryGraph(memoryGraph, outputFileInfo.FullName, "dotnet-gcdump");
                         return true;
@@ -109,15 +115,14 @@ namespace Microsoft.Diagnostics.Tools.GCDump
             }
         }
 
-        internal static bool TryCollectMemoryGraph(CancellationToken ct, int processId, int timeout, bool verbose,
-            out MemoryGraph memoryGraph)
+        internal static bool TryCollectMemoryGraph(CancellationToken ct, int processId, string diagnosticPort, int timeout, bool verbose, out MemoryGraph memoryGraph)
         {
             DotNetHeapInfo heapInfo = new();
             TextWriter log = verbose ? Console.Out : TextWriter.Null;
 
             memoryGraph = new MemoryGraph(50_000);
 
-            if (!EventPipeDotNetHeapDumper.DumpFromEventPipe(ct, processId, memoryGraph, log, timeout, heapInfo))
+            if (!EventPipeDotNetHeapDumper.DumpFromEventPipe(ct, processId, diagnosticPort, memoryGraph, log, timeout, heapInfo))
             {
                 return false;
             }
@@ -134,10 +139,15 @@ namespace Microsoft.Diagnostics.Tools.GCDump
                 // Handler
                 HandlerDescriptor.FromDelegate((CollectDelegate) Collect).GetCommandHandler(),
                 // Options
-                ProcessIdOption(), OutputPathOption(), VerboseOption(), TimeoutOption(), NameOption()
+                ProcessIdOption(),
+                OutputPathOption(),
+                VerboseOption(),
+                TimeoutOption(),
+                NameOption(),
+                DiagnosticPortOption()
             };
 
-        private static Option ProcessIdOption() =>
+        private static Option<int> ProcessIdOption() =>
             new(
                 aliases: new[] { "-p", "--process-id" },
                 description: "The process id to collect the gcdump from.")
@@ -145,7 +155,7 @@ namespace Microsoft.Diagnostics.Tools.GCDump
                 Argument = new Argument<int>(name: "pid"),
             };
 
-        private static Option NameOption() =>
+        private static Option<string> NameOption() =>
             new(
                 aliases: new[] { "-n", "--name" },
                 description: "The name of the process to collect the gcdump from.")
@@ -153,7 +163,7 @@ namespace Microsoft.Diagnostics.Tools.GCDump
                 Argument = new Argument<string>(name: "name")
             };
 
-        private static Option OutputPathOption() =>
+        private static Option<string> OutputPathOption() =>
             new(
                 aliases: new[] { "-o", "--output" },
                 description: $@"The path where collected gcdumps should be written. Defaults to '.\YYYYMMDD_HHMMSS_<pid>.gcdump' where YYYYMMDD is Year/Month/Day and HHMMSS is Hour/Minute/Second. Otherwise, it is the full path and file name of the dump.")
@@ -161,7 +171,7 @@ namespace Microsoft.Diagnostics.Tools.GCDump
                 Argument = new Argument<string>(name: "gcdump-file-path", getDefaultValue: () => string.Empty)
             };
 
-        private static Option VerboseOption() =>
+        private static Option<bool> VerboseOption() =>
             new(
                 aliases: new[] { "-v", "--verbose" },
                 description: "Output the log while collecting the gcdump.")
@@ -170,12 +180,20 @@ namespace Microsoft.Diagnostics.Tools.GCDump
             };
 
         public static int DefaultTimeout = 30;
-        private static Option TimeoutOption() =>
+        private static Option<int> TimeoutOption() =>
             new(
                 aliases: new[] { "-t", "--timeout" },
                 description: $"Give up on collecting the gcdump if it takes longer than this many seconds. The default value is {DefaultTimeout}s.")
             {
                 Argument = new Argument<int>(name: "timeout", getDefaultValue: () => DefaultTimeout)
             };
+
+        private static Option<string> DiagnosticPortOption() =>
+        new(
+            aliases: new[] { "--dport", "--diagnostic-port" },
+            description: "The path to a diagnostic port to collect the dump from.")
+        {
+            Argument = new Argument<string>(name: "diagnostic-port", getDefaultValue: () => string.Empty)
+        };
     }
 }
diff --git a/src/Tools/dotnet-gcdump/CommandLine/ConvertCommandHandler.cs b/src/Tools/dotnet-gcdump/CommandLine/ConvertCommandHandler.cs
new file mode 100644 (file)
index 0000000..60de9b0
--- /dev/null
@@ -0,0 +1,92 @@
+// 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.CommandLine;
+using System.IO;
+using Graphs;
+using Microsoft.Tools.Common;
+
+namespace Microsoft.Diagnostics.Tools.GCDump
+{
+    internal static class ConvertCommandHandler
+    {
+        public static int ConvertFile(FileInfo input, string output, bool verbose)
+        {
+            if (!input.Exists)
+            {
+                Console.Error.WriteLine($"File '{input.FullName}' does not exist.");
+                return -1;
+            }
+
+            output = string.IsNullOrEmpty(output)
+                    ? Path.ChangeExtension(input.FullName, "gcdump")
+                    : output;
+
+            FileInfo outputFileInfo = new(output);
+
+            if (outputFileInfo.Exists)
+            {
+                outputFileInfo.Delete();
+            }
+
+            if (string.IsNullOrEmpty(outputFileInfo.Extension) || outputFileInfo.Extension != ".gcdump")
+            {
+                outputFileInfo = new FileInfo(outputFileInfo.FullName + ".gcdump");
+            }
+
+            Console.Out.WriteLine($"Writing gcdump to '{outputFileInfo.FullName}'...");
+
+            DotNetHeapInfo heapInfo = new();
+            TextWriter log = verbose ? Console.Out : TextWriter.Null;
+
+            MemoryGraph memoryGraph = new(50_000);
+
+            if (!EventPipeDotNetHeapDumper.DumpFromEventPipeFile(input.FullName, memoryGraph, log, heapInfo))
+            {
+                return -1;
+            }
+
+            memoryGraph.AllowReading();
+            GCHeapDump.WriteMemoryGraph(memoryGraph, outputFileInfo.FullName, "dotnet-gcdump");
+
+            return 0;
+        }
+
+        public static System.CommandLine.Command ConvertCommand() =>
+            new(
+                name: "convert",
+                description: "Converts nettrace file into .gcdump file handled by analysis tools. Can only convert from the nettrace format.")
+            {
+                // Handler
+                System.CommandLine.Invocation.CommandHandler.Create<FileInfo, string, bool>(ConvertFile),
+                // Arguments and Options
+                InputPathArgument(),
+                OutputPathOption(),
+                VerboseOption()
+            };
+
+        private static Argument<FileInfo> InputPathArgument() =>
+            new Argument<FileInfo>("input")
+            {
+                Description = "Input trace file to be converted.",
+                Arity = new ArgumentArity(0, 1)
+            }.ExistingOnly();
+
+        private static Option<string> OutputPathOption() =>
+            new(
+                aliases: new[] { "-o", "--output" },
+                description: $@"The path where converted gcdump should be written. Defaults to '<input>.gcdump'")
+            {
+                Argument = new Argument<string>(name: "output", getDefaultValue: () => string.Empty)
+            };
+
+        private static Option<bool> VerboseOption() =>
+            new(
+                aliases: new[] { "-v", "--verbose" },
+                description: "Output the log while converting the gcdump.")
+            {
+                Argument = new Argument<bool>(name: "verbose", getDefaultValue: () => false)
+            };
+    }
+}
index 2f754f7406d26b55819df083e69d470ce3716b2f..27dd2400eeebaef844d0fd465e4bfce1ca438f08 100644 (file)
@@ -9,12 +9,14 @@ using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Diagnostics.Tools.GCDump.CommandLine;
 using Microsoft.Tools.Common;
+using Microsoft.Internal.Common.Utils;
+using Microsoft.Diagnostics.NETCore.Client;
 
 namespace Microsoft.Diagnostics.Tools.GCDump
 {
     internal static class ReportCommandHandler
     {
-        private delegate Task<int> ReportDelegate(CancellationToken ct, IConsole console, FileInfo gcdump_filename, int? processId = null, ReportType reportType = ReportType.HeapStat);
+        private delegate Task<int> ReportDelegate(CancellationToken ct, IConsole console, FileInfo gcdump_filename, int? processId = null, ReportType reportType = ReportType.HeapStat, string diagnosticPort = null);
 
         public static Command ReportCommand() =>
             new(
@@ -24,23 +26,32 @@ namespace Microsoft.Diagnostics.Tools.GCDump
                 // Handler
                 HandlerDescriptor.FromDelegate((ReportDelegate) Report).GetCommandHandler(),
                 // Options
-                FileNameArgument(), ProcessIdOption(), ReportTypeOption()
+                FileNameArgument(),
+                ProcessIdOption(),
+                ReportTypeOption(),
+                DiagnosticPortOption(),
             };
 
-        private static Task<int> Report(CancellationToken ct, IConsole console, FileInfo gcdump_filename, int? processId = null, ReportType type = ReportType.HeapStat)
+        private static Task<int> Report(CancellationToken ct, IConsole console, FileInfo gcdump_filename, int? processId = null, ReportType type = ReportType.HeapStat, string diagnosticPort = null)
         {
             //
             // Validation
             //
-            if (gcdump_filename == null && !processId.HasValue)
+            if (gcdump_filename == null && !processId.HasValue && string.IsNullOrEmpty(diagnosticPort))
             {
-                Console.Error.WriteLine("<gcdump_filename> or -p|--process-id is required");
+                Console.Error.WriteLine("<gcdump_filename> or -p|--process-id or --dport|--diagnostic-port is required");
                 return Task.FromResult(-1);
             }
 
-            if (gcdump_filename != null && processId.HasValue)
+            if (gcdump_filename != null && (processId.HasValue || !string.IsNullOrEmpty(diagnosticPort)))
             {
-                Console.Error.WriteLine("Specify only one of -f|--file or -p|--process-id.");
+                Console.Error.WriteLine("Specify only one of -f|--file or -p|--process-id or --dport|--diagnostic-port.");
+                return Task.FromResult(-1);
+            }
+
+            if (processId.HasValue && !string.IsNullOrEmpty(diagnosticPort))
+            {
+                Console.Error.WriteLine("Specify only one of -p|--process-id or -dport|--diagnostic-port.");
                 return Task.FromResult(-1);
             }
 
@@ -53,14 +64,14 @@ namespace Microsoft.Diagnostics.Tools.GCDump
             {
                 source = ReportSource.DumpFile;
             }
-            else if (processId.HasValue)
+            else if (processId.HasValue || !string.IsNullOrEmpty(diagnosticPort))
             {
                 source = ReportSource.Process;
             }
 
             return (source, type) switch
             {
-                (ReportSource.Process, ReportType.HeapStat) => ReportFromProcess(processId.Value, ct),
+                (ReportSource.Process, ReportType.HeapStat) => ReportFromProcess(processId ?? 0, diagnosticPort, ct),
                 (ReportSource.DumpFile, ReportType.HeapStat) => ReportFromFile(gcdump_filename),
                 _ => HandleUnknownParam()
             };
@@ -72,10 +83,37 @@ namespace Microsoft.Diagnostics.Tools.GCDump
             return Task.FromResult(-1);
         }
 
-        private static Task<int> ReportFromProcess(int processId, CancellationToken ct)
+        private static Task<int> ReportFromProcess(int processId, string diagnosticPort, CancellationToken ct)
         {
+            if (!CommandUtils.ValidateArgumentsForAttach(processId, string.Empty, diagnosticPort, out int resolvedProcessId))
+            {
+                return Task.FromResult(-1);
+            }
+
+            processId = resolvedProcessId;
+
+            if (!string.IsNullOrEmpty(diagnosticPort))
+            {
+                try
+                {
+                    IpcEndpointConfig config = IpcEndpointConfig.Parse(diagnosticPort);
+                    if (!config.IsConnectConfig)
+                    {
+                        Console.Error.WriteLine("--diagnostic-port is only supporting connect mode.");
+                        return Task.FromResult(-1);
+                    }
+                }
+                catch (Exception ex)
+                {
+                    Console.Error.WriteLine($"--diagnostic-port argument error: {ex.Message}");
+                    return Task.FromResult(-1);
+                }
+
+                processId = 0;
+            }
+
             if (!CollectCommandHandler
-                .TryCollectMemoryGraph(ct, processId, CollectCommandHandler.DefaultTimeout, false, out Graphs.MemoryGraph mg))
+                .TryCollectMemoryGraph(ct, processId, diagnosticPort, CollectCommandHandler.DefaultTimeout, false, out Graphs.MemoryGraph mg))
             {
                 Console.Error.WriteLine("An error occured while collecting gcdump.");
                 return Task.FromResult(-1);
@@ -115,12 +153,27 @@ namespace Microsoft.Diagnostics.Tools.GCDump
             }.ExistingOnly();
 
         private static Option<int> ProcessIdOption() =>
-            new(new[] { "-p", "--process-id" }, "The process id to collect the gcdump from.");
+            new(
+                aliases: new[] { "-p", "--process-id" },
+                description: "The process id to collect the gcdump from.")
+            {
+                Argument = new Argument<int>(name: "pid"),
+            };
 
         private static Option<ReportType> ReportTypeOption() =>
-            new(new[] { "-t", "--report-type" }, "The type of report to generate. Available options: heapstat (default)")
+            new(
+                aliases: new[] { "-t", "--report-type" },
+                description: "The type of report to generate. Available options: heapstat (default)")
+            {
+                Argument = new Argument<ReportType>(name: "report-type", () => ReportType.HeapStat)
+            };
+
+        private static Option<string> DiagnosticPortOption() =>
+            new(
+                aliases: new[] { "--dport", "--diagnostic-port" },
+                description: "The path to a diagnostic port to collect the dump from.")
             {
-                Argument = new Argument<ReportType>(() => ReportType.HeapStat)
+                Argument = new Argument<string>(name: "diagnostic-port", getDefaultValue: () => string.Empty)
             };
 
         private enum ReportSource
index fb97bc35551fa3274821c5036f275631ab8f81a5..4d8a40c6bb9aef0deed10a56e0135855a7097923 100644 (file)
@@ -20,18 +20,102 @@ namespace Microsoft.Diagnostics.Tools.GCDump
         internal static volatile bool eventPipeDataPresent;
         internal static volatile bool dumpComplete;
 
+        /// <summary>
+        /// Given a nettrace file from a EventPipe session with the appropriate provider and keywords turned on,
+        /// generate a GCHeapDump using the resulting events.
+        /// </summary>
+        /// <param name="path"></param>
+        /// <param name="memoryGraph"></param>
+        /// <param name="log"></param>
+        /// <param name="dotNetInfo"></param>
+        /// <returns></returns>
+        public static bool DumpFromEventPipeFile(string path, MemoryGraph memoryGraph, TextWriter log, DotNetHeapInfo dotNetInfo)
+        {
+            DateTime start = DateTime.Now;
+            Func<TimeSpan> getElapsed = () => DateTime.Now - start;
+
+            DotNetHeapDumpGraphReader dumper = new(log)
+            {
+                DotNetHeapInfo = dotNetInfo
+            };
+
+            try
+            {
+                TimeSpan lastEventPipeUpdate = getElapsed();
+
+                int gcNum = -1;
+
+                EventPipeEventSource source = new(path);
+
+                source.Clr.GCStart += delegate (GCStartTraceData data)
+                {
+                    eventPipeDataPresent = true;
+
+                    if (gcNum < 0 && data.Depth == 2 && data.Type != GCType.BackgroundGC)
+                    {
+                        gcNum = data.Count;
+                        log.WriteLine("{0,5:n1}s: .NET Dump Started...", getElapsed().TotalSeconds);
+                    }
+                };
+
+                source.Clr.GCStop += delegate (GCEndTraceData data)
+                {
+                    if (data.Count == gcNum)
+                    {
+                        log.WriteLine("{0,5:n1}s: .NET GC Complete.", getElapsed().TotalSeconds);
+                        dumpComplete = true;
+                    }
+                };
+
+                source.Clr.GCBulkNode += delegate (GCBulkNodeTraceData data)
+                {
+                    eventPipeDataPresent = true;
+
+                    if ((getElapsed() - lastEventPipeUpdate).TotalMilliseconds > 500)
+                    {
+                        log.WriteLine("{0,5:n1}s: Making GC Heap Progress...", getElapsed().TotalSeconds);
+                    }
+
+                    lastEventPipeUpdate = getElapsed();
+                };
+
+                if (memoryGraph != null)
+                {
+                    dumper.SetupCallbacks(memoryGraph, source);
+                }
+
+                log.WriteLine("{0,5:n1}s: Starting to process events", getElapsed().TotalSeconds);
+                source.Process();
+                log.WriteLine("{0,5:n1}s: Finished processing events", getElapsed().TotalSeconds);
+
+                if (eventPipeDataPresent)
+                {
+                    dumper.ConvertHeapDataToGraph();
+                }
+            }
+            catch (Exception e)
+            {
+                log.WriteLine($"{getElapsed().TotalSeconds,5:n1}s: [Error] Exception processing events: {e}");
+            }
+
+            log.WriteLine("[{0,5:n1}s: Done Dumping .NET heap success={1}]", getElapsed().TotalSeconds, dumpComplete);
+
+            return dumpComplete;
+        }
+
         /// <summary>
         /// Given a factory for creating an EventPipe session with the appropriate provider and keywords turned on,
         /// generate a GCHeapDump using the resulting events.  The correct keywords and provider name
         /// are given as input to the Func eventPipeEventSourceFactory.
         /// </summary>
-        /// <param name="processID"></param>
-        /// <param name="eventPipeEventSourceFactory">A delegate for creating and stopping EventPipe sessions</param>
+        /// <param name="processId"></param>
+        /// <param name="diagnosticPort"></param>
         /// <param name="memoryGraph"></param>
         /// <param name="log"></param>
+        /// <param name="timeout"></param>
         /// <param name="dotNetInfo"></param>
         /// <returns></returns>
-        public static bool DumpFromEventPipe(CancellationToken ct, int processID, MemoryGraph memoryGraph, TextWriter log, int timeout, DotNetHeapInfo dotNetInfo = null)
+        public static bool DumpFromEventPipe(CancellationToken ct, int processId, string diagnosticPort, MemoryGraph memoryGraph, TextWriter log, int timeout, DotNetHeapInfo dotNetInfo)
         {
             DateTime start = DateTime.Now;
             Func<TimeSpan> getElapsed = () => DateTime.Now - start;
@@ -47,7 +131,7 @@ namespace Microsoft.Diagnostics.Tools.GCDump
                 bool fDone = false;
                 log.WriteLine("{0,5:n1}s: Creating type table flushing task", getElapsed().TotalSeconds);
 
-                using (EventPipeSessionController typeFlushSession = new(processID, new List<EventPipeProvider> {
+                using (EventPipeSessionController typeFlushSession = new(processId, diagnosticPort, new List<EventPipeProvider> {
                     new EventPipeProvider("Microsoft-DotNETCore-SampleProfiler", EventLevel.Informational)
                 }, false))
                 {
@@ -72,7 +156,7 @@ namespace Microsoft.Diagnostics.Tools.GCDump
                 // Start the providers and trigger the GCs.
                 log.WriteLine("{0,5:n1}s: Requesting a .NET Heap Dump", getElapsed().TotalSeconds);
 
-                using EventPipeSessionController gcDumpSession = new(processID, new List<EventPipeProvider> {
+                using EventPipeSessionController gcDumpSession = new(processId, diagnosticPort, new List<EventPipeProvider> {
                     new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Verbose, (long)(ClrTraceEventParser.Keywords.GCHeapSnapshot))
                 });
                 log.WriteLine("{0,5:n1}s: gcdump EventPipe Session started", getElapsed().TotalSeconds);
@@ -81,7 +165,11 @@ namespace Microsoft.Diagnostics.Tools.GCDump
 
                 gcDumpSession.Source.Clr.GCStart += delegate (GCStartTraceData data)
                 {
-                    if (data.ProcessID != processID)
+                    if (gcDumpSession.UseWildcardProcessId)
+                    {
+                        processId = data.ProcessID;
+                    }
+                    if (data.ProcessID != processId)
                     {
                         return;
                     }
@@ -97,7 +185,7 @@ namespace Microsoft.Diagnostics.Tools.GCDump
 
                 gcDumpSession.Source.Clr.GCStop += delegate (GCEndTraceData data)
                 {
-                    if (data.ProcessID != processID)
+                    if (data.ProcessID != processId)
                     {
                         return;
                     }
@@ -111,7 +199,7 @@ namespace Microsoft.Diagnostics.Tools.GCDump
 
                 gcDumpSession.Source.Clr.GCBulkNode += delegate (GCBulkNodeTraceData data)
                 {
-                    if (data.ProcessID != processID)
+                    if (data.ProcessID != processId)
                     {
                         return;
                     }
@@ -128,7 +216,7 @@ namespace Microsoft.Diagnostics.Tools.GCDump
 
                 if (memoryGraph != null)
                 {
-                    dumper.SetupCallbacks(memoryGraph, gcDumpSession.Source, processID.ToString());
+                    dumper.SetupCallbacks(memoryGraph, gcDumpSession.Source, gcDumpSession.UseWildcardProcessId ? null : processId.ToString());
                 }
 
                 // Set up a separate thread that will listen for EventPipe events coming back telling us we succeeded.
@@ -229,15 +317,49 @@ namespace Microsoft.Diagnostics.Tools.GCDump
         private EventPipeSession _session;
         private EventPipeEventSource _source;
         private int _pid;
+        private IpcEndpointConfig _diagnosticPort;
 
         public IReadOnlyList<EventPipeProvider> Providers => _providers.AsReadOnly();
         public EventPipeEventSource Source => _source;
 
-        public EventPipeSessionController(int pid, List<EventPipeProvider> providers, bool requestRundown = true)
+        public bool UseWildcardProcessId => _diagnosticPort != null;
+
+        public EventPipeSessionController(int pid, string diagnosticPort, List<EventPipeProvider> providers, bool requestRundown = true)
         {
+            if (string.IsNullOrEmpty(diagnosticPort))
+            {
+                try
+                {
+                    string defaultAddress = PidIpcEndpoint.GetDefaultAddress(pid);
+                    if (!string.IsNullOrEmpty(defaultAddress) && PidIpcEndpoint.IsDefaultAddressDSRouter(pid, defaultAddress))
+                    {
+                        diagnosticPort = defaultAddress + ",connect";
+                    }
+                }
+                catch { }
+            }
+
+            if (!string.IsNullOrEmpty(diagnosticPort))
+            {
+                _diagnosticPort = IpcEndpointConfig.Parse(diagnosticPort);
+                if (!_diagnosticPort.IsConnectConfig)
+                {
+                    throw new ArgumentException("DiagnosticPort is only supporting connect mode.");
+                }
+            }
+
             _pid = pid;
             _providers = providers;
-            _client = new DiagnosticsClient(pid);
+
+            if (_diagnosticPort != null)
+            {
+                _client = new DiagnosticsClient(_diagnosticPort);
+            }
+            else
+            {
+                _client = new DiagnosticsClient(pid);
+            }
+
             _session = _client.StartEventPipeSession(providers, requestRundown, 1024);
             _source = new EventPipeEventSource(_session.EventStream);
         }
index 8830c743d5f6a83daabd25e2fb104542fc2f7b9c..5de61d81ad575435390d2d4055a43c960edbad07 100644 (file)
@@ -16,6 +16,7 @@ namespace Microsoft.Diagnostics.Tools.GCDump
                 .AddCommand(CollectCommandHandler.CollectCommand())
                 .AddCommand(ProcessStatusCommandHandler.ProcessStatusCommand("Lists the dotnet processes that gcdumps can be collected from."))
                 .AddCommand(ReportCommandHandler.ReportCommand())
+                .AddCommand(ConvertCommandHandler.ConvertCommand())
                 .UseDefaults()
                 .Build();
 
index fbf3fdac413676ce4cc9ac47db3841b6dd775556..15eab9c79992e93ddfcee80bb39fe2c0800211c9 100644 (file)
@@ -526,7 +526,7 @@ namespace Microsoft.Diagnostics.Tools.Trace
             };
         private static Option DiagnosticPortOption() =>
             new(
-                alias: "--diagnostic-port",
+                aliases: new[] { "--dport", "--diagnostic-port" },
                 description: @"The path to a diagnostic port to be used.")
             {
                 Argument = new Argument<string>(name: "diagnosticPort", getDefaultValue: () => string.Empty)