Skip to content
Merged
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
59 changes: 18 additions & 41 deletions src/Build/BackEnd/Client/MSBuildClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using System.Globalization;
using System.IO;
using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Build.BackEnd;
using Microsoft.Build.BackEnd.Client;
Expand All @@ -32,14 +31,10 @@ public sealed class MSBuildClient
private readonly Dictionary<string, string> _serverEnvironmentVariables;

/// <summary>
/// Location of executable file to launch the server process. That should be either dotnet.exe or MSBuild.exe location.
/// Full path to current MSBuild.exe if executable is MSBuild.exe,
/// or to version of MSBuild.dll found to be associated with the current process.
/// </summary>
private readonly string _exeLocation;

/// <summary>
/// Location of dll file to launch the server process if needed. Empty if executable is msbuild.exe and not empty if dotnet.exe.
/// </summary>
private readonly string _dllLocation;
private readonly string _msbuildLocation;

/// <summary>
/// The MSBuild client execution result.
Expand Down Expand Up @@ -85,18 +80,15 @@ public sealed class MSBuildClient
/// <summary>
/// Public constructor with parameters.
/// </summary>
/// <param name="exeLocation">Location of executable file to launch the server process.
/// That should be either dotnet.exe or MSBuild.exe location.</param>
/// <param name="dllLocation">Location of dll file to launch the server process if needed.
/// Empty if executable is msbuild.exe and not empty if dotnet.exe.</param>
public MSBuildClient(string exeLocation, string dllLocation)
/// <param name="msbuildLocation"> Full path to current MSBuild.exe if executable is MSBuild.exe,
/// or to version of MSBuild.dll found to be associated with the current process.</param>
public MSBuildClient(string msbuildLocation)
{
_serverEnvironmentVariables = new();
_exitResult = new();

// dll & exe locations
_exeLocation = exeLocation;
_dllLocation = dllLocation;
_msbuildLocation = msbuildLocation;

// Client <-> Server communication stream
_handshake = GetHandshake();
Expand Down Expand Up @@ -257,48 +249,33 @@ private bool TryLaunchServer()
}

string[] msBuildServerOptions = new string[] {
_dllLocation,
"/nologo",
"/nodemode:8"
};

string? useMSBuildServerEnvVarValue = Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName);
try
{
Process msbuildProcess = LaunchNode(_exeLocation, string.Join(" ", msBuildServerOptions), _serverEnvironmentVariables);
CommunicationsUtilities.Trace("Server is launched with PID: {0}", msbuildProcess.Id);
// Disable MSBuild server for a child process, preventing an infinite recurson.
Environment.SetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName, "");

NodeLauncher nodeLauncher = new NodeLauncher();
CommunicationsUtilities.Trace("Starting Server...");
Process msbuildProcess = nodeLauncher.Start(_msbuildLocation, string.Join(" ", msBuildServerOptions));
CommunicationsUtilities.Trace("Server started with PID: {0}", msbuildProcess?.Id);
}
catch (Exception ex)
{
CommunicationsUtilities.Trace("Failed to launch the msbuild server: {0}", ex);
_exitResult.MSBuildClientExitType = MSBuildClientExitType.LaunchError;
return false;
}

return true;
}

private Process LaunchNode(string exeLocation, string msBuildServerArguments, Dictionary<string, string> serverEnvironmentVariables)
{
CommunicationsUtilities.Trace("Launching server node from {0} with arguments {1}", exeLocation, msBuildServerArguments);
ProcessStartInfo processStartInfo = new()
{
FileName = exeLocation,
Arguments = msBuildServerArguments,
UseShellExecute = false
};

foreach (var entry in serverEnvironmentVariables)
finally
{
processStartInfo.Environment[entry.Key] = entry.Value;
Environment.SetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName, useMSBuildServerEnvVarValue);
}

// We remove env to enable MSBuild Server that might be equal to 1, so we do not get an infinite recursion here.
processStartInfo.Environment[Traits.UseMSBuildServerEnvVarName] = "0";

processStartInfo.CreateNoWindow = true;
processStartInfo.UseShellExecute = false;

return Process.Start(processStartInfo) ?? throw new InvalidOperationException("MSBuild server node failed to launch.");
return true;
}

private bool TrySendBuildCommand(string commandLine)
Expand Down
49 changes: 49 additions & 0 deletions src/Build/BackEnd/Components/Communications/CurrentHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Diagnostics;
using System.IO;
using Microsoft.Build.Shared;

#nullable disable

namespace Microsoft.Build.BackEnd
{
internal static class CurrentHost
{

#if RUNTIME_TYPE_NETCORE || MONO
private static string s_currentHost;
#endif

/// <summary>
/// Identify the .NET host of the current process.
/// </summary>
/// <returns>The full path to the executable hosting the current process, or null if running on Full Framework on Windows.</returns>
public static string GetCurrentHost()
{
#if RUNTIME_TYPE_NETCORE || MONO
if (s_currentHost == null)
{
string dotnetExe = Path.Combine(FileUtilities.GetFolderAbove(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory, 2),
NativeMethodsShared.IsWindows ? "dotnet.exe" : "dotnet");
if (File.Exists(dotnetExe))
{
s_currentHost = dotnetExe;
}
else
{
using (Process currentProcess = Process.GetCurrentProcess())
{
s_currentHost = currentProcess.MainModule.FileName;
}
}
}

return s_currentHost;
#else
return null;
#endif
}
}
}
181 changes: 181 additions & 0 deletions src/Build/BackEnd/Components/Communications/NodeLauncher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Framework;
using Microsoft.Build.Internal;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using BackendNativeMethods = Microsoft.Build.BackEnd.NativeMethods;

#nullable disable

namespace Microsoft.Build.BackEnd
{
internal class NodeLauncher
{
/// <summary>
/// Creates a new MSBuild process
/// </summary>
public Process Start(string msbuildLocation, string commandLineArgs)
{
// Should always have been set already.
ErrorUtilities.VerifyThrowInternalLength(msbuildLocation, nameof(msbuildLocation));

if (!FileSystems.Default.FileExists(msbuildLocation))
{
throw new BuildAbortedException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("CouldNotFindMSBuildExe", msbuildLocation));
}

// Repeat the executable name as the first token of the command line because the command line
// parser logic expects it and will otherwise skip the first argument
commandLineArgs = $"\"{msbuildLocation}\" {commandLineArgs}";

BackendNativeMethods.STARTUP_INFO startInfo = new();
startInfo.cb = Marshal.SizeOf<BackendNativeMethods.STARTUP_INFO>();

// Null out the process handles so that the parent process does not wait for the child process
// to exit before it can exit.
uint creationFlags = 0;
if (Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout)
{
creationFlags = BackendNativeMethods.NORMALPRIORITYCLASS;
}

if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDNODEWINDOW")))
{
if (!Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout)
{
// Redirect the streams of worker nodes so that this MSBuild.exe's
// parent doesn't wait on idle worker nodes to close streams
// after the build is complete.
startInfo.hStdError = BackendNativeMethods.InvalidHandle;
startInfo.hStdInput = BackendNativeMethods.InvalidHandle;
startInfo.hStdOutput = BackendNativeMethods.InvalidHandle;
startInfo.dwFlags = BackendNativeMethods.STARTFUSESTDHANDLES;
creationFlags |= BackendNativeMethods.CREATENOWINDOW;
}
}
else
{
creationFlags |= BackendNativeMethods.CREATE_NEW_CONSOLE;
}

CommunicationsUtilities.Trace("Launching node from {0}", msbuildLocation);

string exeName = msbuildLocation;

#if RUNTIME_TYPE_NETCORE || MONO
// Mono automagically uses the current mono, to execute a managed assembly
if (!NativeMethodsShared.IsMono)
{
// Run the child process with the same host as the currently-running process.
exeName = CurrentHost.GetCurrentHost();
}
#endif

if (!NativeMethodsShared.IsWindows)
{
ProcessStartInfo processStartInfo = new ProcessStartInfo();
processStartInfo.FileName = exeName;
processStartInfo.Arguments = commandLineArgs;
if (!Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout)
{
// Redirect the streams of worker nodes so that this MSBuild.exe's
// parent doesn't wait on idle worker nodes to close streams
// after the build is complete.
processStartInfo.RedirectStandardInput = true;
processStartInfo.RedirectStandardOutput = true;
processStartInfo.RedirectStandardError = true;
processStartInfo.CreateNoWindow = (creationFlags | BackendNativeMethods.CREATENOWINDOW) == BackendNativeMethods.CREATENOWINDOW;
}
processStartInfo.UseShellExecute = false;

Process process;
try
{
process = Process.Start(processStartInfo);
}
catch (Exception ex)
{
CommunicationsUtilities.Trace
(
"Failed to launch node from {0}. CommandLine: {1}" + Environment.NewLine + "{2}",
msbuildLocation,
commandLineArgs,
ex.ToString()
);

throw new NodeFailedToLaunchException(ex);
}

CommunicationsUtilities.Trace("Successfully launched {1} node with PID {0}", process.Id, exeName);
return process;
}
else
{
#if RUNTIME_TYPE_NETCORE
// Repeat the executable name in the args to suit CreateProcess
commandLineArgs = $"\"{exeName}\" {commandLineArgs}";
#endif

BackendNativeMethods.PROCESS_INFORMATION processInfo = new();
BackendNativeMethods.SECURITY_ATTRIBUTES processSecurityAttributes = new();
BackendNativeMethods.SECURITY_ATTRIBUTES threadSecurityAttributes = new();
processSecurityAttributes.nLength = Marshal.SizeOf<BackendNativeMethods.SECURITY_ATTRIBUTES>();
threadSecurityAttributes.nLength = Marshal.SizeOf<BackendNativeMethods.SECURITY_ATTRIBUTES>();

bool result = BackendNativeMethods.CreateProcess
(
exeName,
commandLineArgs,
ref processSecurityAttributes,
ref threadSecurityAttributes,
false,
creationFlags,
BackendNativeMethods.NullPtr,
null,
ref startInfo,
out processInfo
);

if (!result)
{
// Creating an instance of this exception calls GetLastWin32Error and also converts it to a user-friendly string.
System.ComponentModel.Win32Exception e = new System.ComponentModel.Win32Exception();

CommunicationsUtilities.Trace
(
"Failed to launch node from {0}. System32 Error code {1}. Description {2}. CommandLine: {2}",
msbuildLocation,
e.NativeErrorCode.ToString(CultureInfo.InvariantCulture),
e.Message,
commandLineArgs
);

throw new NodeFailedToLaunchException(e.NativeErrorCode.ToString(CultureInfo.InvariantCulture), e.Message);
}

int childProcessId = processInfo.dwProcessId;

if (processInfo.hProcess != IntPtr.Zero && processInfo.hProcess != NativeMethods.InvalidHandle)
{
NativeMethodsShared.CloseHandle(processInfo.hProcess);
}

if (processInfo.hThread != IntPtr.Zero && processInfo.hThread != NativeMethods.InvalidHandle)
{
NativeMethodsShared.CloseHandle(processInfo.hThread);
}

CommunicationsUtilities.Trace("Successfully launched {1} node with PID {0}", childProcessId, exeName);
return Process.GetProcessById(childProcessId);
}
}
}
}
Loading