From 105683bf0a51b103fe1622a2ab8d5c5ce0901cf5 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Fri, 6 Feb 2026 19:20:09 +0000 Subject: [PATCH 1/9] Refactor probe logic into ProbeProcess and GetAndProbeAllProcesses helpers Add UserEventsProbeResult enum (Supported/NotSupported) to replace boolean return. Introduce ProbeProcess helper for probing a single process. Add GetAndProbeAllProcesses helper that enumerates and probes all published processes. Update callers in CollectLinux and SupportsCollectLinux to use new helpers. Update BuildProcessSupportCsv to use UserEventsProbeResult enum. --- .../Commands/CollectLinuxCommand.cs | 96 ++++++++++++------- 1 file changed, 60 insertions(+), 36 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index 08bce2e961..a892e77f79 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -92,7 +92,8 @@ 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)) + UserEventsProbeResult probeResult = ProbeProcess(args.ProcessId, args.Name, out int resolvedProcessId, out string resolvedProcessName, out string detectedRuntimeVersion); + if (probeResult == 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; @@ -204,15 +205,20 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) 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); + UserEventsProbeResult probeResult = ProbeProcess(args.ProcessId, args.Name, out int resolvedPid, out string resolvedName, out string detectedRuntimeVersion); + BuildProcessSupportCsv(resolvedPid, resolvedName, probeResult, supportedCsv, unsupportedCsv); 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; } } } @@ -225,25 +231,7 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) StringBuilder supportedProcesses = new(); StringBuilder unsupportedProcesses = 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, supportedCsv, unsupportedCsv); if (mode == ProbeOutputMode.Console) { @@ -299,11 +287,13 @@ 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 single process for UserEvents support. + /// + private UserEventsProbeResult ProbeProcess(int pid, string processName, out int resolvedPid, out string resolvedName, out string detectedRuntimeVersion) { CommandUtils.ResolveProcess(pid, processName, out resolvedPid, out resolvedName); - bool supports = false; DiagnosticsClient client = new(resolvedPid); ProcessInfo processInfo = client.GetProcessInfo(); detectedRuntimeVersion = processInfo.ClrProductVersionString; @@ -311,13 +301,40 @@ private bool ProcessSupportsUserEventsIpcCommand(int pid, string processName, ou (version > minRuntimeSupportingUserEventsIPCCommand || (version == minRuntimeSupportingUserEventsIPCCommand && !isPrerelease))) { - supports = true; + return UserEventsProbeResult.Supported; } + return UserEventsProbeResult.NotSupported; + } + + /// + /// Gets all published processes and probes them for UserEvents support. + /// + private void GetAndProbeAllProcesses(StringBuilder supportedProcesses, StringBuilder unsupportedProcesses, + StringBuilder supportedCsv, StringBuilder unsupportedCsv) + { + IEnumerable pids = DiagnosticsClient.GetPublishedProcesses(); + foreach (int pid in pids) + { + if (pid == Environment.ProcessId) + { + continue; + } - return supports; + UserEventsProbeResult probeResult = ProbeProcess(pid, string.Empty, out int resolvedPid, out string resolvedName, out string detectedRuntimeVersion); + BuildProcessSupportCsv(resolvedPid, resolvedName, probeResult, supportedCsv, unsupportedCsv); + switch (probeResult) + { + case UserEventsProbeResult.Supported: + supportedProcesses?.AppendLine($"{resolvedPid} {resolvedName}"); + break; + case UserEventsProbeResult.NotSupported: + unsupportedProcesses?.AppendLine($"{resolvedPid} {resolvedName} - Detected runtime: '{detectedRuntimeVersion}'"); + 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) { if (supportedCsv == null && unsupportedCsv == null) { @@ -325,13 +342,14 @@ private static void BuildProcessSupportCsv(int resolvedPid, string resolvedName, } 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; } } @@ -524,6 +542,12 @@ private enum ProbeOutputMode CsvToConsole, } + private enum UserEventsProbeResult + { + Supported, + NotSupported, + } + private enum OutputType : uint { Normal = 0, From fb8ee47e07eee1dfdf082c471b398244087590bd Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Fri, 6 Feb 2026 19:22:07 +0000 Subject: [PATCH 2/9] Handle ServerNotAvailableException and DiagnosticToolException in process probing Add ProcessNotFound and ConnectionFailed values to UserEventsProbeResult enum. Update ProbeProcess to catch DiagnosticToolException (process resolution failed) and ServerNotAvailableException (diagnostic endpoint not accessible) separately. Add FormatProcessIdentifier helper for clean display of process ID/name. Add unknownProcesses/unknownCsv tracking for processes that could not be probed. Update probe mode output to show 'Processes that could not be probed' section. Include 'unknown' value in CSV output for unprobed processes. Update non-probe mode to show distinct errors for each failure type. Change '.NET process' to 'Process' in messages since arbitrary PIDs may not be .NET. Fixes #5694 --- .../Commands/CollectLinuxCommand.cs | 119 ++++++++++++++---- 1 file changed, 93 insertions(+), 26 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index a892e77f79..f8501622c2 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -93,10 +93,17 @@ internal int CollectLinux(CollectLinuxArgs args) if (args.ProcessId != 0 || !string.IsNullOrEmpty(args.Name)) { UserEventsProbeResult probeResult = ProbeProcess(args.ProcessId, args.Name, out int resolvedProcessId, out string resolvedProcessName, out string detectedRuntimeVersion); - if (probeResult == UserEventsProbeResult.NotSupported) + 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 '{FormatProcessIdentifier(resolvedProcessId, resolvedProcessName)}' cannot be traced by collect-linux. Required runtime: {minRuntimeSupportingUserEventsIPCCommand}. Detected runtime: {detectedRuntimeVersion}"); + return (int)ReturnCode.TracingError; + case UserEventsProbeResult.ProcessNotFound: + Console.Error.WriteLine($"[ERROR] Could not resolve process '{FormatProcessIdentifier(resolvedProcessId, resolvedProcessName)}'."); + return (int)ReturnCode.TracingError; + case UserEventsProbeResult.ConnectionFailed: + Console.Error.WriteLine($"[ERROR] Unable to connect to process '{FormatProcessIdentifier(resolvedProcessId, resolvedProcessName)}'. The process may have exited or its diagnostic endpoint is not accessible."); + return (int)ReturnCode.TracingError; } args = args with { Name = resolvedProcessName, ProcessId = resolvedProcessId }; } @@ -202,23 +209,30 @@ 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)) { UserEventsProbeResult probeResult = ProbeProcess(args.ProcessId, args.Name, out int resolvedPid, out string resolvedName, out string detectedRuntimeVersion); - BuildProcessSupportCsv(resolvedPid, resolvedName, probeResult, supportedCsv, unsupportedCsv); + BuildProcessSupportCsv(resolvedPid, resolvedName, probeResult, supportedCsv, unsupportedCsv, unknownCsv); if (mode == ProbeOutputMode.Console) { switch (probeResult) { case UserEventsProbeResult.Supported: - Console.WriteLine($".NET process '{resolvedName} ({resolvedPid})' supports the EventPipe UserEvents IPC command used by collect-linux."); + Console.WriteLine($"Process '{FormatProcessIdentifier(resolvedPid, resolvedName)}' 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($"Process '{FormatProcessIdentifier(resolvedPid, resolvedName)}' does NOT support the EventPipe UserEvents IPC command used by collect-linux."); Console.WriteLine($"Required runtime: '{minRuntimeSupportingUserEventsIPCCommand}'. Detected runtime: '{detectedRuntimeVersion}'."); break; + case UserEventsProbeResult.ProcessNotFound: + Console.WriteLine($"Could not resolve process '{FormatProcessIdentifier(resolvedPid, resolvedName)}'."); + break; + case UserEventsProbeResult.ConnectionFailed: + Console.WriteLine($"Process '{FormatProcessIdentifier(resolvedPid, resolvedName)}' could not be probed. Unable to connect to the process's diagnostic endpoint."); + break; } } } @@ -226,19 +240,25 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) { if (mode == ProbeOutputMode.Console) { - Console.WriteLine($"Probing .NET processes for support of the EventPipe UserEvents IPC command used by collect-linux. Requires runtime '{minRuntimeSupportingUserEventsIPCCommand}' or later."); + Console.WriteLine($"Probing processes for support of the EventPipe UserEvents IPC command used by collect-linux. Requires runtime '{minRuntimeSupportingUserEventsIPCCommand}' or later."); } StringBuilder supportedProcesses = new(); StringBuilder unsupportedProcesses = new(); + StringBuilder unknownProcesses = new(); - GetAndProbeAllProcesses(supportedProcesses, unsupportedProcesses, supportedCsv, unsupportedCsv); + GetAndProbeAllProcesses(supportedProcesses, unsupportedProcesses, unknownProcesses, supportedCsv, unsupportedCsv, unknownCsv); if (mode == ProbeOutputMode.Console) { - Console.WriteLine($".NET processes that support the command:"); + Console.WriteLine($"Processes that support the command:"); Console.WriteLine(supportedProcesses.ToString()); - Console.WriteLine($".NET processes that do NOT support the command:"); + Console.WriteLine($"Processes that do NOT support the command:"); Console.WriteLine(unsupportedProcesses.ToString()); + if (unknownProcesses.Length > 0) + { + Console.WriteLine($"Processes that could not be probed:"); + Console.WriteLine(unknownProcesses.ToString()); + } } } @@ -247,6 +267,7 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) Console.WriteLine("pid,processName,supportsCollectLinux"); Console.Write(supportedCsv?.ToString()); Console.Write(unsupportedCsv?.ToString()); + Console.Write(unknownCsv?.ToString()); } if (mode == ProbeOutputMode.Csv) @@ -255,6 +276,7 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) writer.WriteLine("pid,processName,supportsCollectLinux"); 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}'."); } @@ -288,29 +310,52 @@ private static ProbeOutputMode DetermineProbeOutputMode(string outputName) } /// - /// Probes a single process for UserEvents support. + /// Probes a single process for UserEvents support. Returns ProcessNotFound when the process cannot be resolved, + /// ConnectionFailed when unable to connect to diagnostic endpoint. /// private UserEventsProbeResult ProbeProcess(int pid, string processName, out int resolvedPid, out string resolvedName, out string detectedRuntimeVersion) { - CommandUtils.ResolveProcess(pid, processName, out resolvedPid, out resolvedName); + // Store original values - ResolveProcess modifies out params before throwing exceptions + int originalPid = pid; + string originalName = processName ?? string.Empty; + detectedRuntimeVersion = "unknown"; + + try + { + CommandUtils.ResolveProcess(pid, processName, out resolvedPid, out resolvedName); + } + catch (DiagnosticToolException) + { + resolvedPid = originalPid; + resolvedName = originalName; + return UserEventsProbeResult.ProcessNotFound; + } - 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 + { + 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.Supported; + return UserEventsProbeResult.ConnectionFailed; } - return UserEventsProbeResult.NotSupported; } /// - /// Gets all published processes and probes them for UserEvents support. + /// Gets all published processes and probes them for UserEvents support. ProcessNotFound and ConnectionFailed + /// results are treated as expected (processes may exit between enumeration and probing) and do not cause errors. /// - private void GetAndProbeAllProcesses(StringBuilder supportedProcesses, StringBuilder unsupportedProcesses, - StringBuilder supportedCsv, StringBuilder unsupportedCsv) + private void GetAndProbeAllProcesses(StringBuilder supportedProcesses, StringBuilder unsupportedProcesses, StringBuilder unknownProcesses, + StringBuilder supportedCsv, StringBuilder unsupportedCsv, StringBuilder unknownCsv) { IEnumerable pids = DiagnosticsClient.GetPublishedProcesses(); foreach (int pid in pids) @@ -321,7 +366,7 @@ private void GetAndProbeAllProcesses(StringBuilder supportedProcesses, StringBui } UserEventsProbeResult probeResult = ProbeProcess(pid, string.Empty, out int resolvedPid, out string resolvedName, out string detectedRuntimeVersion); - BuildProcessSupportCsv(resolvedPid, resolvedName, probeResult, supportedCsv, unsupportedCsv); + BuildProcessSupportCsv(resolvedPid, resolvedName, probeResult, supportedCsv, unsupportedCsv, unknownCsv); switch (probeResult) { case UserEventsProbeResult.Supported: @@ -330,13 +375,29 @@ private void GetAndProbeAllProcesses(StringBuilder supportedProcesses, StringBui case UserEventsProbeResult.NotSupported: unsupportedProcesses?.AppendLine($"{resolvedPid} {resolvedName} - Detected runtime: '{detectedRuntimeVersion}'"); break; + case UserEventsProbeResult.ProcessNotFound: + case UserEventsProbeResult.ConnectionFailed: + unknownProcesses?.AppendLine($"{resolvedPid} {resolvedName} - Unable to connect"); + break; } } } - private static void BuildProcessSupportCsv(int resolvedPid, string resolvedName, UserEventsProbeResult probeResult, StringBuilder supportedCsv, StringBuilder unsupportedCsv) + /// + /// Formats process identifier for display. Shows "name (pid)" when name differs from pid, otherwise just "pid". + /// + private static string FormatProcessIdentifier(int pid, string name) + { + if (string.IsNullOrEmpty(name) || name == pid.ToString()) + { + return pid.ToString(); + } + return $"{name} ({pid})"; + } + + 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; } @@ -350,6 +411,10 @@ private static void BuildProcessSupportCsv(int resolvedPid, string resolvedName, case UserEventsProbeResult.NotSupported: unsupportedCsv?.AppendLine($"{resolvedPid},{escapedName},false"); break; + case UserEventsProbeResult.ProcessNotFound: + case UserEventsProbeResult.ConnectionFailed: + unknownCsv?.AppendLine($"{resolvedPid},{escapedName},unknown"); + break; } } @@ -546,6 +611,8 @@ private enum UserEventsProbeResult { Supported, NotSupported, + ProcessNotFound, + ConnectionFailed, } private enum OutputType : uint From 6f420b2497d1f284fa303567db00f8eea5bb3652 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Fri, 6 Feb 2026 19:22:22 +0000 Subject: [PATCH 3/9] Update --probe option help text Document that results are categorized as supported, not supported, or unknown. Clarify that unknown status occurs when diagnostic endpoint is not accessible. --- .../dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index f8501622c2..00919c7f73 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -597,7 +597,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 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's diagnostic endpoint is not accessible). 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 From 0cbcf4408348c3424c3193d14ed4d90c4cb0b148 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Fri, 6 Feb 2026 19:23:57 +0000 Subject: [PATCH 4/9] Update CollectLinuxCommand tests for ProcessNotFound and ConnectionFailed handling Update test expectations to match new behavior: - Add FormatProcessNotFoundError and FormatProcessIdentifier helpers - Update ResolveProcessExceptions test data for ProcessNotFound handling - Update probe error test cases for process resolution errors - Tests now expect ReturnCode.TracingError for failures in non-probe mode - Tests expect ReturnCode.Ok for probe mode with informational output --- .../CollectLinuxCommandFunctionalTests.cs | 66 ++++++++++++++----- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs index f4cea70ba7..e19382594b 100644 --- a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs +++ b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs @@ -100,13 +100,13 @@ public void CollectLinuxCommand_ReportsResolveProcessNameErrors() [ConditionalTheory(nameof(IsCollectLinuxSupported))] [MemberData(nameof(ResolveProcessExceptions))] - public void CollectLinuxCommand_ResolveProcessExceptions(object testArgs, string[] expectedError) + public void CollectLinuxCommand_ResolveProcessExceptions(object testArgs, string[] expectedError, int expectedExitCode) { MockConsole console = new(200, 30, _outputHelper); int exitCode = Run(testArgs, console); - Assert.Equal((int)ReturnCode.ArgumentError, exitCode); + Assert.Equal(expectedExitCode, exitCode); console.AssertSanitizedLinesEqual(null, expectedError); } @@ -170,12 +170,17 @@ public void CollectLinuxCommand_Probe_Csv() public void CollectLinuxCommand_Probe_ReportsResolveProcessErrors_InvalidPid() { MockConsole console = new(200, 30, _outputHelper); - var args = TestArgs(processId: -1, probe: true); + var args = TestArgs(processId: -1, probe: true, output: new FileInfo(CommonOptions.DefaultTraceName)); int exitCode = Run(args, console); - Assert.Equal((int)ReturnCode.ArgumentError, exitCode); + Assert.Equal((int)ReturnCode.Ok, exitCode); - string[] expected = FormatException("-1 is not a valid process ID"); + // ProcessNotFound shows just PID when no name is provided + string[] expected = ExpectPreviewWithMessages( + new[] { + $"Could not resolve process '{FormatProcessIdentifier(-1, string.Empty)}'.", + } + ); console.AssertSanitizedLinesEqual(null, expected); } @@ -184,12 +189,17 @@ public void CollectLinuxCommand_Probe_ReportsResolveProcessErrors_InvalidPid() public void CollectLinuxCommand_Probe_ReportsResolveProcessErrors_InvalidName() { MockConsole console = new(200, 30, _outputHelper); - var args = TestArgs(name: "process-that-should-not-exist", processId: 0, probe: true); + var args = TestArgs(name: "process-that-should-not-exist", processId: 0, probe: true, output: new FileInfo(CommonOptions.DefaultTraceName)); int exitCode = Run(args, console); - Assert.Equal((int)ReturnCode.ArgumentError, exitCode); + Assert.Equal((int)ReturnCode.Ok, exitCode); - string[] expected = FormatException("There is no active process with the given name: process-that-should-not-exist"); + // ProcessNotFound shows "name (pid)" when name is provided - pid is 0 from input + string[] expected = ExpectPreviewWithMessages( + new[] { + $"Could not resolve process '{FormatProcessIdentifier(0, "process-that-should-not-exist")}'.", + } + ); console.AssertSanitizedLinesEqual(null, expected); } @@ -198,14 +208,17 @@ public void CollectLinuxCommand_Probe_ReportsResolveProcessErrors_InvalidName() public void CollectLinuxCommand_Probe_ReportsResolveProcessErrors_BothPidAndName() { MockConsole console = new(200, 30, _outputHelper); - var args = TestArgs(name: "dummy", processId: 1, probe: true); + var args = TestArgs(name: "dummy", processId: 1, probe: true, output: new FileInfo(CommonOptions.DefaultTraceName)); int exitCode = Run(args, console); - Assert.Equal((int)ReturnCode.ArgumentError, exitCode); + Assert.Equal((int)ReturnCode.Ok, exitCode); - // When both PID and name are supplied, the banner still refers to the PID - // because the implementation prioritizes ProcessId when it is non-zero. - string[] expected = FormatException("Only one of the --name or --process-id options may be specified."); + // ProcessNotFound shows "name (pid)" when both are provided + string[] expected = ExpectPreviewWithMessages( + new[] { + $"Could not resolve process '{FormatProcessIdentifier(1, "dummy")}'.", + } + ); console.AssertSanitizedLinesEqual(null, expected); } @@ -386,22 +399,28 @@ public static IEnumerable InvalidProviders() public static IEnumerable ResolveProcessExceptions() { + // ResolveProcess throws DiagnosticToolException for invalid PID - shows ProcessNotFound error yield return new object[] { TestArgs(processId: -1, name: string.Empty), - FormatException("-1 is not a valid process ID") + FormatProcessNotFoundError(FormatProcessIdentifier(-1, string.Empty)), + (int)ReturnCode.TracingError }; + // When both PID and name are supplied, ResolveProcess throws - shows ProcessNotFound with "name (pid)" yield return new object[] { TestArgs(processId: 1, name: "dummy"), - FormatException("Only one of the --name or --process-id options may be specified.") + FormatProcessNotFoundError(FormatProcessIdentifier(1, "dummy")), + (int)ReturnCode.TracingError }; + // ResolveProcess throws for non-existent process - shows ProcessNotFound with just PID yield return new object[] { TestArgs(processId: int.MaxValue, name: string.Empty), - FormatException("No process with ID 2147483647 is currently running.") + FormatProcessNotFoundError(FormatProcessIdentifier(int.MaxValue, string.Empty)), + (int)ReturnCode.TracingError }; } @@ -438,6 +457,21 @@ private static string[] FormatException(string message) "https://learn.microsoft.com/dotnet/core/diagnostics/dotnet-trace.", "==========================================================================================" ]; + private static string[] FormatProcessNotFoundError(string processIdentifier) + { + List result = new(); + result.AddRange(PreviewMessages); + result.Add($"[ERROR] Could not resolve process '{processIdentifier}'."); + return result.ToArray(); + } + private static string FormatProcessIdentifier(int pid, string name) + { + if (string.IsNullOrEmpty(name) || name == pid.ToString()) + { + return pid.ToString(); + } + return $"{name} ({pid})"; + } private static string[] ExpectPreviewWithMessages(string[] messages) { From 893f2f36980bb675913efd4385345d315a83f6ee Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Fri, 6 Mar 2026 18:45:54 +0000 Subject: [PATCH 5/9] Address review feedback: preserve original error messages and return codes Per review feedback, single-process paths (explicit -p PID or -n NAME) now call CommandUtils.ResolveProcess separately so argument validation errors propagate with original specific messages and ArgumentError return code. ProbeProcess is only used for the resolved PID's runtime check and connection attempt. Restore '.NET process(es)' wording in probe output messages. Remove unused FormatProcessIdentifier helper from both source and tests. Revert probe error tests to expect ArgumentError with original messages. Revert ResolveProcessExceptions test data to original error text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Commands/CollectLinuxCommand.cs | 106 ++++++++---------- .../CollectLinuxCommandFunctionalTests.cs | 93 ++++++++------- 2 files changed, 94 insertions(+), 105 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index c13e7f9ad9..ae75f9b991 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -94,17 +94,15 @@ internal int CollectLinux(CollectLinuxArgs args) if (args.ProcessId != 0 || !string.IsNullOrEmpty(args.Name)) { - UserEventsProbeResult probeResult = ProbeProcess(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) { case UserEventsProbeResult.NotSupported: - Console.Error.WriteLine($"[ERROR] Process '{FormatProcessIdentifier(resolvedProcessId, resolvedProcessName)}' cannot be traced by collect-linux. Required runtime: {minRuntimeSupportingUserEventsIPCCommand}. Detected runtime: {detectedRuntimeVersion}"); - return (int)ReturnCode.TracingError; - case UserEventsProbeResult.ProcessNotFound: - Console.Error.WriteLine($"[ERROR] Could not resolve process '{FormatProcessIdentifier(resolvedProcessId, resolvedProcessName)}'."); + 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 '{FormatProcessIdentifier(resolvedProcessId, resolvedProcessName)}'. The process may have exited or its diagnostic endpoint is not accessible."); + Console.Error.WriteLine($"[ERROR] Unable to connect to process '{resolvedProcessName} ({resolvedProcessId})'. The process may have exited or its diagnostic endpoint is not accessible."); return (int)ReturnCode.TracingError; } args = args with { Name = resolvedProcessName, ProcessId = resolvedProcessId }; @@ -229,7 +227,8 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) if (args.ProcessId != 0 || !string.IsNullOrEmpty(args.Name)) { - UserEventsProbeResult probeResult = ProbeProcess(args.ProcessId, args.Name, out int resolvedPid, out string resolvedName, out string detectedRuntimeVersion); + 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) @@ -237,17 +236,14 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) switch (probeResult) { case UserEventsProbeResult.Supported: - Console.WriteLine($"Process '{FormatProcessIdentifier(resolvedPid, resolvedName)}' supports the EventPipe UserEvents IPC command used by collect-linux."); + Console.WriteLine($".NET process '{resolvedName} ({resolvedPid})' supports the EventPipe UserEvents IPC command used by collect-linux."); break; case UserEventsProbeResult.NotSupported: - Console.WriteLine($"Process '{FormatProcessIdentifier(resolvedPid, resolvedName)}' does NOT support the EventPipe UserEvents IPC command used by collect-linux."); + 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.ProcessNotFound: - Console.WriteLine($"Could not resolve process '{FormatProcessIdentifier(resolvedPid, resolvedName)}'."); - break; case UserEventsProbeResult.ConnectionFailed: - Console.WriteLine($"Process '{FormatProcessIdentifier(resolvedPid, resolvedName)}' could not be probed. Unable to connect to the process's diagnostic endpoint."); + Console.WriteLine($"Could not probe process '{resolvedName} ({resolvedPid})'. The process may have exited or its diagnostic endpoint is not accessible."); break; } } @@ -256,7 +252,7 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) { if (mode == ProbeOutputMode.Console) { - Console.WriteLine($"Probing processes for support of the EventPipe UserEvents IPC command used by collect-linux. Requires runtime '{minRuntimeSupportingUserEventsIPCCommand}' or later."); + Console.WriteLine($"Probing .NET processes for support of the EventPipe UserEvents IPC command used by collect-linux. Requires runtime '{minRuntimeSupportingUserEventsIPCCommand}' or later."); } StringBuilder supportedProcesses = new(); StringBuilder unsupportedProcesses = new(); @@ -266,13 +262,13 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) if (mode == ProbeOutputMode.Console) { - Console.WriteLine($"Processes that support the command:"); + Console.WriteLine($".NET processes that support the command:"); Console.WriteLine(supportedProcesses.ToString()); - Console.WriteLine($"Processes that do NOT support the command:"); + Console.WriteLine($".NET processes that do NOT support the command:"); Console.WriteLine(unsupportedProcesses.ToString()); if (unknownProcesses.Length > 0) { - Console.WriteLine($"Processes that could not be probed:"); + Console.WriteLine($".NET processes that could not be probed:"); Console.WriteLine(unknownProcesses.ToString()); } } @@ -326,26 +322,13 @@ private static ProbeOutputMode DetermineProbeOutputMode(string outputName) } /// - /// Probes a single process for UserEvents support. Returns ProcessNotFound when the process cannot be resolved, - /// ConnectionFailed when unable to connect to diagnostic endpoint. + /// Probes a resolved process for UserEvents support. Returns ConnectionFailed when unable to + /// connect to the diagnostic endpoint (e.g. process exited between discovery and probe). + /// Callers must resolve the PID/name before calling this method. /// - private UserEventsProbeResult ProbeProcess(int pid, string processName, out int resolvedPid, out string resolvedName, out string detectedRuntimeVersion) + private UserEventsProbeResult ProbeProcess(int resolvedPid, out string detectedRuntimeVersion) { - // Store original values - ResolveProcess modifies out params before throwing exceptions - int originalPid = pid; - string originalName = processName ?? string.Empty; - detectedRuntimeVersion = "unknown"; - - try - { - CommandUtils.ResolveProcess(pid, processName, out resolvedPid, out resolvedName); - } - catch (DiagnosticToolException) - { - resolvedPid = originalPid; - resolvedName = originalName; - return UserEventsProbeResult.ProcessNotFound; - } + detectedRuntimeVersion = string.Empty; try { @@ -364,11 +347,17 @@ private UserEventsProbeResult ProbeProcess(int pid, string processName, out int { 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; + } } /// - /// Gets all published processes and probes them for UserEvents support. ProcessNotFound and ConnectionFailed - /// results are treated as expected (processes may exit between enumeration and probing) and do not cause errors. + /// Gets all published processes and probes them for UserEvents support. Processes that exit + /// between discovery and probing are reported as unknown. /// private void GetAndProbeAllProcesses(StringBuilder supportedProcesses, StringBuilder unsupportedProcesses, StringBuilder unknownProcesses, StringBuilder supportedCsv, StringBuilder unsupportedCsv, StringBuilder unknownCsv) @@ -381,36 +370,39 @@ private void GetAndProbeAllProcesses(StringBuilder supportedProcesses, StringBui continue; } - UserEventsProbeResult probeResult = ProbeProcess(pid, string.Empty, out int resolvedPid, out string resolvedName, out string detectedRuntimeVersion); - BuildProcessSupportCsv(resolvedPid, resolvedName, probeResult, supportedCsv, unsupportedCsv, unknownCsv); + // 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. + unknownProcesses?.AppendLine($"{pid} Unknown - Process exited"); + BuildProcessSupportCsv(pid, "Unknown", UserEventsProbeResult.ConnectionFailed, supportedCsv, unsupportedCsv, unknownCsv); + continue; + } + + UserEventsProbeResult probeResult = ProbeProcess(pid, out string detectedRuntimeVersion); + BuildProcessSupportCsv(pid, processName, probeResult, supportedCsv, unsupportedCsv, unknownCsv); switch (probeResult) { case UserEventsProbeResult.Supported: - supportedProcesses?.AppendLine($"{resolvedPid} {resolvedName}"); + supportedProcesses?.AppendLine($"{pid} {processName}"); break; case UserEventsProbeResult.NotSupported: - unsupportedProcesses?.AppendLine($"{resolvedPid} {resolvedName} - Detected runtime: '{detectedRuntimeVersion}'"); + unsupportedProcesses?.AppendLine($"{pid} {processName} - Detected runtime: '{detectedRuntimeVersion}'"); break; - case UserEventsProbeResult.ProcessNotFound: case UserEventsProbeResult.ConnectionFailed: - unknownProcesses?.AppendLine($"{resolvedPid} {resolvedName} - Unable to connect"); + unknownProcesses?.AppendLine($"{pid} {processName} - Process exited or diagnostic endpoint not accessible"); break; } } } - /// - /// Formats process identifier for display. Shows "name (pid)" when name differs from pid, otherwise just "pid". - /// - private static string FormatProcessIdentifier(int pid, string name) - { - if (string.IsNullOrEmpty(name) || name == pid.ToString()) - { - return pid.ToString(); - } - return $"{name} ({pid})"; - } - private static void BuildProcessSupportCsv(int resolvedPid, string resolvedName, UserEventsProbeResult probeResult, StringBuilder supportedCsv, StringBuilder unsupportedCsv, StringBuilder unknownCsv) { if (supportedCsv == null && unsupportedCsv == null && unknownCsv == null) @@ -427,7 +419,6 @@ private static void BuildProcessSupportCsv(int resolvedPid, string resolvedName, case UserEventsProbeResult.NotSupported: unsupportedCsv?.AppendLine($"{resolvedPid},{escapedName},false"); break; - case UserEventsProbeResult.ProcessNotFound: case UserEventsProbeResult.ConnectionFailed: unknownCsv?.AppendLine($"{resolvedPid},{escapedName},unknown"); break; @@ -613,7 +604,7 @@ private int OutputHandler(uint type, IntPtr data, UIntPtr dataLen) private static readonly Option ProbeOption = new("--probe") { - Description = "Probe 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's diagnostic endpoint is not accessible). 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's diagnostic endpoint is not accessible). 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 @@ -627,7 +618,6 @@ private enum UserEventsProbeResult { Supported, NotSupported, - ProcessNotFound, ConnectionFailed, } diff --git a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs index db771454f8..72351f5d7e 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; @@ -100,13 +101,13 @@ public void CollectLinuxCommand_ReportsResolveProcessNameErrors() [ConditionalTheory(nameof(IsCollectLinuxSupported))] [MemberData(nameof(ResolveProcessExceptions))] - public void CollectLinuxCommand_ResolveProcessExceptions(object testArgs, string[] expectedError, int expectedExitCode) + public void CollectLinuxCommand_ResolveProcessExceptions(object testArgs, string[] expectedError) { MockConsole console = new(200, 30, _outputHelper); int exitCode = Run(testArgs, console); - Assert.Equal(expectedExitCode, exitCode); + Assert.Equal((int)ReturnCode.ArgumentError, exitCode); console.AssertSanitizedLinesEqual(null, expectedError); } @@ -170,17 +171,12 @@ public void CollectLinuxCommand_Probe_Csv() public void CollectLinuxCommand_Probe_ReportsResolveProcessErrors_InvalidPid() { MockConsole console = new(200, 30, _outputHelper); - var args = TestArgs(processId: -1, probe: true, output: new FileInfo(CommonOptions.DefaultTraceName)); + var args = TestArgs(processId: -1, probe: true); int exitCode = Run(args, console); - Assert.Equal((int)ReturnCode.Ok, exitCode); + Assert.Equal((int)ReturnCode.ArgumentError, exitCode); - // ProcessNotFound shows just PID when no name is provided - string[] expected = ExpectPreviewWithMessages( - new[] { - $"Could not resolve process '{FormatProcessIdentifier(-1, string.Empty)}'.", - } - ); + string[] expected = FormatException("-1 is not a valid process ID"); console.AssertSanitizedLinesEqual(null, expected); } @@ -189,17 +185,12 @@ public void CollectLinuxCommand_Probe_ReportsResolveProcessErrors_InvalidPid() public void CollectLinuxCommand_Probe_ReportsResolveProcessErrors_InvalidName() { MockConsole console = new(200, 30, _outputHelper); - var args = TestArgs(name: "process-that-should-not-exist", processId: 0, probe: true, output: new FileInfo(CommonOptions.DefaultTraceName)); + var args = TestArgs(name: "process-that-should-not-exist", processId: 0, probe: true); int exitCode = Run(args, console); - Assert.Equal((int)ReturnCode.Ok, exitCode); + Assert.Equal((int)ReturnCode.ArgumentError, exitCode); - // ProcessNotFound shows "name (pid)" when name is provided - pid is 0 from input - string[] expected = ExpectPreviewWithMessages( - new[] { - $"Could not resolve process '{FormatProcessIdentifier(0, "process-that-should-not-exist")}'.", - } - ); + string[] expected = FormatException("There is no active process with the given name: process-that-should-not-exist"); console.AssertSanitizedLinesEqual(null, expected); } @@ -208,18 +199,47 @@ public void CollectLinuxCommand_Probe_ReportsResolveProcessErrors_InvalidName() public void CollectLinuxCommand_Probe_ReportsResolveProcessErrors_BothPidAndName() { MockConsole console = new(200, 30, _outputHelper); - var args = TestArgs(name: "dummy", processId: 1, probe: true, output: new FileInfo(CommonOptions.DefaultTraceName)); + var args = TestArgs(name: "dummy", processId: 1, probe: true); int exitCode = Run(args, console); - Assert.Equal((int)ReturnCode.Ok, exitCode); + Assert.Equal((int)ReturnCode.ArgumentError, exitCode); + + // When both PID and name are supplied, the banner still refers to the PID + // because the implementation prioritizes ProcessId when it is non-zero. + string[] expected = FormatException("Only one of the --name or --process-id options may be specified."); - // ProcessNotFound shows "name (pid)" when both are provided + 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 endpoint. + 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 its diagnostic endpoint is not accessible.")); + } + + [ConditionalFact(nameof(IsCollectLinuxSupported))] + public void CollectLinuxCommand_Probe_ReportsConnectionFailed_NonDotNetProcess() + { + // PID 1 (init/systemd) exists but is not a .NET process — no diagnostic endpoint. + 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 resolve process '{FormatProcessIdentifier(1, "dummy")}'.", + $"Could not probe process '{pid1Name} (1)'. The process may have exited or its diagnostic endpoint is not accessible.", } ); - console.AssertSanitizedLinesEqual(null, expected); } @@ -446,28 +466,22 @@ public static IEnumerable InvalidProviders() public static IEnumerable ResolveProcessExceptions() { - // ResolveProcess throws DiagnosticToolException for invalid PID - shows ProcessNotFound error yield return new object[] { TestArgs(processId: -1, name: string.Empty), - FormatProcessNotFoundError(FormatProcessIdentifier(-1, string.Empty)), - (int)ReturnCode.TracingError + FormatException("-1 is not a valid process ID") }; - // When both PID and name are supplied, ResolveProcess throws - shows ProcessNotFound with "name (pid)" yield return new object[] { TestArgs(processId: 1, name: "dummy"), - FormatProcessNotFoundError(FormatProcessIdentifier(1, "dummy")), - (int)ReturnCode.TracingError + FormatException("Only one of the --name or --process-id options may be specified.") }; - // ResolveProcess throws for non-existent process - shows ProcessNotFound with just PID yield return new object[] { TestArgs(processId: int.MaxValue, name: string.Empty), - FormatProcessNotFoundError(FormatProcessIdentifier(int.MaxValue, string.Empty)), - (int)ReturnCode.TracingError + FormatException("No process with ID 2147483647 is currently running.") }; } @@ -504,21 +518,6 @@ private static string[] FormatException(string message) "https://learn.microsoft.com/dotnet/core/diagnostics/dotnet-trace.", "==========================================================================================" ]; - private static string[] FormatProcessNotFoundError(string processIdentifier) - { - List result = new(); - result.AddRange(PreviewMessages); - result.Add($"[ERROR] Could not resolve process '{processIdentifier}'."); - return result.ToArray(); - } - private static string FormatProcessIdentifier(int pid, string name) - { - if (string.IsNullOrEmpty(name) || name == pid.ToString()) - { - return pid.ToString(); - } - return $"{name} ({pid})"; - } private static string[] ExpectPreviewWithMessages(string[] messages) { From 7cd838292ff4f296d95cb9d71cdbcc8260b10ce7 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Fri, 6 Mar 2026 23:45:06 +0000 Subject: [PATCH 6/9] Remove unnecessary null-conditional operators on CSV StringBuilders The supportedCsv, unsupportedCsv, and unknownCsv variables are always non-null when the CsvToConsole and Csv output blocks are reached, since generateCsv is true for those modes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CommandLine/Commands/CollectLinuxCommand.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index ae75f9b991..0a966c06b1 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -277,18 +277,18 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) if (mode == ProbeOutputMode.CsvToConsole) { Console.WriteLine("pid,processName,supportsCollectLinux"); - Console.Write(supportedCsv?.ToString()); - Console.Write(unsupportedCsv?.ToString()); - Console.Write(unknownCsv?.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(unknownCsv?.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}'."); } From 69cda2434cf8319c26343c26ceb6bb83e38b7118 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Mon, 9 Mar 2026 21:59:07 +0000 Subject: [PATCH 7/9] Address review feedback: update error messages and skip exited processes Update connection failure messages per review: use 'diagnostic port' instead of 'diagnostic endpoint', and reword to indicate the process may not have a .NET diagnostic port rather than implying it exists but is inaccessible. Skip processes that exit during name resolution silently rather than reporting them as unknown, per reviewer suggestion that users wouldn't find it surprising. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CommandLine/Commands/CollectLinuxCommand.cs | 14 ++++++-------- .../CollectLinuxCommandFunctionalTests.cs | 8 ++++---- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index 0a966c06b1..bd954d2973 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -102,7 +102,7 @@ internal int CollectLinux(CollectLinuxArgs args) 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 its diagnostic endpoint is not accessible."); + 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 }; @@ -243,7 +243,7 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) 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 its diagnostic endpoint is not accessible."); + Console.WriteLine($"Could not probe process '{resolvedName} ({resolvedPid})'. The process may have exited, or it doesn't have an accessible .NET diagnostic port."); break; } } @@ -323,7 +323,7 @@ private static ProbeOutputMode DetermineProbeOutputMode(string outputName) /// /// Probes a resolved process for UserEvents support. Returns ConnectionFailed when unable to - /// connect to the diagnostic endpoint (e.g. process exited between discovery and probe). + /// 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) @@ -380,9 +380,7 @@ private void GetAndProbeAllProcesses(StringBuilder supportedProcesses, StringBui } catch (ArgumentException) { - // Process exited between discovery and name resolution. - unknownProcesses?.AppendLine($"{pid} Unknown - Process exited"); - BuildProcessSupportCsv(pid, "Unknown", UserEventsProbeResult.ConnectionFailed, supportedCsv, unsupportedCsv, unknownCsv); + // Process exited between discovery and name resolution, no need to report these. continue; } @@ -397,7 +395,7 @@ private void GetAndProbeAllProcesses(StringBuilder supportedProcesses, StringBui unsupportedProcesses?.AppendLine($"{pid} {processName} - Detected runtime: '{detectedRuntimeVersion}'"); break; case UserEventsProbeResult.ConnectionFailed: - unknownProcesses?.AppendLine($"{pid} {processName} - Process exited or diagnostic endpoint not accessible"); + unknownProcesses?.AppendLine($"{pid} {processName} - Process may have exited, or it doesn't have an accessible .NET diagnostic port"); break; } } @@ -604,7 +602,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 are categorized as supported, not supported, or unknown (when the process's diagnostic endpoint is not accessible). 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 diff --git a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs index 72351f5d7e..bfff8596c4 100644 --- a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs +++ b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs @@ -214,7 +214,7 @@ public void CollectLinuxCommand_Probe_ReportsResolveProcessErrors_BothPidAndName [ConditionalFact(nameof(IsCollectLinuxSupported))] public void CollectLinuxCommand_ReportsConnectionFailed_NonDotNetProcess() { - // PID 1 (init/systemd) exists but is not a .NET process — no diagnostic endpoint. + // 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); @@ -222,13 +222,13 @@ public void CollectLinuxCommand_ReportsConnectionFailed_NonDotNetProcess() Assert.Equal((int)ReturnCode.TracingError, exitCode); console.AssertSanitizedLinesEqual(null, FormatException( - $"Unable to connect to process '{pid1Name} (1)'. The process may have exited or its diagnostic endpoint is not accessible.")); + $"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 endpoint. + // 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)); @@ -237,7 +237,7 @@ public void CollectLinuxCommand_Probe_ReportsConnectionFailed_NonDotNetProcess() Assert.Equal((int)ReturnCode.Ok, exitCode); string[] expected = ExpectPreviewWithMessages( new[] { - $"Could not probe process '{pid1Name} (1)'. The process may have exited or its diagnostic endpoint is not accessible.", + $"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); From 4419f59b0b00ea5970ddc2f494a238d514a50c56 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Tue, 10 Mar 2026 02:15:02 +0000 Subject: [PATCH 8/9] Update comment --- .../dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index bd954d2973..bc478f9aa2 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -356,8 +356,7 @@ private UserEventsProbeResult ProbeProcess(int resolvedPid, out string detectedR } /// - /// Gets all published processes and probes them for UserEvents support. Processes that exit - /// between discovery and probing are reported as unknown. + /// 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) From 955adfa54d149df472b180a2725156d0b19c3c2e Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Thu, 26 Mar 2026 03:09:38 +0000 Subject: [PATCH 9/9] Catch IOException in ProbeProcess for mid-response disconnection GetProcessInfo can throw IOException (e.g. 'Connection reset by peer') when a process exits while the IPC response is being read. This is distinct from ServerNotAvailableException which covers connection failures. Observed in CI on main branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index ebaa6223db..6e4269ad43 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -345,6 +345,11 @@ private UserEventsProbeResult ProbeProcess(int resolvedPid, out string detectedR { return UserEventsProbeResult.ConnectionFailed; } + catch (IOException) + { + // Process exited mid-response (e.g. "Connection reset by peer" during IPC read). + return UserEventsProbeResult.ConnectionFailed; + } catch (UnsupportedCommandException) { // can be thrown from an older runtime that doesn't even support GetProcessInfo