diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcTransport.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcTransport.cs index d49b24d007..6789b0aedf 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcTransport.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcTransport.cs @@ -262,25 +262,7 @@ public override async Task WaitForConnectionAsync(CancellationToken token) 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 @@ private static bool TryGetDefaultAddress(int pid, out string defaultAddress) 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 @@ private static bool TryGetDefaultAddress(int pid, out string defaultAddress) 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); diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs index 19cd302023..80abfd44c3 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs @@ -137,6 +137,8 @@ private static async Task runRouter(CancellationToken token, DiagnosticsSer routerFactory.Logger?.LogInformation("Starting automatic shutdown."); throw; } + + routerFactory.Logger?.LogTrace($"runRouter continues after exception: {ex.Message}"); } } } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj b/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj index 094b309355..c4fe96c3e8 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj +++ b/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj @@ -1,4 +1,4 @@ - + Library netstandard2.0;net6.0 @@ -31,6 +31,7 @@ + diff --git a/src/Tools/Common/Commands/Utils.cs b/src/Tools/Common/Commands/Utils.cs index 2d4951e9b5..823bedaf24 100644 --- a/src/Tools/Common/Commands/Utils.cs +++ b/src/Tools/Common/Commands/Utils.cs @@ -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 @@ public static bool ValidateArgumentsForChildProcess(int processId, string name, 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 @@ public static bool ValidateArgumentsForAttach(int processId, string name, string 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 @@ public static bool ValidateArgumentsForAttach(int processId, string name, string return true; } // Resolve name option - else if (name != null) + else if (!string.IsNullOrEmpty(name)) { processId = CommandUtils.FindProcessIdWithName(name); if (processId < 0) diff --git a/src/Tools/dotnet-counters/CounterMonitor.cs b/src/Tools/dotnet-counters/CounterMonitor.cs index a017074386..ea6b2e5f59 100644 --- a/src/Tools/dotnet-counters/CounterMonitor.cs +++ b/src/Tools/dotnet-counters/CounterMonitor.cs @@ -593,7 +593,6 @@ public async Task Collect( { return (int)ReturnCode.ArgumentError; } - ct.Register(() => _shouldExit.TrySetResult((int)ReturnCode.Ok)); DiagnosticsClientBuilder builder = new("dotnet-counters", 10); diff --git a/src/Tools/dotnet-counters/Program.cs b/src/Tools/dotnet-counters/Program.cs index 826b8373b6..ec79a2e6ef 100644 --- a/src/Tools/dotnet-counters/Program.cs +++ b/src/Tools/dotnet-counters/Program.cs @@ -167,7 +167,7 @@ private static Option RuntimeVersionOption() => 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(name: "diagnosticPort", getDefaultValue: () => "") diff --git a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs index f7c37f1177..59692a9e10 100644 --- a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs +++ b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs @@ -105,6 +105,8 @@ public async Task CommonRunLoop(Func routerTask = createRouterTask(logger, Launcher, linkedCancelToken); while (!linkedCancelToken.IsCancellationRequested) @@ -127,7 +129,19 @@ await Task.WhenAny(routerTask, Task.Delay( } } } - 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 @@ public async Task RunIpcClientWebSocketServerRouter(CancellationToken token 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 @@ private static string GetDefaultIpcServerPath(ILogger logger) 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) diff --git a/src/Tools/dotnet-dsrouter/USBMuxTcpClientRouterFactory.cs b/src/Tools/dotnet-dsrouter/USBMuxTcpClientRouterFactory.cs index 95cce47a61..1bea8efc78 100644 --- a/src/Tools/dotnet-dsrouter/USBMuxTcpClientRouterFactory.cs +++ b/src/Tools/dotnet-dsrouter/USBMuxTcpClientRouterFactory.cs @@ -362,6 +362,7 @@ private int ConnectTcpClientOverUSBMux() { 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 @@ private int ConnectTcpClientOverUSBMux() 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}."); } diff --git a/src/Tools/dotnet-gcdump/CommandLine/CollectCommandHandler.cs b/src/Tools/dotnet-gcdump/CommandLine/CollectCommandHandler.cs index ee8723815e..63f8912ecc 100644 --- a/src/Tools/dotnet-gcdump/CommandLine/CollectCommandHandler.cs +++ b/src/Tools/dotnet-gcdump/CommandLine/CollectCommandHandler.cs @@ -8,6 +8,7 @@ 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 CollectDelegate(CancellationToken ct, IConsole console, int processId, string output, int timeout, bool verbose, string name); + private delegate Task CollectDelegate(CancellationToken ct, IConsole console, int processId, string output, int timeout, bool verbose, string name, string diagnosticPort); /// /// Collects a gcdump from a currently running process. @@ -24,37 +25,42 @@ internal static class CollectCommandHandler /// /// The process to collect the gcdump from. /// The output path for the collected gcdump. + /// The timeout for the collected gcdump. + /// Enable verbose logging. + /// The process name to collect the gcdump from. + /// The diagnostic IPC channel to collect the gcdump from. /// - private static async Task Collect(CancellationToken ct, IConsole console, int processId, string output, int timeout, bool verbose, string name) + private static async Task 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 @@ private static async Task Collect(CancellationToken ct, IConsole console, i Console.Out.WriteLine($"Writing gcdump to '{outputFileInfo.FullName}'..."); Task 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 @@ private static async Task Collect(CancellationToken ct, IConsole console, i } } - 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 @@ public static Command CollectCommand() => // 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 ProcessIdOption() => new( aliases: new[] { "-p", "--process-id" }, description: "The process id to collect the gcdump from.") @@ -145,7 +155,7 @@ private static Option ProcessIdOption() => Argument = new Argument(name: "pid"), }; - private static Option NameOption() => + private static Option NameOption() => new( aliases: new[] { "-n", "--name" }, description: "The name of the process to collect the gcdump from.") @@ -153,7 +163,7 @@ private static Option NameOption() => Argument = new Argument(name: "name") }; - private static Option OutputPathOption() => + private static Option OutputPathOption() => new( aliases: new[] { "-o", "--output" }, description: $@"The path where collected gcdumps should be written. Defaults to '.\YYYYMMDD_HHMMSS_.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 @@ private static Option OutputPathOption() => Argument = new Argument(name: "gcdump-file-path", getDefaultValue: () => string.Empty) }; - private static Option VerboseOption() => + private static Option VerboseOption() => new( aliases: new[] { "-v", "--verbose" }, description: "Output the log while collecting the gcdump.") @@ -170,12 +180,20 @@ private static Option VerboseOption() => }; public static int DefaultTimeout = 30; - private static Option TimeoutOption() => + private static Option 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(name: "timeout", getDefaultValue: () => DefaultTimeout) }; + + private static Option DiagnosticPortOption() => + new( + aliases: new[] { "--dport", "--diagnostic-port" }, + description: "The path to a diagnostic port to collect the dump from.") + { + Argument = new Argument(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 index 0000000000..60de9b0e9e --- /dev/null +++ b/src/Tools/dotnet-gcdump/CommandLine/ConvertCommandHandler.cs @@ -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(ConvertFile), + // Arguments and Options + InputPathArgument(), + OutputPathOption(), + VerboseOption() + }; + + private static Argument InputPathArgument() => + new Argument("input") + { + Description = "Input trace file to be converted.", + Arity = new ArgumentArity(0, 1) + }.ExistingOnly(); + + private static Option OutputPathOption() => + new( + aliases: new[] { "-o", "--output" }, + description: $@"The path where converted gcdump should be written. Defaults to '.gcdump'") + { + Argument = new Argument(name: "output", getDefaultValue: () => string.Empty) + }; + + private static Option VerboseOption() => + new( + aliases: new[] { "-v", "--verbose" }, + description: "Output the log while converting the gcdump.") + { + Argument = new Argument(name: "verbose", getDefaultValue: () => false) + }; + } +} diff --git a/src/Tools/dotnet-gcdump/CommandLine/ReportCommandHandler.cs b/src/Tools/dotnet-gcdump/CommandLine/ReportCommandHandler.cs index 2f754f7406..27dd2400ee 100644 --- a/src/Tools/dotnet-gcdump/CommandLine/ReportCommandHandler.cs +++ b/src/Tools/dotnet-gcdump/CommandLine/ReportCommandHandler.cs @@ -9,12 +9,14 @@ 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 ReportDelegate(CancellationToken ct, IConsole console, FileInfo gcdump_filename, int? processId = null, ReportType reportType = ReportType.HeapStat); + private delegate Task 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 @@ public static Command ReportCommand() => // Handler HandlerDescriptor.FromDelegate((ReportDelegate) Report).GetCommandHandler(), // Options - FileNameArgument(), ProcessIdOption(), ReportTypeOption() + FileNameArgument(), + ProcessIdOption(), + ReportTypeOption(), + DiagnosticPortOption(), }; - private static Task Report(CancellationToken ct, IConsole console, FileInfo gcdump_filename, int? processId = null, ReportType type = ReportType.HeapStat) + private static Task 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(" or -p|--process-id is required"); + Console.Error.WriteLine(" 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 @@ private static Task Report(CancellationToken ct, IConsole console, FileInfo { 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 @@ private static Task HandleUnknownParam() return Task.FromResult(-1); } - private static Task ReportFromProcess(int processId, CancellationToken ct) + private static Task 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 @@ private static Argument FileNameArgument() => }.ExistingOnly(); private static Option 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(name: "pid"), + }; private static Option 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(name: "report-type", () => ReportType.HeapStat) + }; + + private static Option DiagnosticPortOption() => + new( + aliases: new[] { "--dport", "--diagnostic-port" }, + description: "The path to a diagnostic port to collect the dump from.") { - Argument = new Argument(() => ReportType.HeapStat) + Argument = new Argument(name: "diagnostic-port", getDefaultValue: () => string.Empty) }; private enum ReportSource diff --git a/src/Tools/dotnet-gcdump/DotNetHeapDump/EventPipeDotNetHeapDumper.cs b/src/Tools/dotnet-gcdump/DotNetHeapDump/EventPipeDotNetHeapDumper.cs index fb97bc3555..4d8a40c6bb 100644 --- a/src/Tools/dotnet-gcdump/DotNetHeapDump/EventPipeDotNetHeapDumper.cs +++ b/src/Tools/dotnet-gcdump/DotNetHeapDump/EventPipeDotNetHeapDumper.cs @@ -20,18 +20,102 @@ public static class EventPipeDotNetHeapDumper internal static volatile bool eventPipeDataPresent; internal static volatile bool dumpComplete; + /// + /// Given a nettrace file from a EventPipe session with the appropriate provider and keywords turned on, + /// generate a GCHeapDump using the resulting events. + /// + /// + /// + /// + /// + /// + public static bool DumpFromEventPipeFile(string path, MemoryGraph memoryGraph, TextWriter log, DotNetHeapInfo dotNetInfo) + { + DateTime start = DateTime.Now; + Func 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; + } + /// /// 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. /// - /// - /// A delegate for creating and stopping EventPipe sessions + /// + /// /// /// + /// /// /// - 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 getElapsed = () => DateTime.Now - start; @@ -47,7 +131,7 @@ public static bool DumpFromEventPipe(CancellationToken ct, int processID, Memory bool fDone = false; log.WriteLine("{0,5:n1}s: Creating type table flushing task", getElapsed().TotalSeconds); - using (EventPipeSessionController typeFlushSession = new(processID, new List { + using (EventPipeSessionController typeFlushSession = new(processId, diagnosticPort, new List { new EventPipeProvider("Microsoft-DotNETCore-SampleProfiler", EventLevel.Informational) }, false)) { @@ -72,7 +156,7 @@ public static bool DumpFromEventPipe(CancellationToken ct, int processID, Memory // 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 { + using EventPipeSessionController gcDumpSession = new(processId, diagnosticPort, new List { 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 @@ public static bool DumpFromEventPipe(CancellationToken ct, int processID, Memory 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 @@ public static bool DumpFromEventPipe(CancellationToken ct, int processID, Memory gcDumpSession.Source.Clr.GCStop += delegate (GCEndTraceData data) { - if (data.ProcessID != processID) + if (data.ProcessID != processId) { return; } @@ -111,7 +199,7 @@ public static bool DumpFromEventPipe(CancellationToken ct, int processID, Memory gcDumpSession.Source.Clr.GCBulkNode += delegate (GCBulkNodeTraceData data) { - if (data.ProcessID != processID) + if (data.ProcessID != processId) { return; } @@ -128,7 +216,7 @@ public static bool DumpFromEventPipe(CancellationToken ct, int processID, Memory 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 @@ internal sealed class EventPipeSessionController : IDisposable private EventPipeSession _session; private EventPipeEventSource _source; private int _pid; + private IpcEndpointConfig _diagnosticPort; public IReadOnlyList Providers => _providers.AsReadOnly(); public EventPipeEventSource Source => _source; - public EventPipeSessionController(int pid, List providers, bool requestRundown = true) + public bool UseWildcardProcessId => _diagnosticPort != null; + + public EventPipeSessionController(int pid, string diagnosticPort, List 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); } diff --git a/src/Tools/dotnet-gcdump/Program.cs b/src/Tools/dotnet-gcdump/Program.cs index 8830c743d5..5de61d81ad 100644 --- a/src/Tools/dotnet-gcdump/Program.cs +++ b/src/Tools/dotnet-gcdump/Program.cs @@ -16,6 +16,7 @@ public static Task Main(string[] args) .AddCommand(CollectCommandHandler.CollectCommand()) .AddCommand(ProcessStatusCommandHandler.ProcessStatusCommand("Lists the dotnet processes that gcdumps can be collected from.")) .AddCommand(ReportCommandHandler.ReportCommand()) + .AddCommand(ConvertCommandHandler.ConvertCommand()) .UseDefaults() .Build(); diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs index fbf3fdac41..15eab9c799 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs @@ -526,7 +526,7 @@ private static Option CLREventLevelOption() => }; 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(name: "diagnosticPort", getDefaultValue: () => string.Empty)