Skip to content

Commit 8f15e96

Browse files
committed
Avoid process output/error redirection completely for MTP on protocol version 1.1.0
1 parent 33f0825 commit 8f15e96

File tree

4 files changed

+80
-64
lines changed

4 files changed

+80
-64
lines changed

src/Cli/dotnet/Commands/Test/CliConstants.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ internal static class HandshakeMessagePropertyNames
6060
internal static class ProtocolConstants
6161
{
6262
/// <summary>
63-
/// The protocol versions that are supported by the current SDK. Multiple versions can be present and be semicolon separated.
63+
/// The protocol versions that are supported by the current SDK. Must be ordered from highest version to lowest version.
6464
/// </summary>
65-
internal const string SupportedVersions = "1.0.0";
65+
internal const string[] SupportedVersions = ["1.1.0", "1.0.0"];
6666
}
6767

6868
internal static class ProjectProperties

src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,7 @@ internal void AssemblyRunCompleted(string executionId,
753753
});
754754
}
755755

756-
internal void HandshakeFailure(string assemblyPath, string? targetFramework, int exitCode, string outputData, string errorData)
756+
internal void HandshakeFailure(string assemblyPath, string? targetFramework, int exitCode, string? outputData, string? errorData)
757757
{
758758
if (_isHelp)
759759
{

src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs

Lines changed: 62 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -54,51 +54,61 @@ public async Task<int> RunAsync()
5454

5555
using var process = Process.Start(processStartInfo)!;
5656

57-
// Reading from process stdout/stderr is done on separate threads to avoid blocking IO on the threadpool.
58-
// Note: even with 'process.StandardOutput.ReadToEndAsync()' or 'process.BeginOutputReadLine()', we ended up with
59-
// many TP threads just doing synchronous IO, slowing down the progress of the test run.
60-
// We want to read requests coming through the pipe and sending responses back to the test app as fast as possible.
61-
// We are using ConcurrentQueue to avoid thread-safety issues for the timeout case.
62-
// In the timeout case, we leave stdOutTask and stdErrTask running, just we stop observing them.
63-
var stdOutBuilder = new ConcurrentQueue<string>();
64-
var stdErrBuilder = new ConcurrentQueue<string>();
65-
66-
var stdOutTask = Task.Factory.StartNew(() =>
57+
int exitCode;
58+
if (!_handler.ShouldRedirectOutputAndError)
6759
{
68-
var stdOut = process.StandardOutput;
69-
string? currentLine;
70-
while ((currentLine = stdOut.ReadLine()) is not null)
60+
await process.WaitForExitAsync();
61+
exitCode = process.ExitCode;
62+
_handler.OnTestProcessExited(exitCode, null, null);
63+
}
64+
else
65+
{
66+
// Reading from process stdout/stderr is done on separate threads to avoid blocking IO on the threadpool.
67+
// Note: even with 'process.StandardOutput.ReadToEndAsync()' or 'process.BeginOutputReadLine()', we ended up with
68+
// many TP threads just doing synchronous IO, slowing down the progress of the test run.
69+
// We want to read requests coming through the pipe and sending responses back to the test app as fast as possible.
70+
// We are using ConcurrentQueue to avoid thread-safety issues for the timeout case.
71+
// In the timeout case, we leave stdOutTask and stdErrTask running, just we stop observing them.
72+
var stdOutBuilder = new ConcurrentQueue<string>();
73+
var stdErrBuilder = new ConcurrentQueue<string>();
74+
75+
var stdOutTask = Task.Factory.StartNew(() =>
7176
{
72-
stdOutBuilder.Enqueue(currentLine);
73-
}
74-
}, TaskCreationOptions.LongRunning);
77+
var stdOut = process.StandardOutput;
78+
string? currentLine;
79+
while ((currentLine = stdOut.ReadLine()) is not null)
80+
{
81+
stdOutBuilder.Enqueue(currentLine);
82+
}
83+
}, TaskCreationOptions.LongRunning);
7584

76-
var stdErrTask = Task.Factory.StartNew(() =>
77-
{
78-
var stdErr = process.StandardError;
79-
string? currentLine;
80-
while ((currentLine = stdErr.ReadLine()) is not null)
85+
var stdErrTask = Task.Factory.StartNew(() =>
8186
{
82-
stdErrBuilder.Enqueue(currentLine);
83-
}
84-
}, TaskCreationOptions.LongRunning);
87+
var stdErr = process.StandardError;
88+
string? currentLine;
89+
while ((currentLine = stdErr.ReadLine()) is not null)
90+
{
91+
stdErrBuilder.Enqueue(currentLine);
92+
}
93+
}, TaskCreationOptions.LongRunning);
8594

86-
// WaitForExitAsync only waits for process exit (and doesn't wait for output) for our usage here.
87-
// If we use BeginOutputReadLine/BeginErrorReadLine, it will also wait for output which can deadlock.
88-
await process.WaitForExitAsync();
95+
// WaitForExitAsync only waits for process exit (and doesn't wait for output) for our usage here.
96+
// If we use BeginOutputReadLine/BeginErrorReadLine, it will also wait for output which can deadlock.
97+
await process.WaitForExitAsync();
8998

90-
// At this point, process already exited. Allow for 5 seconds to consume stdout/stderr.
91-
// We might not be able to consume all the output if the test app has exited but left a child process alive.
92-
try
93-
{
94-
await Task.WhenAll(stdOutTask, stdErrTask).WaitAsync(TimeSpan.FromSeconds(5));
95-
}
96-
catch (TimeoutException)
97-
{
98-
}
99+
// At this point, process already exited. Allow for 5 seconds to consume stdout/stderr.
100+
// We might not be able to consume all the output if the test app has exited but left a child process alive.
101+
try
102+
{
103+
await Task.WhenAll(stdOutTask, stdErrTask).WaitAsync(TimeSpan.FromSeconds(5));
104+
}
105+
catch (TimeoutException)
106+
{
107+
}
99108

100-
var exitCode = process.ExitCode;
101-
_handler.OnTestProcessExited(exitCode, string.Join(Environment.NewLine, stdOutBuilder), string.Join(Environment.NewLine, stdErrBuilder));
109+
exitCode = process.ExitCode;
110+
_handler.OnTestProcessExited(exitCode, string.Join(Environment.NewLine, stdOutBuilder), string.Join(Environment.NewLine, stdErrBuilder));
111+
}
102112

103113
// This condition is to prevent considering the test app as successful when we didn't receive test session end.
104114
// We don't produce the exception if the exit code is already non-zero to avoid surfacing this exception when there is already a known failure.
@@ -121,15 +131,16 @@ public async Task<int> RunAsync()
121131

122132
private ProcessStartInfo CreateProcessStartInfo()
123133
{
134+
var shouldRedirect = _handler.ShouldRedirectOutputAndError;
124135
var processStartInfo = new ProcessStartInfo
125136
{
126137
// We should get correct RunProperties right away.
127138
// For the case of dotnet test --test-modules path/to/dll, the TestModulesFilterHandler is responsible
128139
// for providing the dotnet muxer as RunCommand, and `exec "path/to/dll"` as RunArguments.
129140
FileName = Module.RunProperties.Command,
130141
Arguments = GetArguments(),
131-
RedirectStandardOutput = true,
132-
RedirectStandardError = true,
142+
RedirectStandardOutput = shouldRedirect,
143+
RedirectStandardError = shouldRedirect,
133144
// False is already the default on .NET Core, but prefer to be explicit.
134145
UseShellExecute = false,
135146
};
@@ -254,7 +265,7 @@ private Task<IResponse> OnRequest(NamedPipeServer server, IRequest request)
254265
case HandshakeMessage handshakeMessage:
255266
_handshakes.Add(server, handshakeMessage);
256267
string negotiatedVersion = GetSupportedProtocolVersion(handshakeMessage);
257-
OnHandshakeMessage(handshakeMessage, negotiatedVersion.Length > 0);
268+
OnHandshakeMessage(handshakeMessage, negotiatedVersion);
258269
return Task.FromResult((IResponse)CreateHandshakeMessage(negotiatedVersion));
259270

260271
case CommandLineOptionMessages commandLineOptionMessages:
@@ -317,14 +328,15 @@ private static string GetSupportedProtocolVersion(HandshakeMessage handshakeMess
317328
return string.Empty;
318329
}
319330

320-
// NOTE: Today, ProtocolConstants.Version is only 1.0.0 (i.e, SDK supports only a single version).
321-
// Whenever we support multiple versions in SDK, we should do intersection
322-
// between protocolVersions given by MTP, and the versions supported by SDK.
323-
// Then we return the "highest" version from the intersection.
324-
// The current logic **assumes** that ProtocolConstants.SupportedVersions is a single version.
325-
if (protocolVersions.Split(";").Contains(ProtocolConstants.SupportedVersions))
331+
// ProtocolConstant.SupportedVersions is the SDK supported versions, ordered from highest version to lowest version.
332+
// So, we take the first highest version that is also supported by MTP.
333+
var protocolVersionsByMTP = protocolVersions.Split(";");
334+
foreach (var sdkSupportedVersion in ProtocolConstants.SupportedVersions)
326335
{
327-
return ProtocolConstants.SupportedVersions;
336+
if (protocolVersionsByMTP.Contains(sdkSupportedVersion))
337+
{
338+
return sdkSupportedVersion;
339+
}
328340
}
329341

330342
// The version given by MTP is not supported by SDK.
@@ -341,8 +353,8 @@ private static HandshakeMessage CreateHandshakeMessage(string version) =>
341353
{ HandshakeMessagePropertyNames.SupportedProtocolVersions, version }
342354
});
343355

344-
public void OnHandshakeMessage(HandshakeMessage handshakeMessage, bool gotSupportedVersion)
345-
=> _handler.OnHandshakeReceived(handshakeMessage, gotSupportedVersion);
356+
public void OnHandshakeMessage(HandshakeMessage handshakeMessage, string negotiatedVersion)
357+
=> _handler.OnHandshakeReceived(handshakeMessage, negotiatedVersion);
346358

347359
private void OnCommandLineOptionMessages(CommandLineOptionMessages commandLineOptionMessages)
348360
{

src/Cli/dotnet/Commands/Test/MTP/TestApplicationHandler.cs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ internal sealed class TestApplicationHandler
1414
private readonly Lock _lock = new();
1515
private readonly Dictionary<string, (int TestSessionStartCount, int TestSessionEndCount)> _testSessionEventCountPerSessionUid = new();
1616

17-
private (string? TargetFramework, string? Architecture, string ExecutionId)? _handshakeInfo;
17+
private (string? TargetFramework, string? Architecture, string ExecutionId, string NegotiatedVersion)? _handshakeInfo;
1818
private bool _receivedTestHostHandshake;
1919

2020
public TestApplicationHandler(TerminalTestReporter output, TestModule module, TestOptions options)
@@ -24,11 +24,14 @@ public TestApplicationHandler(TerminalTestReporter output, TestModule module, Te
2424
_options = options;
2525
}
2626

27-
internal void OnHandshakeReceived(HandshakeMessage handshakeMessage, bool gotSupportedVersion)
27+
// Only 1.0.0 specifically should redirect output/err.
28+
internal bool ShouldRedirectOutputAndError => !_handshakeInfo.HasValue || _handshakeInfo.Value.NegotiatedVersion == "1.0.0";
29+
30+
internal void OnHandshakeReceived(HandshakeMessage handshakeMessage, string negotiatedVersion)
2831
{
2932
LogHandshake(handshakeMessage);
3033

31-
if (!gotSupportedVersion)
34+
if (negotiatedVersion.Length == 0)
3235
{
3336
_output.HandshakeFailure(
3437
_module.TargetPath,
@@ -37,7 +40,7 @@ internal void OnHandshakeReceived(HandshakeMessage handshakeMessage, bool gotSup
3740
string.Format(
3841
CliCommandStrings.DotnetTestIncompatibleHandshakeVersion,
3942
handshakeMessage.Properties[HandshakeMessagePropertyNames.SupportedProtocolVersions],
40-
ProtocolConstants.SupportedVersions),
43+
string.Join(';', ProtocolConstants.SupportedVersions)),
4144
string.Empty);
4245

4346
// Protocol version is not supported.
@@ -48,7 +51,7 @@ internal void OnHandshakeReceived(HandshakeMessage handshakeMessage, bool gotSup
4851
var executionId = handshakeMessage.Properties[HandshakeMessagePropertyNames.ExecutionId];
4952
var arch = handshakeMessage.Properties[HandshakeMessagePropertyNames.Architecture]?.ToLower();
5053
var tfm = TargetFrameworkParser.GetShortTargetFramework(handshakeMessage.Properties[HandshakeMessagePropertyNames.Framework]);
51-
var currentHandshakeInfo = (tfm, arch, executionId);
54+
var currentHandshakeInfo = (tfm, arch, executionId, negotiatedVersion);
5255

5356
if (!_handshakeInfo.HasValue)
5457
{
@@ -137,6 +140,7 @@ internal void OnTestResultsReceived(TestResultMessages testResultMessage)
137140
}
138141

139142
var handshakeInfo = _handshakeInfo.Value;
143+
var shouldRedirect = ShouldRedirectOutputAndError;
140144
foreach (var testResult in testResultMessage.SuccessfulTestMessages)
141145
{
142146
_output.TestCompleted(_module.TargetPath, handshakeInfo.TargetFramework, handshakeInfo.Architecture, handshakeInfo.ExecutionId,
@@ -149,8 +153,8 @@ internal void OnTestResultsReceived(TestResultMessages testResultMessage)
149153
exceptions: null,
150154
expected: null,
151155
actual: null,
152-
standardOutput: testResult.StandardOutput,
153-
errorOutput: testResult.ErrorOutput);
156+
standardOutput: shouldRedirect ? testResult.StandardOutput : null,
157+
errorOutput: shouldRedirect ? testResult.ErrorOutput : null);
154158
}
155159

156160
foreach (var testResult in testResultMessage.FailedTestMessages)
@@ -164,8 +168,8 @@ internal void OnTestResultsReceived(TestResultMessages testResultMessage)
164168
exceptions: [.. testResult.Exceptions!.Select(fe => new Terminal.FlatException(fe.ErrorMessage, fe.ErrorType, fe.StackTrace))],
165169
expected: null,
166170
actual: null,
167-
standardOutput: testResult.StandardOutput,
168-
errorOutput: testResult.ErrorOutput);
171+
standardOutput: shouldRedirect ? testResult.StandardOutput : null,
172+
errorOutput: shouldRedirect ? testResult.ErrorOutput : null);
169173
}
170174
}
171175

@@ -263,7 +267,7 @@ internal bool HasMismatchingTestSessionEventCount()
263267
return false;
264268
}
265269

266-
internal void OnTestProcessExited(int exitCode, string outputData, string errorData)
270+
internal void OnTestProcessExited(int exitCode, string? outputData, string? errorData)
267271
{
268272
if (_receivedTestHostHandshake && _handshakeInfo.HasValue)
269273
{
@@ -378,7 +382,7 @@ private static void LogFileArtifacts(FileArtifactMessages fileArtifactMessages)
378382
Logger.LogTrace(logMessageBuilder, static logMessageBuilder => logMessageBuilder.ToString());
379383
}
380384

381-
private static void LogTestProcessExit(int exitCode, string outputData, string errorData)
385+
private static void LogTestProcessExit(int exitCode, string? outputData, string? errorData)
382386
{
383387
if (!Logger.TraceEnabled)
384388
{

0 commit comments

Comments
 (0)