public async Task <DiagnosticsClientHolder> Build(CancellationToken ct, int processId, string diagnosticPort, bool showChildIO, bool printLaunchCommand)
        {
            IpcEndpointConfig portConfig = null;

            if (!string.IsNullOrEmpty(diagnosticPort))
            {
                portConfig = IpcEndpointConfig.Parse(diagnosticPort);
            }

            if (ProcessLauncher.Launcher.HasChildProc)
            {
                // Create and start the reversed server
                string diagnosticTransportName   = GetTransportName(_toolName);
                ReversedDiagnosticsServer server = new ReversedDiagnosticsServer(diagnosticTransportName);
                server.Start();

                // Start the child proc
                if (!ProcessLauncher.Launcher.Start(diagnosticTransportName, ct, showChildIO, printLaunchCommand))
                {
                    throw new InvalidOperationException($"Failed to start '{ProcessLauncher.Launcher.ChildProc.StartInfo.FileName} {ProcessLauncher.Launcher.ChildProc.StartInfo.Arguments}'.");
                }
                IpcEndpointInfo endpointInfo;
                try
                {
                    // Wait for attach
                    endpointInfo = server.Accept(TimeSpan.FromSeconds(_timeoutInSec));

                    // If for some reason a different process attached to us, wait until the expected process attaches.
                    while (endpointInfo.ProcessId != ProcessLauncher.Launcher.ChildProc.Id)
                    {
                        endpointInfo = server.Accept(TimeSpan.FromSeconds(_timeoutInSec));
                    }
                }
                catch (TimeoutException)
                {
                    Console.Error.WriteLine("Unable to start tracing session - the target app failed to connect to the diagnostics port. This may happen if the target application is running .NET Core 3.1 or older versions. Attaching at startup is only available from .NET 5.0 or later.");
                    throw;
                }
                return(new DiagnosticsClientHolder(new DiagnosticsClient(endpointInfo.Endpoint), endpointInfo, server));
            }
            else if (portConfig != null && portConfig.IsListenConfig)
            {
                string fullPort = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? portConfig.Address : Path.GetFullPath(portConfig.Address);
                ReversedDiagnosticsServer server = new ReversedDiagnosticsServer(fullPort);
                server.Start();
                Console.WriteLine($"Waiting for connection on {fullPort}");
                Console.WriteLine($"Start an application with the following environment variable: DOTNET_DiagnosticPorts={fullPort}");

                try
                {
                    IpcEndpointInfo endpointInfo = await server.AcceptAsync(ct);

                    return(new DiagnosticsClientHolder(new DiagnosticsClient(endpointInfo.Endpoint), endpointInfo, fullPort, server));
                }
                catch (TaskCanceledException)
                {
                    if (!ct.IsCancellationRequested)
                    {
                        throw;
                    }
                    return(null);
                }
            }
            else if (portConfig != null && portConfig.IsConnectConfig)
            {
                return(new DiagnosticsClientHolder(new DiagnosticsClient(portConfig)));
            }
            else
            {
                return(new DiagnosticsClientHolder(new DiagnosticsClient(processId)));
            }
        }
Exemple #2
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="diagnosticPort">Path to the diagnostic port to be used.</param>
        /// <param name="showchildio">Should IO from a child process be hidden.</param>
        /// <param name="resumeRuntime">Resume runtime once session has been initialized.</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, bool resumeRuntime)
        {
            bool collectionStopped   = false;
            bool cancelOnEnter       = true;
            bool cancelOnCtrlC       = true;
            bool printStatusOverTime = true;
            int  ret = ReturnCode.Ok;

            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.IsOutputRedirected;
                }

                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(ReturnCode.ArgumentError);
                    }
                    if (CommandUtils.ValidateArgumentsForAttach(processId, name, diagnosticPort, out int resolvedProcessId))
                    {
                        processId = resolvedProcessId;
                    }
                    else
                    {
                        return(ReturnCode.ArgumentError);
                    }
                }
                else if (!CommandUtils.ValidateArgumentsForChildProcess(processId, name, diagnosticPort))
                {
                    return(ReturnCode.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(ReturnCode.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(ReturnCode.ArgumentError);
                }

                PrintProviders(providerCollection, enabledBy);

                DiagnosticsClient        diagnosticsClient;
                Process                  process = null;
                DiagnosticsClientBuilder builder = new DiagnosticsClientBuilder("dotnet-trace", 10);
                var shouldExit = new ManualResetEvent(false);
                ct.Register(() => shouldExit.Set());

                using (DiagnosticsClientHolder holder = await builder.Build(ct, processId, diagnosticPort, showChildIO: showchildio, printLaunchCommand: true))
                {
                    string processMainModuleFileName = "";

                    // 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(ReturnCode.Ok));
                    }
                    diagnosticsClient = holder.Client;
                    if (ProcessLauncher.Launcher.HasChildProc)
                    {
                        process = Process.GetProcessById(holder.EndpointInfo.ProcessId);
                    }
                    else if (IpcEndpointConfig.TryParse(diagnosticPort, out IpcEndpointConfig portConfig) && (portConfig.IsConnectConfig || portConfig.IsListenConfig))
                    {
                        // No information regarding process (could even be a routed process),
                        // use "file" part of IPC channel name as process main module file name.
                        processMainModuleFileName = Path.GetFileName(portConfig.Address);
                    }
                    else
                    {
                        process = Process.GetProcessById(processId);
                    }

                    if (process != null)
                    {
                        // 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(ReturnCode.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 (resumeRuntime)
                            {
                                try
                                {
                                    diagnosticsClient.ResumeRuntime();
                                }
                                catch (UnsupportedCommandException)
                                {
                                    // Noop if command is unsupported, since the target is most likely a 3.1 app.
                                }
                            }
                        }
                        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(ReturnCode.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 several 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()}");
                collectionStopped = true;
                ret = ReturnCode.TracingError;
            }
            finally
            {
                if (printStatusOverTime)
                {
                    if (console.GetTerminal() != null)
                    {
                        Console.CursorVisible = true;
                    }
                }

                if (ProcessLauncher.Launcher.HasChildProc)
                {
                    if (!collectionStopped || ct.IsCancellationRequested)
                    {
                        ret = ReturnCode.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));
        }