diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/EventMonitor.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/EventMonitor.cs index 147167f5c6..5e773d2394 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/EventMonitor.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/EventMonitor.cs @@ -24,6 +24,8 @@ internal sealed class EventMonitor : IAsyncDisposable private readonly bool _leaveEventStreamOpen; private EventPipeEventSource _eventSource; + private TraceEventParser _eventParser; + private readonly string _providerName; private readonly string _eventName; @@ -78,8 +80,16 @@ public Task ProcessAsync(CancellationToken token) token.ThrowIfCancellationRequested(); using IDisposable registration = token.Register(() => _eventSource.Dispose()); - _eventSource.Dynamic.AddCallbackForProviderEvent(_providerName, _eventName, TraceEventCallback); + if (string.Equals(_providerName, "Microsoft-Windows-DotNETRuntime", StringComparison.Ordinal)) + { + _eventParser = _eventSource.Clr; + } + else + { + _eventParser = _eventSource.Dynamic; + } + _eventParser.AddCallbackForProviderEvent(_providerName, _eventName, TraceEventCallback); _eventSource.Process(); token.ThrowIfCancellationRequested(); }, token); @@ -91,7 +101,7 @@ public Task ProcessAsync(CancellationToken token) /// private void StopMonitoringForEvent() { - _eventSource?.Dynamic.RemoveCallback(TraceEventCallback); + _eventParser?.RemoveCallback(TraceEventCallback); } private void TraceEventCallback(TraceEvent obj) diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Microsoft.Diagnostics.Monitoring.EventPipe.csproj b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Microsoft.Diagnostics.Monitoring.EventPipe.csproj index c104003875..4558b2ed0c 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Microsoft.Diagnostics.Monitoring.EventPipe.csproj +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Microsoft.Diagnostics.Monitoring.EventPipe.csproj @@ -40,6 +40,7 @@ + diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs index 15eab9c799..720373be6d 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs @@ -12,6 +12,7 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using Microsoft.Diagnostics.Monitoring.EventPipe; using Microsoft.Diagnostics.NETCore.Client; using Microsoft.Internal.Common.Utils; using Microsoft.Tools.Common; @@ -31,7 +32,7 @@ private static void ConsoleWriteLine(string str) } } - private delegate Task CollectDelegate(CancellationToken ct, IConsole console, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel, string name, string port, bool showchildio, bool resumeRuntime); + private delegate Task CollectDelegate(CancellationToken ct, IConsole console, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel, string name, string port, bool showchildio, bool resumeRuntime, string stoppingEventProviderName, string stoppingEventEventName, string stoppingEventPayloadFilter); /// /// Collects a diagnostic trace from a currently running process or launch a child process and trace it. @@ -52,8 +53,11 @@ private static void ConsoleWriteLine(string str) /// Path to the diagnostic port to be used. /// Should IO from a child process be hidden. /// Resume runtime once session has been initialized. + /// A string, parsed as-is, that will stop the trace upon hitting an event with the matching provider name. For a more specific stopping event, additionally provide `--stopping-event-event-name` and/or `--stopping-event-payload-filter`. + /// A string, parsed as-is, that will stop the trace upon hitting an event with the matching event name. Requires `--stopping-event-provider-name` to be set. For a more specific stopping event, additionally provide `--stopping-event-payload-filter`. + /// A string, parsed as [payload_field_name]:[payload_field_value] pairs separated by commas, that will stop the trace upon hitting an event with a matching payload. Requires `--stopping-event-provider-name` and `--stopping-event-event-name` to be set. /// - private static async Task Collect(CancellationToken ct, IConsole console, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel, string name, string diagnosticPort, bool showchildio, bool resumeRuntime) + private static async Task Collect(CancellationToken ct, IConsole console, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel, string name, string diagnosticPort, bool showchildio, bool resumeRuntime, string stoppingEventProviderName, string stoppingEventEventName, string stoppingEventPayloadFilter) { bool collectionStopped = false; bool cancelOnEnter = true; @@ -159,6 +163,39 @@ private static async Task Collect(CancellationToken ct, IConsole console, i PrintProviders(providerCollection, enabledBy); + // Validate and parse stoppingEvent parameters: stoppingEventProviderName, stoppingEventEventName, stoppingEventPayloadFilter + + bool hasStoppingEventProviderName = !string.IsNullOrEmpty(stoppingEventProviderName); + bool hasStoppingEventEventName = !string.IsNullOrEmpty(stoppingEventEventName); + bool hasStoppingEventPayloadFilter = !string.IsNullOrEmpty(stoppingEventPayloadFilter); + if (!hasStoppingEventProviderName && (hasStoppingEventEventName || hasStoppingEventPayloadFilter)) + { + Console.Error.WriteLine($"`{nameof(stoppingEventProviderName)}` is required to stop tracing after a specific event for a particular `{nameof(stoppingEventEventName)}` event name or `{nameof(stoppingEventPayloadFilter)}` payload filter."); + return (int)ReturnCode.ArgumentError; + } + if (!hasStoppingEventEventName && hasStoppingEventPayloadFilter) + { + Console.Error.WriteLine($"`{nameof(stoppingEventEventName)}` is required to stop tracing after a specific event for a particular `{nameof(stoppingEventPayloadFilter)}` payload filter."); + return (int)ReturnCode.ArgumentError; + } + + Dictionary payloadFilter = new(); + if (hasStoppingEventPayloadFilter) + { + string[] payloadFieldNameValuePairs = stoppingEventPayloadFilter.Split(','); + foreach (string pair in payloadFieldNameValuePairs) + { + string[] payloadFieldNameValuePair = pair.Split(':'); + if (payloadFieldNameValuePair.Length != 2) + { + Console.Error.WriteLine($"`{nameof(stoppingEventPayloadFilter)}` does not have valid format. Ensure that it has `payload_field_name:payload_field_value` pairs separated by commas."); + return (int)ReturnCode.ArgumentError; + } + + payloadFilter[payloadFieldNameValuePair[0]] = payloadFieldNameValuePair[1]; + } + } + DiagnosticsClient diagnosticsClient; Process process = null; DiagnosticsClientBuilder builder = new("dotnet-trace", 10); @@ -220,7 +257,6 @@ private static async Task Collect(CancellationToken ct, IConsole console, i bool rundownRequested = false; System.Timers.Timer durationTimer = null; - using (VirtualTerminalMode vTermMode = printStatusOverTime ? VirtualTerminalMode.TryEnable() : null) { EventPipeSession session = null; @@ -281,7 +317,31 @@ private static async Task Collect(CancellationToken ct, IConsole console, i ConsoleWriteLine("\n\n"); FileInfo fileInfo = new(output.FullName); - Task copyTask = session.EventStream.CopyToAsync(fs); + EventMonitor eventMonitor = null; + Task copyTask = null; + if (hasStoppingEventProviderName) + { + eventMonitor = new( + stoppingEventProviderName, + stoppingEventEventName, + payloadFilter, + onEvent: (traceEvent) => + { + shouldExit.Set(); + }, + onPayloadFilterMismatch: (traceEvent) => + { + ConsoleWriteLine($"One or more field names specified in the payload filter for event '{traceEvent.ProviderName}/{traceEvent.EventName}' do not match any of the known field names: '{string.Join(' ', traceEvent.PayloadNames)}'. As a result the requested stopping event is unreachable; will continue to collect the trace for the remaining specified duration."); + }, + eventStream: new PassthroughStream(session.EventStream, fs, (int)buffersize, leaveDestinationStreamOpen: true), + callOnEventOnlyOnce: true); + + copyTask = eventMonitor.ProcessAsync(CancellationToken.None); + } + else + { + copyTask = session.EventStream.CopyToAsync(fs); + } Task shouldExitTask = copyTask.ContinueWith( (task) => shouldExit.Set(), CancellationToken.None, @@ -339,6 +399,11 @@ private static async Task Collect(CancellationToken ct, IConsole console, i { printStatus(); } while (!copyTask.Wait(100)); + + if (eventMonitor != null) + { + await eventMonitor.DisposeAsync().ConfigureAwait(false); + } } // At this point the copyTask will have finished, so wait on the shouldExitTask in case it threw // an exception or had some other interesting behavior @@ -455,7 +520,10 @@ public static Command CollectCommand() => CommonOptions.NameOption(), DiagnosticPortOption(), ShowChildIOOption(), - ResumeRuntimeOption() + ResumeRuntimeOption(), + StoppingEventProviderNameOption(), + StoppingEventEventNameOption(), + StoppingEventPayloadFilterOption() }; private static uint DefaultCircularBufferSizeInMB() => 256; @@ -546,5 +614,29 @@ private static Option ResumeRuntimeOption() => { Argument = new Argument(name: "resumeRuntime", getDefaultValue: () => true) }; + + private static Option StoppingEventProviderNameOption() => + new( + alias: "--stopping-event-provider-name", + description: @"A string, parsed as-is, that will stop the trace upon hitting an event with the matching provider name. For a more specific stopping event, additionally provide `--stopping-event-event-name` and/or `--stopping-event-payload-filter`.") + { + Argument = new Argument(name: "stoppingEventProviderName", getDefaultValue: () => null) + }; + + private static Option StoppingEventEventNameOption() => + new( + alias: "--stopping-event-event-name", + description: @"A string, parsed as-is, that will stop the trace upon hitting an event with the matching event name. Requires `--stopping-event-provider-name` to be set. For a more specific stopping event, additionally provide `--stopping-event-payload-filter`.") + { + Argument = new Argument(name: "stoppingEventEventName", getDefaultValue: () => null) + }; + + private static Option StoppingEventPayloadFilterOption() => + new( + alias: "--stopping-event-payload-filter", + description: @"A string, parsed as [payload_field_name]:[payload_field_value] pairs separated by commas, that will stop the trace upon hitting an event with a matching payload. Requires `--stopping-event-provider-name` and `--stopping-event-event-name` to be set.") + { + Argument = new Argument(name: "stoppingEventPayloadFilter", getDefaultValue: () => null) + }; } } diff --git a/src/Tools/dotnet-trace/dotnet-trace.csproj b/src/Tools/dotnet-trace/dotnet-trace.csproj index 1beb21c32d..a4ed81dde4 100644 --- a/src/Tools/dotnet-trace/dotnet-trace.csproj +++ b/src/Tools/dotnet-trace/dotnet-trace.csproj @@ -17,6 +17,7 @@ +