Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6e215d4
Implement out-of-proc RAR node lifecycle
ccastanedaucf Mar 13, 2025
4b7e5b1
Fix CI test (bump EngineServices version)
ccastanedaucf Mar 19, 2025
6613006
Fix connection logic for Node reuse scenarios
ccastanedaucf Mar 19, 2025
eb00e2f
Rebase + fix missing ValueTask (reference System.Threading.Channels)
ccastanedaucf Mar 27, 2025
0ec1d0b
Switch server to async APIs from shared IPC
ccastanedaucf Mar 27, 2025
4a30ed7
Move channels under netstandard itemgroup
ccastanedaucf Mar 27, 2025
4add6ce
Changes for rebase (won't compile till !11650 is merged)
ccastanedaucf Apr 5, 2025
ab8eb4b
Rename / flip out-of-proc override
ccastanedaucf May 7, 2025
a37ff7f
Condition multi-instance pipe flag, assert all succeed
ccastanedaucf May 7, 2025
2b8aecd
Remove fire-and-forget TODO
ccastanedaucf May 8, 2025
654f4ec
Seal types
ccastanedaucf May 8, 2025
096916f
Startup perf: Fire and forget launcher
ccastanedaucf May 8, 2025
fb8fb89
Startup perf: Skip expensive process ID check in ServerNodeHandshake
ccastanedaucf May 8, 2025
df240e1
Accessor oopsie
ccastanedaucf May 8, 2025
2dc7fef
Disposal comment
ccastanedaucf May 20, 2025
4e68b55
Propogate UnauthorizedAccessException
ccastanedaucf May 20, 2025
f60d135
Trace endpoint cancellation
ccastanedaucf May 20, 2025
c6a90cf
Change prop 'Disabled' to 'Allow'
ccastanedaucf May 21, 2025
7c2d80f
Startup perf: Pre-run RAR static resource intializers
ccastanedaucf May 21, 2025
a21a685
Startup perf: Lift common state initialization to host node
ccastanedaucf May 21, 2025
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
15 changes: 15 additions & 0 deletions src/Build/BackEnd/BuildManager/BuildManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,21 @@ public void BeginBuild(BuildParameters parameters)
_buildParameters.OutputResultsCacheFile = FileUtilities.NormalizePath("msbuild-cache");
}

// Launch the RAR node before the detoured launcher overrides the default node launcher.
if (_buildParameters.EnableRarNode)
{
NodeLauncher nodeLauncher = ((IBuildComponentHost)this).GetComponent<NodeLauncher>(BuildComponentType.NodeLauncher);
_ = Task.Run(() =>
{
RarNodeLauncher rarNodeLauncher = new(nodeLauncher);

if (!rarNodeLauncher.Start())
{
_buildParameters.EnableRarNode = false;
}
});
}

#if FEATURE_REPORTFILEACCESSES
if (_buildParameters.ReportFileAccesses)
{
Expand Down
13 changes: 13 additions & 0 deletions src/Build/BackEnd/BuildManager/BuildParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ public class BuildParameters : ITranslatable
private bool _enableNodeReuse = false;
#endif

private bool _enableRarNode;

/// <summary>
/// The original process environment.
/// </summary>
Expand Down Expand Up @@ -277,6 +279,7 @@ internal BuildParameters(BuildParameters other, bool resetEnvironment = false)
_culture = other._culture;
_defaultToolsVersion = other._defaultToolsVersion;
_enableNodeReuse = other._enableNodeReuse;
_enableRarNode = other._enableRarNode;
_buildProcessEnvironment = resetEnvironment
? CommunicationsUtilities.GetEnvironmentVariables()
: other._buildProcessEnvironment != null
Expand Down Expand Up @@ -424,6 +427,15 @@ public bool EnableNodeReuse
set => _enableNodeReuse = Environment.GetEnvironmentVariable("MSBUILDDISABLENODEREUSE") == "1" ? false : value;
}

/// <summary>
/// When true, the ResolveAssemblyReferences task executes in an out-of-proc node which persists across builds.
/// </summary>
public bool EnableRarNode
{
get => _enableRarNode;
set => _enableRarNode = value;
}

/// <summary>
/// Gets an immutable collection of environment properties.
/// </summary>
Expand Down Expand Up @@ -915,6 +927,7 @@ void ITranslatable.Translate(ITranslator translator)
translator.Translate(ref _defaultToolsVersion);
translator.Translate(ref _disableInProcNode);
translator.Translate(ref _enableNodeReuse);
translator.Translate(ref _enableRarNode);
translator.TranslateProjectPropertyInstanceDictionary(ref _environmentProperties);
/* No forwarding logger information sent here - that goes with the node configuration */
translator.TranslateProjectPropertyInstanceDictionary(ref _globalProperties);
Expand Down
79 changes: 79 additions & 0 deletions src/Build/BackEnd/Components/Communications/RarNodeLauncher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// 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.IO;
using System.Linq;
using Microsoft.Build.Internal;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;

namespace Microsoft.Build.BackEnd
{
internal sealed class RarNodeLauncher
{
private readonly INodeLauncher _nodeLauncher;

private readonly string _pipeName;

internal RarNodeLauncher(INodeLauncher nodeLauncher)
{
_nodeLauncher = nodeLauncher;
_pipeName = NamedPipeUtil.GetRarNodePipeName(new(HandshakeOptions.None));
}

/// <summary>
/// Creates a new MSBuild process with the RAR nodemode.
/// </summary>
public bool Start()
{
if (IsRarNodeRunning())
{
CommunicationsUtilities.Trace("Existing RAR node found.");
return true;
}

CommunicationsUtilities.Trace("Launching RAR node...");

try
{
LaunchNode();
}
catch (NodeFailedToLaunchException ex)
{
CommunicationsUtilities.Trace("Failed to launch RAR node: {0}", ex);
return false;
}

return true;
}

private bool IsRarNodeRunning()
{
// Determine if the node is running by checking if the expected named pipe exists.
if (NativeMethodsShared.IsWindows)
{
const string NamedPipeRoot = @"\\.\pipe\";

// File.Exists() will crash the pipe server, as the underlying Windows APIs have undefined behavior
// when used with pipe objects. Enumerating the pipe directory avoids this issue.
IEnumerable<string> pipeNames = FileSystems.Default.EnumerateFiles(NamedPipeRoot);

return pipeNames.Contains(Path.Combine(NamedPipeRoot, _pipeName));
}
else
{
// On Unix, named pipes are implemented via sockets, and the pipe name is simply the file path.
return FileSystems.Default.FileExists(_pipeName);
}
}

private void LaunchNode()
{
string msbuildLocation = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath;
string commandLineArgs = string.Join(" ", ["/nologo", "/nodemode:3"]);
_ = _nodeLauncher.Start(msbuildLocation, commandLineArgs, nodeId: 0);
}
}
}
2 changes: 2 additions & 0 deletions src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,8 @@ public override bool LogsMessagesOfImportance(MessageImportance importance)
/// <inheritdoc/>
public override bool IsTaskInputLoggingEnabled => _taskHost._host.BuildParameters.LogTaskInputs;

public override bool IsOutOfProcRarNodeEnabled => _taskHost._host.BuildParameters.EnableRarNode;

#if FEATURE_REPORTFILEACCESSES
/// <summary>
/// Reports a file access from a task.
Expand Down
1 change: 1 addition & 0 deletions src/Build/Microsoft.Build.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@
<Compile Include="BackEnd\Components\Communications\NodeInfo.cs" />
<Compile Include="BackEnd\Components\Communications\NodeLauncher.cs" />
<Compile Include="BackEnd\Components\Communications\NodeProviderInProc.cs" />
<Compile Include="BackEnd\Components\Communications\RarNodeLauncher.cs" />
<Compile Include="BackEnd\Components\IBuildComponent.cs" />
<Compile Include="BackEnd\Components\IBuildComponentHost.cs" />
<Compile Include="BackEnd\Components\Scheduler\IScheduler.cs" />
Expand Down
9 changes: 8 additions & 1 deletion src/Framework/EngineServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,19 @@ public abstract class EngineServices
/// </summary>
public const int Version1 = 1;

/// <summary>
/// Version 2 with IsOutOfProcRarNodeEnabled().
/// </summary>
public const int Version2 = 2;

/// <summary>
/// Gets an explicit version of this class.
/// </summary>
/// <remarks>
/// Must be incremented whenever new members are added. Derived classes should override
/// the property to return the version actually being implemented.
/// </remarks>
public virtual int Version => Version1;
public virtual int Version => Version2;

/// <summary>
/// Returns <see langword="true"/> if the given message importance is not guaranteed to be ignored by registered loggers.
Expand All @@ -48,5 +53,7 @@ public abstract class EngineServices
/// This is a performance optimization allowing tasks to skip expensive double-logging.
/// </remarks>
public virtual bool IsTaskInputLoggingEnabled => throw new NotImplementedException();

public virtual bool IsOutOfProcRarNodeEnabled => throw new NotImplementedException();
}
}
6 changes: 6 additions & 0 deletions src/Framework/Traits.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ public Traits()
/// </summary>
public readonly int DictionaryBasedItemRemoveThreshold = ParseIntFromEnvironmentVariableOrDefault("MSBUILDDICTIONARYBASEDITEMREMOVETHRESHOLD", 100);

/// <summary>
/// Launches a persistent RAR process.
/// </summary>
/// TODO: Replace with command line flag when feature is completed. The environment variable is intented to avoid exposing the flag early.
public readonly bool EnableRarNode = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBuildRarNode"));

/// <summary>
/// Name of environment variables used to enable MSBuild server.
/// </summary>
Expand Down
21 changes: 21 additions & 0 deletions src/MSBuild/XMake.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.Debugging;
using Microsoft.Build.Shared.FileSystem;
using Microsoft.Build.Tasks.AssemblyDependency;
using BinaryLogger = Microsoft.Build.Logging.BinaryLogger;
using ConsoleLogger = Microsoft.Build.Logging.ConsoleLogger;
using FileLogger = Microsoft.Build.Logging.FileLogger;
Expand Down Expand Up @@ -1529,6 +1530,11 @@ internal static bool BuildProject(
}
}

if (Traits.Instance.EnableRarNode)
{
parameters.EnableRarNode = true;
}

List<BuildManager.DeferredBuildMessage> messagesToLogInBuildLoggers = new();

BuildManager buildManager = BuildManager.DefaultBuildManager;
Expand Down Expand Up @@ -3432,6 +3438,21 @@ private static void StartLocalNode(CommandLineSwitches commandLineSwitches, bool
OutOfProcTaskHostNode node = new OutOfProcTaskHostNode();
shutdownReason = node.Run(out nodeException);
}
else if (nodeModeNumber == 3)
{
// The RAR service persists between builds, and will continue to process requests until terminated.
OutOfProcRarNode rarNode = new();
RarNodeShutdownReason rarShutdownReason = rarNode.Run(out nodeException, s_buildCancellationSource.Token);

shutdownReason = rarShutdownReason switch
{
RarNodeShutdownReason.Complete => NodeEngineShutdownReason.BuildComplete,
RarNodeShutdownReason.Error => NodeEngineShutdownReason.Error,
RarNodeShutdownReason.AlreadyRunning => NodeEngineShutdownReason.Error,
RarNodeShutdownReason.ConnectionTimedOut => NodeEngineShutdownReason.ConnectionFailed,
_ => throw new ArgumentOutOfRangeException(nameof(rarShutdownReason), $"Unexpected value: {rarShutdownReason}"),
};
}
else if (nodeModeNumber == 8)
{
// Since build function has to reuse code from *this* class and OutOfProcServerNode is in different assembly
Expand Down
18 changes: 14 additions & 4 deletions src/Shared/CommunicationsUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,12 @@ internal class Handshake
protected readonly int fileVersionPrivate;
private readonly int sessionId;

protected internal Handshake(HandshakeOptions nodeType)
internal Handshake(HandshakeOptions nodeType)
: this(nodeType, includeSessionId: true)
{
}

protected Handshake(HandshakeOptions nodeType, bool includeSessionId)
{
const int handshakeVersion = (int)CommunicationsUtilities.handshakeVersion;

Expand All @@ -110,8 +115,13 @@ protected internal Handshake(HandshakeOptions nodeType)
fileVersionMinor = fileVersion.Minor;
fileVersionBuild = fileVersion.Build;
fileVersionPrivate = fileVersion.Revision;
using Process currentProcess = Process.GetCurrentProcess();
sessionId = currentProcess.SessionId;

// This reaches out to NtQuerySystemInformation. Due to latency, allow skipping for derived handshake if unused.
if (includeSessionId)
{
using Process currentProcess = Process.GetCurrentProcess();
sessionId = currentProcess.SessionId;
}
}

// This is used as a key, so it does not need to be human readable.
Expand Down Expand Up @@ -149,7 +159,7 @@ internal sealed class ServerNodeHandshake : Handshake
public override byte? ExpectedVersionInFirstByte => null;

internal ServerNodeHandshake(HandshakeOptions nodeType)
: base(nodeType)
: base(nodeType, includeSessionId: false)
{
}

Expand Down
10 changes: 10 additions & 0 deletions src/Shared/INodePacket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,16 @@ internal enum NodePacketType : byte
/// </summary>
ProcessReport,

/// <summary>
/// A request contains the inputs to the RAR task.
/// </summary>
RarNodeExecuteRequest,

/// <summary>
/// A request contains the outputs and log events of a completed RAR task.
/// </summary>
RarNodeExecuteResponse,

/// <summary>
/// Command in form of MSBuild command line for server node - MSBuild Server.
/// Keep this enum value constant intact as this is part of contract with dotnet CLI
Expand Down
7 changes: 7 additions & 0 deletions src/Shared/NamedPipeUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO;
using Microsoft.Build.Internal;

namespace Microsoft.Build.Shared
{
Expand Down Expand Up @@ -42,5 +43,11 @@ internal static string GetPlatformSpecificPipeName(string pipeName)
return pipeName;
}
}

internal static string GetRarNodePipeName(ServerNodeHandshake handshake)
=> GetPlatformSpecificPipeName($"MSBuildRarNode-{handshake.ComputeHash()}");

internal static string GetRarNodeEndpointPipeName(ServerNodeHandshake handshake)
=> GetPlatformSpecificPipeName($"MSBuildRarNodeEndpoint-{handshake.ComputeHash()}");
}
}
8 changes: 8 additions & 0 deletions src/Shared/NodePipeBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ protected NodePipeBase(string pipeName, Handshake handshake)
_writeTranslator = BinaryTranslator.GetWriteTranslator(_writeBuffer);
}

/// <summary>
/// Gets a value indicating whether the pipe is in the connected state. Note that this is not real-time and
/// will only be updated when an operation on the pipe fails.
/// When a pipe is broken, Disconnect() must be called for the pipe to be reused - otherwise any attempts to
/// connect to a new client will throw.
/// </summary>
internal bool IsConnected => NodeStream.IsConnected;

protected abstract PipeStream NodeStream { get; }

protected string PipeName { get; }
Expand Down
9 changes: 8 additions & 1 deletion src/Shared/NodePipeServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,14 @@ internal NodePipeServer(string pipeName, Handshake handshake, int maxNumberOfSer
// SIDs or the client will reject this server. This is used to avoid attacks where a
// hacked server creates a less restricted pipe in an attempt to lure us into using it and
// then sending build requests to the real pipe client (which is the MSBuild Build Manager.)
PipeAccessRule rule = new(WindowsIdentity.GetCurrent().Owner, PipeAccessRights.ReadWrite, AccessControlType.Allow);
PipeAccessRights pipeAccessRights = PipeAccessRights.ReadWrite;
if (maxNumberOfServerInstances > 1)
{
// Multi-instance pipes will fail without this flag.
pipeAccessRights |= PipeAccessRights.CreateNewInstance;
}

PipeAccessRule rule = new(WindowsIdentity.GetCurrent().Owner, pipeAccessRights, AccessControlType.Allow);
PipeSecurity security = new();
security.AddAccessRule(rule);
security.SetOwner(rule.IdentityReference);
Expand Down
Loading