From 58294b7cec1ea3f49bbae645974d6e09651d4d3c Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Sat, 6 Dec 2025 04:29:34 +0000 Subject: [PATCH 1/5] [IPC Protocol] Update known error codes --- documentation/design-docs/ipc-protocol.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/documentation/design-docs/ipc-protocol.md b/documentation/design-docs/ipc-protocol.md index 2ee2a3440e..a02c3582e7 100644 --- a/documentation/design-docs/ipc-protocol.md +++ b/documentation/design-docs/ipc-protocol.md @@ -1573,15 +1573,21 @@ In the event an error occurs in the handling of an Ipc Message, the Diagnostic S Errors are `HRESULTS` encoded as `int32_t` when sent back to the client. There are a few Diagnostics IPC specific `HRESULT`s: ```c -#define CORDIAGIPC_E_BAD_ENCODING = 0x80131384 -#define CORDIAGIPC_E_UNKNOWN_COMMAND = 0x80131385 -#define CORDIAGIPC_E_UNKNOWN_MAGIC = 0x80131386 -#define CORDIAGIPC_E_UNKNOWN_ERROR = 0x80131387 +#define DS_IPC_E_BAD_ENCODING ((ds_ipc_result_t)(0x80131384L)) +#define DS_IPC_E_UNKNOWN_COMMAND ((ds_ipc_result_t)(0x80131385L)) +#define DS_IPC_E_UNKNOWN_MAGIC ((ds_ipc_result_t)(0x80131386L)) +#define DS_IPC_E_NOTSUPPORTED ((ds_ipc_result_t)(0x80131515L)) +#define DS_IPC_E_FAIL ((ds_ipc_result_t)(0x80004005L)) +#define DS_IPC_E_NOT_YET_AVAILABLE ((ds_ipc_result_t)(0x8013135bL)) +#define DS_IPC_E_RUNTIME_UNINITIALIZED ((ds_ipc_result_t)(0x80131371L)) +#define DS_IPC_E_INVALIDARG ((ds_ipc_result_t)(0x80070057L)) +#define DS_IPC_E_INSUFFICIENT_BUFFER ((ds_ipc_result_t)(0x8007007A)) +#define DS_IPC_E_ENVVAR_NOT_FOUND ((ds_ipc_result_t)(0x800000CB)) ``` Diagnostic Server errors are sent as a Diagnostic IPC Message with: -* a `command_set` of `0xFF` -* a `command_id` of `0xFF` +* a `command_set` of `0xFF` (Server) +* a `command_id` of `0xFF` (Error) * a Payload consisting of a `int32_t` representing the error encountered (described above) All errors will result in the Server closing the connection. From b9e1b6aa9c57f636793f40f8b9d5b86efb1be6c9 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Tue, 9 Dec 2025 23:57:37 +0000 Subject: [PATCH 2/5] [ProcessInfo] Extend TryGetProcessClrVersion to emit prerelease info --- .../DiagnosticsIpc/ProcessInfo.cs | 4 +++- src/Tools/dotnet-counters/CounterMonitor.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessInfo.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessInfo.cs index 4a5c75a61c..f5abe69f9f 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessInfo.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessInfo.cs @@ -98,9 +98,10 @@ private static ProcessInfo ParseCommon(byte[] payload, ref int index) return processInfo; } - internal bool TryGetProcessClrVersion(out Version version) + internal bool TryGetProcessClrVersion(out Version version, out bool isPrerelease) { version = null; + isPrerelease = true; if (string.IsNullOrEmpty(ClrProductVersionString)) { return false; @@ -120,6 +121,7 @@ internal bool TryGetProcessClrVersion(out Version version) int prereleaseIndex = noMetadataVersion.IndexOf('-'); if (-1 == prereleaseIndex) { + isPrerelease = false; prereleaseIndex = metadataIndex; } diff --git a/src/Tools/dotnet-counters/CounterMonitor.cs b/src/Tools/dotnet-counters/CounterMonitor.cs index 230bea4940..87cfd4da79 100644 --- a/src/Tools/dotnet-counters/CounterMonitor.cs +++ b/src/Tools/dotnet-counters/CounterMonitor.cs @@ -217,7 +217,7 @@ public async Task Monitor( _settings.UseCounterRateAndValuePayloads = true; bool useSharedSession = false; - if (_diagnosticsClient.GetProcessInfo().TryGetProcessClrVersion(out Version version)) + if (_diagnosticsClient.GetProcessInfo().TryGetProcessClrVersion(out Version version, out bool _)) { useSharedSession = version.Major >= 8 ? true : false; } From 654ce24f3161e3beb1056408a8ebebb3838662f6 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Mon, 8 Dec 2025 00:49:56 +0000 Subject: [PATCH 3/5] [dotnet-trace][CollectLinux] Add probe for user_events support in .NET processes --- .../Commands/CollectLinuxCommand.cs | 145 +++++++++++++++++- 1 file changed, 140 insertions(+), 5 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index 63bb18dabb..743b001310 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -22,6 +22,7 @@ internal partial class CollectLinuxCommandHandler private Stopwatch stopwatch = new(); private LineRewriter rewriter; private long statusUpdateTimestamp; + private Version minRuntimeSupportingUserEventsIPCCommand = new(10, 0, 0); internal sealed record CollectLinuxArgs( CancellationToken Ct, @@ -33,7 +34,8 @@ internal sealed record CollectLinuxArgs( FileInfo Output, TimeSpan Duration, string Name, - int ProcessId); + int ProcessId, + bool Probe); public CollectLinuxCommandHandler(IConsole console = null) { @@ -82,9 +84,19 @@ internal int CollectLinux(CollectLinuxArgs args) string scriptPath = null; try { + if (args.Probe) + { + ret = SupportsCollectLinux(args); + return ret; + } + if (args.ProcessId != 0 || !string.IsNullOrEmpty(args.Name)) { - CommandUtils.ResolveProcess(args.ProcessId, args.Name, out int resolvedProcessId, out string resolvedProcessName); + if (!ProcessSupportsUserEventsIpcCommand(args.ProcessId, args.Name, out int resolvedProcessId, out string resolvedProcessName, out string detectedRuntimeVersion)) + { + Console.Error.WriteLine($"[ERROR] Process '{resolvedProcessName} ({resolvedProcessId})' cannot be traced by collect-linux. Required runtime: {minRuntimeSupportingUserEventsIPCCommand}. Detected runtime: {detectedRuntimeVersion}"); + return (int)ReturnCode.TracingError; + } args = args with { Name = resolvedProcessName, ProcessId = resolvedProcessId }; } @@ -146,14 +158,15 @@ public static Command CollectLinuxCommand() CommonOptions.CLREventLevelOption, CommonOptions.CLREventsOption, PerfEventsOption, + ProbeOption, CommonOptions.ProfileOption, CommonOptions.OutputPathOption, CommonOptions.DurationOption, CommonOptions.NameOption, - CommonOptions.ProcessIdOption + CommonOptions.ProcessIdOption, }; collectLinuxCommand.TreatUnmatchedTokensAsErrors = true; // collect-linux currently does not support child process tracing. - collectLinuxCommand.Description = "Collects diagnostic traces using perf_events, a Linux OS technology. collect-linux requires admin privileges to capture kernel- and user-mode events, and by default, captures events from all processes. This Linux-only command includes the same .NET events as dotnet-trace collect, and it uses the kernel’s user_events mechanism to emit .NET events as perf events, enabling unification of user-space .NET events with kernel-space system events."; + collectLinuxCommand.Description = "Collects diagnostic traces using perf_events, a Linux OS technology. collect-linux requires admin privileges to capture kernel- and user-mode events, and by default, captures events from all processes. This Linux-only command includes the same .NET events as dotnet-trace collect, and it uses the kernel’s user_events mechanism to emit .NET events as perf events, enabling unification of user-space .NET events with kernel-space system events. Use --probe (optionally with -p|--process-id or -n|--name) to only check which processes can be traced by collect-linux without collecting a trace."; collectLinuxCommand.SetAction((parseResult, ct) => { string providersValue = parseResult.GetValue(CommonOptions.ProvidersOption) ?? string.Empty; @@ -171,13 +184,129 @@ public static Command CollectLinuxCommand() Output: parseResult.GetValue(CommonOptions.OutputPathOption) ?? new FileInfo(CommonOptions.DefaultTraceName), Duration: parseResult.GetValue(CommonOptions.DurationOption), Name: parseResult.GetValue(CommonOptions.NameOption) ?? string.Empty, - ProcessId: parseResult.GetValue(CommonOptions.ProcessIdOption))); + ProcessId: parseResult.GetValue(CommonOptions.ProcessIdOption), + Probe: parseResult.GetValue(ProbeOption))); return Task.FromResult(rc); }); return collectLinuxCommand; } + internal int SupportsCollectLinux(CollectLinuxArgs args) + { + int ret; + try + { + bool generateCsv = !string.Equals(args.Output.Name, CommonOptions.DefaultTraceName, StringComparison.OrdinalIgnoreCase); + StringBuilder supportedCsv = generateCsv ? new StringBuilder() : null; + StringBuilder unsupportedCsv = 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); + + Console.WriteLine($"Does .NET process '{resolvedName} ({resolvedPid})' support the EventPipe UserEvents IPC command used by collect-linux?"); + Console.WriteLine(supports.ToString().ToLower()); + if (!supports) + { + Console.WriteLine($"Required runtime: {minRuntimeSupportingUserEventsIPCCommand}. Detected runtime: {detectedRuntimeVersion}"); + } + } + else + { + 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(); + + 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}'"); + } + } + + + Console.WriteLine($".NET processes that support the command:"); + Console.WriteLine(supportedProcesses.ToString()); + Console.WriteLine($".NET processes that do NOT support the command:"); + Console.WriteLine(unsupportedProcesses.ToString()); + } + + if (generateCsv) + { + Console.WriteLine($"Writing to CSV file {args.Output.FullName}..."); + using StreamWriter writer = new(args.Output.FullName, append: false, Encoding.UTF8); + writer.WriteLine("pid,processName,supportsCollectLinux"); + writer.Write(supportedCsv?.ToString()); + writer.Write(unsupportedCsv?.ToString()); + } + + ret = (int)ReturnCode.Ok; + } + catch (DiagnosticToolException dte) + { + Console.WriteLine($"[ERROR] {dte.Message}"); + ret = (int)ReturnCode.ArgumentError; + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + ret = (int)ReturnCode.UnknownError; + } + + return ret; + } + + private bool ProcessSupportsUserEventsIpcCommand(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; + if (processInfo.TryGetProcessClrVersion(out Version version, out bool isPrerelease) && + (version > minRuntimeSupportingUserEventsIPCCommand || + (version == minRuntimeSupportingUserEventsIPCCommand && !isPrerelease))) + { + supports = true; + } + + return supports; + } + + private static void BuildProcessSupportCsv(int resolvedPid, string resolvedName, bool supports, StringBuilder supportedCsv, StringBuilder unsupportedCsv) + { + if (supportedCsv == null && unsupportedCsv == null) + { + return; + } + + string escapedName = (resolvedName ?? string.Empty).Replace(",", string.Empty); + if (supports) + { + supportedCsv?.AppendLine($"{resolvedPid},{escapedName},true"); + } + else + { + unsupportedCsv?.AppendLine($"{resolvedPid},{escapedName},false"); + } + } + private byte[] BuildRecordTraceArgs(CollectLinuxArgs args, out string scriptPath) { scriptPath = null; @@ -354,6 +483,12 @@ private int OutputHandler(uint type, IntPtr data, UIntPtr dataLen) Description = @"Comma-separated list of perf events (e.g. syscalls:sys_enter_execve,sched:sched_switch)." }; + private static readonly Option ProbeOption = + new("--probe") + { + Description = "Probes .NET processes capable of being traced by collect-linux, without collecting a trace. Can be combined with -p|--process-id or -n|--name to probe a specific process. Can be combined with -o|--output to write results to a CSV file in the format 'pid,processName,supportsCollectLinux' (for example '1234,MyApp,true'), ordered with supported processes first followed by unsupported.", + }; + private enum OutputType : uint { Normal = 0, From 8312e6c09e9ec0f3358debb54d855af04a33594e Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Mon, 8 Dec 2025 00:50:30 +0000 Subject: [PATCH 4/5] [Tests][CollectLinux] Add functional tests for probe scenarios --- .../CollectLinuxCommandFunctionalTests.cs | 109 ++++++++++++++++-- 1 file changed, 101 insertions(+), 8 deletions(-) diff --git a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs index ac5a271119..7351cfa2ba 100644 --- a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs +++ b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Diagnostics.Tests.Common; @@ -31,7 +32,8 @@ private static CollectLinuxCommandHandler.CollectLinuxArgs TestArgs( FileInfo output = null, TimeSpan duration = default, string name = "", - int processId = 0) + int processId = 0, + bool probe = false) { return new CollectLinuxCommandHandler.CollectLinuxArgs(ct, providers ?? Array.Empty(), @@ -42,7 +44,8 @@ private static CollectLinuxCommandHandler.CollectLinuxArgs TestArgs( output ?? new FileInfo("trace.nettrace"), duration, name, - processId); + processId, + probe); } [ConditionalTheory(nameof(IsCollectLinuxSupported))] @@ -99,6 +102,70 @@ public void CollectLinuxCommand_ResolveProcessExceptions(object testArgs, string console.AssertSanitizedLinesEqual(null, expectedError); } + [ConditionalFact(nameof(IsCollectLinuxSupported))] + public void CollectLinuxCommand_Probe_ListsProcesses_WhenNoArgs() + { + MockConsole console = new(200, 2000); + var args = TestArgs(probe: true, output: new FileInfo(CommonOptions.DefaultTraceName)); + int exitCode = Run(args, console); + + Assert.Equal((int)ReturnCode.Ok, exitCode); + string[] expected = ExpectPreviewWithMessages( + new[] { + "Probing .NET processes for support of the EventPipe UserEvents IPC command used by collect-linux. Requires runtime '10.0.0' or later.", + ".NET processes that support the command:", + "", + ".NET processes that do NOT support the command:", + "", + } + ); + console.AssertSanitizedLinesEqual(CollectLinuxProbeSanitizer, expected); + } + + [ConditionalFact(nameof(IsCollectLinuxSupported))] + public void CollectLinuxCommand_Probe_ReportsResolveProcessErrors_InvalidPid() + { + MockConsole console = new(200, 30); + var args = TestArgs(processId: -1, probe: true); + int exitCode = Run(args, console); + + Assert.Equal((int)ReturnCode.ArgumentError, exitCode); + + string[] expected = FormatException("-1 is not a valid process ID"); + + console.AssertSanitizedLinesEqual(null, expected); + } + + [ConditionalFact(nameof(IsCollectLinuxSupported))] + public void CollectLinuxCommand_Probe_ReportsResolveProcessErrors_InvalidName() + { + MockConsole console = new(200, 30); + var args = TestArgs(name: "process-that-should-not-exist", processId: 0, probe: true); + int exitCode = Run(args, console); + + Assert.Equal((int)ReturnCode.ArgumentError, exitCode); + + string[] expected = FormatException("There is no active process with the given name: process-that-should-not-exist"); + + console.AssertSanitizedLinesEqual(null, expected); + } + + [ConditionalFact(nameof(IsCollectLinuxSupported))] + public void CollectLinuxCommand_Probe_ReportsResolveProcessErrors_BothPidAndName() + { + MockConsole console = new(200, 30); + var args = TestArgs(name: "dummy", processId: 1, probe: true); + int exitCode = Run(args, console); + + 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."); + + console.AssertSanitizedLinesEqual(null, expected); + } + [ConditionalFact(nameof(IsCollectLinuxNotSupported))] public void CollectLinuxCommand_NotSupported_OnNonLinux() { @@ -138,11 +205,26 @@ private static string[] CollectLinuxSanitizer(string[] lines) return result.ToArray(); } + private static string[] CollectLinuxProbeSanitizer(string[] lines) + { + List result = new(); + foreach (string line in lines) + { + // Only filter out lines that start with digits followed by a space and some text (PID and process name) + if (Regex.IsMatch(line, @"^\d+\s+.+")) + { + continue; + } + result.Add(line); + } + return result.ToArray(); + } + public static IEnumerable BasicCases() { yield return new object[] { TestArgs(), - ExpectProvidersAndLinuxWithMessages( + ExpectProvidersAndPerfEventsWithMessages( new[]{"No providers, profiles, ClrEvents, or PerfEvents were specified, defaulting to trace profiles 'dotnet-common' + 'cpu-sampling'."}, new[]{FormatProvider("Microsoft-Windows-DotNETRuntime","000000100003801D","Informational",4,"--profile")}, new[]{LinuxProfile("cpu-sampling")}) @@ -167,7 +249,7 @@ public static IEnumerable BasicCases() yield return new object[] { TestArgs(profile: new[]{"cpu-sampling"}), - ExpectProvidersAndLinuxWithMessages( + ExpectProvidersAndPerfEventsWithMessages( new[]{"No .NET providers were configured."}, Array.Empty(), new[]{LinuxProfile("cpu-sampling")}) @@ -196,7 +278,7 @@ public static IEnumerable BasicCases() yield return new object[] { TestArgs(providers: new[]{"Microsoft-Windows-DotNETRuntime:0x1:4"}, clrEvents: "gc"), - ExpectProvidersAndLinuxWithMessages( + ExpectProvidersAndPerfEventsWithMessages( new[]{"Warning: The CLR provider was already specified through --providers or --profile. Ignoring --clrevents."}, new[]{FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000001","Informational",4,"--providers")}, Array.Empty()) @@ -218,7 +300,7 @@ public static IEnumerable BasicCases() yield return new object[] { TestArgs(perfEvents: new[]{"sched:sched_switch"}), - ExpectProvidersAndLinuxWithMessages( + ExpectProvidersAndPerfEventsWithMessages( new[]{"No .NET providers were configured."}, Array.Empty(), new[]{LinuxPerfEvent("sched:sched_switch")}) @@ -313,10 +395,21 @@ private static string[] FormatException(string message) "==========================================================================================" ]; + private static string[] ExpectPreviewWithMessages(string[] messages) + { + List result = new(); + result.AddRange(PreviewMessages); + if (messages.Length > 0) + { + result.AddRange(messages); + } + return result.ToArray(); + } + private static string[] ExpectProvidersAndLinux(string[] dotnetProviders, string[] linuxPerfEvents) - => ExpectProvidersAndLinuxWithMessages(Array.Empty(), dotnetProviders, linuxPerfEvents); + => ExpectProvidersAndPerfEventsWithMessages(Array.Empty(), dotnetProviders, linuxPerfEvents); - private static string[] ExpectProvidersAndLinuxWithMessages(string[] messages, string[] dotnetProviders, string[] linuxPerfEvents) + private static string[] ExpectProvidersAndPerfEventsWithMessages(string[] messages, string[] dotnetProviders, string[] linuxPerfEvents) { List result = new(); From 5198cbb3e408daa0d66bd472a5c8e9118e026f5a Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Wed, 10 Dec 2025 20:27:26 +0000 Subject: [PATCH 5/5] Address feedback --- .../Commands/CollectLinuxCommand.cs | 61 +++++++++++++++---- .../CollectLinuxCommandFunctionalTests.cs | 40 +++++++++++- 2 files changed, 86 insertions(+), 15 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index 743b001310..08bce2e961 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -197,7 +197,8 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) int ret; try { - bool generateCsv = !string.Equals(args.Output.Name, CommonOptions.DefaultTraceName, StringComparison.OrdinalIgnoreCase); + ProbeOutputMode mode = DetermineProbeOutputMode(args.Output.Name); + bool generateCsv = mode == ProbeOutputMode.CsvToConsole || mode == ProbeOutputMode.Csv; StringBuilder supportedCsv = generateCsv ? new StringBuilder() : null; StringBuilder unsupportedCsv = generateCsv ? new StringBuilder() : null; @@ -206,16 +207,21 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) bool supports = ProcessSupportsUserEventsIpcCommand(args.ProcessId, args.Name, out int resolvedPid, out string resolvedName, out string detectedRuntimeVersion); BuildProcessSupportCsv(resolvedPid, resolvedName, supports, supportedCsv, unsupportedCsv); - Console.WriteLine($"Does .NET process '{resolvedName} ({resolvedPid})' support the EventPipe UserEvents IPC command used by collect-linux?"); - Console.WriteLine(supports.ToString().ToLower()); - if (!supports) + if (mode == ProbeOutputMode.Console) { - Console.WriteLine($"Required runtime: {minRuntimeSupportingUserEventsIPCCommand}. Detected runtime: {detectedRuntimeVersion}"); + Console.WriteLine($".NET process '{resolvedName} ({resolvedPid})' {(supports ? "supports" : "does NOT support")} the EventPipe UserEvents IPC command used by collect-linux."); + if (!supports) + { + Console.WriteLine($"Required runtime: '{minRuntimeSupportingUserEventsIPCCommand}'. Detected runtime: '{detectedRuntimeVersion}'."); + } } } else { - Console.WriteLine($"Probing .NET processes for support of the EventPipe UserEvents IPC command used by collect-linux. Requires runtime '{minRuntimeSupportingUserEventsIPCCommand}' or later."); + 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."); + } StringBuilder supportedProcesses = new(); StringBuilder unsupportedProcesses = new(); @@ -239,20 +245,29 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) } } + if (mode == ProbeOutputMode.Console) + { + Console.WriteLine($".NET processes that support the command:"); + Console.WriteLine(supportedProcesses.ToString()); + Console.WriteLine($".NET processes that do NOT support the command:"); + Console.WriteLine(unsupportedProcesses.ToString()); + } + } - Console.WriteLine($".NET processes that support the command:"); - Console.WriteLine(supportedProcesses.ToString()); - Console.WriteLine($".NET processes that do NOT support the command:"); - Console.WriteLine(unsupportedProcesses.ToString()); + if (mode == ProbeOutputMode.CsvToConsole) + { + Console.WriteLine("pid,processName,supportsCollectLinux"); + Console.Write(supportedCsv?.ToString()); + Console.Write(unsupportedCsv?.ToString()); } - if (generateCsv) + if (mode == ProbeOutputMode.Csv) { - Console.WriteLine($"Writing to CSV file {args.Output.FullName}..."); using StreamWriter writer = new(args.Output.FullName, append: false, Encoding.UTF8); writer.WriteLine("pid,processName,supportsCollectLinux"); writer.Write(supportedCsv?.ToString()); writer.Write(unsupportedCsv?.ToString()); + Console.WriteLine($"Successfully wrote EventPipe UserEvents IPC command support results to '{args.Output.FullName}'."); } ret = (int)ReturnCode.Ok; @@ -271,6 +286,19 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) return ret; } + private static ProbeOutputMode DetermineProbeOutputMode(string outputName) + { + if (string.Equals(outputName, CommonOptions.DefaultTraceName, StringComparison.OrdinalIgnoreCase)) + { + return ProbeOutputMode.Console; + } + if (string.Equals(outputName, "stdout", StringComparison.OrdinalIgnoreCase)) + { + return ProbeOutputMode.CsvToConsole; + } + return ProbeOutputMode.Csv; + } + private bool ProcessSupportsUserEventsIpcCommand(int pid, string processName, out int resolvedPid, out string resolvedName, out string detectedRuntimeVersion) { CommandUtils.ResolveProcess(pid, processName, out resolvedPid, out resolvedName); @@ -486,9 +514,16 @@ private int OutputHandler(uint type, IntPtr data, UIntPtr dataLen) private static readonly Option ProbeOption = new("--probe") { - Description = "Probes .NET processes capable of being traced by collect-linux, without collecting a trace. Can be combined with -p|--process-id or -n|--name to probe a specific process. Can be combined with -o|--output to write results to a CSV file in the format 'pid,processName,supportsCollectLinux' (for example '1234,MyApp,true'), ordered with supported processes first followed by unsupported.", + 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.", }; + private enum ProbeOutputMode + { + Console, + Csv, + CsvToConsole, + } + private enum OutputType : uint { Normal = 0, diff --git a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs index 7351cfa2ba..71477b55c5 100644 --- a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs +++ b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs @@ -122,6 +122,42 @@ public void CollectLinuxCommand_Probe_ListsProcesses_WhenNoArgs() console.AssertSanitizedLinesEqual(CollectLinuxProbeSanitizer, expected); } + [ConditionalFact(nameof(IsCollectLinuxSupported))] + public void CollectLinuxCommand_Probe_CsvToConsole() + { + MockConsole console = new(200, 2000); + var args = TestArgs(probe: true, output: new FileInfo("stdout")); + int exitCode = Run(args, console); + + Assert.Equal((int)ReturnCode.Ok, exitCode); + string[] expected = ExpectPreviewWithMessages( + new[] { + "pid,processName,supportsCollectLinux", + "" + } + ); + console.AssertSanitizedLinesEqual(CollectLinuxProbeSanitizer, expected); + } + + [ConditionalFact(nameof(IsCollectLinuxSupported))] + public void CollectLinuxCommand_Probe_Csv() + { + MockConsole console = new(200, 2000); + string tempFilePath = Path.GetTempFileName(); + var args = TestArgs(probe: true, output: new FileInfo(tempFilePath)); + int exitCode = Run(args, console); + + Assert.Equal((int)ReturnCode.Ok, exitCode); + string[] expected = ExpectPreviewWithMessages( + new[] { + "Successfully wrote EventPipe UserEvents IPC command support results to '" + tempFilePath + "'.", + } + ); + + File.Delete(tempFilePath); + console.AssertSanitizedLinesEqual(null, expected); + } + [ConditionalFact(nameof(IsCollectLinuxSupported))] public void CollectLinuxCommand_Probe_ReportsResolveProcessErrors_InvalidPid() { @@ -210,8 +246,8 @@ private static string[] CollectLinuxProbeSanitizer(string[] lines) List result = new(); foreach (string line in lines) { - // Only filter out lines that start with digits followed by a space and some text (PID and process name) - if (Regex.IsMatch(line, @"^\d+\s+.+")) + // Filter out possible pid lines + if (Regex.IsMatch(line, @"^\d")) { continue; }