|
// 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.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using Microsoft.Build.BackEnd;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.BackEnd.SdkResolution;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Eventing;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Experimental;
using Microsoft.Build.Experimental.BuildCheck;
using Microsoft.Build.Experimental.BuildCheck.Infrastructure;
using Microsoft.Build.Experimental.ProjectCache;
using Microsoft.Build.FileAccesses;
using Microsoft.Build.Framework;
using Microsoft.Build.Framework.Telemetry;
using Microsoft.Build.Graph;
using Microsoft.Build.Internal;
using Microsoft.Build.Logging;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.Debugging;
using Microsoft.NET.StringTools;
using ExceptionHandling = Microsoft.Build.Shared.ExceptionHandling;
using ForwardingLoggerRecord = Microsoft.Build.Logging.ForwardingLoggerRecord;
using LoggerDescription = Microsoft.Build.Logging.LoggerDescription;
namespace Microsoft.Build.Execution
{
/// <summary>
/// This class is the public entry point for executing builds.
/// </summary>
[SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Refactoring at the end of Beta1 is not appropriate.")]
public class BuildManager : INodePacketHandler, IBuildComponentHost, IDisposable
{
// TODO: Figure out a more elegant way to do this.
// The rationale for this is that we can detect during design-time builds in the Evaluator (which populates this) that the project cache will be used so that we don't
// need to evaluate the project at build time just to figure that out, which would regress perf for scenarios which don't use the project cache.
internal static ConcurrentDictionary<ProjectCacheDescriptor, ProjectCacheDescriptor> ProjectCacheDescriptors { get; } = new(ProjectCacheDescriptorEqualityComparer.Instance);
/// <summary>
/// The object used for thread-safe synchronization of static members.
/// </summary>
private static readonly Object s_staticSyncLock = new Object();
/// <summary>
/// The object used for thread-safe synchronization of BuildManager shared data and the Scheduler.
/// </summary>
private readonly Object _syncLock = new Object();
/// <summary>
/// The singleton instance for the BuildManager.
/// </summary>
private static BuildManager? s_singletonInstance;
/// <summary>
/// The next build id;
/// </summary>
private static int s_nextBuildId;
/// <summary>
/// The next build request configuration ID to use.
/// These must be unique across build managers, as they
/// are used as part of cache file names, for example.
/// </summary>
private static int s_nextBuildRequestConfigurationId;
/// <summary>
/// The cache for build request configurations.
/// </summary>
private IConfigCache? _configCache;
/// <summary>
/// The cache for build results.
/// </summary>
private IResultsCache? _resultsCache;
/// <summary>
/// The object responsible for creating and managing nodes.
/// </summary>
private INodeManager? _nodeManager;
/// <summary>
/// The object responsible for creating and managing task host nodes.
/// </summary>
private INodeManager? _taskHostNodeManager;
/// <summary>
/// The object which determines which projects to build, and where.
/// </summary>
private IScheduler? _scheduler;
/// <summary>
/// The node configuration to use for spawning new nodes.
/// </summary>
private NodeConfiguration? _nodeConfiguration;
/// <summary>
/// Any exception which occurs on a logging thread will go here.
/// </summary>
private ExceptionDispatchInfo? _threadException;
/// <summary>
/// Set of active nodes in the system.
/// </summary>
private readonly HashSet<int> _activeNodes;
/// <summary>
/// Event signalled when all nodes have shutdown.
/// </summary>
private AutoResetEvent? _noNodesActiveEvent;
/// <summary>
/// Mapping of nodes to the configurations they know about.
/// </summary>
private readonly Dictionary<int, HashSet<int>> _nodeIdToKnownConfigurations;
/// <summary>
/// Flag indicating if we are currently shutting down. When set, we stop processing packets other than NodeShutdown.
/// </summary>
private bool _shuttingDown;
/// <summary>
/// CancellationTokenSource to use for async operations. This will be cancelled when we are shutting down to cancel any async operations.
/// </summary>
private CancellationTokenSource? _executionCancellationTokenSource;
/// <summary>
/// The current state of the BuildManager.
/// </summary>
private BuildManagerState _buildManagerState;
/// <summary>
/// The name given to this BuildManager as the component host.
/// </summary>
private readonly string _hostName;
/// <summary>
/// The parameters with which the build was started.
/// </summary>
private BuildParameters? _buildParameters;
/// <summary>
/// The current pending and active submissions.
/// </summary>
/// <remarks>
/// { submissionId, BuildSubmission }
/// </remarks>
private readonly Dictionary<int, BuildSubmissionBase> _buildSubmissions;
/// <summary>
/// Event signalled when all build submissions are complete.
/// </summary>
private AutoResetEvent? _noActiveSubmissionsEvent;
/// <summary>
/// The overall success of the build.
/// </summary>
private bool _overallBuildSuccess;
/// <summary>
/// The next build submission id.
/// </summary>
private int _nextBuildSubmissionId;
/// <summary>
/// The last BuildParameters used for building.
/// </summary>
private bool? _previousLowPriority = null;
/// <summary>
/// Mapping of unnamed project instances to the file names assigned to them.
/// </summary>
private readonly Dictionary<ProjectInstance, string> _unnamedProjectInstanceToNames;
/// <summary>
/// The next ID to assign to a project which has no name.
/// </summary>
private int _nextUnnamedProjectId;
/// <summary>
/// The build component factories.
/// </summary>
private readonly BuildComponentFactoryCollection _componentFactories;
/// <summary>
/// Mapping of submission IDs to their first project started events.
/// </summary>
private readonly Dictionary<int, BuildEventArgs> _projectStartedEvents;
/// <summary>
/// Whether a cache has been provided by a project instance, meaning
/// we've acquired at least one build submission that included a project instance.
/// Once that has happened, we use the provided one, rather than our default.
/// </summary>
private bool _acquiredProjectRootElementCacheFromProjectInstance;
/// <summary>
/// The project started event handler
/// </summary>
private readonly ProjectStartedEventHandler _projectStartedEventHandler;
/// <summary>
/// The project finished event handler
/// </summary>
private readonly ProjectFinishedEventHandler _projectFinishedEventHandler;
/// <summary>
/// The logging exception event handler
/// </summary>
private readonly LoggingExceptionDelegate _loggingThreadExceptionEventHandler;
/// <summary>
/// Legacy threading semantic data associated with this build manager.
/// </summary>
private readonly LegacyThreadingData _legacyThreadingData;
/// <summary>
/// The worker queue.
/// </summary>
private ActionBlock<Action>? _workQueue;
/// <summary>
/// Flag indicating we have disposed.
/// </summary>
private bool _disposed;
/// <summary>
/// When the BuildManager was created.
/// </summary>
private DateTime _instantiationTimeUtc;
/// <summary>
/// Messages to be logged
/// </summary>
private IEnumerable<DeferredBuildMessage>? _deferredBuildMessages;
/// <summary>
/// Build telemetry to be send when this build ends.
/// <remarks>Could be null</remarks>
/// </summary>
private BuildTelemetry? _buildTelemetry;
private ProjectCacheService? _projectCacheService;
private bool _hasProjectCacheServiceInitializedVsScenario;
#if DEBUG
/// <summary>
/// <code>true</code> to wait for a debugger to be attached, otherwise <code>false</code>.
/// </summary>
[SuppressMessage("ApiDesign",
"RS0016:Add public types and members to the declared API",
Justification = "Only available in the Debug configuration.")]
public static bool WaitForDebugger { get; set; }
#endif
/// <summary>
/// Creates a new unnamed build manager.
/// Normally there is only one build manager in a process, and it is the default build manager.
/// Access it with <see cref="BuildManager.DefaultBuildManager"/>
/// </summary>
public BuildManager()
: this("Unnamed")
{
}
/// <summary>
/// Creates a new build manager with an arbitrary distinct name.
/// Normally there is only one build manager in a process, and it is the default build manager.
/// Access it with <see cref="BuildManager.DefaultBuildManager"/>
/// </summary>
public BuildManager(string hostName)
{
ErrorUtilities.VerifyThrowArgumentNull(hostName);
_hostName = hostName;
_buildManagerState = BuildManagerState.Idle;
_buildSubmissions = new Dictionary<int, BuildSubmissionBase>();
_noActiveSubmissionsEvent = new AutoResetEvent(true);
_activeNodes = new HashSet<int>();
_noNodesActiveEvent = new AutoResetEvent(true);
_nodeIdToKnownConfigurations = new Dictionary<int, HashSet<int>>();
_unnamedProjectInstanceToNames = new Dictionary<ProjectInstance, string>();
_nextUnnamedProjectId = 1;
_componentFactories = new BuildComponentFactoryCollection(this);
_componentFactories.RegisterDefaultFactories();
SerializationContractInitializer.Initialize();
_projectStartedEvents = new Dictionary<int, BuildEventArgs>();
_projectStartedEventHandler = OnProjectStarted;
_projectFinishedEventHandler = OnProjectFinished;
_loggingThreadExceptionEventHandler = OnLoggingThreadException;
_legacyThreadingData = new LegacyThreadingData();
_instantiationTimeUtc = DateTime.UtcNow;
}
/// <summary>
/// Finalizes an instance of the <see cref="BuildManager"/> class.
/// </summary>
~BuildManager()
{
Dispose(false /* disposing */);
}
/// <summary>
/// Enumeration describing the current state of the build manager.
/// </summary>
private enum BuildManagerState
{
/// <summary>
/// This is the default state. <see cref="BeginBuild(BuildParameters)"/> may be called in this state. All other methods raise InvalidOperationException
/// </summary>
Idle,
/// <summary>
/// This is the state the BuildManager is in after <see cref="BeginBuild(BuildParameters)"/> has been called but before <see cref="EndBuild()"/> has been called.
/// <see cref="BuildManager.PendBuildRequest(Microsoft.Build.Execution.BuildRequestData)"/>, <see cref="BuildManager.BuildRequest(Microsoft.Build.Execution.BuildRequestData)"/>, <see cref="BuildManager.PendBuildRequest(GraphBuildRequestData)"/>, <see cref="BuildManager.BuildRequest(GraphBuildRequestData)"/>, and <see cref="BuildManager.EndBuild()"/> may be called in this state.
/// </summary>
Building,
/// <summary>
/// This is the state the BuildManager is in after <see cref="BuildManager.EndBuild()"/> has been called but before all existing submissions have completed.
/// </summary>
WaitingForBuildToComplete
}
/// <summary>
/// Gets the singleton instance of the Build Manager.
/// </summary>
public static BuildManager DefaultBuildManager
{
get
{
if (s_singletonInstance == null)
{
lock (s_staticSyncLock)
{
if (s_singletonInstance == null)
{
s_singletonInstance = new BuildManager("Default");
}
}
}
return s_singletonInstance;
}
}
/// <summary>
/// Retrieves a hosted<see cref="ISdkResolverService"/> instance for resolving SDKs.
/// </summary>
private ISdkResolverService SdkResolverService => ((this as IBuildComponentHost).GetComponent(BuildComponentType.SdkResolverService) as ISdkResolverService)!;
/// <summary>
/// Retrieves the logging service associated with a particular build
/// </summary>
/// <returns>The logging service.</returns>
ILoggingService IBuildComponentHost.LoggingService => _componentFactories.GetComponent<ILoggingService>(BuildComponentType.LoggingService);
/// <summary>
/// Retrieves the name of the component host.
/// </summary>
string IBuildComponentHost.Name => _hostName;
/// <summary>
/// Retrieves the build parameters associated with this build.
/// </summary>
/// <returns>The build parameters.</returns>
BuildParameters? IBuildComponentHost.BuildParameters => _buildParameters;
/// <summary>
/// Retrieves the LegacyThreadingData associated with a particular build manager
/// </summary>
LegacyThreadingData IBuildComponentHost.LegacyThreadingData => _legacyThreadingData;
/// <summary>
/// <see cref="BuildManager.BeginBuild(BuildParameters,IEnumerable{DeferredBuildMessage})"/>
/// </summary>
public readonly struct DeferredBuildMessage
{
public MessageImportance Importance { get; }
public string Text { get; }
public string? FilePath { get; }
public DeferredBuildMessage(string text, MessageImportance importance)
{
Importance = importance;
Text = text;
FilePath = null;
}
public DeferredBuildMessage(string text, MessageImportance importance, string filePath)
{
Importance = importance;
Text = text;
FilePath = filePath;
}
}
/// <summary>
/// Prepares the BuildManager to receive build requests.
/// </summary>
/// <param name="parameters">The build parameters. May be null.</param>
/// <param name="deferredBuildMessages"> Build messages to be logged before the build begins. </param>
/// <exception cref="InvalidOperationException">Thrown if a build is already in progress.</exception>
public void BeginBuild(BuildParameters parameters, IEnumerable<DeferredBuildMessage> deferredBuildMessages)
{
// TEMP can be modified from the environment. Most of Traits is lasts for the duration of the process (with a manual reset for tests)
// and environment variables we use as properties are stored in a dictionary at the beginning of the build, so they also cannot be
// changed during a build. Some of our older stuff uses live environment variable checks. The TEMP directory previously used a live
// environment variable check, but it now uses a cached value. Nevertheless, we should support changing it between builds, so reset
// it here in case the user is using Visual Studio or the MSBuild server, as those each last for multiple builds without changing
// BuildManager.
FileUtilities.ClearTempFileDirectory();
// deferredBuildMessages cannot be an optional parameter on a single BeginBuild method because it would break binary compatibility.
_deferredBuildMessages = deferredBuildMessages;
BeginBuild(parameters);
_deferredBuildMessages = null;
}
private void UpdatePriority(Process p, ProcessPriorityClass priority)
{
try
{
p.PriorityClass = priority;
}
catch (Win32Exception) { }
}
/// <summary>
/// Prepares the BuildManager to receive build requests.
/// </summary>
/// <param name="parameters">The build parameters. May be null.</param>
/// <exception cref="InvalidOperationException">Thrown if a build is already in progress.</exception>
public void BeginBuild(BuildParameters parameters)
{
if (_previousLowPriority != null)
{
if (parameters.LowPriority != _previousLowPriority)
{
if (NativeMethodsShared.IsWindows || parameters.LowPriority)
{
ProcessPriorityClass priority = parameters.LowPriority ? ProcessPriorityClass.BelowNormal : ProcessPriorityClass.Normal;
IEnumerable<Process>? processes = _nodeManager?.GetProcesses();
if (processes is not null)
{
foreach (Process p in processes)
{
UpdatePriority(p, priority);
}
}
processes = _taskHostNodeManager?.GetProcesses();
if (processes is not null)
{
foreach (Process p in processes)
{
UpdatePriority(p, priority);
}
}
}
else
{
_nodeManager?.ShutdownAllNodes();
_taskHostNodeManager?.ShutdownAllNodes();
}
}
}
_previousLowPriority = parameters.LowPriority;
if (Traits.Instance.DebugEngine)
{
parameters.DetailedSummary = true;
parameters.LogTaskInputs = true;
}
lock (_syncLock)
{
AttachDebugger();
// Check for build in progress.
RequireState(BuildManagerState.Idle, "BuildInProgress");
MSBuildEventSource.Log.BuildStart();
// Initiate build telemetry data
DateTime now = DateTime.UtcNow;
// Acquire it from static variable so we can apply data collected up to this moment
_buildTelemetry = KnownTelemetry.PartialBuildTelemetry;
if (_buildTelemetry != null)
{
KnownTelemetry.PartialBuildTelemetry = null;
}
else
{
_buildTelemetry = new()
{
StartAt = now,
};
}
_buildTelemetry.InnerStartAt = now;
if (BuildParameters.DumpOpportunisticInternStats)
{
Strings.EnableDiagnostics();
}
_executionCancellationTokenSource = new CancellationTokenSource();
_overallBuildSuccess = true;
// Clone off the build parameters.
_buildParameters = parameters?.Clone() ?? new BuildParameters();
// Initialize additional build parameters.
_buildParameters.BuildId = GetNextBuildId();
if (_buildParameters.UsesCachedResults() && _buildParameters.ProjectIsolationMode == ProjectIsolationMode.False)
{
// If input or output caches are used and the project isolation mode is set to
// ProjectIsolationMode.False, then set it to ProjectIsolationMode.True. The explicit
// condition on ProjectIsolationMode is necessary to ensure that, if we're using input
// or output caches and ProjectIsolationMode is set to ProjectIsolationMode.MessageUponIsolationViolation,
// ProjectIsolationMode isn't changed to ProjectIsolationMode.True.
_buildParameters.ProjectIsolationMode = ProjectIsolationMode.True;
}
if (_buildParameters.UsesOutputCache() && string.IsNullOrWhiteSpace(_buildParameters.OutputResultsCacheFile))
{
_buildParameters.OutputResultsCacheFile = FileUtilities.NormalizePath("msbuild-cache");
}
#if FEATURE_REPORTFILEACCESSES
if (_buildParameters.ReportFileAccesses)
{
EnableDetouredNodeLauncher();
}
#endif
// Initialize components.
_nodeManager = ((IBuildComponentHost)this).GetComponent(BuildComponentType.NodeManager) as INodeManager;
var loggingService = InitializeLoggingService();
// Log deferred messages and response files
LogDeferredMessages(loggingService, _deferredBuildMessages);
// Log if BuildCheck is enabled
if (_buildParameters.IsBuildCheckEnabled)
{
loggingService.LogComment(buildEventContext: BuildEventContext.Invalid, MessageImportance.Normal, "BuildCheckEnabled");
}
// Log known deferred telemetry
loggingService.LogTelemetry(buildEventContext: null, KnownTelemetry.LoggingConfigurationTelemetry.EventName, KnownTelemetry.LoggingConfigurationTelemetry.GetProperties());
InitializeCaches();
#if FEATURE_REPORTFILEACCESSES
var fileAccessManager = ((IBuildComponentHost)this).GetComponent<IFileAccessManager>(BuildComponentType.FileAccessManager);
#endif
_projectCacheService = new ProjectCacheService(
this,
loggingService,
#if FEATURE_REPORTFILEACCESSES
fileAccessManager,
#endif
_configCache!,
_buildParameters.ProjectCacheDescriptor);
_taskHostNodeManager = ((IBuildComponentHost)this).GetComponent<INodeManager>(BuildComponentType.TaskHostNodeManager);
_scheduler = ((IBuildComponentHost)this).GetComponent<IScheduler>(BuildComponentType.Scheduler);
_nodeManager!.RegisterPacketHandler(NodePacketType.BuildRequestBlocker, BuildRequestBlocker.FactoryForDeserialization, this);
_nodeManager.RegisterPacketHandler(NodePacketType.BuildRequestConfiguration, BuildRequestConfiguration.FactoryForDeserialization, this);
_nodeManager.RegisterPacketHandler(NodePacketType.BuildRequestConfigurationResponse, BuildRequestConfigurationResponse.FactoryForDeserialization, this);
_nodeManager.RegisterPacketHandler(NodePacketType.BuildResult, BuildResult.FactoryForDeserialization, this);
_nodeManager.RegisterPacketHandler(NodePacketType.FileAccessReport, FileAccessReport.FactoryForDeserialization, this);
_nodeManager.RegisterPacketHandler(NodePacketType.NodeShutdown, NodeShutdown.FactoryForDeserialization, this);
_nodeManager.RegisterPacketHandler(NodePacketType.ProcessReport, ProcessReport.FactoryForDeserialization, this);
_nodeManager.RegisterPacketHandler(NodePacketType.ResolveSdkRequest, SdkResolverRequest.FactoryForDeserialization, SdkResolverService as INodePacketHandler);
_nodeManager.RegisterPacketHandler(NodePacketType.ResourceRequest, ResourceRequest.FactoryForDeserialization, this);
if (_threadException != null)
{
ShutdownLoggingService(loggingService);
_threadException.Throw();
}
if (_workQueue == null)
{
_workQueue = new ActionBlock<Action>(action => ProcessWorkQueue(action));
}
_buildManagerState = BuildManagerState.Building;
_noActiveSubmissionsEvent!.Set();
_noNodesActiveEvent!.Set();
}
ILoggingService InitializeLoggingService()
{
ILoggingService loggingService = CreateLoggingService(
AppendDebuggingLoggers(_buildParameters.Loggers),
_buildParameters.ForwardingLoggers,
_buildParameters.WarningsAsErrors,
_buildParameters.WarningsNotAsErrors,
_buildParameters.WarningsAsMessages);
_nodeManager!.RegisterPacketHandler(NodePacketType.LogMessage, LogMessagePacket.FactoryForDeserialization, loggingService as INodePacketHandler);
try
{
loggingService.LogBuildStarted();
if (_buildParameters.UsesInputCaches())
{
loggingService.LogComment(BuildEventContext.Invalid, MessageImportance.Normal, "UsingInputCaches", string.Join(";", _buildParameters.InputResultsCacheFiles));
}
if (_buildParameters.UsesOutputCache())
{
loggingService.LogComment(BuildEventContext.Invalid, MessageImportance.Normal, "WritingToOutputCache", _buildParameters.OutputResultsCacheFile);
}
}
catch (Exception)
{
ShutdownLoggingService(loggingService);
throw;
}
return loggingService;
}
// VS builds discard many msbuild events so attach a binlogger to capture them all.
IEnumerable<ILogger> AppendDebuggingLoggers(IEnumerable<ILogger> loggers)
{
if (DebugUtils.ShouldDebugCurrentProcess is false ||
Traits.Instance.DebugEngine is false)
{
return loggers;
}
var binlogPath = DebugUtils.FindNextAvailableDebugFilePath($"{DebugUtils.ProcessInfoString}_BuildManager_{_hostName}.binlog");
var logger = new BinaryLogger { Parameters = binlogPath };
return (loggers ?? [logger]);
}
void InitializeCaches()
{
Debug.Assert(Monitor.IsEntered(_syncLock));
var usesInputCaches = _buildParameters.UsesInputCaches();
if (usesInputCaches)
{
ReuseOldCaches(_buildParameters.InputResultsCacheFiles);
}
_configCache = ((IBuildComponentHost)this).GetComponent<IConfigCache>(BuildComponentType.ConfigCache);
_resultsCache = ((IBuildComponentHost)this).GetComponent<IResultsCache>(BuildComponentType.ResultsCache);
if (!usesInputCaches && (_buildParameters.ResetCaches || _configCache!.IsConfigCacheSizeLargerThanThreshold()))
{
ResetCaches();
}
else
{
if (!usesInputCaches)
{
List<int> configurationsCleared = _configCache!.ClearNonExplicitlyLoadedConfigurations();
if (configurationsCleared != null)
{
foreach (int configurationId in configurationsCleared)
{
_resultsCache!.ClearResultsForConfiguration(configurationId);
}
}
}
foreach (var config in _configCache!)
{
config.ResultsNodeId = Scheduler.InvalidNodeId;
}
_buildParameters.ProjectRootElementCache.DiscardImplicitReferences();
}
}
}
#if FEATURE_REPORTFILEACCESSES
/// <summary>
/// Configure the build to use I/O tracking for nodes.
/// </summary>
/// <remarks>
/// Must be a separate non-inlinable method to avoid loading the BuildXL assembly when not opted in.
/// </remarks>
[MethodImpl(MethodImplOptions.NoInlining)]
private void EnableDetouredNodeLauncher()
{
// Currently BuildXL only supports x64. Once this feature moves out of the experimental phase, this will need to be addressed.
ErrorUtilities.VerifyThrowInvalidOperation(NativeMethodsShared.ProcessorArchitecture == NativeMethodsShared.ProcessorArchitectures.X64, "ReportFileAccessesX64Only");
// To properly report file access, we need to disable the in-proc node which won't be detoured.
_buildParameters!.DisableInProcNode = true;
// Node reuse must be disabled as future builds will not be able to listen to events raised by detours.
_buildParameters.EnableNodeReuse = false;
_componentFactories.ReplaceFactory(BuildComponentType.NodeLauncher, DetouredNodeLauncher.CreateComponent);
}
#endif
private static void AttachDebugger()
{
if (Debugger.IsAttached)
{
return;
}
if (!DebugUtils.ShouldDebugCurrentProcess)
{
return;
}
switch (Environment.GetEnvironmentVariable("MSBuildDebugBuildManagerOnStart"))
{
#if FEATURE_DEBUG_LAUNCH
case "1":
Debugger.Launch();
break;
#endif
case "2":
// Sometimes easier to attach rather than deal with JIT prompt
Process currentProcess = Process.GetCurrentProcess();
Console.WriteLine($"Waiting for debugger to attach ({currentProcess.MainModule!.FileName} PID {currentProcess.Id}). Press enter to continue...");
Console.ReadLine();
break;
}
}
/// <summary>
/// Cancels all outstanding submissions asynchronously.
/// </summary>
public void CancelAllSubmissions()
{
CancelAllSubmissions(true);
}
private void CancelAllSubmissions(bool async)
{
ILoggingService loggingService = ((IBuildComponentHost)this).LoggingService;
loggingService.LogBuildCanceled();
var parentThreadCulture = _buildParameters != null
? _buildParameters.Culture
: CultureInfo.CurrentCulture;
var parentThreadUICulture = _buildParameters != null
? _buildParameters.UICulture
: CultureInfo.CurrentUICulture;
void Callback(object? state)
{
lock (_syncLock)
{
// If the state is Idle - then there is yet or already nothing to cancel
// If state is WaitingForBuildToComplete - we might be already waiting gracefully - but CancelAllSubmissions
// is a request for quick abort - so it's fine to resubmit the request
if (_buildManagerState == BuildManagerState.Idle)
{
return;
}
_overallBuildSuccess = false;
foreach (BuildSubmissionBase submission in _buildSubmissions.Values)
{
if (submission.IsStarted)
{
BuildResultBase buildResult = submission.CompleteResultsWithException(new BuildAbortedException());
if (buildResult is BuildResult result)
{
_resultsCache!.AddResult(result);
}
}
}
ShutdownConnectedNodes(true /* abort */);
CheckForActiveNodesAndCleanUpSubmissions();
}
}
ThreadPoolExtensions.QueueThreadPoolWorkItemWithCulture(Callback, parentThreadCulture, parentThreadUICulture);
}
/// <summary>
/// Clears out all of the cached information.
/// </summary>
public void ResetCaches()
{
lock (_syncLock)
{
ErrorIfState(BuildManagerState.WaitingForBuildToComplete, "WaitingForEndOfBuild");
ErrorIfState(BuildManagerState.Building, "BuildInProgress");
_configCache = ((IBuildComponentHost)this).GetComponent<IConfigCache>(BuildComponentType.ConfigCache);
_resultsCache = ((IBuildComponentHost)this).GetComponent<IResultsCache>(BuildComponentType.ResultsCache);
_resultsCache!.ClearResults();
// This call clears out the directory.
_configCache!.ClearConfigurations();
_buildParameters?.ProjectRootElementCache.DiscardImplicitReferences();
}
}
/// <summary>
/// This methods requests the BuildManager to find a matching ProjectInstance in its cache of previously-built projects.
/// If none exist, a new instance will be created from the specified project.
/// </summary>
/// <param name="project">The Project for which an instance should be retrieved.</param>
/// <returns>The instance.</returns>
public ProjectInstance GetProjectInstanceForBuild(Project project)
{
lock (_syncLock)
{
_configCache = ((IBuildComponentHost)this).GetComponent(BuildComponentType.ConfigCache) as IConfigCache;
BuildRequestConfiguration configuration = _configCache!.GetMatchingConfiguration(
new ConfigurationMetadata(project),
(config, loadProject) => CreateConfiguration(project, config),
loadProject: true);
ErrorUtilities.VerifyThrow(configuration.Project != null, "Configuration should have been loaded.");
return configuration.Project!;
}
}
/// <summary>
/// Submits a build request to the current build but does not start it immediately. Allows the user to
/// perform asynchronous execution or access the submission ID prior to executing the request.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if StartBuild has not been called or if EndBuild has been called.</exception>
public BuildSubmission PendBuildRequest(BuildRequestData requestData)
=> (BuildSubmission)PendBuildRequest<BuildRequestData, BuildResult>(requestData);
/// <summary>
/// Submits a graph build request to the current build but does not start it immediately. Allows the user to
/// perform asynchronous execution or access the submission ID prior to executing the request.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if StartBuild has not been called or if EndBuild has been called.</exception>
public GraphBuildSubmission PendBuildRequest(GraphBuildRequestData requestData)
=> (GraphBuildSubmission)PendBuildRequest<GraphBuildRequestData, GraphBuildResult>(requestData);
/// <summary>
/// Submits a build request to the current build but does not start it immediately. Allows the user to
/// perform asynchronous execution or access the submission ID prior to executing the request.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if StartBuild has not been called or if EndBuild has been called.</exception>
private BuildSubmissionBase<TRequestData, TResultData> PendBuildRequest<TRequestData, TResultData>(
TRequestData requestData)
where TRequestData : BuildRequestData<TRequestData, TResultData>
where TResultData : BuildResultBase
{
lock (_syncLock)
{
ErrorUtilities.VerifyThrowArgumentNull(requestData);
ErrorIfState(BuildManagerState.WaitingForBuildToComplete, "WaitingForEndOfBuild");
ErrorIfState(BuildManagerState.Idle, "NoBuildInProgress");
VerifyStateInternal(BuildManagerState.Building);
var newSubmission = requestData.CreateSubmission(this, GetNextSubmissionId(), requestData,
_buildParameters!.LegacyThreadingSemantics);
if (_buildTelemetry != null)
{
// Project graph can have multiple entry points, for purposes of identifying event for same build project,
// we believe that including only one entry point will provide enough precision.
_buildTelemetry.Project ??= requestData.EntryProjectsFullPath.FirstOrDefault();
_buildTelemetry.Target ??= string.Join(",", requestData.TargetNames);
}
_buildSubmissions.Add(newSubmission.SubmissionId, newSubmission);
_noActiveSubmissionsEvent!.Reset();
return newSubmission;
}
}
private TResultData BuildRequest<TRequestData, TResultData>(TRequestData requestData)
where TRequestData : BuildRequestData<TRequestData, TResultData>
where TResultData : BuildResultBase
=> PendBuildRequest<TRequestData, TResultData>(requestData).Execute();
/// <summary>
/// Convenience method. Submits a build request and blocks until the results are available.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if StartBuild has not been called or if EndBuild has been called.</exception>
public BuildResult BuildRequest(BuildRequestData requestData)
=> BuildRequest<BuildRequestData, BuildResult>(requestData);
/// <summary>
/// Convenience method. Submits a graph build request and blocks until the results are available.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if StartBuild has not been called or if EndBuild has been called.</exception>
public GraphBuildResult BuildRequest(GraphBuildRequestData requestData)
=> BuildRequest<GraphBuildRequestData, GraphBuildResult>(requestData);
/// <summary>
/// Signals that no more build requests are expected (or allowed) and the BuildManager may clean up.
/// </summary>
/// <remarks>
/// This call blocks until all currently pending requests are complete.
/// </remarks>
/// <exception cref="InvalidOperationException">Thrown if there is no build in progress.</exception>
public void EndBuild()
{
lock (_syncLock)
{
ErrorIfState(BuildManagerState.WaitingForBuildToComplete, "WaitingForEndOfBuild");
ErrorIfState(BuildManagerState.Idle, "NoBuildInProgress");
VerifyStateInternal(BuildManagerState.Building);
_buildManagerState = BuildManagerState.WaitingForBuildToComplete;
}
var exceptionsThrownInEndBuild = false;
try
{
lock (_syncLock)
{
// If there are any submissions which never started, remove them now.
var submissionsToCheck = new List<BuildSubmissionBase>(_buildSubmissions.Values);
foreach (BuildSubmissionBase submission in submissionsToCheck)
{
CheckSubmissionCompletenessAndRemove(submission);
}
}
_noActiveSubmissionsEvent!.WaitOne();
ShutdownConnectedNodes(false /* normal termination */);
_noNodesActiveEvent!.WaitOne();
// Wait for all of the actions in the work queue to drain.
// _workQueue.Completion.Wait() could throw here if there was an unhandled exception in the work queue,
// but the top level exception handler there should catch everything and have forwarded it to the
// OnThreadException method in this class already.
_workQueue!.Complete();
_workQueue.Completion.Wait();
Task projectCacheDispose = _projectCacheService!.DisposeAsync().AsTask();
ErrorUtilities.VerifyThrow(_buildSubmissions.Count == 0, "All submissions not yet complete.");
ErrorUtilities.VerifyThrow(_activeNodes.Count == 0, "All nodes not yet shut down.");
if (_buildParameters!.UsesOutputCache())
{
SerializeCaches();
}
projectCacheDispose.Wait();
#if DEBUG
if (_projectStartedEvents.Count != 0)
{
bool allMismatchedProjectStartedEventsDueToLoggerErrors = true;
foreach (KeyValuePair<int, BuildEventArgs> projectStartedEvent in _projectStartedEvents)
{
BuildResult result = _resultsCache!.GetResultsForConfiguration(projectStartedEvent.Value.BuildEventContext!.ProjectInstanceId);
// It's valid to have a mismatched project started event IFF that particular
// project had some sort of unhandled exception. If there is no result, we
// can't tell for sure one way or the other, so err on the side of throwing
// the assert, but if there is a result, make sure that it actually has an
// exception attached.
if (result?.Exception == null)
{
allMismatchedProjectStartedEventsDueToLoggerErrors = false;
break;
}
}
Debug.Assert(allMismatchedProjectStartedEventsDueToLoggerErrors, "There was a mismatched project started event not caused by an exception result");
}
#endif
if (_buildParameters.DiscardBuildResults)
{
_resultsCache!.ClearResults();
}
}
catch (Exception e)
{
exceptionsThrownInEndBuild = true;
if (e is AggregateException ae && ae.InnerExceptions.Count == 1)
{
ExceptionDispatchInfo.Capture(ae.InnerExceptions[0]).Throw();
}
throw;
}
finally
{
try
{
ILoggingService? loggingService = ((IBuildComponentHost)this).LoggingService;
if (loggingService != null)
{
// Override the build success if the user specified /warnaserror and any errors were logged outside of a build submission.
if (exceptionsThrownInEndBuild ||
(_overallBuildSuccess && loggingService.HasBuildSubmissionLoggedErrors(BuildEventContext.InvalidSubmissionId)))
{
_overallBuildSuccess = false;
}
loggingService.LogBuildFinished(_overallBuildSuccess);
if (_buildTelemetry != null)
{
_buildTelemetry.FinishedAt = DateTime.UtcNow;
_buildTelemetry.Success = _overallBuildSuccess;
_buildTelemetry.Version = ProjectCollection.Version;
_buildTelemetry.DisplayVersion = ProjectCollection.DisplayVersion;
_buildTelemetry.FrameworkName = NativeMethodsShared.FrameworkName;
string? host = null;
if (BuildEnvironmentState.s_runningInVisualStudio)
{
host = "VS";
}
else if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILD_HOST_NAME")))
{
host = Environment.GetEnvironmentVariable("MSBUILD_HOST_NAME");
}
else if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("VSCODE_CWD")) || Environment.GetEnvironmentVariable("TERM_PROGRAM") == "vscode")
{
host = "VSCode";
}
_buildTelemetry.Host = host;
_buildTelemetry.BuildCheckEnabled = _buildParameters!.IsBuildCheckEnabled;
var sacState = NativeMethodsShared.GetSACState();
// The Enforcement would lead to build crash - but let's have the check for completeness sake.
_buildTelemetry.SACEnabled = sacState == NativeMethodsShared.SAC_State.Evaluation || sacState == NativeMethodsShared.SAC_State.Enforcement;
loggingService.LogTelemetry(buildEventContext: null, _buildTelemetry.EventName, _buildTelemetry.GetProperties());
// Clean telemetry to make it ready for next build submission.
_buildTelemetry = null;
}
}
ShutdownLoggingService(loggingService);
}
finally
{
if (_buildParameters!.LegacyThreadingSemantics)
{
_legacyThreadingData.MainThreadSubmissionId = -1;
}
Reset();
_buildManagerState = BuildManagerState.Idle;
MSBuildEventSource.Log.BuildStop();
_threadException?.Throw();
if (BuildParameters.DumpOpportunisticInternStats)
{
Console.WriteLine(Strings.CreateDiagnosticReport());
}
}
}
void SerializeCaches()
{
string errorMessage = CacheSerialization.SerializeCaches(
_configCache,
_resultsCache,
_buildParameters.OutputResultsCacheFile,
_buildParameters.ProjectIsolationMode);
if (!string.IsNullOrEmpty(errorMessage))
{
LogErrorAndShutdown(errorMessage);
}
}
}
/// <summary>
/// Convenience method. Submits a lone build request and blocks until results are available.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if a build is already in progress.</exception>
private TResultData Build<TRequestData, TResultData>(BuildParameters parameters, TRequestData requestData)
where TRequestData : BuildRequestData<TRequestData, TResultData>
where TResultData : BuildResultBase
{
TResultData result;
BeginBuild(parameters);
try
{
result = BuildRequest<TRequestData, TResultData>(requestData);
if (result.Exception == null && _threadException != null)
{
result.Exception = _threadException.SourceException;
_threadException = null;
}
}
finally
{
EndBuild();
}
return result;
}
/// <summary>
/// Convenience method. Submits a lone build request and blocks until results are available.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if a build is already in progress.</exception>
public BuildResult Build(BuildParameters parameters, BuildRequestData requestData)
=> Build<BuildRequestData, BuildResult>(parameters, requestData);
/// <summary>
/// Convenience method. Submits a lone graph build request and blocks until results are available.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if a build is already in progress.</exception>
public GraphBuildResult Build(BuildParameters parameters, GraphBuildRequestData requestData)
=> Build<GraphBuildRequestData, GraphBuildResult>(parameters, requestData);
/// <summary>
/// Shuts down all idle MSBuild nodes on the machine
/// </summary>
public void ShutdownAllNodes()
{
MSBuildClient.ShutdownServer(CancellationToken.None);
_nodeManager ??= (INodeManager)((IBuildComponentHost)this).GetComponent(BuildComponentType.NodeManager);
_nodeManager.ShutdownAllNodes();
}
/// <summary>
/// Dispose of the build manager.
/// </summary>
public void Dispose()
{
Dispose(true /* disposing */);
GC.SuppressFinalize(this);
}
#region INodePacketHandler Members
/// <summary>
/// This method is invoked by the NodePacketRouter when a packet is received and is intended for
/// this recipient.
/// </summary>
/// <param name="node">The node from which the packet was received.</param>
/// <param name="packet">The packet.</param>
void INodePacketHandler.PacketReceived(int node, INodePacket packet)
{
_workQueue!.Post(() => ProcessPacket(node, packet));
}
#endregion
#region IBuildComponentHost Members
/// <summary>
/// Registers a factory which will be used to create the necessary components of the build
/// system.
/// </summary>
/// <param name="componentType">The type which is created by this factory.</param>
/// <param name="factory">The factory to be registered.</param>
/// <remarks>
/// It is not necessary to register any factories. If no factory is registered for a specific kind
/// of object, the system will use the default factory.
/// </remarks>
void IBuildComponentHost.RegisterFactory(BuildComponentType componentType, BuildComponentFactoryDelegate factory)
{
_componentFactories.ReplaceFactory(componentType, factory);
}
/// <summary>
/// Gets an instance of the specified component type from the host.
/// </summary>
/// <param name="type">The component type to be retrieved</param>
/// <returns>The component</returns>
IBuildComponent IBuildComponentHost.GetComponent(BuildComponentType type)
{
return _componentFactories.GetComponent(type);
}
TComponent IBuildComponentHost.GetComponent<TComponent>(BuildComponentType type)
{
return _componentFactories.GetComponent<TComponent>(type);
}
#endregion
/// <summary>
/// This method adds the request in the specified submission to the set of requests being handled by the scheduler.
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Standard ExpectedException pattern used")]
[SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Complex class might need refactoring to separate scheduling elements from submission elements.")]
private void ExecuteSubmission(BuildSubmission submission, bool allowMainThreadBuild)
{
ErrorUtilities.VerifyThrowArgumentNull(submission);
ErrorUtilities.VerifyThrow(!submission.IsCompleted, "Submission already complete.");
BuildRequestConfiguration? resolvedConfiguration = null;
bool shuttingDown = false;
try
{
lock (_syncLock)
{
submission.IsStarted = true;
ProjectInstance? projectInstance = submission.BuildRequestData.ProjectInstance;
if (projectInstance != null)
{
if (_acquiredProjectRootElementCacheFromProjectInstance)
{
ErrorUtilities.VerifyThrowArgument(
_buildParameters!.ProjectRootElementCache == projectInstance.ProjectRootElementCache,
"OM_BuildSubmissionsMultipleProjectCollections");
}
else
{
_buildParameters!.ProjectRootElementCache = projectInstance.ProjectRootElementCache;
_acquiredProjectRootElementCacheFromProjectInstance = true;
}
}
else if (_buildParameters!.ProjectRootElementCache == null)
{
// Create our own cache; if we subsequently get a build submission with a project instance attached,
// we'll dump our cache and use that one.
_buildParameters!.ProjectRootElementCache =
new ProjectRootElementCache(false /* do not automatically reload from disk */);
}
VerifyStateInternal(BuildManagerState.Building);
// If we have an unnamed project, assign it a temporary name.
if (string.IsNullOrEmpty(submission.BuildRequestData.ProjectFullPath))
{
ErrorUtilities.VerifyThrow(
submission.BuildRequestData.ProjectInstance != null,
"Unexpected null path for a submission with no ProjectInstance.");
// If we have already named this instance when it was submitted previously during this build, use the same
// name so that we get the same configuration (and thus don't cause it to rebuild.)
if (!_unnamedProjectInstanceToNames.TryGetValue(submission.BuildRequestData.ProjectInstance!, out var tempName))
{
tempName = "Unnamed_" + _nextUnnamedProjectId++;
_unnamedProjectInstanceToNames[submission.BuildRequestData.ProjectInstance!] = tempName;
}
submission.BuildRequestData.ProjectFullPath = Path.Combine(
submission.BuildRequestData.ProjectInstance!.GetProperty(ReservedPropertyNames.projectDirectory)!.EvaluatedValue,
tempName);
}
// Create/Retrieve a configuration for each request
var buildRequestConfiguration = new BuildRequestConfiguration(submission.BuildRequestData, _buildParameters.DefaultToolsVersion);
var matchingConfiguration = _configCache!.GetMatchingConfiguration(buildRequestConfiguration);
resolvedConfiguration = ResolveConfiguration(
buildRequestConfiguration,
matchingConfiguration,
submission.BuildRequestData.Flags.HasFlag(BuildRequestDataFlags.ReplaceExistingProjectInstance));
resolvedConfiguration.ExplicitlyLoaded = true;
// assign shutting down to local variable to avoid race condition: "setting _shuttingDown after this point during this method execution"
shuttingDown = _shuttingDown;
if (!shuttingDown)
{
if (!_hasProjectCacheServiceInitializedVsScenario
&& BuildEnvironmentHelper.Instance.RunningInVisualStudio
&& !ProjectCacheDescriptors.IsEmpty)
{
// Only initialize once as it should be the same for all projects.
_hasProjectCacheServiceInitializedVsScenario = true;
_projectCacheService!.InitializePluginsForVsScenario(
ProjectCacheDescriptors.Values,
resolvedConfiguration,
submission.BuildRequestData.TargetNames,
_executionCancellationTokenSource!.Token);
}
if (_projectCacheService!.ShouldUseCache(resolvedConfiguration))
{
IssueCacheRequestForBuildSubmission(new CacheRequest(submission, resolvedConfiguration));
}
else
{
AddBuildRequestToSubmission(submission, resolvedConfiguration.ConfigurationId);
IssueBuildRequestForBuildSubmission(submission, resolvedConfiguration, allowMainThreadBuild);
}
}
}
}
catch (Exception ex) when (!ExceptionHandling.IsCriticalException(ex))
{
if (resolvedConfiguration is not null)
{
CompleteSubmissionWithException(submission, resolvedConfiguration, ex);
}
else
{
HandleSubmissionException(submission, ex);
throw;
}
}
// We are shutting down so submission has to be completed with BuildAbortedException
Debug.Assert(!Monitor.IsEntered(_syncLock));
if (shuttingDown)
{
ErrorUtilities.VerifyThrow(resolvedConfiguration is not null, "Cannot call project cache without having BuildRequestConfiguration");
// We were already canceled!
CompleteSubmissionWithException(submission, resolvedConfiguration!, new BuildAbortedException());
}
}
// Cache requests on configuration N do not block future build submissions depending on configuration N.
// It is assumed that the higher level build orchestrator (static graph scheduler, VS, quickbuild) submits a
// project build request only when its references have finished building.
private void IssueCacheRequestForBuildSubmission(CacheRequest cacheRequest)
{
Debug.Assert(Monitor.IsEntered(_syncLock));
_workQueue!.Post(() =>
{
try
{
_projectCacheService!.PostCacheRequest(cacheRequest, _executionCancellationTokenSource!.Token);
}
catch (Exception e)
{
CompleteSubmissionWithException(cacheRequest.Submission, cacheRequest.Configuration, e);
}
});
}
internal void ExecuteSubmission<TRequestData, TResultData>(
BuildSubmissionBase<TRequestData, TResultData> submission, bool allowMainThreadBuild)
where TRequestData : BuildRequestDataBase
where TResultData : BuildResultBase
{
// For the current submission we only know the SubmissionId and that it happened on scheduler node - all other BuildEventContext dimensions are unknown now.
BuildEventContext buildEventContext = new BuildEventContext(
submission.SubmissionId,
nodeId: 1,
BuildEventContext.InvalidProjectInstanceId,
BuildEventContext.InvalidProjectContextId,
BuildEventContext.InvalidTargetId,
BuildEventContext.InvalidTaskId);
BuildSubmissionStartedEventArgs submissionStartedEvent = new(
submission.BuildRequestDataBase.GlobalPropertiesLookup,
submission.BuildRequestDataBase.EntryProjectsFullPath,
submission.BuildRequestDataBase.TargetNames,
submission.BuildRequestDataBase.Flags,
submission.SubmissionId);
submissionStartedEvent.BuildEventContext = buildEventContext;
((IBuildComponentHost)this).LoggingService.LogBuildEvent(submissionStartedEvent);
if (submission is BuildSubmission buildSubmission)
{
ExecuteSubmission(buildSubmission, allowMainThreadBuild);
}
else if (submission is GraphBuildSubmission graphBuildSubmission)
{
ExecuteSubmission(graphBuildSubmission);
}
}
/// <summary>
/// This method adds the graph build request in the specified submission to the set of requests being handled by the scheduler.
/// </summary>
private void ExecuteSubmission(GraphBuildSubmission submission)
{
VerifyStateInternal(BuildManagerState.Building);
try
{
lock (_syncLock)
{
submission.IsStarted = true;
if (_shuttingDown)
{
// We were already canceled!
var result = new GraphBuildResult(submission.SubmissionId, new BuildAbortedException());
submission.CompleteResults(result);
CheckSubmissionCompletenessAndRemove(submission);
return;
}
// Do the scheduling in a separate thread to unblock the calling thread
Task.Factory.StartNew(
() =>
{
try
{
ExecuteGraphBuildScheduler(submission);
}
catch (Exception ex) when (!ExceptionHandling.IsCriticalException(ex))
{
HandleSubmissionException(submission, ex);
}
},
_executionCancellationTokenSource!.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
}
}
// The handling of submission exception needs to be done outside of the lock
catch (Exception ex) when (!ExceptionHandling.IsCriticalException(ex))
{
HandleSubmissionException(submission, ex);
throw;
}
}
/// <summary>
/// Creates the traversal and metaproject instances necessary to represent the solution and populates new configurations with them.
/// </summary>
private void LoadSolutionIntoConfiguration(BuildRequestConfiguration config, BuildRequest request)
{
Debug.Assert(Monitor.IsEntered(_syncLock));
if (config.IsLoaded)
{
// We've already processed it, nothing to do.
return;
}
ErrorUtilities.VerifyThrow(FileUtilities.IsSolutionFilename(config.ProjectFullPath), "{0} is not a solution", config.ProjectFullPath);
var buildEventContext = request.BuildEventContext;
if (buildEventContext == BuildEventContext.Invalid)
{
buildEventContext = new BuildEventContext(request.SubmissionId, 0, BuildEventContext.InvalidProjectInstanceId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidTaskId);
}
var instances = ProjectInstance.LoadSolutionForBuild(
config.ProjectFullPath,
config.GlobalProperties,
config.ExplicitToolsVersionSpecified ? config.ToolsVersion : null,
_buildParameters,
((IBuildComponentHost)this).LoggingService,
buildEventContext,
false /* loaded by solution parser*/,
config.RequestedTargets,
SdkResolverService,
request.SubmissionId);
// The first instance is the traversal project, which goes into this configuration
config.Project = instances[0];
// The remaining instances are the metaprojects which describe the dependencies for each project as well as how to invoke the project itself.
for (int i = 1; i < instances.Length; i++)
{
// Create new configurations for each of these if they don't already exist. That could happen if there are multiple
// solutions in this build which refer to the same project, in which case we want them to refer to the same
// metaproject as well.
var newConfig = new BuildRequestConfiguration(
GetNewConfigurationId(),
instances[i])
{ ExplicitlyLoaded = config.ExplicitlyLoaded };
if (_configCache!.GetMatchingConfiguration(newConfig) == null)
{
_configCache.AddConfiguration(newConfig);
}
}
}
/// <summary>
/// Gets the next build id.
/// </summary>
private static int GetNextBuildId()
{
return Interlocked.Increment(ref s_nextBuildId);
}
/// <summary>
/// Creates and optionally populates a new configuration.
/// </summary>
private BuildRequestConfiguration CreateConfiguration(Project project, BuildRequestConfiguration? existingConfiguration)
{
ProjectInstance newInstance = project.CreateProjectInstance();
if (existingConfiguration == null)
{
existingConfiguration = new BuildRequestConfiguration(GetNewConfigurationId(), new BuildRequestData(newInstance, []), null /* use the instance's tools version */);
}
else
{
existingConfiguration.Project = newInstance;
}
return existingConfiguration;
}
/// <summary>
/// Processes the next action in the work queue.
/// </summary>
/// <param name="action">The action to be processed.</param>
private void ProcessWorkQueue(Action action)
{
try
{
var oldCulture = CultureInfo.CurrentCulture;
var oldUICulture = CultureInfo.CurrentUICulture;
try
{
if (!Equals(CultureInfo.CurrentCulture, _buildParameters!.Culture))
{
CultureInfo.CurrentCulture = _buildParameters.Culture;
}
if (!Equals(CultureInfo.CurrentUICulture, _buildParameters.UICulture))
{
CultureInfo.CurrentUICulture = _buildParameters.UICulture;
}
action();
}
catch (Exception ex)
{
// These need to go to the main thread exception handler. We can't rethrow here because that will just silently stop the
// action block. Instead, send them over to the main handler for the BuildManager.
OnThreadException(ex);
}
finally
{
// Set the culture back to the original one so that if something else reuses this thread then it will not have a culture which it was not expecting.
if (!Equals(CultureInfo.CurrentCulture, oldCulture))
{
CultureInfo.CurrentCulture = oldCulture;
}
if (!Equals(CultureInfo.CurrentUICulture, oldUICulture))
{
CultureInfo.CurrentUICulture = oldUICulture;
}
}
}
catch (Exception e)
{
// On the off chance we get an exception from our exception handler (oh, the irony!), we want to know about it (and still not kill this block
// which could lead to a somewhat mysterious hang.)
ExceptionHandling.DumpExceptionToFile(e);
}
}
/// <summary>
/// Processes a packet
/// </summary>
private void ProcessPacket(int node, INodePacket packet)
{
lock (_syncLock)
{
if (_shuttingDown && packet.Type != NodePacketType.NodeShutdown)
{
// Console.WriteLine("Discarding packet {0} from node {1} because we are shutting down.", packet.Type, node);
return;
}
switch (packet.Type)
{
case NodePacketType.BuildRequestBlocker:
BuildRequestBlocker blocker = ExpectPacketType<BuildRequestBlocker>(packet, NodePacketType.BuildRequestBlocker);
HandleNewRequest(node, blocker);
break;
case NodePacketType.BuildRequestConfiguration:
BuildRequestConfiguration requestConfiguration = ExpectPacketType<BuildRequestConfiguration>(packet, NodePacketType.BuildRequestConfiguration);
HandleConfigurationRequest(node, requestConfiguration);
break;
case NodePacketType.BuildResult:
BuildResult result = ExpectPacketType<BuildResult>(packet, NodePacketType.BuildResult);
HandleResult(node, result);
break;
case NodePacketType.ResourceRequest:
ResourceRequest request = ExpectPacketType<ResourceRequest>(packet, NodePacketType.ResourceRequest);
HandleResourceRequest(node, request);
break;
case NodePacketType.NodeShutdown:
// Remove the node from the list of active nodes. When they are all done, we have shut down fully
NodeShutdown shutdownPacket = ExpectPacketType<NodeShutdown>(packet, NodePacketType.NodeShutdown);
HandleNodeShutdown(node, shutdownPacket);
break;
case NodePacketType.FileAccessReport:
FileAccessReport fileAccessReport = ExpectPacketType<FileAccessReport>(packet, NodePacketType.FileAccessReport);
HandleFileAccessReport(node, fileAccessReport);
break;
case NodePacketType.ProcessReport:
ProcessReport processReport = ExpectPacketType<ProcessReport>(packet, NodePacketType.ProcessReport);
HandleProcessReport(node, processReport);
break;
default:
ErrorUtilities.ThrowInternalError("Unexpected packet received by BuildManager: {0}", packet.Type);
break;
}
}
}
/// <remarks>
/// To avoid deadlock possibility, this method MUST NOT be called inside of 'lock (_syncLock)'
/// </remarks>
private void CompleteSubmissionWithException(BuildSubmission submission, BuildRequestConfiguration configuration, Exception exception)
{
Debug.Assert(!Monitor.IsEntered(_syncLock));
lock (_syncLock)
{
if (submission.BuildRequest is null)
{
AddBuildRequestToSubmission(submission, configuration.ConfigurationId);
}
}
HandleSubmissionException(submission, exception);
}
/// <summary>
/// Deals with exceptions that may be thrown when handling a submission.
/// </summary>
/// <remarks>
/// To avoid deadlock possibility, this method MUST NOT be called inside of 'lock (_syncLock)'
/// </remarks>
private void HandleSubmissionException(BuildSubmissionBase submission, Exception ex)
{
Debug.Assert(!Monitor.IsEntered(_syncLock));
if (ex is AggregateException ae)
{
// If there's exactly 1, just flatten it
if (ae.InnerExceptions.Count == 1)
{
ex = ae.InnerExceptions[0];
}
else
{
// Log each InvalidProjectFileException encountered
foreach (Exception innerException in ae.InnerExceptions)
{
if (innerException is InvalidProjectFileException innerProjectException)
{
LogInvalidProjectFileError(innerProjectException);
}
}
}
}
if (ex is InvalidProjectFileException projectException)
{
LogInvalidProjectFileError(projectException);
}
if (ex is CircularDependencyException)
{
LogInvalidProjectFileError(new InvalidProjectFileException(ex.Message, ex));
}
bool submissionNeedsCompletion;
lock (_syncLock)
{
// BuildRequest may be null if the submission fails early on.
submissionNeedsCompletion = submission.IsStarted;
if (submissionNeedsCompletion)
{
submission.CompleteResultsWithException(ex);
}
}
if (submissionNeedsCompletion)
{
WaitForAllLoggingServiceEventsToBeProcessed();
}
lock (_syncLock)
{
if (submissionNeedsCompletion)
{
submission.CompleteLogging();
}
_overallBuildSuccess = false;
CheckSubmissionCompletenessAndRemove(submission);
}
void LogInvalidProjectFileError(InvalidProjectFileException projectException)
{
if (!projectException.HasBeenLogged)
{
BuildEventContext buildEventContext = new BuildEventContext(submission.SubmissionId, 1, BuildEventContext.InvalidProjectInstanceId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidTaskId);
((IBuildComponentHost)this).LoggingService.LogInvalidProjectFileError(buildEventContext, projectException);
projectException.HasBeenLogged = true;
}
}
}
/// <summary>
/// Waits to drain all events of logging service.
/// This method shall be used carefully because during draining, LoggingService will block all incoming events.
/// </summary>
/// <remarks>
/// To avoid deadlock possibility, this method MUST NOT be called inside of 'lock (_syncLock)'
/// </remarks>
private void WaitForAllLoggingServiceEventsToBeProcessed()
{
// this has to be called out of the lock (_syncLock)
// because processing events can callback to 'this' instance and cause deadlock
Debug.Assert(!Monitor.IsEntered(_syncLock));
((LoggingService)((IBuildComponentHost)this).LoggingService).WaitForLoggingToProcessEvents();
}
private static void AddBuildRequestToSubmission(BuildSubmission submission, int configurationId, int projectContextId = BuildEventContext.InvalidProjectContextId)
{
submission.BuildRequest = new BuildRequest(
submission.SubmissionId,
BackEnd.BuildRequest.InvalidNodeRequestId,
configurationId,
submission.BuildRequestData.TargetNames,
submission.BuildRequestData.HostServices,
parentBuildEventContext: BuildEventContext.Invalid,
parentRequest: null,
submission.BuildRequestData.Flags,
submission.BuildRequestData.RequestedProjectState,
projectContextId: projectContextId);
}
private static void AddProxyBuildRequestToSubmission(
BuildSubmission submission,
int configurationId,
ProxyTargets proxyTargets,
int projectContextId)
{
submission.BuildRequest = new BuildRequest(
submission.SubmissionId,
BackEnd.BuildRequest.InvalidNodeRequestId,
configurationId,
proxyTargets,
submission.BuildRequestData.HostServices,
submission.BuildRequestData.Flags,
submission.BuildRequestData.RequestedProjectState,
projectContextId);
}
/// <summary>
/// The submission is a top level build request entering the BuildManager.
/// Sends the request to the scheduler with optional legacy threading semantics behavior.
/// </summary>
private void IssueBuildRequestForBuildSubmission(BuildSubmission submission, BuildRequestConfiguration configuration, bool allowMainThreadBuild = false)
{
_workQueue!.Post(
() =>
{
try
{
IssueBuildSubmissionToSchedulerImpl(submission, allowMainThreadBuild);
}
catch (BuildAbortedException bae)
{
CompleteSubmissionWithException(submission, configuration, bae);
}
catch (Exception ex) when (!ExceptionHandling.IsCriticalException(ex))
{
HandleSubmissionException(submission, ex);
}
});
void IssueBuildSubmissionToSchedulerImpl(BuildSubmission submission, bool allowMainThreadBuild)
{
var resetMainThreadOnFailure = false;
try
{
lock (_syncLock)
{
if (_shuttingDown)
{
throw new BuildAbortedException();
}
if (allowMainThreadBuild && _buildParameters!.LegacyThreadingSemantics)
{
if (_legacyThreadingData.MainThreadSubmissionId == -1)
{
resetMainThreadOnFailure = true;
_legacyThreadingData.MainThreadSubmissionId = submission.SubmissionId;
}
}
BuildRequestBlocker blocker = new BuildRequestBlocker(-1, [], [submission.BuildRequest]);
HandleNewRequest(Scheduler.VirtualNode, blocker);
}
}
catch (Exception ex) when (IsInvalidProjectOrIORelatedException(ex))
{
if (ex is InvalidProjectFileException projectException)
{
if (!projectException.HasBeenLogged)
{
BuildEventContext projectBuildEventContext = new BuildEventContext(submission.SubmissionId, 1, BuildEventContext.InvalidProjectInstanceId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidTaskId);
((IBuildComponentHost)this).LoggingService.LogInvalidProjectFileError(projectBuildEventContext, projectException);
projectException.HasBeenLogged = true;
}
}
lock (_syncLock)
{
if (resetMainThreadOnFailure)
{
_legacyThreadingData.MainThreadSubmissionId = -1;
}
if (ex is not InvalidProjectFileException)
{
var buildEventContext = new BuildEventContext(submission.SubmissionId, 1, BuildEventContext.InvalidProjectInstanceId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidTaskId);
((IBuildComponentHost)this).LoggingService.LogFatalBuildError(buildEventContext, ex, new BuildEventFileInfo(submission.BuildRequestData.ProjectFullPath));
}
}
WaitForAllLoggingServiceEventsToBeProcessed();
lock (_syncLock)
{
submission.CompleteLogging();
ReportResultsToSubmission<BuildRequestData, BuildResult>(new BuildResult(submission.BuildRequest!, ex));
_overallBuildSuccess = false;
}
}
}
}
private bool IsInvalidProjectOrIORelatedException(Exception e)
{
return !ExceptionHandling.IsCriticalException(e) && !ExceptionHandling.NotExpectedException(e) && e is not BuildAbortedException;
}
private void ExecuteGraphBuildScheduler(GraphBuildSubmission submission)
{
if (_shuttingDown)
{
throw new BuildAbortedException();
}
var projectGraph = submission.BuildRequestData.ProjectGraph;
if (projectGraph == null)
{
projectGraph = new ProjectGraph(
submission.BuildRequestData.ProjectGraphEntryPoints,
ProjectCollection.GlobalProjectCollection,
(path, properties, collection) =>
{
ProjectLoadSettings projectLoadSettings = _buildParameters!.ProjectLoadSettings;
if (submission.BuildRequestData.Flags.HasFlag(BuildRequestDataFlags.IgnoreMissingEmptyAndInvalidImports))
{
projectLoadSettings |= ProjectLoadSettings.IgnoreMissingImports | ProjectLoadSettings.IgnoreInvalidImports | ProjectLoadSettings.IgnoreEmptyImports;
}
if (submission.BuildRequestData.Flags.HasFlag(BuildRequestDataFlags.FailOnUnresolvedSdk))
{
projectLoadSettings |= ProjectLoadSettings.FailOnUnresolvedSdk;
}
return new ProjectInstance(
path,
properties,
null,
_buildParameters,
((IBuildComponentHost)this).LoggingService,
new BuildEventContext(
submission.SubmissionId,
_buildParameters.NodeId,
BuildEventContext.InvalidEvaluationId,
BuildEventContext.InvalidProjectInstanceId,
BuildEventContext.InvalidProjectContextId,
BuildEventContext.InvalidTargetId,
BuildEventContext.InvalidTaskId),
SdkResolverService,
submission.SubmissionId,
projectLoadSettings);
});
}
LogMessage(
ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword(
"StaticGraphConstructionMetrics",
Math.Round(projectGraph.ConstructionMetrics.ConstructionTime.TotalSeconds, 3),
projectGraph.ConstructionMetrics.NodeCount,
projectGraph.ConstructionMetrics.EdgeCount));
Dictionary<ProjectGraphNode, BuildResult>? resultsPerNode = null;
if (submission.BuildRequestData.GraphBuildOptions.Build)
{
_projectCacheService!.InitializePluginsForGraph(projectGraph, submission.BuildRequestData.TargetNames, _executionCancellationTokenSource!.Token);
IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetsPerNode = projectGraph.GetTargetLists(submission.BuildRequestData.TargetNames);
DumpGraph(projectGraph, targetsPerNode);
// Non-graph builds verify this in RequestBuilder, but for graph builds we need to disambiguate
// between entry nodes and other nodes in the graph since only entry nodes should error. Just do
// the verification explicitly before the build even starts.
foreach (ProjectGraphNode entryPointNode in projectGraph.EntryPointNodes)
{
ProjectErrorUtilities.VerifyThrowInvalidProject(entryPointNode.ProjectInstance.Targets.Count > 0, entryPointNode.ProjectInstance.ProjectFileLocation, "NoTargetSpecified");
}
resultsPerNode = BuildGraph(projectGraph, targetsPerNode, submission.BuildRequestData);
}
else
{
DumpGraph(projectGraph);
}
ErrorUtilities.VerifyThrow(
submission.BuildResult?.Exception == null,
"Exceptions only get set when the graph submission gets completed with an exception in OnThreadException. That should not happen during graph builds.");
// The overall submission is complete, so report it as complete
ReportResultsToSubmission<GraphBuildRequestData, GraphBuildResult>(
new GraphBuildResult(
submission.SubmissionId,
new ReadOnlyDictionary<ProjectGraphNode, BuildResult>(resultsPerNode ?? new Dictionary<ProjectGraphNode, BuildResult>())));
static void DumpGraph(ProjectGraph graph, IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>>? targetList = null)
{
if (Traits.Instance.DebugEngine is false)
{
return;
}
var logPath = DebugUtils.FindNextAvailableDebugFilePath($"{DebugUtils.ProcessInfoString}_ProjectGraph.dot");
File.WriteAllText(logPath, graph.ToDot(targetList));
}
}
private Dictionary<ProjectGraphNode, BuildResult> BuildGraph(
ProjectGraph projectGraph,
IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetsPerNode,
GraphBuildRequestData graphBuildRequestData)
{
// The handle is used within captured async scope. If error occurs during the build
// and we return from the function before async call signals - it causes unhandled ObjectDisposedException
// upon attempt to signal the handle (and hence unfinished logs).
#pragma warning disable CA2000
var waitHandle = new AutoResetEvent(true);
#pragma warning restore CA2000
var graphBuildStateLock = new object();
var blockedNodes = new HashSet<ProjectGraphNode>(projectGraph.ProjectNodes);
var finishedNodes = new HashSet<ProjectGraphNode>(projectGraph.ProjectNodes.Count);
var buildingNodes = new Dictionary<BuildSubmissionBase, ProjectGraphNode>();
var resultsPerNode = new Dictionary<ProjectGraphNode, BuildResult>(projectGraph.ProjectNodes.Count);
ExceptionDispatchInfo? submissionException = null;
while (blockedNodes.Count > 0 || buildingNodes.Count > 0)
{
waitHandle.WaitOne();
// When a cache plugin is present, ExecuteSubmission(BuildSubmission) executes on a separate thread whose exceptions do not get observed.
// Observe them here to keep the same exception flow with the case when there's no plugins and ExecuteSubmission(BuildSubmission) does not run on a separate thread.
if (submissionException != null)
{
submissionException.Throw();
}
lock (graphBuildStateLock)
{
var unblockedNodes = blockedNodes
.Where(node => node.ProjectReferences.All(projectReference => finishedNodes.Contains(projectReference)))
.ToList();
foreach (var node in unblockedNodes)
{
var targetList = targetsPerNode[node];
if (targetList.Count == 0)
{
// An empty target list here means "no targets" instead of "default targets", so don't even build it.
finishedNodes.Add(node);
blockedNodes.Remove(node);
waitHandle.Set();
continue;
}
var request = new BuildRequestData(
node.ProjectInstance,
targetList.ToArray(),
graphBuildRequestData.HostServices,
graphBuildRequestData.Flags);
// TODO Tack onto the existing submission instead of pending a whole new submission for every node
// Among other things, this makes BuildParameters.DetailedSummary produce a summary for each node, which is not desirable.
// We basically want to submit all requests to the scheduler all at once and describe dependencies by requests being blocked by other requests.
// However today the scheduler only keeps track of MSBuild nodes being blocked by other MSBuild nodes, and MSBuild nodes haven't been assigned to the graph nodes yet.
var innerBuildSubmission = PendBuildRequest(request);
buildingNodes.Add(innerBuildSubmission, node);
blockedNodes.Remove(node);
innerBuildSubmission.ExecuteAsync(finishedBuildSubmission =>
{
lock (graphBuildStateLock)
{
if (submissionException == null && finishedBuildSubmission.BuildResult?.Exception != null)
{
// Preserve the original stack.
submissionException = ExceptionDispatchInfo.Capture(finishedBuildSubmission.BuildResult.Exception);
}
ProjectGraphNode finishedNode = buildingNodes[finishedBuildSubmission];
finishedNodes.Add(finishedNode);
buildingNodes.Remove(finishedBuildSubmission);
resultsPerNode.Add(finishedNode, finishedBuildSubmission.BuildResult!);
}
waitHandle.Set();
}, null);
}
}
}
return resultsPerNode;
}
/// <summary>
/// Asks the nodeManager to tell the currently connected nodes to shut down and sets a flag preventing all non-shutdown-related packets from
/// being processed.
/// </summary>
private void ShutdownConnectedNodes(bool abort)
{
lock (_syncLock)
{
_shuttingDown = true;
_executionCancellationTokenSource?.Cancel();
// If we are aborting, we will NOT reuse the nodes because their state may be compromised by attempts to shut down while the build is in-progress.
_nodeManager?.ShutdownConnectedNodes(!abort && _buildParameters!.EnableNodeReuse);
// if we are aborting, the task host will hear about it in time through the task building infrastructure;
// so only shut down the task host nodes if we're shutting down tidily (in which case, it is assumed that all
// tasks are finished building and thus that there's no risk of a race between the two shutdown pathways).
if (!abort)
{
_taskHostNodeManager?.ShutdownConnectedNodes(_buildParameters!.EnableNodeReuse);
}
}
}
/// <summary>
/// Retrieves the next build submission id.
/// </summary>
private int GetNextSubmissionId()
{
return _nextBuildSubmissionId++;
}
/// <summary>
/// Errors if the BuildManager is in the specified state.
/// </summary>
private void ErrorIfState(BuildManagerState disallowedState, string exceptionResouorce)
{
if (_buildManagerState == disallowedState)
{
ErrorUtilities.ThrowInvalidOperation(exceptionResouorce);
}
}
/// <summary>
/// Verifies the BuildManager is in the required state, and throws a <see cref="System.InvalidOperationException"/> if it is not.
/// </summary>
private void RequireState(BuildManagerState requiredState, string exceptionResouorce)
{
ErrorUtilities.VerifyThrowInvalidOperation(_buildManagerState == requiredState, exceptionResouorce);
}
/// <summary>
/// Verifies the BuildManager is in the required state, and throws a <see cref="System.InvalidOperationException"/> if it is not.
/// </summary>
private void VerifyStateInternal(BuildManagerState requiredState)
{
if (_buildManagerState != requiredState)
{
ErrorUtilities.ThrowInternalError("Expected state {0}, actual state {1}", requiredState, _buildManagerState);
}
}
/// <summary>
/// Method called to reset the state of the system after a build.
/// </summary>
private void Reset()
{
_nodeManager?.UnregisterPacketHandler(NodePacketType.BuildRequestBlocker);
_nodeManager?.UnregisterPacketHandler(NodePacketType.BuildRequestConfiguration);
_nodeManager?.UnregisterPacketHandler(NodePacketType.BuildRequestConfigurationResponse);
_nodeManager?.UnregisterPacketHandler(NodePacketType.BuildResult);
_nodeManager?.UnregisterPacketHandler(NodePacketType.NodeShutdown);
_nodeManager?.ClearPerBuildState();
_nodeManager = null;
_shuttingDown = false;
_executionCancellationTokenSource?.Dispose();
_executionCancellationTokenSource = null;
_nodeConfiguration = null;
_buildSubmissions.Clear();
_scheduler?.Reset();
_scheduler = null;
_workQueue = null;
_projectCacheService = null;
_hasProjectCacheServiceInitializedVsScenario = false;
_acquiredProjectRootElementCacheFromProjectInstance = false;
_unnamedProjectInstanceToNames.Clear();
_projectStartedEvents.Clear();
_nodeIdToKnownConfigurations.Clear();
_nextUnnamedProjectId = 1;
if (_configCache != null)
{
foreach (BuildRequestConfiguration config in _configCache)
{
config.ActivelyBuildingTargets.Clear();
}
}
if (Environment.GetEnvironmentVariable("MSBUILDCLEARXMLCACHEONBUILDMANAGER") == "1")
{
// Optionally clear out the cache. This has the advantage of releasing memory,
// but the disadvantage of causing the next build to repeat the load and parse.
// We'll experiment here and ship with the best default.
_buildParameters?.ProjectRootElementCache.Clear();
}
}
/// <summary>
/// Returns a new, valid configuration id.
/// </summary>
private int GetNewConfigurationId()
{
int newId = Interlocked.Increment(ref s_nextBuildRequestConfigurationId);
if (_scheduler != null)
{
// Minimum configuration id is always the lowest valid configuration id available, so increment after returning.
while (newId <= _scheduler.MinimumAssignableConfigurationId) // Currently this minimum is one
{
newId = Interlocked.Increment(ref s_nextBuildRequestConfigurationId);
}
}
return newId;
}
/// <summary>
/// Finds a matching configuration in the cache and returns it, or stores the configuration passed in.
/// </summary>
private BuildRequestConfiguration ResolveConfiguration(BuildRequestConfiguration unresolvedConfiguration, BuildRequestConfiguration? matchingConfigurationFromCache, bool replaceProjectInstance)
{
Debug.Assert(Monitor.IsEntered(_syncLock));
BuildRequestConfiguration resolvedConfiguration = matchingConfigurationFromCache ?? _configCache!.GetMatchingConfiguration(unresolvedConfiguration);
if (resolvedConfiguration == null)
{
resolvedConfiguration = AddNewConfiguration(unresolvedConfiguration);
}
else if (unresolvedConfiguration.Project != null && replaceProjectInstance)
{
ReplaceExistingProjectInstance(unresolvedConfiguration, resolvedConfiguration);
}
else if (unresolvedConfiguration.Project != null && resolvedConfiguration.Project != null && !ReferenceEquals(unresolvedConfiguration.Project, resolvedConfiguration.Project))
{
// The user passed in a different instance than the one we already had. Throw away any corresponding results.
ReplaceExistingProjectInstance(unresolvedConfiguration, resolvedConfiguration);
}
else if (unresolvedConfiguration.Project != null && resolvedConfiguration.Project == null)
{
// Workaround for https://github.com/dotnet/msbuild/issues/1748
// If the submission has a project instance but the existing configuration does not, it probably means that the project was
// built on another node (e.g. the project was encountered as a p2p reference and scheduled to a node).
// Add a dummy property to force cache invalidation in the scheduler and the nodes.
// TODO find a better solution than a dummy property
unresolvedConfiguration.CreateUniqueGlobalProperty();
resolvedConfiguration = AddNewConfiguration(unresolvedConfiguration);
}
return resolvedConfiguration;
}
private void ReplaceExistingProjectInstance(BuildRequestConfiguration newConfiguration, BuildRequestConfiguration existingConfiguration)
{
Debug.Assert(Monitor.IsEntered(_syncLock));
existingConfiguration.Project = newConfiguration.Project;
_resultsCache!.ClearResultsForConfiguration(existingConfiguration.ConfigurationId);
}
private BuildRequestConfiguration AddNewConfiguration(BuildRequestConfiguration unresolvedConfiguration)
{
Debug.Assert(Monitor.IsEntered(_syncLock));
var newConfigurationId = _scheduler!.GetConfigurationIdFromPlan(unresolvedConfiguration.ProjectFullPath);
if (_configCache!.HasConfiguration(newConfigurationId) || (newConfigurationId == BuildRequestConfiguration.InvalidConfigurationId))
{
// There is already a configuration like this one or one didn't exist in a plan, so generate a new ID.
newConfigurationId = GetNewConfigurationId();
}
var newConfiguration = unresolvedConfiguration.ShallowCloneWithNewId(newConfigurationId);
_configCache.AddConfiguration(newConfiguration);
return newConfiguration;
}
internal void PostCacheResult(CacheRequest cacheRequest, CacheResult cacheResult, int projectContextId)
{
_workQueue!.Post(() =>
{
if (cacheResult.Exception is not null)
{
CompleteSubmissionWithException(cacheRequest.Submission, cacheRequest.Configuration, cacheResult.Exception);
return;
}
HandleCacheResult();
});
void HandleCacheResult()
{
lock (_syncLock)
{
try
{
var submission = cacheRequest.Submission;
var configuration = cacheRequest.Configuration;
if (cacheResult.ResultType != CacheResultType.CacheHit)
{
// Issue the real build request.
AddBuildRequestToSubmission(submission, configuration.ConfigurationId, projectContextId);
IssueBuildRequestForBuildSubmission(submission, configuration, allowMainThreadBuild: false);
}
else if (cacheResult.ResultType == CacheResultType.CacheHit && cacheResult.ProxyTargets != null)
{
// Setup submission.BuildRequest with proxy targets. The proxy request is built on the inproc node (to avoid
// ProjectInstance serialization). The proxy target results are used as results for the real targets.
AddProxyBuildRequestToSubmission(submission, configuration.ConfigurationId, cacheResult.ProxyTargets, projectContextId);
IssueBuildRequestForBuildSubmission(submission, configuration, allowMainThreadBuild: false);
}
else if (cacheResult.ResultType == CacheResultType.CacheHit && cacheResult.BuildResult != null)
{
// Mark the build submission as complete with the provided results and return.
// There must be a build request for the results, so fake one.
AddBuildRequestToSubmission(submission, configuration.ConfigurationId, projectContextId);
var result = new BuildResult(submission.BuildRequest!);
foreach (var cacheResultInner in cacheResult.BuildResult?.ResultsByTarget ?? Enumerable.Empty<KeyValuePair<string, TargetResult>>())
{
result.AddResultsForTarget(cacheResultInner.Key, cacheResultInner.Value);
}
_resultsCache!.AddResult(result);
submission.CompleteLogging();
ReportResultsToSubmission<BuildRequestData, BuildResult>(result);
}
}
catch (Exception e)
{
CompleteSubmissionWithException(cacheRequest.Submission, cacheRequest.Configuration, e);
}
}
}
}
/// <summary>
/// Handles a new request coming from a node.
/// </summary>
private void HandleNewRequest(int node, BuildRequestBlocker blocker)
{
// If we received any solution files, populate their configurations now.
if (blocker.BuildRequests != null)
{
foreach (BuildRequest request in blocker.BuildRequests)
{
BuildRequestConfiguration config = _configCache![request.ConfigurationId];
if (FileUtilities.IsSolutionFilename(config.ProjectFullPath))
{
try
{
LoadSolutionIntoConfiguration(config, request);
}
catch (InvalidProjectFileException e)
{
// Throw the error in the cache. The Scheduler will pick it up and return the results correctly.
_resultsCache!.AddResult(new BuildResult(request, e));
if (node == Scheduler.VirtualNode)
{
throw;
}
}
}
}
}
IEnumerable<ScheduleResponse> response = _scheduler!.ReportRequestBlocked(node, blocker);
PerformSchedulingActions(response);
}
/// <summary>
/// Handles a resource request coming from a node.
/// </summary>
private void HandleResourceRequest(int node, ResourceRequest request)
{
Debug.Assert(Monitor.IsEntered(_syncLock));
if (request.IsResourceAcquire)
{
// Resource request requires a response and may be blocking. Our continuation is effectively a callback
// to be called once at least one core becomes available.
_scheduler!.RequestCores(request.GlobalRequestId, request.NumCores, request.IsBlocking).ContinueWith((Task<int> task) =>
{
var response = new ResourceResponse(request.GlobalRequestId, task.Result);
_nodeManager!.SendData(node, response);
}, TaskContinuationOptions.ExecuteSynchronously);
}
else
{
// Resource release is a one-way call, no response is expected. We release the cores as instructed
// and kick the scheduler because there may be work waiting for cores to become available.
IEnumerable<ScheduleResponse> response = _scheduler!.ReleaseCores(request.GlobalRequestId, request.NumCores);
PerformSchedulingActions(response);
}
}
/// <summary>
/// Handles a configuration request coming from a node.
/// </summary>
private void HandleConfigurationRequest(int node, BuildRequestConfiguration unresolvedConfiguration)
{
Debug.Assert(Monitor.IsEntered(_syncLock));
BuildRequestConfiguration resolvedConfiguration = ResolveConfiguration(unresolvedConfiguration, null, false);
var response = new BuildRequestConfigurationResponse(unresolvedConfiguration.ConfigurationId, resolvedConfiguration.ConfigurationId, resolvedConfiguration.ResultsNodeId);
if (!_nodeIdToKnownConfigurations.TryGetValue(node, out HashSet<int>? configurationsOnNode))
{
configurationsOnNode = new HashSet<int>();
_nodeIdToKnownConfigurations[node] = configurationsOnNode;
}
configurationsOnNode.Add(resolvedConfiguration.ConfigurationId);
_nodeManager!.SendData(node, response);
}
/// <summary>
/// Handles a build result coming from a node.
/// </summary>
private void HandleResult(int node, BuildResult result)
{
// Update cache with the default, initial, and project targets, as needed.
BuildRequestConfiguration configuration = _configCache![result.ConfigurationId];
if (result.DefaultTargets != null)
{
// If the result has Default, Initial, and project targets, we populate the configuration cache with them if it
// doesn't already have entries. This can happen if we created a configuration based on a request from
// an external node, but hadn't yet received a result since we may not have loaded the Project locally
// and thus wouldn't know what the default, initial, and project targets were.
configuration.ProjectDefaultTargets ??= result.DefaultTargets;
configuration.ProjectInitialTargets ??= result.InitialTargets;
configuration.ProjectTargets ??= result.ProjectTargets;
}
// Only report results to the project cache services if it's the result for a build submission.
// Note that graph builds create a submission for each node in the graph, so each node in the graph will be
// handled here. This intentionally mirrors the behavior for cache requests, as it doesn't make sense to
// report for projects which aren't going to be requested. Ideally, *any* request could be handled, but that
// would require moving the cache service interactions to the Scheduler.
if (_buildSubmissions.TryGetValue(result.SubmissionId, out BuildSubmissionBase? buildSubmissionBase) && buildSubmissionBase is BuildSubmission buildSubmission)
{
// The result may be associated with the build submission due to it being the submission which
// caused the build, but not the actual request which was originally used with the build submission.
// ie. it may be a dependency of the "root-level" project which is associated with this submission, which
// isn't what we're looking for. Ensure only the actual submission's request is considered.
if (buildSubmission.BuildRequest != null
&& buildSubmission.BuildRequest.ConfigurationId == configuration.ConfigurationId
&& _projectCacheService!.ShouldUseCache(configuration))
{
BuildEventContext buildEventContext = _projectStartedEvents.TryGetValue(result.SubmissionId, out BuildEventArgs? buildEventArgs)
? buildEventArgs.BuildEventContext!
: new BuildEventContext(result.SubmissionId, node, configuration.Project?.EvaluationId ?? BuildEventContext.InvalidEvaluationId, configuration.ConfigurationId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidTaskId);
try
{
_projectCacheService.HandleBuildResultAsync(configuration, result, buildEventContext, _executionCancellationTokenSource!.Token).Wait();
}
catch (AggregateException ex) when (ex.InnerExceptions.All(inner => inner is OperationCanceledException))
{
// The build is being cancelled. Swallow any exceptions related specifically to cancellation.
}
catch (OperationCanceledException)
{
// The build is being cancelled. Swallow any exceptions related specifically to cancellation.
}
}
}
IEnumerable<ScheduleResponse> response = _scheduler!.ReportResult(node, result);
PerformSchedulingActions(response);
}
/// <summary>
/// Handles the NodeShutdown packet
/// </summary>
private void HandleNodeShutdown(int node, NodeShutdown shutdownPacket)
{
Debug.Assert(Monitor.IsEntered(_syncLock));
_shuttingDown = true;
_executionCancellationTokenSource?.Cancel();
ErrorUtilities.VerifyThrow(_activeNodes.Contains(node), "Unexpected shutdown from node {0} which shouldn't exist.", node);
_activeNodes.Remove(node);
if (shutdownPacket.Reason != NodeShutdownReason.Requested)
{
if (shutdownPacket.Reason == NodeShutdownReason.ConnectionFailed)
{
ILoggingService loggingService = ((IBuildComponentHost)this).GetComponent<ILoggingService>(BuildComponentType.LoggingService);
foreach (BuildSubmissionBase submission in _buildSubmissions.Values)
{
BuildEventContext buildEventContext = new BuildEventContext(submission.SubmissionId, BuildEventContext.InvalidNodeId, BuildEventContext.InvalidProjectInstanceId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidTaskId);
string exception = ExceptionHandling.ReadAnyExceptionFromFile(_instantiationTimeUtc);
loggingService?.LogError(buildEventContext, new BuildEventFileInfo(string.Empty) /* no project file */, "ChildExitedPrematurely", node, ExceptionHandling.DebugDumpPath, exception);
}
}
else if (shutdownPacket.Reason == NodeShutdownReason.Error && _buildSubmissions.Values.Count == 0)
{
// We have no submissions to attach any exceptions to, lets just log it here.
if (shutdownPacket.Exception != null)
{
ILoggingService loggingService = ((IBuildComponentHost)this).GetComponent<ILoggingService>(BuildComponentType.LoggingService);
loggingService?.LogError(BuildEventContext.Invalid, new BuildEventFileInfo(string.Empty) /* no project file */, "ChildExitedPrematurely", node, ExceptionHandling.DebugDumpPath, shutdownPacket.Exception.ToString());
OnThreadException(shutdownPacket.Exception);
}
}
_nodeManager!.ShutdownConnectedNodes(_buildParameters!.EnableNodeReuse);
_taskHostNodeManager!.ShutdownConnectedNodes(_buildParameters.EnableNodeReuse);
foreach (BuildSubmissionBase submission in _buildSubmissions.Values)
{
// The submission has not started
if (!submission.IsStarted)
{
continue;
}
if (submission is BuildSubmission buildSubmission && buildSubmission.BuildRequest != null)
{
_resultsCache!.AddResult(new BuildResult(buildSubmission.BuildRequest,
shutdownPacket.Exception ?? new BuildAbortedException()));
}
}
_scheduler!.ReportBuildAborted(node);
}
CheckForActiveNodesAndCleanUpSubmissions();
}
/// <summary>
/// Report the received <paramref name="fileAccessReport"/> to the file access manager.
/// </summary>
/// <param name="nodeId">The id of the node from which the <paramref name="fileAccessReport"/> was received.</param>
/// <param name="fileAccessReport">The file access report.</param>
private void HandleFileAccessReport(int nodeId, FileAccessReport fileAccessReport)
{
#if FEATURE_REPORTFILEACCESSES
if (_buildParameters!.ReportFileAccesses)
{
((FileAccessManager)((IBuildComponentHost)this).GetComponent(BuildComponentType.FileAccessManager)).ReportFileAccess(fileAccessReport.FileAccessData, nodeId);
}
#endif
}
/// <summary>
/// Report the received <paramref name="processReport"/> to the file access manager.
/// </summary>
/// <param name="nodeId">The id of the node from which the <paramref name="processReport"/> was received.</param>
/// <param name="processReport">The process data report.</param>
private void HandleProcessReport(int nodeId, ProcessReport processReport)
{
#if FEATURE_REPORTFILEACCESSES
if (_buildParameters!.ReportFileAccesses)
{
((FileAccessManager)((IBuildComponentHost)this).GetComponent(BuildComponentType.FileAccessManager)).ReportProcess(processReport.ProcessData, nodeId);
}
#endif
}
/// <summary>
/// If there are no more active nodes, cleans up any remaining submissions.
/// </summary>
/// <remarks>
/// Must only be called from within the sync lock.
/// </remarks>
private void CheckForActiveNodesAndCleanUpSubmissions()
{
Debug.Assert(Monitor.IsEntered(_syncLock));
if (_activeNodes.Count == 0)
{
var submissions = new List<BuildSubmissionBase>(_buildSubmissions.Values);
foreach (BuildSubmissionBase submission in submissions)
{
// The submission has not started do not add it to the results cache
if (!submission.IsStarted)
{
continue;
}
if (!CompleteSubmissionFromCache(submission))
{
submission.CompleteResultsWithException(new BuildAbortedException());
}
// If we never received a project started event, consider logging complete anyhow, since the nodes have
// shut down.
submission.CompleteLogging();
CheckSubmissionCompletenessAndRemove(submission);
}
_noNodesActiveEvent?.Set();
}
}
private bool CompleteSubmissionFromCache(BuildSubmissionBase submissionBase)
{
if (submissionBase is BuildSubmission submission)
{
BuildResult? result = submission.BuildRequest == null ? null : _resultsCache?.GetResultsForConfiguration(submission.BuildRequest.ConfigurationId);
if (result != null)
{
submission.CompleteResults(result);
return true;
}
}
return false;
}
/// <summary>
/// Carries out the actions specified by the scheduler.
/// </summary>
private void PerformSchedulingActions(IEnumerable<ScheduleResponse> responses)
{
Debug.Assert(Monitor.IsEntered(_syncLock));
foreach (ScheduleResponse response in responses)
{
switch (response.Action)
{
case ScheduleActionType.NoAction:
break;
case ScheduleActionType.SubmissionComplete:
if (_buildParameters!.DetailedSummary)
{
_scheduler!.WriteDetailedSummary(response.BuildResult.SubmissionId);
}
ReportResultsToSubmission<BuildRequestData, BuildResult>(response.BuildResult);
break;
case ScheduleActionType.CircularDependency:
case ScheduleActionType.ResumeExecution:
case ScheduleActionType.ReportResults:
_nodeManager!.SendData(response.NodeId, response.Unblocker);
break;
case ScheduleActionType.CreateNode:
IList<NodeInfo> newNodes = _nodeManager!.CreateNodes(GetNodeConfiguration(), response.RequiredNodeType, response.NumberOfNodesToCreate);
if (newNodes?.Count != response.NumberOfNodesToCreate || newNodes.Any(n => n == null))
{
BuildEventContext buildEventContext = new BuildEventContext(0, Scheduler.VirtualNode, BuildEventContext.InvalidProjectInstanceId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidTaskId);
((IBuildComponentHost)this).LoggingService.LogError(buildEventContext, new BuildEventFileInfo(String.Empty), "UnableToCreateNode", response.RequiredNodeType.ToString("G"));
throw new BuildAbortedException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("UnableToCreateNode", response.RequiredNodeType.ToString("G")));
}
foreach (var node in newNodes)
{
_noNodesActiveEvent?.Reset();
_activeNodes.Add(node.NodeId);
}
IEnumerable<ScheduleResponse> newResponses = _scheduler!.ReportNodesCreated(newNodes);
PerformSchedulingActions(newResponses);
break;
case ScheduleActionType.Schedule:
case ScheduleActionType.ScheduleWithConfiguration:
if (response.Action == ScheduleActionType.ScheduleWithConfiguration)
{
// Only actually send the configuration if the node doesn't know about it. The scheduler only keeps track
// of which nodes have had configurations specifically assigned to them for building. However, a node may
// have created a configuration based on a build request it needs to wait on. In this
// case we need not send the configuration since it will already have been mapped earlier.
if (!_nodeIdToKnownConfigurations.TryGetValue(response.NodeId, out HashSet<int>? configurationsOnNode) ||
!configurationsOnNode.Contains(response.BuildRequest.ConfigurationId))
{
IConfigCache configCache = _componentFactories.GetComponent<IConfigCache>(BuildComponentType.ConfigCache);
_nodeManager!.SendData(response.NodeId, configCache[response.BuildRequest.ConfigurationId]);
}
}
_nodeManager!.SendData(response.NodeId, response.BuildRequest);
break;
default:
ErrorUtilities.ThrowInternalError("Scheduling action {0} not handled.", response.Action);
break;
}
}
}
internal void ReportResultsToSubmission<TRequestData, TResultData>(TResultData result)
where TRequestData : BuildRequestDataBase
where TResultData : BuildResultBase
{
lock (_syncLock)
{
// The build submission has not already been completed.
if (_buildSubmissions.TryGetValue(result.SubmissionId, out BuildSubmissionBase? submissionBase) &&
submissionBase is BuildSubmissionBase<TRequestData, TResultData> submission)
{
/* If the request failed because we caught an exception from the loggers, we can assume we will receive no more logging messages for
* this submission, therefore set the logging as complete. InternalLoggerExceptions are unhandled exceptions from the logger. If the logger author does
* not handle an exception the eventsource wraps all exceptions (except a logging exception) into an internal logging exception.
* These exceptions will have their stack logged on the commandline as an unexpected failure. If a logger author wants the logger
* to fail gracefully then can catch an exception and log a LoggerException. This has the same effect of stopping the build but it logs only
* the exception error message rather than the whole stack trace.
*
* If any other exception happened and logging is not completed, then go ahead and complete it now since this is the last place to do it.
* Otherwise the submission would remain uncompleted, potentially causing hangs (EndBuild waiting on all BuildSubmissions, users waiting on BuildSubmission, or expecting a callback, etc)
*/
if (!submission.LoggingCompleted && result.Exception != null)
{
submission.CompleteLogging();
}
submission.CompleteResults(result);
CheckSubmissionCompletenessAndRemove(submission);
}
}
}
/// <summary>
/// Determines if the submission is fully completed.
/// </summary>
private void CheckSubmissionCompletenessAndRemove(BuildSubmissionBase submission)
{
lock (_syncLock)
{
// If the submission has completed or never started, remove it.
if (submission.IsCompleted || !submission.IsStarted)
{
_overallBuildSuccess &= (submission.BuildResultBase?.OverallResult == BuildResultCode.Success);
_buildSubmissions.Remove(submission.SubmissionId);
// Clear all cached SDKs for the submission
SdkResolverService.ClearCache(submission.SubmissionId);
}
CheckAllSubmissionsComplete(submission.BuildRequestDataBase.Flags);
}
}
private void CheckAllSubmissionsComplete(BuildRequestDataFlags? flags)
{
Debug.Assert(Monitor.IsEntered(_syncLock));
if (_buildSubmissions.Count == 0)
{
if (flags.HasValue && flags.Value.HasFlag(BuildRequestDataFlags.ClearCachesAfterBuild))
{
// Reset the project root element cache if specified which ensures that projects will be re-loaded from disk. We do not need to reset the
// cache on child nodes because the OutOfProcNode class sets "autoReloadFromDisk" to "true" which handles the case when a restore modifies
// part of the import graph.
_buildParameters?.ProjectRootElementCache?.Clear();
FileMatcher.ClearFileEnumerationsCache();
#if !CLR2COMPATIBILITY
FileUtilities.ClearFileExistenceCache();
#endif
}
_noActiveSubmissionsEvent?.Set();
}
}
/// <summary>
/// Retrieves the configuration structure for a node.
/// </summary>
private NodeConfiguration GetNodeConfiguration()
{
Debug.Assert(Monitor.IsEntered(_syncLock));
if (_nodeConfiguration == null)
{
// Get the remote loggers
ILoggingService loggingService = ((IBuildComponentHost)this).GetComponent<ILoggingService>(BuildComponentType.LoggingService);
var remoteLoggers = new List<LoggerDescription>(loggingService.LoggerDescriptions);
_nodeConfiguration = new NodeConfiguration(
-1, /* must be assigned by the NodeManager */
_buildParameters,
remoteLoggers.ToArray()
#if FEATURE_APPDOMAIN
, AppDomain.CurrentDomain.SetupInformation
#endif
, new LoggingNodeConfiguration(
loggingService.IncludeEvaluationMetaprojects,
loggingService.IncludeEvaluationProfile,
loggingService.IncludeEvaluationPropertiesAndItemsInProjectStartedEvent,
loggingService.IncludeEvaluationPropertiesAndItemsInEvaluationFinishedEvent,
loggingService.IncludeTaskInputs));
}
return _nodeConfiguration;
}
/// <summary>
/// Handler for thread exceptions. This handler will only get called if the exception did not previously
/// get handled by a node exception handlers (for instance because the build is complete for the node.) In this case we
/// get the exception and will put it into the OverallBuildResult so that the host can see what happened.
/// </summary>
private void OnThreadException(Exception e)
{
lock (_syncLock)
{
if (_threadException == null)
{
if (e is AggregateException ae && ae.InnerExceptions.Count == 1)
{
e = ae.InnerExceptions.First();
}
_threadException = ExceptionDispatchInfo.Capture(e);
var submissions = new List<BuildSubmissionBase>(_buildSubmissions.Values);
foreach (BuildSubmissionBase submission in submissions)
{
// Submission has not started
if (!submission.IsStarted)
{
continue;
}
// Attach the exception to this submission if it does not already have an exception associated with it
if (!submission.IsCompleted && submission.BuildResultBase != null && submission.BuildResultBase.Exception == null)
{
submission.BuildResultBase.Exception = e;
}
submission.CompleteLogging();
if (submission.BuildResultBase != null)
{
submission.CheckForCompletion();
}
else
{
submission.CompleteResultsWithException(e);
}
CheckSubmissionCompletenessAndRemove(submission);
}
}
}
}
/// <summary>
/// Handler for LoggingService thread exceptions.
/// </summary>
private void OnLoggingThreadException(Exception e)
{
_workQueue!.Post(() => OnThreadException(e));
}
/// <summary>
/// Raised when a project finished logging message has been processed.
/// </summary>
private void OnProjectFinished(object sender, ProjectFinishedEventArgs e)
{
_workQueue!.Post(() =>
{
lock (_syncLock)
{
if (_projectStartedEvents.TryGetValue(e.BuildEventContext!.SubmissionId, out var originalArgs))
{
if (originalArgs.BuildEventContext!.Equals(e.BuildEventContext))
{
_projectStartedEvents.Remove(e.BuildEventContext.SubmissionId);
if (_buildSubmissions.TryGetValue(e.BuildEventContext.SubmissionId, out var submission))
{
submission.CompleteLogging();
CheckSubmissionCompletenessAndRemove(submission);
}
}
}
}
});
}
/// <summary>
/// Raised when a project started logging message is about to be processed.
/// </summary>
private void OnProjectStarted(object sender, ProjectStartedEventArgs e)
{
_workQueue!.Post(() =>
{
lock (_syncLock)
{
if (!_projectStartedEvents.ContainsKey(e.BuildEventContext!.SubmissionId))
{
_projectStartedEvents[e.BuildEventContext.SubmissionId] = e;
}
}
});
}
/// <summary>
/// Sets <see cref="BuildParameters.IsBuildCheckEnabled"/> to true. Used for BuildCheck Replay Mode.
/// </summary>
internal void EnableBuildCheck()
{
_buildParameters ??= new BuildParameters();
_buildParameters.IsBuildCheckEnabled = true;
}
/// <summary>
/// Creates a logging service around the specified set of loggers.
/// </summary>
private ILoggingService CreateLoggingService(
IEnumerable<ILogger>? loggers,
IEnumerable<ForwardingLoggerRecord>? forwardingLoggers,
ISet<string> warningsAsErrors,
ISet<string> warningsNotAsErrors,
ISet<string> warningsAsMessages)
{
Debug.Assert(Monitor.IsEntered(_syncLock));
int cpuCount = _buildParameters!.MaxNodeCount;
LoggerMode loggerMode = cpuCount == 1 && _buildParameters.UseSynchronousLogging
? LoggerMode.Synchronous
: LoggerMode.Asynchronous;
ILoggingService loggingService = LoggingService.CreateLoggingService(loggerMode,
1 /*This logging service is used for the build manager and the inproc node, therefore it should have the first nodeId*/);
((IBuildComponent)loggingService).InitializeComponent(this);
_componentFactories.ReplaceFactory(BuildComponentType.LoggingService, loggingService as IBuildComponent);
_threadException = null;
loggingService.OnLoggingThreadException += _loggingThreadExceptionEventHandler;
loggingService.OnProjectStarted += _projectStartedEventHandler;
loggingService.OnProjectFinished += _projectFinishedEventHandler;
loggingService.WarningsAsErrors = warningsAsErrors;
loggingService.WarningsNotAsErrors = warningsNotAsErrors;
loggingService.WarningsAsMessages = warningsAsMessages;
if (_buildParameters.IsBuildCheckEnabled)
{
var buildCheckManagerProvider =
((IBuildComponentHost)this).GetComponent(BuildComponentType.BuildCheckManagerProvider) as IBuildCheckManagerProvider;
buildCheckManagerProvider!.Instance.SetDataSource(BuildCheckDataSource.EventArgs);
// We do want to dictate our own forwarding logger (otherwise CentralForwardingLogger with minimum transferred importance MessageImportnace.Low is used)
// In the future we might optimize for single, in-node build scenario - where forwarding logger is not needed (but it's just quick pass-through)
LoggerDescription forwardingLoggerDescription = new LoggerDescription(
loggerClassName: typeof(BuildCheckForwardingLogger).FullName,
loggerAssemblyName: typeof(BuildCheckForwardingLogger).GetTypeInfo().Assembly.GetName().FullName,
loggerAssemblyFile: null,
loggerSwitchParameters: null,
verbosity: LoggerVerbosity.Quiet);
ILogger buildCheckLogger =
new BuildCheckConnectorLogger(new CheckLoggingContextFactory(loggingService),
buildCheckManagerProvider.Instance);
ForwardingLoggerRecord[] forwardingLogger = { new ForwardingLoggerRecord(buildCheckLogger, forwardingLoggerDescription) };
forwardingLoggers = forwardingLoggers?.Concat(forwardingLogger) ?? forwardingLogger;
}
try
{
if (loggers != null)
{
foreach (ILogger logger in loggers)
{
loggingService.RegisterLogger(logger);
}
}
if (loggingService.Loggers.Count == 0)
{
// if no loggers have been registered - let's make sure that at least on forwarding logger
// will forward events we need (project started and finished events)
forwardingLoggers = ProcessForwardingLoggers(forwardingLoggers);
}
if (forwardingLoggers != null)
{
foreach (ForwardingLoggerRecord forwardingLoggerRecord in forwardingLoggers)
{
loggingService.RegisterDistributedLogger(forwardingLoggerRecord.CentralLogger, forwardingLoggerRecord.ForwardingLoggerDescription);
}
}
}
catch (Exception ex) when (!ExceptionHandling.IsCriticalException(ex))
{
ShutdownLoggingService(loggingService);
throw;
}
return loggingService;
// We need to register SOME logger if we don't have any. This ensures the out of proc nodes will still send us message,
// ensuring we receive project started and finished events.
static List<ForwardingLoggerRecord> ProcessForwardingLoggers(IEnumerable<ForwardingLoggerRecord>? forwarders)
{
Type configurableLoggerType = typeof(ConfigurableForwardingLogger);
string engineAssemblyName = configurableLoggerType.GetTypeInfo().Assembly.GetName().FullName;
string configurableLoggerName = configurableLoggerType.FullName!;
if (forwarders == null)
{
return [CreateMinimalForwarder()];
}
List<ForwardingLoggerRecord> result = forwarders.ToList();
// The forwarding loggers that are registered are unknown to us - we cannot make any assumptions.
// So to be on a sure side - we need to add ours.
if (!result.Any(l => l.ForwardingLoggerDescription.Name.Contains(engineAssemblyName)))
{
result.Add(CreateMinimalForwarder());
return result;
}
// Those are the cases where we are sure that we have the forwarding setup as need.
if (result.Any(l =>
l.ForwardingLoggerDescription.Name.Contains(typeof(CentralForwardingLogger).FullName!)
||
(l.ForwardingLoggerDescription.Name.Contains(configurableLoggerName)
&&
l.ForwardingLoggerDescription.LoggerSwitchParameters.Contains("PROJECTSTARTEDEVENT")
&&
l.ForwardingLoggerDescription.LoggerSwitchParameters.Contains("PROJECTFINISHEDEVENT")
&&
l.ForwardingLoggerDescription.LoggerSwitchParameters.Contains("FORWARDPROJECTCONTEXTEVENTS")
)))
{
return result;
}
// In case there is a ConfigurableForwardingLogger, that is not configured as we'd need - we can adjust the config
ForwardingLoggerRecord? configurableLogger = result.FirstOrDefault(l =>
l.ForwardingLoggerDescription.Name.Contains(configurableLoggerName));
// If there is not - we need to add our own.
if (configurableLogger == null)
{
result.Add(CreateMinimalForwarder());
return result;
}
configurableLogger.ForwardingLoggerDescription.LoggerSwitchParameters += ";PROJECTSTARTEDEVENT;PROJECTFINISHEDEVENT;FORWARDPROJECTCONTEXTEVENTS;RESPECTVERBOSITY";
return result;
ForwardingLoggerRecord CreateMinimalForwarder()
{
// We need to register SOME logger if we don't have any. This ensures the out of proc nodes will still send us message,
// ensuring we receive project started and finished events.
LoggerDescription forwardingLoggerDescription = new LoggerDescription(
loggerClassName: configurableLoggerName,
loggerAssemblyName: engineAssemblyName,
loggerAssemblyFile: null,
loggerSwitchParameters: "PROJECTSTARTEDEVENT;PROJECTFINISHEDEVENT;FORWARDPROJECTCONTEXTEVENTS",
verbosity: LoggerVerbosity.Quiet);
return new ForwardingLoggerRecord(new NullLogger(), forwardingLoggerDescription);
}
}
}
private static void LogDeferredMessages(ILoggingService loggingService, IEnumerable<DeferredBuildMessage>? deferredBuildMessages)
{
if (deferredBuildMessages == null)
{
return;
}
foreach (var message in deferredBuildMessages)
{
loggingService.LogCommentFromText(BuildEventContext.Invalid, message.Importance, message.Text);
// If message includes a file path, include that file
if (message.FilePath is not null)
{
loggingService.LogIncludeFile(BuildEventContext.Invalid, message.FilePath);
}
}
}
/// <summary>
/// Ensures that the packet type matches the expected type
/// </summary>
/// <typeparam name="I">The instance-type of packet being expected</typeparam>
private static I ExpectPacketType<I>(INodePacket packet, NodePacketType expectedType) where I : class, INodePacket
{
I? castPacket = packet as I;
// PERF: Not using VerifyThrow here to avoid boxing of expectedType.
if (castPacket == null)
{
ErrorUtilities.ThrowInternalError("Incorrect packet type: {0} should have been {1}", packet.Type, expectedType);
}
return castPacket!;
}
/// <summary>
/// Shutdown the logging service
/// </summary>
private void ShutdownLoggingService(ILoggingService? loggingService)
{
try
{
if (loggingService != null)
{
loggingService.OnLoggingThreadException -= _loggingThreadExceptionEventHandler;
loggingService.OnProjectFinished -= _projectFinishedEventHandler;
loggingService.OnProjectStarted -= _projectStartedEventHandler;
_componentFactories.ShutdownComponent(BuildComponentType.LoggingService);
}
}
finally
{
// Even if an exception is thrown, we want to make sure we null out the logging service so that
// we don't try to shut it down again in some other cleanup code.
_componentFactories.ReplaceFactory(BuildComponentType.LoggingService, (IBuildComponent?)null);
}
}
/// <summary>
/// Dispose implementation
/// </summary>
private void Dispose(bool disposing)
{
if (disposing && !_disposed)
{
lock (_syncLock)
{
if (_disposed)
{
// Multiple caller raced for enter into the lock
return;
}
// We should always have finished cleaning up before calling Dispose.
RequireState(BuildManagerState.Idle, "ShouldNotDisposeWhenBuildManagerActive");
_componentFactories?.ShutdownComponents();
if (_workQueue != null)
{
_workQueue.Complete();
_workQueue = null;
}
if (_executionCancellationTokenSource != null)
{
_executionCancellationTokenSource.Cancel();
_executionCancellationTokenSource = null;
}
if (_noActiveSubmissionsEvent != null)
{
_noActiveSubmissionsEvent.Dispose();
_noActiveSubmissionsEvent = null;
}
if (_noNodesActiveEvent != null)
{
_noNodesActiveEvent.Dispose();
_noNodesActiveEvent = null;
}
if (ReferenceEquals(this, s_singletonInstance))
{
s_singletonInstance = null;
}
_disposed = true;
}
}
}
private bool ReuseOldCaches(string[] inputCacheFiles)
{
Debug.Assert(Monitor.IsEntered(_syncLock));
ErrorUtilities.VerifyThrowInternalNull(inputCacheFiles);
ErrorUtilities.VerifyThrow(_configCache == null, "caches must not be set at this point");
ErrorUtilities.VerifyThrow(_resultsCache == null, "caches must not be set at this point");
try
{
if (inputCacheFiles.Length == 0)
{
return false;
}
if (inputCacheFiles.Any(f => !File.Exists(f)))
{
LogErrorAndShutdown(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("InputCacheFilesDoNotExist", string.Join(";", inputCacheFiles.Where(f => !File.Exists(f)))));
return false;
}
var cacheAggregator = new CacheAggregator(() => GetNewConfigurationId());
foreach (var inputCacheFile in inputCacheFiles)
{
var (configCache, resultsCache, exception) = CacheSerialization.DeserializeCaches(inputCacheFile);
if (exception != null)
{
LogErrorAndShutdown(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("ErrorReadingCacheFile", inputCacheFile, exception.Message));
return false;
}
cacheAggregator.Add(configCache, resultsCache);
}
var cacheAggregation = cacheAggregator.Aggregate();
// using caches with override (override queried first before current cache) based on the assumption that during single project cached builds
// there's many old results, but just one single actively building project.
_componentFactories.ReplaceFactory(BuildComponentType.ConfigCache, new ConfigCacheWithOverride(cacheAggregation.ConfigCache));
_componentFactories.ReplaceFactory(BuildComponentType.ResultsCache, new ResultsCacheWithOverride(cacheAggregation.ResultsCache));
return true;
}
catch
{
CancelAndMarkAsFailure();
throw;
}
}
private void LogMessage(string message)
{
var loggingService = ((IBuildComponentHost)this).LoggingService;
loggingService?.LogCommentFromText(BuildEventContext.Invalid, MessageImportance.High, message);
}
private void LogErrorAndShutdown(string message)
{
var loggingService = ((IBuildComponentHost)this).LoggingService;
loggingService?.LogErrorFromText(
BuildEventContext.Invalid,
null,
null,
null,
BuildEventFileInfo.Empty,
message);
CancelAndMarkAsFailure();
if (loggingService == null)
{
// todo should we write this to temp file instead (like failing nodes do)
throw new Exception(message);
}
}
private void CancelAndMarkAsFailure()
{
Debug.Assert(Monitor.IsEntered(_syncLock));
CancelAllSubmissions();
// CancelAllSubmissions also ends up setting _shuttingDown and _overallBuildSuccess but it does so in a separate thread to avoid deadlocks.
// This might cause a race with the first builds which might miss the shutdown update and succeed instead of fail.
_shuttingDown = true;
_executionCancellationTokenSource?.Cancel();
_overallBuildSuccess = false;
}
/// <summary>
/// The logger registered to the logging service when no other one is.
/// </summary>
internal class NullLogger : ILogger
{
#region ILogger Members
/// <summary>
/// The logger verbosity.
/// </summary>
public LoggerVerbosity Verbosity
{
get => LoggerVerbosity.Normal;
set { }
}
/// <summary>
/// The logger parameters.
/// </summary>
public string? Parameters
{
get => String.Empty;
set { }
}
/// <summary>
/// Initialize.
/// </summary>
public void Initialize(IEventSource eventSource)
{
// Most checks in LoggingService are "does any attached logger
// specifically opt into this new behavior?". As such, the
// NullLogger shouldn't opt into them explicitly and should
// let other loggers opt in.
// IncludeEvaluationPropertiesAndItems was different,
// because it checked "do ALL attached loggers opt into
// the new behavior?".
// It was fixed and hence we need to be careful not to opt in
// the behavior as it was done before - but let the other loggers choose.
//
// For this reason NullLogger MUST NOT call
// ((IEventSource4)eventSource).IncludeEvaluationPropertiesAndItems();
}
/// <summary>
/// Shutdown.
/// </summary>
public void Shutdown()
{
}
#endregion
}
}
}
|