/// <summary> /// Collects a diagnostic trace from a currently running process. /// </summary> /// <param name="ct">The cancellation token</param> /// <param name="console"></param> /// <param name="processId">The process to collect the trace from.</param> /// <param name="name">The name of process to collect the trace from.</param> /// <param name="output">The output path for the collected trace data.</param> /// <param name="buffersize">Sets the size of the in-memory circular buffer in megabytes.</param> /// <param name="providers">A list of EventPipe providers to be enabled. This is in the form 'Provider[,Provider]', where Provider is in the form: 'KnownProviderName[:Flags[:Level][:KeyValueArgs]]', and KeyValueArgs is in the form: '[key1=value1][;key2=value2]'</param> /// <param name="profile">A named pre-defined set of provider configurations that allows common tracing scenarios to be specified succinctly.</param> /// <param name="format">The desired format of the created trace file.</param> /// <param name="duration">The duration of trace to be taken. </param> /// <param name="clrevents">A list of CLR events to be emitted.</param> /// <param name="clreventlevel">The verbosity level of CLR events</param> /// <returns></returns> private static async Task <int> 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) { try { Debug.Assert(output != null); Debug.Assert(profile != null); // Either processName or processId has to be specified. if (name != null) { if (processId != 0) { Console.WriteLine("Can only specify either --name or --process-id option."); return(ErrorCodes.ArgumentError); } processId = CommandUtils.FindProcessIdWithName(name); if (processId < 0) { return(ErrorCodes.ArgumentError); } } if (processId < 0) { Console.Error.WriteLine("Process ID should not be negative."); return(ErrorCodes.ArgumentError); } else if (processId == 0) { Console.Error.WriteLine("--process-id is required"); return(ErrorCodes.ArgumentError); } if (profile.Length == 0 && providers.Length == 0 && clrevents.Length == 0) { Console.Out.WriteLine("No profile or providers specified, defaulting to trace profile 'cpu-sampling'"); profile = "cpu-sampling"; } Dictionary <string, string> enabledBy = new Dictionary <string, string>(); var providerCollection = Extensions.ToProviders(providers); foreach (EventPipeProvider providerCollectionProvider in providerCollection) { enabledBy[providerCollectionProvider.Name] = "--providers "; } if (profile.Length != 0) { var selectedProfile = ListProfilesCommandHandler.DotNETRuntimeProfiles .FirstOrDefault(p => p.Name.Equals(profile, StringComparison.OrdinalIgnoreCase)); if (selectedProfile == null) { Console.Error.WriteLine($"Invalid profile name: {profile}"); return(ErrorCodes.ArgumentError); } Profile.MergeProfileAndProviders(selectedProfile, providerCollection, enabledBy); } // Parse --clrevents parameter if (clrevents.Length != 0) { // Ignore --clrevents if CLR event provider was already specified via --profile or --providers command. if (enabledBy.ContainsKey(Extensions.CLREventProviderName)) { Console.WriteLine($"The argument --clrevents {clrevents} will be ignored because the CLR provider was configured via either --profile or --providers command."); } else { var clrProvider = Extensions.ToCLREventPipeProvider(clrevents, clreventlevel); providerCollection.Add(clrProvider); enabledBy[Extensions.CLREventProviderName] = "--clrevents"; } } if (providerCollection.Count <= 0) { Console.Error.WriteLine("No providers were specified to start a trace."); return(ErrorCodes.ArgumentError); } PrintProviders(providerCollection, enabledBy); var process = Process.GetProcessById(processId); var shouldExit = new ManualResetEvent(false); var shouldStopAfterDuration = duration != default(TimeSpan); var rundownRequested = false; System.Timers.Timer durationTimer = null; ct.Register(() => shouldExit.Set()); var diagnosticsClient = new DiagnosticsClient(processId); using (VirtualTerminalMode vTermMode = VirtualTerminalMode.TryEnable()) { EventPipeSession session = null; try { session = diagnosticsClient.StartEventPipeSession(providerCollection, true, (int)buffersize); } catch (DiagnosticsClientException e) { Console.Error.WriteLine($"Unable to start a tracing session: {e.ToString()}"); } if (session == null) { Console.Error.WriteLine("Unable to create session."); return(ErrorCodes.SessionCreationError); } if (shouldStopAfterDuration) { durationTimer = new System.Timers.Timer(duration.TotalMilliseconds); durationTimer.Elapsed += (s, e) => shouldExit.Set(); durationTimer.AutoReset = false; } var stopwatch = new Stopwatch(); durationTimer?.Start(); stopwatch.Start(); LineRewriter rewriter = null; using (var fs = new FileStream(output.FullName, FileMode.Create, FileAccess.Write)) { Console.Out.WriteLine($"Process : {process.MainModule.FileName}"); Console.Out.WriteLine($"Output File : {fs.Name}"); if (shouldStopAfterDuration) { Console.Out.WriteLine($"Trace Duration : {duration.ToString(@"dd\:hh\:mm\:ss")}"); } Console.Out.WriteLine("\n\n"); var fileInfo = new FileInfo(output.FullName); Task copyTask = session.EventStream.CopyToAsync(fs); if (!Console.IsOutputRedirected) { rewriter = new LineRewriter { LineToClear = Console.CursorTop - 1 }; Console.CursorVisible = false; } Action printStatus = () => { if (!Console.IsOutputRedirected) { rewriter?.RewriteConsoleLine(); } fileInfo.Refresh(); Console.Out.WriteLine($"[{stopwatch.Elapsed.ToString(@"dd\:hh\:mm\:ss")}]\tRecording trace {GetSize(fileInfo.Length)}"); Console.Out.WriteLine("Press <Enter> or <Ctrl+C> to exit..."); if (rundownRequested) { Console.Out.WriteLine("Stopping the trace. This may take up to minutes depending on the application being traced."); } }; while (!shouldExit.WaitOne(100) && !(!Console.IsInputRedirected && Console.KeyAvailable && Console.ReadKey(true).Key == ConsoleKey.Enter)) { printStatus(); } // Behavior concerning Enter moving text in the terminal buffer when at the bottom of the buffer // is different between Console/Terminals on Windows and Mac/Linux if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !Console.IsOutputRedirected && rewriter != null && Math.Abs(Console.CursorTop - Console.BufferHeight) == 1) { rewriter.LineToClear--; } durationTimer?.Stop(); rundownRequested = true; session.Stop(); do { printStatus(); } while (!copyTask.Wait(100)); } Console.Out.WriteLine("\nTrace completed."); if (format != TraceFileFormat.NetTrace) { TraceFileFormatConverter.ConvertToFormat(format, output.FullName); } } } catch (Exception ex) { Console.Error.WriteLine($"[ERROR] {ex.ToString()}"); return(ErrorCodes.TracingError); } finally { if (console.GetTerminal() != null) { Console.CursorVisible = true; } } return(await Task.FromResult(0)); }
/// <summary> /// Collects a diagnostic trace from a currently running process or launch a child process and trace it. /// Append -- to the collect command to instruct the tool to run a command and trace it immediately. By default the IO from this process is hidden, but the --show-child-io option may be used to show the child process IO. /// </summary> /// <param name="ct">The cancellation token</param> /// <param name="console"></param> /// <param name="processId">The process to collect the trace from.</param> /// <param name="name">The name of process to collect the trace from.</param> /// <param name="output">The output path for the collected trace data.</param> /// <param name="buffersize">Sets the size of the in-memory circular buffer in megabytes.</param> /// <param name="providers">A list of EventPipe providers to be enabled. This is in the form 'Provider[,Provider]', where Provider is in the form: 'KnownProviderName[:Flags[:Level][:KeyValueArgs]]', and KeyValueArgs is in the form: '[key1=value1][;key2=value2]'</param> /// <param name="profile">A named pre-defined set of provider configurations that allows common tracing scenarios to be specified succinctly.</param> /// <param name="format">The desired format of the created trace file.</param> /// <param name="duration">The duration of trace to be taken. </param> /// <param name="clrevents">A list of CLR events to be emitted.</param> /// <param name="clreventlevel">The verbosity level of CLR events</param> /// <param name="port">Path to the diagnostic port to be created.</param> /// <param name="showchildio">Should IO from a child process be hidden.</param> /// <returns></returns> private static async Task <int> 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) { int ret = 0; bool collectionStopped = false; bool cancelOnEnter = true; bool cancelOnCtrlC = true; bool printStatusOverTime = true; try { Debug.Assert(output != null); Debug.Assert(profile != null); if (ProcessLauncher.Launcher.HasChildProc && showchildio) { // If showing IO, then all IO (including CtrlC) behavior is delegated to the child process cancelOnCtrlC = false; cancelOnEnter = false; printStatusOverTime = false; } else { cancelOnCtrlC = true; cancelOnEnter = !Console.IsInputRedirected; printStatusOverTime = !Console.IsInputRedirected; } if (!cancelOnCtrlC) { ct = CancellationToken.None; } if (!ProcessLauncher.Launcher.HasChildProc) { if (showchildio) { Console.WriteLine("--show-child-io must not be specified when attaching to a process"); return(ErrorCodes.ArgumentError); } if (CommandUtils.ValidateArgumentsForAttach(processId, name, diagnosticPort, out int resolvedProcessId)) { processId = resolvedProcessId; } else { return(ErrorCodes.ArgumentError); } } else if (!CommandUtils.ValidateArgumentsForChildProcess(processId, name, diagnosticPort)) { return(ErrorCodes.ArgumentError); } if (profile.Length == 0 && providers.Length == 0 && clrevents.Length == 0) { Console.Out.WriteLine("No profile or providers specified, defaulting to trace profile 'cpu-sampling'"); profile = "cpu-sampling"; } Dictionary <string, string> enabledBy = new Dictionary <string, string>(); var providerCollection = Extensions.ToProviders(providers); foreach (EventPipeProvider providerCollectionProvider in providerCollection) { enabledBy[providerCollectionProvider.Name] = "--providers "; } if (profile.Length != 0) { var selectedProfile = ListProfilesCommandHandler.DotNETRuntimeProfiles .FirstOrDefault(p => p.Name.Equals(profile, StringComparison.OrdinalIgnoreCase)); if (selectedProfile == null) { Console.Error.WriteLine($"Invalid profile name: {profile}"); return(ErrorCodes.ArgumentError); } Profile.MergeProfileAndProviders(selectedProfile, providerCollection, enabledBy); } // Parse --clrevents parameter if (clrevents.Length != 0) { // Ignore --clrevents if CLR event provider was already specified via --profile or --providers command. if (enabledBy.ContainsKey(Extensions.CLREventProviderName)) { Console.WriteLine($"The argument --clrevents {clrevents} will be ignored because the CLR provider was configured via either --profile or --providers command."); } else { var clrProvider = Extensions.ToCLREventPipeProvider(clrevents, clreventlevel); providerCollection.Add(clrProvider); enabledBy[Extensions.CLREventProviderName] = "--clrevents"; } } if (providerCollection.Count <= 0) { Console.Error.WriteLine("No providers were specified to start a trace."); return(ErrorCodes.ArgumentError); } PrintProviders(providerCollection, enabledBy); DiagnosticsClient diagnosticsClient; Process process; DiagnosticsClientBuilder builder = new DiagnosticsClientBuilder("dotnet-trace", 10); bool shouldResumeRuntime = ProcessLauncher.Launcher.HasChildProc || !string.IsNullOrEmpty(diagnosticPort); var shouldExit = new ManualResetEvent(false); ct.Register(() => shouldExit.Set()); using (DiagnosticsClientHolder holder = await builder.Build(ct, processId, diagnosticPort, showChildIO: showchildio, printLaunchCommand: true)) { // if builder returned null, it means we received ctrl+C while waiting for clients to connect. Exit gracefully. if (holder == null) { return(await Task.FromResult(ret)); } diagnosticsClient = holder.Client; if (shouldResumeRuntime) { process = Process.GetProcessById(holder.EndpointInfo.ProcessId); } else { process = Process.GetProcessById(processId); } string processMainModuleFileName = ""; // Reading the process MainModule filename can fail if the target process closes // or isn't fully setup. Retry a few times to attempt to address the issue for (int attempts = 0; true; attempts++) { try { processMainModuleFileName = process.MainModule.FileName; break; } catch { if (attempts > 10) { Console.Error.WriteLine("Unable to examine process."); return(ErrorCodes.SessionCreationError); } Thread.Sleep(200); } } if (String.Equals(output.Name, DefaultTraceName, StringComparison.OrdinalIgnoreCase)) { DateTime now = DateTime.Now; var processMainModuleFileInfo = new FileInfo(processMainModuleFileName); output = new FileInfo($"{processMainModuleFileInfo.Name}_{now:yyyyMMdd}_{now:HHmmss}.nettrace"); } var shouldStopAfterDuration = duration != default(TimeSpan); var rundownRequested = false; System.Timers.Timer durationTimer = null; using (VirtualTerminalMode vTermMode = printStatusOverTime ? VirtualTerminalMode.TryEnable() : null) { EventPipeSession session = null; try { session = diagnosticsClient.StartEventPipeSession(providerCollection, true, (int)buffersize); if (shouldResumeRuntime) { diagnosticsClient.ResumeRuntime(); } } catch (DiagnosticsClientException e) { Console.Error.WriteLine($"Unable to start a tracing session: {e.ToString()}"); } if (session == null) { Console.Error.WriteLine("Unable to create session."); return(ErrorCodes.SessionCreationError); } if (shouldStopAfterDuration) { durationTimer = new System.Timers.Timer(duration.TotalMilliseconds); durationTimer.Elapsed += (s, e) => shouldExit.Set(); durationTimer.AutoReset = false; } var stopwatch = new Stopwatch(); durationTimer?.Start(); stopwatch.Start(); LineRewriter rewriter = null; using (var fs = new FileStream(output.FullName, FileMode.Create, FileAccess.Write)) { Console.Out.WriteLine($"Process : {processMainModuleFileName}"); Console.Out.WriteLine($"Output File : {fs.Name}"); if (shouldStopAfterDuration) { Console.Out.WriteLine($"Trace Duration : {duration.ToString(@"dd\:hh\:mm\:ss")}"); } Console.Out.WriteLine("\n\n"); var fileInfo = new FileInfo(output.FullName); Task copyTask = session.EventStream.CopyToAsync(fs); Task shouldExitTask = copyTask.ContinueWith((task) => shouldExit.Set()); if (printStatusOverTime) { rewriter = new LineRewriter { LineToClear = Console.CursorTop - 1 }; Console.CursorVisible = false; } Action printStatus = () => { if (printStatusOverTime) { rewriter?.RewriteConsoleLine(); fileInfo.Refresh(); Console.Out.WriteLine($"[{stopwatch.Elapsed.ToString(@"dd\:hh\:mm\:ss")}]\tRecording trace {GetSize(fileInfo.Length)}"); Console.Out.WriteLine("Press <Enter> or <Ctrl+C> to exit..."); } if (rundownRequested) { Console.Out.WriteLine("Stopping the trace. This may take up to minutes depending on the application being traced."); } }; while (!shouldExit.WaitOne(100) && !(cancelOnEnter && Console.KeyAvailable && Console.ReadKey(true).Key == ConsoleKey.Enter)) { printStatus(); } // if the CopyToAsync ended early (target program exited, etc.), the we don't need to stop the session. if (!copyTask.Wait(0)) { // Behavior concerning Enter moving text in the terminal buffer when at the bottom of the buffer // is different between Console/Terminals on Windows and Mac/Linux if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && printStatusOverTime && rewriter != null && Math.Abs(Console.CursorTop - Console.BufferHeight) == 1) { rewriter.LineToClear--; } collectionStopped = true; durationTimer?.Stop(); rundownRequested = true; session.Stop(); do { printStatus(); } while (!copyTask.Wait(100)); } // 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 shouldExitTask.Wait(); } Console.Out.WriteLine($"\nTrace completed."); if (format != TraceFileFormat.NetTrace) { TraceFileFormatConverter.ConvertToFormat(format, output.FullName); } } if (!collectionStopped && !ct.IsCancellationRequested) { // If the process is shutting down by itself print the return code from the process. // Capture this before leaving the using, as the Dispose of the DiagnosticsClientHolder // may terminate the target process causing it to have the wrong error code if (ProcessLauncher.Launcher.HasChildProc && ProcessLauncher.Launcher.ChildProc.WaitForExit(5000)) { ret = ProcessLauncher.Launcher.ChildProc.ExitCode; Console.WriteLine($"Process exited with code '{ret}'."); collectionStopped = true; } } } } catch (Exception ex) { Console.Error.WriteLine($"[ERROR] {ex.ToString()}"); ret = ErrorCodes.TracingError; collectionStopped = true; } finally { if (printStatusOverTime) { if (console.GetTerminal() != null) { Console.CursorVisible = true; } } if (ProcessLauncher.Launcher.HasChildProc) { if (!collectionStopped || ct.IsCancellationRequested) { ret = ErrorCodes.TracingError; } // If we launched a child proc that hasn't exited yet, terminate it before we exit. if (!ProcessLauncher.Launcher.ChildProc.HasExited) { ProcessLauncher.Launcher.ChildProc.Kill(); } } } return(await Task.FromResult(ret)); }