diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index 32fec2755d..bc478f9aa2 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -94,10 +94,16 @@ internal int CollectLinux(CollectLinuxArgs args) if (args.ProcessId != 0 || !string.IsNullOrEmpty(args.Name)) { - if (!ProcessSupportsUserEventsIpcCommand(args.ProcessId, args.Name, out int resolvedProcessId, out string resolvedProcessName, out string detectedRuntimeVersion)) + CommandUtils.ResolveProcess(args.ProcessId, args.Name, out int resolvedProcessId, out string resolvedProcessName); + UserEventsProbeResult probeResult = ProbeProcess(resolvedProcessId, out string detectedRuntimeVersion); + switch (probeResult) { - Console.Error.WriteLine($"[ERROR] Process '{resolvedProcessName} ({resolvedProcessId})' cannot be traced by collect-linux. Required runtime: {minRuntimeSupportingUserEventsIPCCommand}. Detected runtime: {detectedRuntimeVersion}"); - return (int)ReturnCode.TracingError; + case UserEventsProbeResult.NotSupported: + Console.Error.WriteLine($"[ERROR] Process '{resolvedProcessName} ({resolvedProcessId})' cannot be traced by collect-linux. Required runtime: {minRuntimeSupportingUserEventsIPCCommand}. Detected runtime: {detectedRuntimeVersion}"); + return (int)ReturnCode.TracingError; + case UserEventsProbeResult.ConnectionFailed: + Console.Error.WriteLine($"[ERROR] Unable to connect to process '{resolvedProcessName} ({resolvedProcessId})'. The process may have exited, or it doesn't have an accessible .NET diagnostic port."); + return (int)ReturnCode.TracingError; } args = args with { Name = resolvedProcessName, ProcessId = resolvedProcessId }; } @@ -217,18 +223,28 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) bool generateCsv = mode == ProbeOutputMode.CsvToConsole || mode == ProbeOutputMode.Csv; StringBuilder supportedCsv = generateCsv ? new StringBuilder() : null; StringBuilder unsupportedCsv = generateCsv ? new StringBuilder() : null; + StringBuilder unknownCsv = generateCsv ? new StringBuilder() : null; if (args.ProcessId != 0 || !string.IsNullOrEmpty(args.Name)) { - bool supports = ProcessSupportsUserEventsIpcCommand(args.ProcessId, args.Name, out int resolvedPid, out string resolvedName, out string detectedRuntimeVersion); - BuildProcessSupportCsv(resolvedPid, resolvedName, supports, supportedCsv, unsupportedCsv); + CommandUtils.ResolveProcess(args.ProcessId, args.Name, out int resolvedPid, out string resolvedName); + UserEventsProbeResult probeResult = ProbeProcess(resolvedPid, out string detectedRuntimeVersion); + BuildProcessSupportCsv(resolvedPid, resolvedName, probeResult, supportedCsv, unsupportedCsv, unknownCsv); if (mode == ProbeOutputMode.Console) { - Console.WriteLine($".NET process '{resolvedName} ({resolvedPid})' {(supports ? "supports" : "does NOT support")} the EventPipe UserEvents IPC command used by collect-linux."); - if (!supports) + switch (probeResult) { - Console.WriteLine($"Required runtime: '{minRuntimeSupportingUserEventsIPCCommand}'. Detected runtime: '{detectedRuntimeVersion}'."); + case UserEventsProbeResult.Supported: + Console.WriteLine($".NET process '{resolvedName} ({resolvedPid})' supports the EventPipe UserEvents IPC command used by collect-linux."); + break; + case UserEventsProbeResult.NotSupported: + Console.WriteLine($".NET process '{resolvedName} ({resolvedPid})' does NOT support the EventPipe UserEvents IPC command used by collect-linux."); + Console.WriteLine($"Required runtime: '{minRuntimeSupportingUserEventsIPCCommand}'. Detected runtime: '{detectedRuntimeVersion}'."); + break; + case UserEventsProbeResult.ConnectionFailed: + Console.WriteLine($"Could not probe process '{resolvedName} ({resolvedPid})'. The process may have exited, or it doesn't have an accessible .NET diagnostic port."); + break; } } } @@ -240,26 +256,9 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) } StringBuilder supportedProcesses = new(); StringBuilder unsupportedProcesses = new(); + StringBuilder unknownProcesses = new(); - IEnumerable pids = DiagnosticsClient.GetPublishedProcesses(); - foreach (int pid in pids) - { - if (pid == Environment.ProcessId) - { - continue; - } - - bool supports = ProcessSupportsUserEventsIpcCommand(pid, string.Empty, out int resolvedPid, out string resolvedName, out string detectedRuntimeVersion); - BuildProcessSupportCsv(resolvedPid, resolvedName, supports, supportedCsv, unsupportedCsv); - if (supports) - { - supportedProcesses.AppendLine($"{resolvedPid} {resolvedName}"); - } - else - { - unsupportedProcesses.AppendLine($"{resolvedPid} {resolvedName} - Detected runtime: '{detectedRuntimeVersion}'"); - } - } + GetAndProbeAllProcesses(supportedProcesses, unsupportedProcesses, unknownProcesses, supportedCsv, unsupportedCsv, unknownCsv); if (mode == ProbeOutputMode.Console) { @@ -267,22 +266,29 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) Console.WriteLine(supportedProcesses.ToString()); Console.WriteLine($".NET processes that do NOT support the command:"); Console.WriteLine(unsupportedProcesses.ToString()); + if (unknownProcesses.Length > 0) + { + Console.WriteLine($".NET processes that could not be probed:"); + Console.WriteLine(unknownProcesses.ToString()); + } } } if (mode == ProbeOutputMode.CsvToConsole) { Console.WriteLine("pid,processName,supportsCollectLinux"); - Console.Write(supportedCsv?.ToString()); - Console.Write(unsupportedCsv?.ToString()); + Console.Write(supportedCsv.ToString()); + Console.Write(unsupportedCsv.ToString()); + Console.Write(unknownCsv.ToString()); } if (mode == ProbeOutputMode.Csv) { using StreamWriter writer = new(args.Output.FullName, append: false, Encoding.UTF8); writer.WriteLine("pid,processName,supportsCollectLinux"); - writer.Write(supportedCsv?.ToString()); - writer.Write(unsupportedCsv?.ToString()); + writer.Write(supportedCsv.ToString()); + writer.Write(unsupportedCsv.ToString()); + writer.Write(unknownCsv.ToString()); Console.WriteLine($"Successfully wrote EventPipe UserEvents IPC command support results to '{args.Output.FullName}'."); } @@ -315,39 +321,104 @@ private static ProbeOutputMode DetermineProbeOutputMode(string outputName) return ProbeOutputMode.Csv; } - private bool ProcessSupportsUserEventsIpcCommand(int pid, string processName, out int resolvedPid, out string resolvedName, out string detectedRuntimeVersion) + /// + /// Probes a resolved process for UserEvents support. Returns ConnectionFailed when unable to + /// connect to the .NET diagnostic port (e.g. process exited between discovery and probe). + /// Callers must resolve the PID/name before calling this method. + /// + private UserEventsProbeResult ProbeProcess(int resolvedPid, out string detectedRuntimeVersion) { - CommandUtils.ResolveProcess(pid, processName, out resolvedPid, out resolvedName); + detectedRuntimeVersion = string.Empty; - bool supports = false; - DiagnosticsClient client = new(resolvedPid); - ProcessInfo processInfo = client.GetProcessInfo(); - detectedRuntimeVersion = processInfo.ClrProductVersionString; - if (processInfo.TryGetProcessClrVersion(out Version version, out bool isPrerelease) && - (version > minRuntimeSupportingUserEventsIPCCommand || - (version == minRuntimeSupportingUserEventsIPCCommand && !isPrerelease))) + try { - supports = true; + DiagnosticsClient client = new(resolvedPid); + ProcessInfo processInfo = client.GetProcessInfo(); + detectedRuntimeVersion = processInfo.ClrProductVersionString; + if (processInfo.TryGetProcessClrVersion(out Version version, out bool isPrerelease) && + (version > minRuntimeSupportingUserEventsIPCCommand || + (version == minRuntimeSupportingUserEventsIPCCommand && !isPrerelease))) + { + return UserEventsProbeResult.Supported; + } + return UserEventsProbeResult.NotSupported; } + catch (ServerNotAvailableException) + { + return UserEventsProbeResult.ConnectionFailed; + } + catch (UnsupportedCommandException) + { + // can be thrown from an older runtime that doesn't even support GetProcessInfo + // treat as NotSupported instead of propagating the exception. + return UserEventsProbeResult.NotSupported; + } + } - return supports; + /// + /// Gets all published processes and probes them for UserEvents support. + /// + private void GetAndProbeAllProcesses(StringBuilder supportedProcesses, StringBuilder unsupportedProcesses, StringBuilder unknownProcesses, + StringBuilder supportedCsv, StringBuilder unsupportedCsv, StringBuilder unknownCsv) + { + IEnumerable pids = DiagnosticsClient.GetPublishedProcesses(); + foreach (int pid in pids) + { + if (pid == Environment.ProcessId) + { + continue; + } + + // Resolve name before probing: a process that exits after probing is untraceable + // regardless of its probe result, so knowing the name for the failure message + // is more valuable than knowing the probe result without a name. + string processName; + try + { + processName = Process.GetProcessById(pid).ProcessName; + } + catch (ArgumentException) + { + // Process exited between discovery and name resolution, no need to report these. + continue; + } + + UserEventsProbeResult probeResult = ProbeProcess(pid, out string detectedRuntimeVersion); + BuildProcessSupportCsv(pid, processName, probeResult, supportedCsv, unsupportedCsv, unknownCsv); + switch (probeResult) + { + case UserEventsProbeResult.Supported: + supportedProcesses?.AppendLine($"{pid} {processName}"); + break; + case UserEventsProbeResult.NotSupported: + unsupportedProcesses?.AppendLine($"{pid} {processName} - Detected runtime: '{detectedRuntimeVersion}'"); + break; + case UserEventsProbeResult.ConnectionFailed: + unknownProcesses?.AppendLine($"{pid} {processName} - Process may have exited, or it doesn't have an accessible .NET diagnostic port"); + break; + } + } } - private static void BuildProcessSupportCsv(int resolvedPid, string resolvedName, bool supports, StringBuilder supportedCsv, StringBuilder unsupportedCsv) + private static void BuildProcessSupportCsv(int resolvedPid, string resolvedName, UserEventsProbeResult probeResult, StringBuilder supportedCsv, StringBuilder unsupportedCsv, StringBuilder unknownCsv) { - if (supportedCsv == null && unsupportedCsv == null) + if (supportedCsv == null && unsupportedCsv == null && unknownCsv == null) { return; } string escapedName = (resolvedName ?? string.Empty).Replace(",", string.Empty); - if (supports) - { - supportedCsv?.AppendLine($"{resolvedPid},{escapedName},true"); - } - else + switch (probeResult) { - unsupportedCsv?.AppendLine($"{resolvedPid},{escapedName},false"); + case UserEventsProbeResult.Supported: + supportedCsv?.AppendLine($"{resolvedPid},{escapedName},true"); + break; + case UserEventsProbeResult.NotSupported: + unsupportedCsv?.AppendLine($"{resolvedPid},{escapedName},false"); + break; + case UserEventsProbeResult.ConnectionFailed: + unknownCsv?.AppendLine($"{resolvedPid},{escapedName},unknown"); + break; } } @@ -530,7 +601,7 @@ private int OutputHandler(uint type, IntPtr data, UIntPtr dataLen) private static readonly Option ProbeOption = new("--probe") { - Description = "Probe .NET processes for support of the EventPipe UserEvents IPC command used by collect-linux, without collecting a trace. Results list supported processes first. Use '-o stdout' to print CSV (pid,processName,supportsCollectLinux) to the console, or '-o ' to write the CSV. Probe a single process with -n|--name or -p|--process-id.", + Description = "Probe .NET processes for support of the EventPipe UserEvents IPC command used by collect-linux, without collecting a trace. Results are categorized as supported, not supported, or unknown (when the process doesn't have an accessible .NET diagnostic port). Use '-o stdout' to print CSV (pid,processName,supportsCollectLinux) to the console, or '-o ' to write the CSV. Probe a single process with -n|--name or -p|--process-id.", }; private enum ProbeOutputMode @@ -540,6 +611,13 @@ private enum ProbeOutputMode CsvToConsole, } + private enum UserEventsProbeResult + { + Supported, + NotSupported, + ConnectionFailed, + } + private enum OutputType : uint { Normal = 0, diff --git a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs index 8d0a6639aa..bfff8596c4 100644 --- a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs +++ b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.CommandLine; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text; @@ -210,6 +211,38 @@ public void CollectLinuxCommand_Probe_ReportsResolveProcessErrors_BothPidAndName console.AssertSanitizedLinesEqual(null, expected); } + [ConditionalFact(nameof(IsCollectLinuxSupported))] + public void CollectLinuxCommand_ReportsConnectionFailed_NonDotNetProcess() + { + // PID 1 (init/systemd) exists but is not a .NET process — no diagnostic port. + string pid1Name = Process.GetProcessById(1).ProcessName; + MockConsole console = new(200, 30, _outputHelper); + var args = TestArgs(processId: 1); + int exitCode = Run(args, console); + + Assert.Equal((int)ReturnCode.TracingError, exitCode); + console.AssertSanitizedLinesEqual(null, FormatException( + $"Unable to connect to process '{pid1Name} (1)'. The process may have exited, or it doesn't have an accessible .NET diagnostic port.")); + } + + [ConditionalFact(nameof(IsCollectLinuxSupported))] + public void CollectLinuxCommand_Probe_ReportsConnectionFailed_NonDotNetProcess() + { + // PID 1 (init/systemd) exists but is not a .NET process — no diagnostic port. + string pid1Name = Process.GetProcessById(1).ProcessName; + MockConsole console = new(200, 2000, _outputHelper); + var args = TestArgs(processId: 1, probe: true, output: new FileInfo(CommonOptions.DefaultTraceName)); + int exitCode = Run(args, console); + + Assert.Equal((int)ReturnCode.Ok, exitCode); + string[] expected = ExpectPreviewWithMessages( + new[] { + $"Could not probe process '{pid1Name} (1)'. The process may have exited, or it doesn't have an accessible .NET diagnostic port.", + } + ); + console.AssertSanitizedLinesEqual(null, expected); + } + [ConditionalFact(nameof(IsCollectLinuxNotSupported))] public void CollectLinuxCommand_NotSupported_OnNonLinux() {