* Fix deadlock when redirecting large amounts of child process output, and add ability to not redirect child process behavior
- Use a small set of tasks to consume and ignore arbitrary data passed through standard output/error
- Add a new switch to dotnet-trace called --redirect-child-output which can be set to false to allow viewing data. (By default the option defaults to true, which may be confusing. I'd like some feedback on that.
* Adjust dotnet-trace to add --show-child-output and improve exit code handling
* Add testing
* Update doc in repo
* Fix issues identified in testing
* Move logic which waited for process to exit to avoid premature Dispose operations
* Code review feedback
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.DebugServices.UnitTests", "src\tests\Microsoft.Diagnostics.DebugServices.UnitTests\Microsoft.Diagnostics.DebugServices.UnitTests.csproj", "{064BC7DD-D44C-400E-9215-7546E092AB98}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExitCodeTracee", "src\tests\ExitCodeTracee\ExitCodeTracee.csproj", "{61F73DD0-F346-4D7A-AB12-63415B4EEEE1}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Checked|Any CPU = Checked|Any CPU
{064BC7DD-D44C-400E-9215-7546E092AB98}.RelWithDebInfo|x64.Build.0 = Release|Any CPU
{064BC7DD-D44C-400E-9215-7546E092AB98}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU
{064BC7DD-D44C-400E-9215-7546E092AB98}.RelWithDebInfo|x86.Build.0 = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Checked|Any CPU.ActiveCfg = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Checked|Any CPU.Build.0 = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Checked|ARM.ActiveCfg = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Checked|ARM.Build.0 = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Checked|ARM64.ActiveCfg = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Checked|ARM64.Build.0 = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Checked|x64.ActiveCfg = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Checked|x64.Build.0 = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Checked|x86.ActiveCfg = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Checked|x86.Build.0 = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Debug|ARM.Build.0 = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Debug|x64.Build.0 = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Debug|x86.Build.0 = Debug|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Release|ARM.ActiveCfg = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Release|ARM.Build.0 = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Release|ARM64.Build.0 = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Release|x64.ActiveCfg = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Release|x64.Build.0 = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Release|x86.ActiveCfg = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.Release|x86.Build.0 = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.RelWithDebInfo|ARM.ActiveCfg = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.RelWithDebInfo|ARM.Build.0 = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.RelWithDebInfo|ARM64.ActiveCfg = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.RelWithDebInfo|ARM64.Build.0 = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.RelWithDebInfo|x64.Build.0 = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.RelWithDebInfo|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
{EEC90A42-CDCD-4EE3-B47D-C109D604E7E2} = {41638A4C-0DAF-47ED-A774-ECBBAC0315D7}
{5FC66A16-41E9-4D22-A44C-FEBB7DCCAAF8} = {19FAB78C-3351-4911-8F0C-8C6056401740}
{064BC7DD-D44C-400E-9215-7546E092AB98} = {03479E19-3F18-49A6-910A-F5041E27E7C0}
+ {61F73DD0-F346-4D7A-AB12-63415B4EEEE1} = {03479E19-3F18-49A6-910A-F5041E27E7C0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {46465737-C938-44FC-BE1A-4CE139EBB5E0}
[--providers <list-of-comma-separated-providers>]
[--format <trace-file-format>]
- Collects a diagnostic trace from a currently running process
+ 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.
-p, --process-id
The process to collect the trace from
--format
The format of the output trace file. The default value is nettrace.
+ --show-child-io
+ Shows the input and output streams of a launched child process in the current console.
Examples:
You can stop collecting the trace by pressing `<Enter>` or `<Ctrl + C>` key. Doing this will also exit `hello.exe`.
### NOTE
-* Launching `hello.exe` via dotnet-trace will make its input/output to be redirected and you won't be able to interact with its stdin/stdout.
+* Launching `hello.exe` via dotnet-trace will redirect its input/output and you will not be able to interact with it on the console by default. Use the --show-child-io switch to interact with its stdin/stdout.
* Exiting the tool via CTRL+C or SIGTERM will safely end both the tool and the child process.
}
/// <summary>
- /// A helper method for validating --process-id, --name, --diagnostic-port options for collect and monitor commands.
+ /// A helper method for validating --process-id, --name, --diagnostic-port options for collect with child process commands.
+ /// None of these options can be specified, so it checks for them and prints the appropriate error message.
+ /// </summary>
+ /// <param name="processId">process ID</param>
+ /// <param name="name">name</param>
+ /// <param name="port">port</param>
+ /// <returns></returns>
+ public static bool ValidateArgumentsForChildProcess(int processId, string name, string port)
+ {
+ if (processId != 0 && name != null && !string.IsNullOrEmpty(port))
+ {
+ Console.WriteLine("None of the --name, --process-id, or --diagnostic-port options may be specified when launching a child process.");
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// A helper method for validating --process-id, --name, --diagnostic-port options for collect commands.
/// Only one of these options can be specified, so it checks for duplicate options specified and if there is
/// such duplication, it prints the appropriate error message.
/// </summary>
/// <param name="processId">process ID</param>
/// <param name="name">name</param>
/// <param name="port">port</param>
+ /// <param name="resolvedProcessId">resolvedProcessId</param>
/// <returns></returns>
- public static bool ValidateArguments(int processId, string name, string port, out int resolvedProcessId)
+ public static bool ValidateArgumentsForAttach(int processId, string name, string port, out int resolvedProcessId)
{
resolvedProcessId = -1;
if (processId == 0 && name == null && string.IsNullOrEmpty(port))
return false;
}
}
+ else if (processId == 0)
+ {
+ Console.WriteLine("One of the --name, --process-id, or --diagnostic-port options must be specified when attaching to a process.");
+ return false;
+ }
resolvedProcessId = processId;
return true;
}
return _childProc;
}
}
- public bool Start(string diagnosticTransportName, CancellationToken ct)
+ public bool Start( string diagnosticTransportName, CancellationToken ct, bool showChildIO, bool printLaunchCommand)
{
_childProc.StartInfo.UseShellExecute = false;
- _childProc.StartInfo.RedirectStandardOutput = true;
- _childProc.StartInfo.RedirectStandardError = true;
- _childProc.StartInfo.RedirectStandardInput = true;
+ _childProc.StartInfo.RedirectStandardOutput = !showChildIO;
+ _childProc.StartInfo.RedirectStandardError = !showChildIO;
+ _childProc.StartInfo.RedirectStandardInput = !showChildIO;
_childProc.StartInfo.Environment.Add("DOTNET_DiagnosticPorts", $"{diagnosticTransportName}");
try
{
+ if (printLaunchCommand)
+ {
+ Console.WriteLine($"Launching: {_childProc.StartInfo.FileName} {_childProc.StartInfo.Arguments}");
+ }
_childProc.Start();
}
catch (Exception e)
Console.WriteLine(e.ToString());
return false;
}
- _stdOutTask = ReadAndIgnoreAllStreamAsync(_childProc.StandardOutput, ct);
- _stdErrTask = ReadAndIgnoreAllStreamAsync(_childProc.StandardError, ct);
+ if (!showChildIO)
+ {
+ _stdOutTask = ReadAndIgnoreAllStreamAsync(_childProc.StandardOutput, ct);
+ _stdErrTask = ReadAndIgnoreAllStreamAsync(_childProc.StandardError, ct);
+ }
return true;
}
_timeoutInSec = timeoutInSec;
}
- public async Task<DiagnosticsClientHolder> Build(CancellationToken ct, int processId, string portName)
+ public async Task<DiagnosticsClientHolder> Build(CancellationToken ct, int processId, string portName, bool showChildIO, bool printLaunchCommand)
{
if (ProcessLauncher.Launcher.HasChildProc)
{
server.Start();
// Start the child proc
- if (!ProcessLauncher.Launcher.Start(diagnosticTransportName, ct))
+ if (!ProcessLauncher.Launcher.Start(diagnosticTransportName, ct, showChildIO, printLaunchCommand))
{
- throw new InvalidOperationException($"Failed to start {ProcessLauncher.Launcher.ChildProc.ProcessName}.");
+ throw new InvalidOperationException($"Failed to start '{ProcessLauncher.Launcher.ChildProc.StartInfo.FileName} {ProcessLauncher.Launcher.ChildProc.StartInfo.Arguments}'.");
}
IpcEndpointInfo endpointInfo;
try
public async Task<int> Monitor(CancellationToken ct, List<string> counter_list, string counters, IConsole console, int processId, int refreshInterval, string name, string diagnosticPort)
{
- if (!ProcessLauncher.Launcher.HasChildProc && !CommandUtils.ValidateArguments(processId, name, diagnosticPort, out _processId))
+ if (!ProcessLauncher.Launcher.HasChildProc && !CommandUtils.ValidateArgumentsForAttach(processId, name, diagnosticPort, out _processId))
{
return 0;
}
DiagnosticsClientBuilder builder = new DiagnosticsClientBuilder("dotnet-counters", 10);
- using (DiagnosticsClientHolder holder = await builder.Build(ct, _processId, diagnosticPort))
+ using (DiagnosticsClientHolder holder = await builder.Build(ct, _processId, diagnosticPort, showChildIO: false, printLaunchCommand: false))
{
try
{
public async Task<int> Collect(CancellationToken ct, List<string> counter_list, string counters, IConsole console, int processId, int refreshInterval, CountersExportFormat format, string output, string name, string diagnosticPort)
{
- if (!ProcessLauncher.Launcher.HasChildProc && !CommandUtils.ValidateArguments(processId, name, diagnosticPort, out _processId))
+ if (!ProcessLauncher.Launcher.HasChildProc && !CommandUtils.ValidateArgumentsForAttach(processId, name, diagnosticPort, out _processId))
{
return 0;
}
DiagnosticsClientBuilder builder = new DiagnosticsClientBuilder("dotnet-counters", 10);
- using (DiagnosticsClientHolder holder = await builder.Build(ct, _processId, diagnosticPort))
+ using (DiagnosticsClientHolder holder = await builder.Build(ct, _processId, diagnosticPort, showChildIO: false, printLaunchCommand: false))
{
try
{
{
internal static class CollectCommandHandler
{
- delegate Task<int> 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);
+ delegate Task<int> 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);
/// <summary>
- /// Collects a diagnostic trace from a currently running process.
+ /// 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="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)
+ 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 (CommandUtils.ValidateArguments(processId, name, diagnosticPort, out int resolvedProcessId))
+ 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;
}
return ErrorCodes.ArgumentError;
}
}
+ else if (!CommandUtils.ValidateArgumentsForChildProcess(processId, name, diagnosticPort))
+ {
+ return ErrorCodes.ArgumentError;
+ }
if (profile.Length == 0 && providers.Length == 0 && clrevents.Length == 0)
{
DiagnosticsClientBuilder builder = new DiagnosticsClientBuilder("dotnet-trace", 10);
bool shouldResumeRuntime = ProcessLauncher.Launcher.HasChildProc || !string.IsNullOrEmpty(diagnosticPort);
- using (DiagnosticsClientHolder holder = await builder.Build(ct, processId, diagnosticPort))
+ using (DiagnosticsClientHolder holder = await builder.Build(ct, processId, diagnosticPort, showChildIO: showchildio, printLaunchCommand: true))
{
diagnosticsClient = holder.Client;
if (shouldResumeRuntime)
ct.Register(() => shouldExit.Set());
- using (VirtualTerminalMode vTermMode = VirtualTerminalMode.TryEnable())
+ using (VirtualTerminalMode vTermMode = printStatusOverTime ? VirtualTerminalMode.TryEnable() : null)
{
EventPipeSession session = null;
try
Console.Out.WriteLine("\n\n");
var fileInfo = new FileInfo(output.FullName);
- Task copyTask = session.EventStream.CopyToAsync(fs).ContinueWith((task) => shouldExit.Set());
+ Task copyTask = session.EventStream.CopyToAsync(fs);
+ Task shouldExitTask = copyTask.ContinueWith((task) => shouldExit.Set());
- if (!Console.IsOutputRedirected)
+ if (printStatusOverTime)
{
rewriter = new LineRewriter { LineToClear = Console.CursorTop - 1 };
Console.CursorVisible = false;
Action printStatus = () =>
{
- if (!Console.IsOutputRedirected)
+ if (printStatusOverTime)
{
rewriter?.RewriteConsoleLine();
fileInfo.Refresh();
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))
+ 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.
// 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 &&
+ printStatusOverTime &&
rewriter != null &&
Math.Abs(Console.CursorTop - Console.BufferHeight) == 1)
{
rewriter.LineToClear--;
}
+ collectionStopped = true;
durationTimer?.Stop();
rundownRequested = true;
session.Stop();
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.");
+ 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.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 (console.GetTerminal() != null)
- Console.CursorVisible = true;
+ if (printStatusOverTime)
+ {
+ if (console.GetTerminal() != null)
+ Console.CursorVisible = true;
+ }
- // If we launched a child proc that hasn't exited yet, terminate it before we exit.
- if (ProcessLauncher.Launcher.HasChildProc && !ProcessLauncher.Launcher.ChildProc.HasExited)
+ if (ProcessLauncher.Launcher.HasChildProc)
{
- ProcessLauncher.Launcher.ChildProc.Kill();
+ 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);
public static Command CollectCommand() =>
new Command(
name: "collect",
- description: "Collects a diagnostic trace from a currently running process")
+ description: "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. When tracing a child process, the exit code of dotnet-trace shall be that of the traced process unless the trace process encounters an error.")
{
// Handler
HandlerDescriptor.FromDelegate((CollectDelegate)Collect).GetCommandHandler(),
CLREventLevelOption(),
CommonOptions.NameOption(),
DiagnosticPortOption(),
+ ShowChildIOOption()
};
private static uint DefaultCircularBufferSizeInMB() => 256;
{
Argument = new Argument<string>(name: "diagnosticPort", getDefaultValue: () => string.Empty)
};
+ private static Option ShowChildIOOption() =>
+ new Option(
+ alias: "--show-child-io",
+ description: @"Shows the input and output streams of a launched child process in the current console.")
+ {
+ Argument = new Argument<bool>(name: "show-child-io", getDefaultValue: () => false)
+ };
}
}
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework Condition="'$(BuildProjectFramework)' != ''">$(BuildProjectFramework)</TargetFramework>
+ <TargetFrameworks Condition="'$(BuildProjectFramework)' == ''">netcoreapp3.1;net5.0</TargetFrameworks>
+ </PropertyGroup>
+</Project>
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace Tracee
+{
+ class Program
+ {
+ static int Main(string[] args)
+ {
+ foreach (string s in args)
+ Console.WriteLine(s);
+
+ return Int32.Parse(args[0]);
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Diagnostics.NETCore.Client;
+using System;
+using Xunit;
+using Xunit.Abstractions;
+using System.Collections.Generic;
+using System.Linq;
+using System.Diagnostics;
+
+namespace Microsoft.Diagnostics.Tools.Trace
+{
+
+ public class ChildProcessTests
+ {
+ // Pass ITestOutputHelper into the test class, which xunit provides per-test
+ public ChildProcessTests(ITestOutputHelper outputHelper)
+ {
+ OutputHelper = outputHelper;
+ }
+
+ private ITestOutputHelper OutputHelper { get; }
+
+ private void LaunchDotNetTrace(string command, out int exitCode, out string stdOut, out string stdErr)
+ {
+ string dotnetTracePathWithArgs = CommonHelper.GetTraceePathWithArgs(traceeName: "dotnet-trace").Replace("net5.0", "netcoreapp2.1");
+ ProcessStartInfo startInfo = new ProcessStartInfo(CommonHelper.HostExe, $"{dotnetTracePathWithArgs} {command}");
+
+ OutputHelper.WriteLine($"Launching: {startInfo.FileName} {startInfo.Arguments}");
+ startInfo.RedirectStandardInput = true;
+ startInfo.RedirectStandardError = true;
+ startInfo.RedirectStandardOutput = true;
+
+ using (Process process = Process.Start(startInfo))
+ {
+ const int processTimeout = 15000;
+ bool processExitedCleanly = process.WaitForExit(processTimeout);
+ if (!processExitedCleanly)
+ {
+ OutputHelper.WriteLine($"Forced kill of process after {processTimeout}ms");
+ process.Kill();
+ }
+
+ OutputHelper.WriteLine("StdErr");
+ stdErr = process.StandardError.ReadToEnd();
+ OutputHelper.WriteLine(stdErr);
+ OutputHelper.WriteLine("StdOut");
+ stdOut = process.StandardOutput.ReadToEnd();
+ OutputHelper.WriteLine(stdOut);
+
+ Assert.True(processExitedCleanly, "Launched process failed to exit");
+ exitCode = process.ExitCode;
+ }
+ }
+
+ [Theory]
+ [InlineData("232", 232)]
+ [InlineData("0", 0)]
+ public void VerifyExitCode(string commandLineArg, int exitCode)
+ {
+ string exitCodeTraceePath = CommonHelper.GetTraceePathWithArgs(traceeName: "ExitCodeTracee", targetFramework: "net5.0");
+
+ LaunchDotNetTrace($"collect -o verifyexitcode.nettrace -- {CommonHelper.HostExe} {exitCodeTraceePath} {commandLineArg}", out int dotnetTraceExitCode, out string stdOut, out string stdErr);
+ Assert.Equal(exitCode, dotnetTraceExitCode);
+
+ Assert.Contains($"Process exited with code '{exitCode}'.", stdOut);
+ }
+
+ [Theory]
+ [InlineData("0 this is a message", new string[] { "\nthis\n", "\nis\n", "\na\n" })]
+ public void VerifyHideIO(string commandLineArg, string[] stringsInOutput)
+ {
+ string exitCodeTraceePath = CommonHelper.GetTraceePathWithArgs(traceeName: "ExitCodeTracee", targetFramework: "net5.0");
+
+ LaunchDotNetTrace($"collect -o VerifyHideIO.nettrace -- {CommonHelper.HostExe} {exitCodeTraceePath} {commandLineArg}", out int dotnetTraceExitCode, out string stdOut, out string stdErr);
+ Assert.Equal(0, dotnetTraceExitCode);
+ stdOut = stdOut.Replace("\r", "");
+
+ foreach (string s in stringsInOutput)
+ Assert.DoesNotContain(s, stdOut);
+ }
+
+ [Theory]
+ [InlineData("0 this is a message", new string[] { "\nthis\n", "\nis\n", "\na\n" })]
+ public void VerifyShowIO(string commandLineArg, string[] stringsInOutput)
+ {
+ string exitCodeTraceePath = CommonHelper.GetTraceePathWithArgs(traceeName: "ExitCodeTracee", targetFramework: "net5.0");
+
+ LaunchDotNetTrace($"collect -o VerifyShowIO.nettrace --show-child-io -- {CommonHelper.HostExe} {exitCodeTraceePath} {commandLineArg}", out int dotnetTraceExitCode, out string stdOut, out string stdErr);
+ Assert.Equal(0, dotnetTraceExitCode);
+ stdOut = stdOut.Replace("\r", "");
+
+ foreach (string s in stringsInOutput)
+ Assert.Contains(s, stdOut);
+ }
+ }
+}
<ItemGroup>
<ProjectReference Include="../../Tools/dotnet-trace/dotnet-trace.csproj" />
+ <ProjectReference Include="..\ExitCodeTracee\ExitCodeTracee.csproj" />
+ <ProjectReference Include="..\Microsoft.Diagnostics.NETCore.Client\Microsoft.Diagnostics.NETCore.Client.UnitTests.csproj" />
</ItemGroup>
</Project>