-
Notifications
You must be signed in to change notification settings - Fork 389
Handle ServerNotAvailableException in CollectLinuxCommand process probing #5705
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+167
−58
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
105683b
Refactor probe logic into ProbeProcess and GetAndProbeAllProcesses he…
mdh1418 fb8ee47
Handle ServerNotAvailableException and DiagnosticToolException in pro…
mdh1418 6f420b2
Update --probe option help text
mdh1418 0cbcf44
Update CollectLinuxCommand tests for ProcessNotFound and ConnectionFa…
mdh1418 bb96bfc
Merge branch 'main' of https://github.com/dotnet/diagnostics into fix…
mdh1418 893f2f3
Address review feedback: preserve original error messages and return …
mdh1418 7cd8382
Remove unnecessary null-conditional operators on CSV StringBuilders
mdh1418 69cda24
Address review feedback: update error messages and skip exited processes
mdh1418 4419f59
Update comment
mdh1418 0cb10bc
Merge branch 'main' into copilot/fix-diagnostics-client-exception
mdh1418 955adfa
Catch IOException in ProbeProcess for mid-response disconnection
mdh1418 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -96,10 +96,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 }; | ||
| } | ||
|
|
@@ -215,18 +221,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; | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -238,56 +254,39 @@ internal int SupportsCollectLinux(CollectLinuxArgs args) | |
| } | ||
| StringBuilder supportedProcesses = new(); | ||
| StringBuilder unsupportedProcesses = new(); | ||
| StringBuilder unknownProcesses = new(); | ||
|
|
||
| IEnumerable<int> pids = DiagnosticsClient.GetPublishedProcesses(); | ||
| foreach (int pid in pids) | ||
| { | ||
| if (pid == Environment.ProcessId) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| 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}'"); | ||
| } | ||
| } | ||
| catch (Exception ex) when (ex is DiagnosticToolException or DiagnosticsClientException) | ||
| { | ||
| // Process may have exited between enumeration and probing — skip it. | ||
| } | ||
| } | ||
| GetAndProbeAllProcesses(supportedProcesses, unsupportedProcesses, unknownProcesses, supportedCsv, unsupportedCsv, unknownCsv); | ||
|
|
||
| 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()); | ||
| if (unknownProcesses.Length > 0) | ||
| { | ||
| Console.WriteLine($".NET processes that could not be probed:"); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's many cases where we are presenting info to the user that's not the most useful. For example, if iterating and we see a process and it died or exited, reporting them as unknown doesn't feel productive during global enumeration. |
||
| 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}'."); | ||
| } | ||
|
|
||
|
|
@@ -320,39 +319,109 @@ 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) | ||
| /// <summary> | ||
| /// 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. | ||
| /// </summary> | ||
| 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 | ||
| { | ||
| 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) | ||
| { | ||
| supports = true; | ||
| 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 | ||
| // treat as NotSupported instead of propagating the exception. | ||
| return UserEventsProbeResult.NotSupported; | ||
| } | ||
| } | ||
|
|
||
| return supports; | ||
| /// <summary> | ||
| /// Gets all published processes and probes them for UserEvents support. | ||
| /// </summary> | ||
| private void GetAndProbeAllProcesses(StringBuilder supportedProcesses, StringBuilder unsupportedProcesses, StringBuilder unknownProcesses, | ||
| StringBuilder supportedCsv, StringBuilder unsupportedCsv, StringBuilder unknownCsv) | ||
| { | ||
| IEnumerable<int> 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) | ||
| switch (probeResult) | ||
| { | ||
| supportedCsv?.AppendLine($"{resolvedPid},{escapedName},true"); | ||
| } | ||
| else | ||
| { | ||
| unsupportedCsv?.AppendLine($"{resolvedPid},{escapedName},false"); | ||
| case UserEventsProbeResult.Supported: | ||
| supportedCsv?.AppendLine($"{resolvedPid},{escapedName},true"); | ||
hoyosjs marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| break; | ||
| case UserEventsProbeResult.NotSupported: | ||
| unsupportedCsv?.AppendLine($"{resolvedPid},{escapedName},false"); | ||
| break; | ||
| case UserEventsProbeResult.ConnectionFailed: | ||
| unknownCsv?.AppendLine($"{resolvedPid},{escapedName},unknown"); | ||
| break; | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -536,7 +605,7 @@ private int OutputHandler(uint type, IntPtr data, UIntPtr dataLen) | |
| private static readonly Option<bool> 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 <file>' 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 <file>' to write the CSV. Probe a single process with -n|--name or -p|--process-id.", | ||
| }; | ||
|
|
||
| private enum ProbeOutputMode | ||
|
|
@@ -546,6 +615,13 @@ private enum ProbeOutputMode | |
| CsvToConsole, | ||
| } | ||
|
|
||
| private enum UserEventsProbeResult | ||
| { | ||
| Supported, | ||
| NotSupported, | ||
| ConnectionFailed, | ||
| } | ||
|
|
||
| private enum OutputType : uint | ||
| { | ||
| Normal = 0, | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.