Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions documentation/design-docs/ipc-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -120,6 +121,7 @@ internal bool TryGetProcessClrVersion(out Version version)
int prereleaseIndex = noMetadataVersion.IndexOf('-');
if (-1 == prereleaseIndex)
{
isPrerelease = false;
prereleaseIndex = metadataIndex;
}

Expand Down
2 changes: 1 addition & 1 deletion src/Tools/dotnet-counters/CounterMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ public async Task<ReturnCode> 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;
}
Expand Down
180 changes: 175 additions & 5 deletions src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
{
Expand Down Expand Up @@ -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 };
}

Expand Down Expand Up @@ -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;
Expand All @@ -171,13 +184,157 @@ 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
{
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;

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);

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)
{
Console.WriteLine($"Required runtime: '{minRuntimeSupportingUserEventsIPCCommand}'. Detected runtime: '{detectedRuntimeVersion}'.");
}
}
}
else
{
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();

IEnumerable<int> 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}'");
}
}

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 (mode == ProbeOutputMode.CsvToConsole)
{
Console.WriteLine("pid,processName,supportsCollectLinux");
Console.Write(supportedCsv?.ToString());
Console.Write(unsupportedCsv?.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());
Console.WriteLine($"Successfully wrote EventPipe UserEvents IPC command support results to '{args.Output.FullName}'.");
}

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 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);

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;
Expand Down Expand Up @@ -354,6 +511,19 @@ 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<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.",
};

private enum ProbeOutputMode
{
Console,
Csv,
CsvToConsole,
}

private enum OutputType : uint
{
Normal = 0,
Expand Down
Loading