File: BackEnd\Components\Communications\NodeLauncher.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
 
#if RUNTIME_TYPE_NETCORE
using System.IO;
#endif
 
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
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 sealed class NodeLauncher : INodeLauncher, IBuildComponent
    {
        public static IBuildComponent CreateComponent(BuildComponentType type)
        {
            ErrorUtilities.VerifyThrowArgumentOutOfRange(type == BuildComponentType.NodeLauncher, nameof(type));
            return new NodeLauncher();
        }
 
        public void InitializeComponent(IBuildComponentHost host)
        {
        }
 
        public void ShutdownComponent()
        {
        }
 
        /// <summary>
        /// Creates a new MSBuild process using the specified launch configuration.
        /// </summary>
        public Process Start(NodeLaunchData launchData, int nodeId)
        {
            // Disable MSBuild server for a child process.
            // In case of starting msbuild server it prevents an infinite recursion. In case of starting msbuild node we also do not want this variable to be set.
            return DisableMSBuildServer(() => StartInternal(launchData));
        }
 
        /// <summary>
        /// Creates new MSBuild or dotnet process.
        /// </summary>
        private Process StartInternal(NodeLaunchData nodeLaunchData)
        {
            ValidateMSBuildLocation(nodeLaunchData.MSBuildLocation);
 
            string exeName = ResolveExecutableName(nodeLaunchData.MSBuildLocation, out bool isNativeAppHost);
            uint creationFlags = GetCreationFlags(out bool redirectStreams);
 
            CommunicationsUtilities.Trace("Launching node from {0}", nodeLaunchData.MSBuildLocation);
 
            return NativeMethodsShared.IsWindows
                ? StartProcessWindows(nodeLaunchData, exeName, creationFlags, redirectStreams, isNativeAppHost)
                : StartProcessUnix(nodeLaunchData, exeName, creationFlags, redirectStreams, isNativeAppHost);
 
            static void ValidateMSBuildLocation(string msbuildLocation)
            {
                // Should always have been set already.
                ErrorUtilities.VerifyThrowInternalLength(msbuildLocation, nameof(msbuildLocation));
 
                if (!FileSystems.Default.FileExists(msbuildLocation))
                {
                    throw new BuildAbortedException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("CouldNotFindMSBuildExe", msbuildLocation));
                }
            }
        }
 
        private string ResolveExecutableName(string msbuildLocation, out bool isNativeAppHost)
        {
            isNativeAppHost = false;
 
#if RUNTIME_TYPE_NETCORE
            string fileName = Path.GetFileName(msbuildLocation);
 
            // Only managed assemblies (.dll) need dotnet.exe as a host.
            // All native executables — MSBuild app host, MSBuildTaskHost.exe, etc. — run directly.
            if (fileName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
            {
                return CurrentHost.GetCurrentHost();
            }
 
            // Any .exe or extensionless binary (Linux app host) is a native executable.
            isNativeAppHost = true;
#endif
            return msbuildLocation;
        }
 
        private uint GetCreationFlags(out bool redirectStreams)
        {
            bool ensureStdOut = Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout;
            bool showNodeWindow = !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDNODEWINDOW"));
 
            redirectStreams = !ensureStdOut && !showNodeWindow;
 
            uint flags = (ensureStdOut, showNodeWindow) switch
            {
                (true, true) => BackendNativeMethods.NORMALPRIORITYCLASS | BackendNativeMethods.CREATE_NEW_CONSOLE,
                (true, false) => BackendNativeMethods.NORMALPRIORITYCLASS,
                (false, true) => BackendNativeMethods.CREATE_NEW_CONSOLE,
                (false, false) => BackendNativeMethods.CREATENOWINDOW,
            };
 
            return flags;
        }
 
        [UnsupportedOSPlatform("windows")]
        private Process StartProcessUnix(NodeLaunchData nodeLaunchData, string exeName, uint creationFlags, bool redirectStreams, bool isNativeAppHost)
        {
            // Builds command line args for Unix Process.Start, which sets argv[0] from FileName
            // automatically. We must not duplicate the executable name in Arguments for native
            // app hosts. For dotnet-hosted launches, the assembly path must be included so dotnet
            // knows which assembly to run.
            string commandLineArgs = isNativeAppHost ? nodeLaunchData.CommandLineArgs : $"\"{nodeLaunchData.MSBuildLocation}\" {nodeLaunchData.CommandLineArgs}";
 
            var processStartInfo = new ProcessStartInfo
            {
                FileName = exeName,
                Arguments = commandLineArgs,
                UseShellExecute = false,
                RedirectStandardInput = redirectStreams,
                RedirectStandardOutput = redirectStreams,
                RedirectStandardError = redirectStreams,
                CreateNoWindow = redirectStreams && (creationFlags & BackendNativeMethods.CREATENOWINDOW) != 0,
            };
 
            DotnetHostEnvironmentHelper.ApplyEnvironmentOverrides(processStartInfo.Environment, nodeLaunchData.EnvironmentOverrides);
 
            try
            {
                Process process = Process.Start(processStartInfo);
                CommunicationsUtilities.Trace("Successfully launched {1} node with PID {0}", process.Id, exeName);
                return process;
            }
            catch (Exception ex)
            {
                CommunicationsUtilities.Trace(
                    "Failed to launch node from {0}. CommandLine: {1}" + Environment.NewLine + "{2}",
                    nodeLaunchData.MSBuildLocation,
                    commandLineArgs,
                    ex.ToString());
 
                throw new NodeFailedToLaunchException(ex);
            }
        }
 
        [SupportedOSPlatform("windows")]
        private static Process StartProcessWindows(NodeLaunchData nodeLaunchData, string exeName, uint creationFlags, bool redirectStreams, bool isNativeAppHost)
        {
            // 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
            string commandLineArgs = $"\"{nodeLaunchData.MSBuildLocation}\" {nodeLaunchData.CommandLineArgs}";
 
#if RUNTIME_TYPE_NETCORE
            if (!isNativeAppHost)
            {
                commandLineArgs = $"\"{exeName}\" {commandLineArgs}";
            }
#endif
 
            BackendNativeMethods.STARTUP_INFO startInfo = CreateStartupInfo(redirectStreams);
            BackendNativeMethods.SECURITY_ATTRIBUTES processSecurityAttributes = new() { nLength = Marshal.SizeOf<BackendNativeMethods.SECURITY_ATTRIBUTES>() };
            BackendNativeMethods.SECURITY_ATTRIBUTES threadSecurityAttributes = new() { nLength = Marshal.SizeOf<BackendNativeMethods.SECURITY_ATTRIBUTES>() };
 
            IntPtr environmentBlock = BuildEnvironmentBlock(nodeLaunchData.EnvironmentOverrides);
 
            // When passing a Unicode environment block, we must set CREATE_UNICODE_ENVIRONMENT.
            // Without this flag, CreateProcess interprets the block as ANSI, causing error 87.
            uint effectiveCreationFlags = creationFlags;
            if (environmentBlock != BackendNativeMethods.NullPtr)
            {
                effectiveCreationFlags |= BackendNativeMethods.CREATE_UNICODE_ENVIRONMENT;
            }
 
            try
            {
                bool result = BackendNativeMethods.CreateProcess(
                    exeName,
                    commandLineArgs,
                    ref processSecurityAttributes,
                    ref threadSecurityAttributes,
                    false,
                    effectiveCreationFlags,
                    environmentBlock,
                    null,
                    ref startInfo,
                    out BackendNativeMethods.PROCESS_INFORMATION processInfo);
 
                if (!result)
                {
                    var e = new System.ComponentModel.Win32Exception();
 
                    CommunicationsUtilities.Trace(
                        "Failed to launch node from {0}. System32 Error code {1}. Description {2}. CommandLine: {3}",
                        nodeLaunchData.MSBuildLocation,
                        e.NativeErrorCode.ToString(CultureInfo.InvariantCulture),
                        e.Message,
                        commandLineArgs);
 
                    throw new NodeFailedToLaunchException(e.NativeErrorCode.ToString(CultureInfo.InvariantCulture), e.Message);
                }
 
                CloseProcessHandles(processInfo);
 
                CommunicationsUtilities.Trace("Successfully launched {1} node with PID {0}", processInfo.dwProcessId, exeName);
                return Process.GetProcessById(processInfo.dwProcessId);
            }
            finally
            {
                if (environmentBlock != BackendNativeMethods.NullPtr)
                {
                    Marshal.FreeHGlobal(environmentBlock);
                }
            }
 
            static void CloseProcessHandles(BackendNativeMethods.PROCESS_INFORMATION processInfo)
            {
                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);
                }
            }
        }
 
        [SupportedOSPlatform("windows")]
        private static BackendNativeMethods.STARTUP_INFO CreateStartupInfo(bool redirectStreams)
        {
            var startInfo = new BackendNativeMethods.STARTUP_INFO
            {
                cb = Marshal.SizeOf<BackendNativeMethods.STARTUP_INFO>(),
            };
 
            if (redirectStreams)
            {
                startInfo.hStdError = BackendNativeMethods.InvalidHandle;
                startInfo.hStdInput = BackendNativeMethods.InvalidHandle;
                startInfo.hStdOutput = BackendNativeMethods.InvalidHandle;
                startInfo.dwFlags = BackendNativeMethods.STARTFUSESTDHANDLES;
            }
 
            return startInfo;
        }
 
        /// <summary>
        /// Builds a Windows environment block for CreateProcess.
        /// </summary>
        /// <param name="environmentOverrides">Environment variable overrides. Null values remove variables.</param>
        /// <returns>Pointer to environment block that must be freed with Marshal.FreeHGlobal, or BackendNativeMethods.NullPtr.</returns>
        [SupportedOSPlatform("windows")]
        private static IntPtr BuildEnvironmentBlock(IDictionary<string, string> environmentOverrides)
        {
            if (environmentOverrides == null || environmentOverrides.Count == 0)
            {
                return BackendNativeMethods.NullPtr;
            }
 
            var environment = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            foreach (System.Collections.DictionaryEntry entry in Environment.GetEnvironmentVariables())
            {
                environment[(string)entry.Key] = (string)entry.Value;
            }
 
            DotnetHostEnvironmentHelper.ApplyEnvironmentOverrides(environment, environmentOverrides);
 
            // Build the environment block: "key=value\0key=value\0\0"
            // Windows CreateProcess requires the environment block to be sorted alphabetically by name (case-insensitive).
            var sortedKeys = new List<string>(environment.Keys);
            sortedKeys.Sort(StringComparer.OrdinalIgnoreCase);
 
            var sb = new StringBuilder();
            foreach (string key in sortedKeys)
            {
                sb.Append(key);
                sb.Append('=');
                sb.Append(environment[key]);
                sb.Append('\0');
            }
 
            sb.Append('\0');
 
            return Marshal.StringToHGlobalUni(sb.ToString());
        }
 
        private static Process DisableMSBuildServer(Func<Process> func)
        {
            string useMSBuildServerEnvVarValue = Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName);
            try
            {
                if (useMSBuildServerEnvVarValue is not null)
                {
                    Environment.SetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName, "0");
                }
                return func();
            }
            finally
            {
                if (useMSBuildServerEnvVarValue is not null)
                {
                    Environment.SetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName, useMSBuildServerEnvVarValue);
                }
            }
        }
    }
}