Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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