Skip to content
151 changes: 71 additions & 80 deletions src/Build/BackEnd/Client/MSBuildClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.BackEnd;
using Microsoft.Build.BackEnd.Client;
using Microsoft.Build.BackEnd.Logging;
Expand Down Expand Up @@ -55,11 +57,6 @@ public sealed class MSBuildClient
/// </summary>
private readonly MSBuildClientExitResult _exitResult;

/// <summary>
/// Whether MSBuild server finished the build.
/// </summary>
private bool _buildFinished = false;

/// <summary>
/// Handshake between server and client.
/// </summary>
Expand All @@ -73,7 +70,7 @@ public sealed class MSBuildClient
/// <summary>
/// The named pipe stream for client-server communication.
/// </summary>
private NamedPipeClientStream _nodeStream = null!;
private NamedPipeClientStream _nodeStream;

/// <summary>
/// A way to cache a byte array when writing out packets
Expand All @@ -99,7 +96,7 @@ public sealed class MSBuildClient
/// <summary>
/// Incoming packet pump and redirection.
/// </summary>
private MSBuildClientPacketPump _packetPump = null!;
private MSBuildClientPacketPump _packetPump;

/// <summary>
/// PID of the server process this client launched (or null if no launch was attempted /
Expand Down Expand Up @@ -132,6 +129,9 @@ public MSBuildClient(string[] commandLine, string msbuildLocation)
CreateNodePipeStream();
}

#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant
[MemberNotNull(nameof(_nodeStream), nameof(_packetPump))]
#pragma warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant
private void CreateNodePipeStream()
{
#pragma warning disable SA1111, SA1009 // Closing parenthesis should be on line of last parameter
Expand All @@ -145,7 +145,7 @@ private void CreateNodePipeStream()
#endif
);
#pragma warning restore SA1111, SA1009 // Closing parenthesis should be on line of last parameter
_packetPump = new MSBuildClientPacketPump(_nodeStream);
_packetPump = new MSBuildClientPacketPump(_nodeStream, DeserializePacket);
}

/// <summary>
Expand All @@ -156,6 +156,17 @@ private void CreateNodePipeStream()
/// <returns>A value of type <see cref="MSBuildClientExitResult"/> that indicates whether the build succeeded,
/// or the manner in which it failed.</returns>
public MSBuildClientExitResult Execute(CancellationToken cancellationToken)
{
return ExecuteAsync(cancellationToken).GetAwaiter().GetResult();
}

/// <summary>
/// Orchestrates the execution of the build on the server, responsible for client-server communication.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> that will complete with a value of type <see cref="MSBuildClientExitResult"/>
/// that indicates whether the build succeeded, or the manner in which it failed.</returns>
public async Task<MSBuildClientExitResult> ExecuteAsync(CancellationToken cancellationToken)
{
// Command line in one string used only in human readable content.
string descriptiveCommandLine = string.Join(" ", _commandLine);
Expand Down Expand Up @@ -209,12 +220,12 @@ public MSBuildClientExitResult Execute(CancellationToken cancellationToken)
// Send build command.
// Let's send it outside the packet pump so that we easier and quicker deal with possible issues with connection to server.
MSBuildEventSource.Log.MSBuildServerBuildStart(descriptiveCommandLine);
if (TrySendBuildCommand())
if (await TrySendBuildCommandAsync().ConfigureAwait(false))
{
_numConsoleWritePackets = 0;
_sizeOfConsoleWritePackets = 0;

ReadPacketsLoop(cancellationToken);
await ReadPacketsLoop(cancellationToken).ConfigureAwait(false);

MSBuildEventSource.Log.MSBuildServerBuildStop(descriptiveCommandLine, _numConsoleWritePackets, _sizeOfConsoleWritePackets, _exitResult.MSBuildClientExitType.ToString(), _exitResult.MSBuildAppExitTypeString ?? string.Empty);
CommunicationsUtilities.Trace("Build finished.");
Expand All @@ -238,10 +249,10 @@ public static bool ShutdownServer(CancellationToken cancellationToken)
// Neither commandLine nor msbuildlocation is involved in node shutdown
var client = new MSBuildClient(commandLine: null!, msbuildLocation: null!);

return client.TryShutdownServer(cancellationToken);
return client.TryShutdownServerAsync(cancellationToken).GetAwaiter().GetResult();
}

private bool TryShutdownServer(CancellationToken cancellationToken)
private async Task<bool> TryShutdownServerAsync(CancellationToken cancellationToken)
{
CommunicationsUtilities.Trace("Trying shutdown server node.");

Expand All @@ -268,13 +279,13 @@ private bool TryShutdownServer(CancellationToken cancellationToken)
return false;
}

if (!TrySendShutdownCommand())
if (!await TrySendShutdownCommandAsync().ConfigureAwait(false))
{
CommunicationsUtilities.Trace("Failed to send shutdown command to the server.");
return false;
}

ReadPacketsLoop(cancellationToken);
await ReadPacketsLoop(cancellationToken).ConfigureAwait(false);

return _exitResult.MSBuildClientExitType == MSBuildClientExitType.Success;
}
Expand Down Expand Up @@ -307,51 +318,46 @@ private bool ServerWasBusy()
return serverWasBusy;
}

private void ReadPacketsLoop(CancellationToken cancellationToken)
private INodePacket DeserializePacket(NodePacketType packetType, ITranslator translator) => packetType switch
{
NodePacketType.ServerNodeConsoleWrite => ServerNodeConsoleWrite.FactoryForDeserialization(translator),
NodePacketType.ServerNodeBuildResult => ServerNodeBuildResult.FactoryForDeserialization(translator),
_ => throw new InvalidOperationException($"Unexpected packet type {packetType}"),
};

private async Task ReadPacketsLoop(CancellationToken cancellationToken)
{
try
{
// Start packet pump
using MSBuildClientPacketPump packetPump = _packetPump;

packetPump.RegisterPacketHandler(NodePacketType.ServerNodeConsoleWrite, ServerNodeConsoleWrite.FactoryForDeserialization, packetPump);
packetPump.RegisterPacketHandler(NodePacketType.ServerNodeBuildResult, ServerNodeBuildResult.FactoryForDeserialization, packetPump);
await using MSBuildClientPacketPump packetPump = _packetPump;
packetPump.Start();

WaitHandle[] waitHandles =
while (true)
{
cancellationToken.WaitHandle,
packetPump.PacketPumpCompleted,
packetPump.PacketReceivedEvent
};
bool hasPackets;
try
{
hasPackets = await packetPump.ReceivedPackets.WaitToReadAsync(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException e) when (e.CancellationToken == cancellationToken)
{
await HandleCancellationAsync().ConfigureAwait(false);

while (!_buildFinished)
{
int index = WaitHandle.WaitAny(waitHandles);
switch (index)
// After the cancellation, we want to wait to server gracefully finish the build.
// We have to replace the cancelation token, because the thrown OCE would cause to repeatedly hit this branch of code.
cancellationToken = CancellationToken.None;
continue;
}

if (!hasPackets)
{
break;
}

while (packetPump.ReceivedPackets.TryRead(out INodePacket? packet))
{
case 0:
HandleCancellation();
// After the cancelation, we want to wait to server gracefuly finish the build.
// We have to replace the cancelation handle, because WaitAny would cause to repeatedly hit this branch of code.
waitHandles[0] = CancellationToken.None.WaitHandle;
break;

case 1:
HandlePacketPumpCompleted(packetPump);
break;

case 2:
while (packetPump.ReceivedPacketsQueue.TryDequeue(out INodePacket? packet) &&
!_buildFinished)
{
if (packet != null)
{
HandlePacket(packet);
}
}

break;
HandlePacket(packet);
}
}
}
Expand Down Expand Up @@ -408,13 +414,13 @@ private ConsoleColor QueryConsoleBackgroundColor()
return consoleBackgroundColor;
}

private bool TrySendPacket(Func<INodePacket> packetResolver)
private async ValueTask<bool> TrySendPacketAsync(Func<INodePacket> packetResolver)
{
INodePacket? packet = null;
try
{
packet = packetResolver();
WritePacket(_nodeStream, packet);
await WritePacketAsync(_nodeStream, packet).ConfigureAwait(false);
CommunicationsUtilities.Trace($"Command packet of type '{packet.Type}' sent...");
}
catch (Exception ex)
Expand Down Expand Up @@ -489,15 +495,15 @@ private bool TryLaunchServer()
return true;
}

private bool TrySendBuildCommand() => TrySendPacket(() => GetServerNodeBuildCommand());
private ValueTask<bool> TrySendBuildCommandAsync() => TrySendPacketAsync(() => GetServerNodeBuildCommand());

private bool TrySendCancelCommand() => TrySendPacket(() => new ServerNodeBuildCancel());
private ValueTask<bool> TrySendCancelCommandAsync() => TrySendPacketAsync(() => new ServerNodeBuildCancel());

private bool TrySendShutdownCommand()
private ValueTask<bool> TrySendShutdownCommandAsync()
{
CommunicationsUtilities.Trace("Sending shutdown command to server.");
_packetPump.ServerWillDisconnect();
return TrySendPacket(() => new NodeBuildComplete(false /* no node reuse */));
return TrySendPacketAsync(() => new NodeBuildComplete(false /* no node reuse */));
}

private ServerNodeBuildCommand GetServerNodeBuildCommand()
Expand Down Expand Up @@ -544,27 +550,13 @@ private ServerNodeBuildCommand GetServerNodeBuildCommand()
/// <summary>
/// Handle cancellation.
/// </summary>
private void HandleCancellation()
private async ValueTask HandleCancellationAsync()
{
TrySendCancelCommand();
_ = await TrySendCancelCommandAsync().ConfigureAwait(false);

CommunicationsUtilities.Trace("MSBuild client sent cancellation command.");
}

/// <summary>
/// Handle when packet pump is completed both successfully or with error.
/// </summary>
private void HandlePacketPumpCompleted(MSBuildClientPacketPump packetPump)
{
if (packetPump.PacketPumpException != null)
{
CommunicationsUtilities.Trace($"MSBuild client error: packet pump unexpectedly shut down: {packetPump.PacketPumpException}");
throw packetPump.PacketPumpException ?? new InternalErrorException("Packet pump unexpectedly shut down");
}

_buildFinished = true;
}

/// <summary>
/// Dispatches the packet to the correct handler.
/// </summary>
Expand Down Expand Up @@ -606,7 +598,6 @@ private void HandleServerNodeBuildResult(ServerNodeBuildResult response)
CommunicationsUtilities.Trace($"Build response received: exit code '{response.ExitCode}', exit type '{response.ExitType}'");
_exitResult.MSBuildClientExitType = MSBuildClientExitType.Success;
_exitResult.MSBuildAppExitTypeString = response.ExitType;
_buildFinished = true;
}

/// <summary>
Expand All @@ -615,13 +606,10 @@ private void HandleServerNodeBuildResult(ServerNodeBuildResult response)
/// <returns> Whether the client connected to MSBuild server successfully.</returns>
private bool TryConnectToServer(int timeoutMilliseconds)
{
bool tryAgain = true;
Stopwatch sw = Stopwatch.StartNew();

while (tryAgain && sw.ElapsedMilliseconds < timeoutMilliseconds)
while (sw.ElapsedMilliseconds < timeoutMilliseconds)
{
tryAgain = false;

HandshakeResult result;
bool connected;
try
Expand Down Expand Up @@ -662,7 +650,6 @@ private bool TryConnectToServer(int timeoutMilliseconds)
CommunicationsUtilities.Trace($"Retrying to connect to server after {sw.ElapsedMilliseconds} ms");
// This solves race condition for time in which server started but have not yet listen on pipe or
// when it just finished build request and is recycling pipe.
tryAgain = true;
CreateNodePipeStream();
}
else
Expand Down Expand Up @@ -728,7 +715,7 @@ private void LogConnectFailureDiagnostics(int timeoutMilliseconds, bool isTimeou
"If the server child process exited immediately, ensure DOTNET_ROOT is set correctly so the apphost can locate the .NET runtime.");
}

private void WritePacket(Stream nodeStream, INodePacket packet)
private async ValueTask WritePacketAsync(Stream nodeStream, INodePacket packet)
{
MemoryStream memoryStream = _packetMemoryStream;
memoryStream.SetLength(0);
Expand All @@ -750,7 +737,11 @@ private void WritePacket(Stream nodeStream, INodePacket packet)
memoryStream.Position = 1;
_binaryWriter.Write(packetStreamLength - 5);

nodeStream.Write(memoryStream.GetBuffer(), 0, packetStreamLength);
#if NET
await nodeStream.WriteAsync(memoryStream.GetBuffer().AsMemory(0, packetStreamLength)).ConfigureAwait(false);
#else
await nodeStream.WriteAsync(memoryStream.GetBuffer(), 0, packetStreamLength).ConfigureAwait(false);
#endif
}
}
}
Loading