File: BackEnd\Components\RequestBuilder\RequestBuilder.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Collections;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Eventing;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Execution;
using Microsoft.Build.Experimental.BuildCheck;
using Microsoft.Build.Experimental.BuildCheck.Infrastructure;
using Microsoft.Build.Framework;
using Microsoft.Build.Internal;
using Microsoft.Build.Shared;
using NodeLoggingContext = Microsoft.Build.BackEnd.Logging.NodeLoggingContext;
using ProjectLoggingContext = Microsoft.Build.BackEnd.Logging.ProjectLoggingContext;
 
#nullable disable
 
namespace Microsoft.Build.BackEnd
{
    /// <summary>
    /// Implementation of IRequestBuilder
    /// </summary>
    internal class RequestBuilder : IRequestBuilder, IRequestBuilderCallback, IBuildComponent
    {
        /// <summary>
        /// The dedicated scheduler object.
        /// </summary>
        private static readonly TaskScheduler s_dedicatedScheduler = new DedicatedThreadsTaskScheduler();
 
        /// <summary>
        /// The event used to signal that this request should immediately terminate.
        /// </summary>
        private ManualResetEvent _terminateEvent;
 
        /// <summary>
        /// The event used to signal that this request should wake up from its wait state.
        /// </summary>
        private AutoResetEvent _continueEvent;
 
        /// <summary>
        /// The results used when a build request entry continues.
        /// </summary>
        private IDictionary<int, BuildResult> _continueResults;
 
        /// <summary>
        /// Queue of actions to call when a resource request is responded to.
        /// </summary>
        private ConcurrentQueue<Action<ResourceResponse>> _pendingResourceRequests;
 
        /// <summary>
        /// The task representing the currently-executing build request.
        /// </summary>
        private Task _requestTask;
 
        /// <summary>
        /// The cancellation token source for the currently-executing build request.
        /// </summary>
        private CancellationTokenSource _cancellationTokenSource;
 
        /// <summary>
        /// The build request entry being built.
        /// </summary>
        private BuildRequestEntry _requestEntry;
 
        /// <summary>
        /// The component host.
        /// </summary>
        private IBuildComponentHost _componentHost;
 
        /// <summary>
        /// The node logging context
        /// </summary>
        private NodeLoggingContext _nodeLoggingContext;
 
        /// <summary>
        /// The project logging context
        /// </summary>
        private ProjectLoggingContext _projectLoggingContext;
 
        /// <summary>
        /// The target builder.
        /// </summary>
        private ITargetBuilder _targetBuilder;
 
        /// <summary>
        /// Block type
        /// </summary>
        private BlockType _blockType = BlockType.Unblocked;
 
        /// <summary>
        /// Flag indicating we are in an MSBuild callback
        /// </summary>
        private bool _inMSBuildCallback = false;
 
        /// <summary>
        /// Flag indicating whether this request builder has been zombied by a cancellation request.
        /// </summary>
        private bool _isZombie = false;
 
        /// <summary>
        /// Creates a new request builder.
        /// </summary>
        internal RequestBuilder()
        {
            _terminateEvent = new ManualResetEvent(false);
            _continueEvent = new AutoResetEvent(false);
            _pendingResourceRequests = new ConcurrentQueue<Action<ResourceResponse>>();
        }
 
        /// <summary>
        /// The event raised when a new build request should be issued.
        /// </summary>
        public event NewBuildRequestsDelegate OnNewBuildRequests;
 
        /// <summary>
        /// The event raised when the build request has completed.
        /// </summary>
        public event BuildRequestCompletedDelegate OnBuildRequestCompleted;
 
        /// <summary>
        /// The event raised when the build request has completed.
        /// </summary>
        public event BuildRequestBlockedDelegate OnBuildRequestBlocked;
 
        /// <summary>
        /// The event raised when resources are requested.
        /// </summary>
        public event ResourceRequestDelegate OnResourceRequest;
 
        /// <summary>
        /// The current block type
        /// </summary>
        private enum BlockType
        {
            /// <summary>
            /// We are blocked waiting on a target in progress.
            /// </summary>
            BlockedOnTargetInProgress,
 
            /// <summary>
            /// We are blocked waiting for results from child requests.
            /// </summary>
            BlockedOnChildRequests,
 
            /// <summary>
            /// We are blocked because we have yielded control
            /// </summary>
            Yielded,
 
            /// <summary>
            /// We are not blocked at all.
            /// </summary>
            Unblocked
        }
 
        /// <summary>
        /// Retrieves the request entry associated with this RequestBuilder.
        /// </summary>
        internal BuildRequestEntry RequestEntry
        {
            get
            {
                VerifyIsNotZombie();
                return _requestEntry;
            }
        }
 
        /// <summary>
        /// Returns true if this RequestBuilder has an active build request
        /// </summary>
        internal bool HasActiveBuildRequest
        {
            get
            {
                VerifyIsNotZombie();
 
                return (_requestTask?.IsCompleted == false) || (_componentHost.LegacyThreadingData.MainThreadSubmissionId != -1);
            }
        }
 
        /// <summary>
        /// Starts a build request
        /// </summary>
        /// <param name="loggingContext">The logging context for the node.</param>
        /// <param name="entry">The entry to build.</param>
        public void BuildRequest(NodeLoggingContext loggingContext, BuildRequestEntry entry)
        {
            ErrorUtilities.VerifyThrowArgumentNull(loggingContext);
            ErrorUtilities.VerifyThrowArgumentNull(entry);
            ErrorUtilities.VerifyThrow(_componentHost != null, "Host not set.");
            ErrorUtilities.VerifyThrow(_targetBuilder == null, "targetBuilder not null");
            ErrorUtilities.VerifyThrow(_nodeLoggingContext == null, "nodeLoggingContext not null");
            ErrorUtilities.VerifyThrow(_requestEntry == null, "requestEntry not null");
            ErrorUtilities.VerifyThrow(!_terminateEvent.WaitOne(0), "Cancel already called");
 
            _nodeLoggingContext = loggingContext;
            _blockType = BlockType.Unblocked;
            _requestEntry = entry;
            _requestEntry.Continue();
            _continueResults = null;
 
            _targetBuilder = (ITargetBuilder)_componentHost.GetComponent(BuildComponentType.TargetBuilder);
 
            VerifyEntryInActiveState();
            InitializeOperatingEnvironment();
            StartBuilderThread();
        }
 
        /// <summary>
        /// Continues a build request
        /// </summary>
        public void ContinueRequest()
        {
            ErrorUtilities.VerifyThrow(HasActiveBuildRequest, "Request not building");
            ErrorUtilities.VerifyThrow(!_terminateEvent.WaitOne(0), "Request already terminated");
            ErrorUtilities.VerifyThrow(!_continueEvent.WaitOne(0), "Request already continued");
            VerifyEntryInReadyState();
 
            _continueResults = _requestEntry.Continue();
            ErrorUtilities.VerifyThrow(_blockType == BlockType.BlockedOnTargetInProgress || _blockType == BlockType.Yielded || (_continueResults != null), "Unexpected null results for request {0} (nr {1})", _requestEntry.Request.GlobalRequestId, _requestEntry.Request.NodeRequestId);
 
            // Setting the continue event will wake up the build thread, which is suspended in StartNewBuildRequests.
            _continueEvent.Set();
        }
 
        /// <summary>
        /// Continues a build request after receiving a resource response.
        /// </summary>
        public void ContinueRequestWithResources(ResourceResponse response)
        {
            ErrorUtilities.VerifyThrow(HasActiveBuildRequest, "Request not building");
            ErrorUtilities.VerifyThrow(!_terminateEvent.WaitOne(0), "Request already terminated");
            ErrorUtilities.VerifyThrow(_pendingResourceRequests.Any(), "No pending resource requests");
            VerifyEntryInActiveOrWaitingState();
 
            _pendingResourceRequests.Dequeue()(response);
        }
 
        /// <summary>
        /// Terminates the build request
        /// </summary>
        /// <remarks>
        /// Once we have entered this method, no more methods will be invoked on this class (save ShutdownComponent)
        /// as we should no longer be receiving any messages from the BuildManager.
        /// </remarks>
        public void CancelRequest()
        {
            this.BeginCancel();
            this.WaitForCancelCompletion();
        }
 
        /// <summary>
        /// Starts to cancel an existing request.
        /// </summary>
        public void BeginCancel()
        {
            _terminateEvent.Set();
 
            // Cancel the current build.
            if (_cancellationTokenSource != null)
            {
                if (_cancellationTokenSource.IsCancellationRequested)
                {
                    return;
                }
 
                _cancellationTokenSource.Cancel();
            }
        }
 
        /// <summary>
        /// Waits for the cancellation until it's completed, and cleans up the internal states.
        /// </summary>
        public void WaitForCancelCompletion()
        {
            // Wait for the request thread to terminate.
            if (_requestTask != null)
            {
                bool taskCleanedUp = false;
 
                try
                {
                    taskCleanedUp = _requestTask.Wait(BuildParameters.RequestBuilderShutdownTimeout);
                }
                catch (AggregateException e) when (InnerExceptionsAreAllCancelledExceptions(e))
                {
                    // ignore -- just indicates that the task finished cancelling before we got a chance to wait on it.
                    taskCleanedUp = true;
                }
 
                if (!taskCleanedUp)
                {
                    // This can happen when a task has locked us up.
                    _projectLoggingContext.LogError(new BuildEventFileInfo(String.Empty), "FailedToReceiveTaskThreadStatus", BuildParameters.RequestBuilderShutdownTimeout);
                    ErrorUtilities.ThrowInvalidOperation("UnableToCancel");
                }
            }
 
            _isZombie = true;
        }
 
        private bool InnerExceptionsAreAllCancelledExceptions(AggregateException e)
        {
            return e.Flatten().InnerExceptions.All(ex => ex is TaskCanceledException || ex is OperationCanceledException);
        }
 
        #region IRequestBuilderCallback Members
 
        /// <summary>
        /// This method instructs the request builder to build the specified projects using the specified parameters.  This is
        /// what is ultimately used by something like an MSBuild task which needs to invoke a project-to-project reference.  IBuildEngine
        /// and IBuildEngine2 have BuildProjectFile methods which boil down to an invocation of this method as well.
        /// </summary>
        /// <param name="projectFiles">An array of projects to be built.</param>
        /// <param name="properties">The property groups to use for each project.  Must be the same number as there are project files.</param>
        /// <param name="toolsVersions">The tools version to use for each project.  Must be the same number as there are project files.</param>
        /// <param name="targets">The targets to be built.  Each project will be built with the same targets.</param>
        /// <param name="waitForResults">True to wait for the results </param>
        /// <param name="skipNonexistentTargets">If set, skip targets that are not defined in the projects to be built.</param>
        /// <returns>True if the requests were satisfied, false if they were aborted.</returns>
        public async Task<BuildResult[]> BuildProjects(string[] projectFiles, PropertyDictionary<ProjectPropertyInstance>[] properties, string[] toolsVersions, string[] targets, bool waitForResults, bool skipNonexistentTargets = false)
        {
            VerifyIsNotZombie();
            ErrorUtilities.VerifyThrowArgumentNull(projectFiles);
            ErrorUtilities.VerifyThrowArgumentNull(properties);
            ErrorUtilities.VerifyThrowArgumentNull(targets);
            ErrorUtilities.VerifyThrowArgumentNull(toolsVersions);
            ErrorUtilities.VerifyThrow(_componentHost != null, "No host object set");
            ErrorUtilities.VerifyThrow(projectFiles.Length == properties.Length, "Properties and project counts not the same");
            ErrorUtilities.VerifyThrow(projectFiles.Length == toolsVersions.Length, "Tools versions and project counts not the same");
 
            FullyQualifiedBuildRequest[] requests = new FullyQualifiedBuildRequest[projectFiles.Length];
 
            for (int i = 0; i < projectFiles.Length; ++i)
            {
                if (!Path.IsPathRooted(projectFiles[i]))
                {
                    projectFiles[i] = Path.Combine(_requestEntry.ProjectRootDirectory, projectFiles[i]);
                }
 
                // Canonicalize
                projectFiles[i] = FileUtilities.NormalizePath(projectFiles[i]);
 
                // A tools version specified by an MSBuild task or similar has priority
                string explicitToolsVersion = toolsVersions[i];
 
                // Otherwise go to any explicit tools version on the project who made this callback
                if (explicitToolsVersion == null && _requestEntry.RequestConfiguration.ExplicitToolsVersionSpecified)
                {
                    explicitToolsVersion = _requestEntry.RequestConfiguration.ToolsVersion;
                }
 
                // Otherwise let the BuildRequestConfiguration figure out what tools version will be used
                BuildRequestData data = new BuildRequestData(projectFiles[i], properties[i].ToDictionary(), explicitToolsVersion, targets, null);
 
                BuildRequestConfiguration config = new BuildRequestConfiguration(data, _componentHost.BuildParameters.DefaultToolsVersion);
                ProjectIsolationMode isolateProjects = _componentHost.BuildParameters.ProjectIsolationMode;
                bool skipStaticGraphIsolationConstraints = (isolateProjects != ProjectIsolationMode.False && _requestEntry.RequestConfiguration.ShouldSkipIsolationConstraintsForReference(config.ProjectFullPath))
                    || isolateProjects == ProjectIsolationMode.MessageUponIsolationViolation;
                requests[i] = new FullyQualifiedBuildRequest(
                    config: config,
                    targets: targets,
                    resultsNeeded: waitForResults,
                    skipStaticGraphIsolationConstraints: skipStaticGraphIsolationConstraints,
                    flags: skipNonexistentTargets
                        ? BuildRequestDataFlags.SkipNonexistentTargets
                        : BuildRequestDataFlags.None);
            }
 
            // Send the requests off
            BuildResult[] results = await StartNewBuildRequests(requests);
 
            ErrorUtilities.VerifyThrow(requests.Length == results.Length, "# results != # requests");
 
            return results;
        }
 
        /// <summary>
        /// This method is called when the current request needs to build a target which is already in progress on this configuration, but which
        /// is being built by another request.
        /// </summary>
        /// <param name="blockingGlobalRequestId">The id of the request on which we are blocked.</param>
        /// <param name="blockingTarget">The target on which we are blocked.</param>
        /// <param name="partialBuildResult">A BuildResult with results from an incomplete build request.</param>
        public async Task BlockOnTargetInProgress(int blockingGlobalRequestId, string blockingTarget, BuildResult partialBuildResult = null)
        {
            VerifyIsNotZombie();
            SaveOperatingEnvironment();
 
            _blockType = BlockType.BlockedOnTargetInProgress;
 
            RaiseOnBlockedRequest(blockingGlobalRequestId, blockingTarget, partialBuildResult);
 
            WaitHandle[] handles = [_terminateEvent, _continueEvent];
 
            int handle;
            if (IsBuilderUsingLegacyThreadingSemantics(_componentHost, _requestEntry))
            {
                handle = WaitHandle.WaitAny(handles);
            }
            else
            {
                handle = await handles.ToTask();
            }
 
            RestoreOperatingEnvironment();
 
            if (handle == 0)
            {
                // We've been aborted
                throw new BuildAbortedException();
            }
 
            _blockType = BlockType.Unblocked;
 
            VerifyEntryInActiveState();
        }
 
        /// <summary>
        /// Yields the node.
        /// </summary>
        public void Yield()
        {
            VerifyIsNotZombie();
            SaveOperatingEnvironment();
 
            _blockType = BlockType.Yielded;
 
            RaiseOnBlockedRequest(_requestEntry.Request.GlobalRequestId, null);
        }
 
        /// <summary>
        /// Waits for the node to be reacquired.
        /// </summary>
        public void Reacquire()
        {
            VerifyIsNotZombie();
            RaiseOnBlockedRequest(_requestEntry.Request.GlobalRequestId, String.Empty);
 
            WaitHandle[] handles = [_terminateEvent, _continueEvent];
 
            int handle = WaitHandle.WaitAny(handles);
 
            RestoreOperatingEnvironment();
 
            if (handle == 0)
            {
                // We've been aborted
                throw new BuildAbortedException();
            }
 
            _blockType = BlockType.Unblocked;
 
            VerifyEntryInActiveState();
        }
 
        /// <summary>
        /// Enters the state where we are going to perform a build request callback.
        /// </summary>
        public void EnterMSBuildCallbackState()
        {
            VerifyIsNotZombie();
            ErrorUtilities.VerifyThrow(!_inMSBuildCallback, "Already in an MSBuild callback!");
            _inMSBuildCallback = true;
        }
 
        /// <summary>
        /// Exits the build request callback state.
        /// </summary>
        public void ExitMSBuildCallbackState()
        {
            VerifyIsNotZombie();
            ErrorUtilities.VerifyThrow(_inMSBuildCallback, "Not in an MSBuild callback!");
            _inMSBuildCallback = false;
        }
 
        /// <summary>
        /// Requests CPU resources from the scheduler.
        /// </summary>
        public int RequestCores(object monitorLockObject, int requestedCores, bool waitForCores)
        {
            ErrorUtilities.VerifyThrow(Monitor.IsEntered(monitorLockObject), "Not running under the given lock");
            VerifyIsNotZombie();
 
            // The task may be calling RequestCores from multiple threads and the call may be blocking, so in general, we have to maintain
            // a queue of pending requests.
            ResourceResponse responseObject = null;
            using AutoResetEvent responseEvent = new AutoResetEvent(false);
            _pendingResourceRequests.Enqueue((ResourceResponse response) =>
            {
                responseObject = response;
                responseEvent.Set();
            });
 
            RaiseResourceRequest(ResourceRequest.CreateAcquireRequest(_requestEntry.Request.GlobalRequestId, requestedCores, waitForCores));
 
            // Wait for one of two events to be signaled: 1) The build was canceled, 2) The response to our request was received.
            WaitHandle[] waitHandles = [_terminateEvent, responseEvent];
            int waitResult;
 
            // Drop the lock so that the same task can call ReleaseCores from other threads to unblock itself.
            Monitor.Exit(monitorLockObject);
            try
            {
                waitResult = WaitHandle.WaitAny(waitHandles);
            }
            finally
            {
                // Now re-take the lock before continuing.
                Monitor.Enter(monitorLockObject);
            }
 
            if (waitResult == 0)
            {
                // We've been aborted.
                throw new BuildAbortedException();
            }
 
            VerifyEntryInActiveOrWaitingState();
            return responseObject.NumCores;
        }
 
        /// <summary>
        /// Returns CPU resources to the scheduler.
        /// </summary>
        public void ReleaseCores(int coresToRelease)
        {
            VerifyIsNotZombie();
            RaiseResourceRequest(ResourceRequest.CreateReleaseRequest(_requestEntry.Request.GlobalRequestId, coresToRelease));
        }
 
        #endregion
 
        #region IBuildComponent Members
 
        /// <summary>
        /// Sets the component host.
        /// </summary>
        /// <param name="host">The component host.</param>
        public void InitializeComponent(IBuildComponentHost host)
        {
            ErrorUtilities.VerifyThrowArgumentNull(host);
            ErrorUtilities.VerifyThrow(_componentHost == null, "RequestBuilder already initialized.");
            _componentHost = host;
        }
 
        /// <summary>
        /// Shuts down this component
        /// </summary>
        public void ShutdownComponent()
        {
            _componentHost = null;
        }
 
        #endregion
 
        /// <summary>
        /// Returns true if this builder is using legacy threading semantics.
        /// </summary>
        internal static bool IsBuilderUsingLegacyThreadingSemantics(IBuildComponentHost host, BuildRequestEntry entry)
        {
            return host.BuildParameters.LegacyThreadingSemantics && (host.LegacyThreadingData.MainThreadSubmissionId == entry.Request.SubmissionId);
        }
 
        /// <summary>
        /// This method waits for the specified handles, but will also spawn a request builder "thread" if that event is set.
        /// This mechanism is used to implement running RequestBuilder threads on the main UI thread in VS.
        /// </summary>
        /// <returns>The index of the handle which was signaled.</returns>
        internal static int WaitWithBuilderThreadStart(WaitHandle[] handles, bool recursive, LegacyThreadingData threadingData, int submissionId)
        {
            WaitHandle[] allHandles = new WaitHandle[handles.Length + 1];
            allHandles[0] = threadingData.GetStartRequestBuilderMainThreadEventForSubmission(submissionId);
            Array.Copy(handles, 0, allHandles, 1, handles.Length);
 
            while (true)
            {
                try
                {
                    int signaledIndex = WaitHandle.WaitAny(allHandles, Timeout.Infinite);
 
                    if (signaledIndex == 0)
                    {
                        // Grab the request builder reserved for running on this thread.
                        RequestBuilder builder = threadingData.InstanceForMainThread;
 
                        // This clears out the value so we can re-enter with legacy-threading semantics on another request builder
                        // which must use this same thread.  It is safe to perform this operation because request activations cannot
                        // happen in parallel on the same thread, so there is no race.
                        threadingData.InstanceForMainThread = null;
 
                        // Now wait for the request to build.
                        builder.RequestThreadProc(setThreadParameters: false).Wait();
                    }
                    else
                    {
                        // We were signalled on one of the other handles.  Return control to the caller.
                        return signaledIndex - 1;
                    }
                }
                finally
                {
                    // If this was the top level submission doing the waiting, we are done with this submission and it's
                    // main thread building context
                    if (!recursive)
                    {
                        // Set the event indicating the legacy thread is no longer being used, so it is safe to clean up.
                        threadingData.SignalLegacyThreadEnd(submissionId);
                    }
                }
            }
        }
 
        /// <summary>
        /// Class factory for component creation.
        /// </summary>
        internal static IBuildComponent CreateComponent(BuildComponentType type)
        {
            ErrorUtilities.VerifyThrow(type == BuildComponentType.RequestBuilder, "Cannot create components of type {0}", type);
            return new RequestBuilder();
        }
 
        /// <summary>
        /// Starts the thread used to build
        /// </summary>
        private void StartBuilderThread()
        {
            ErrorUtilities.VerifyThrow(_requestTask == null, "Already have a task.");
 
            _cancellationTokenSource = new CancellationTokenSource();
 
            // IMPLEMENTATION NOTE: It may look strange that we are creating new tasks here which immediately turn around and create
            // more tasks that look async.  The reason for this is that while these methods are technically async, they really only
            // unwind at very specific times according to the needs of MSBuild, in particular when we are waiting for results from
            // another project or when we are Yielding the Build Engine while running certain tasks.  Essentially, the Request Builder
            // and related components form a giant state machine and the tasks are used to implement one very deep co-routine.
            if (IsBuilderUsingLegacyThreadingSemantics(_componentHost, _requestEntry))
            {
                // Create a task which completes when the legacy threading task thread is finished.
                _componentHost.LegacyThreadingData.SignalLegacyThreadStart(this);
 
                _requestTask = Task.Factory.StartNew(
                    () =>
                    {
                        // If this is a very quick-running request, it is possible that the request will have built and completed in
                        // the time between when StartBuilderThread is called, and when the threadpool gets around to actually servicing
                        // this request.  If that's the case, it's also possible that ShutdownComponent() could have already been called,
                        // in which case the componentHost will be null.
 
                        // In that circumstance, by definition we don't have anyone who will want to wait on the LegacyThreadInactiveEvent
                        // task, so we can safely just return. Take a snapshot so that we don't fall victim to componentHost being set
                        // to null between the null check and asking the LegacyThreadingData for the Task.
                        IBuildComponentHost componentHostSnapshot = _componentHost;
 
                        if (componentHostSnapshot?.LegacyThreadingData != null)
                        {
                            return componentHostSnapshot.LegacyThreadingData.GetLegacyThreadInactiveTask(_requestEntry.Request.SubmissionId);
                        }
                        else
                        {
                            return Task.FromResult<object>(null);
                        }
                    },
                    _cancellationTokenSource.Token,
                    TaskCreationOptions.None,
                    TaskScheduler.Default).Unwrap();
            }
            else
            {
                ErrorUtilities.VerifyThrow(_componentHost.LegacyThreadingData.MainThreadSubmissionId != _requestEntry.Request.SubmissionId, "Can't start builder thread when we are using legacy threading semantics for this request.");
 
                // We do not run in STA by default.  Most code does not
                // require the STA apartment and the .Net default is to
                // create threads with MTA semantics.  We provide this
                // switch so that those few tasks which may require it
                // can be made to work.
                if (Environment.GetEnvironmentVariable("MSBUILDFORCESTA") == "1")
                {
                    _requestTask = Task.Factory.StartNew(
                        () =>
                        {
                            return this.RequestThreadProc(setThreadParameters: true);
                        },
                        _cancellationTokenSource.Token,
                        TaskCreationOptions.None,
                        AwaitExtensions.OneSTAThreadPerTaskSchedulerInstance).Unwrap();
                }
                else
                {
                    // Start up the request thread.  When it starts it will begin building our current entry.
                    _requestTask = Task.Factory.StartNew(
                        () =>
                        {
                            return this.RequestThreadProc(setThreadParameters: true);
                        },
                        _cancellationTokenSource.Token,
                        TaskCreationOptions.None,
                        s_dedicatedScheduler).Unwrap();
                }
            }
        }
 
        /// <summary>
        /// Set some parameters common to all worker threads we use
        /// </summary>
        private void SetCommonWorkerThreadParameters()
        {
            CultureInfo.CurrentCulture = _componentHost.BuildParameters.Culture;
            CultureInfo.CurrentUICulture = _componentHost.BuildParameters.UICulture;
 
            Thread.CurrentThread.Priority = _componentHost.BuildParameters.BuildThreadPriority;
            Thread.CurrentThread.IsBackground = true;
 
            // NOTE: This is safe to do because we have specified long-running so we get our own new thread.
            string threadName = "RequestBuilder thread";
 
#if FEATURE_APARTMENT_STATE
            if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA)
            {
                // NOTE: This is safe to do because the STA scheduler always gives us our own new thread.
                threadName = "RequestBuilder STA thread";
            }
#endif
            if (string.IsNullOrEmpty(Thread.CurrentThread.Name))
            {
                Thread.CurrentThread.Name = threadName;
            }
        }
 
        /// <summary>
        /// Asserts that the entry is in the ready state.
        /// </summary>
        private void VerifyEntryInReadyState()
        {
            ErrorUtilities.VerifyThrow(_requestEntry.State == BuildRequestEntryState.Ready, "Entry is not in the Ready state, it is in the {0} state.", _requestEntry.State);
        }
 
        /// <summary>
        /// Asserts that the entry is in the active state.
        /// </summary>
        private void VerifyEntryInActiveState()
        {
            ErrorUtilities.VerifyThrow(_requestEntry.State == BuildRequestEntryState.Active, "Entry is not in the Active state, it is in the {0} state.", _requestEntry.State);
        }
 
        /// <summary>
        /// Asserts that the entry is in the active or waiting state.
        /// </summary>
        private void VerifyEntryInActiveOrWaitingState()
        {
            ErrorUtilities.VerifyThrow(_requestEntry.State == BuildRequestEntryState.Active || _requestEntry.State == BuildRequestEntryState.Waiting,
                "Entry is not in the Active or Waiting state, it is in the {0} state.", _requestEntry.State);
        }
 
        /// <summary>
        /// The entry point for the request builder thread.
        /// Launch the project and gather the results, reporting them back to the BuildRequestEngine.
        /// </summary>
        private async Task RequestThreadProc(bool setThreadParameters)
        {
            Exception thrownException = null;
            BuildResult result = null;
 
            try
            {
                if (setThreadParameters)
                {
                    SetCommonWorkerThreadParameters();
                }
                MSBuildEventSource.Log.RequestThreadProcStart();
                VerifyEntryInActiveState();
                result = await BuildProject();
                MSBuildEventSource.Log.RequestThreadProcStop();
            }
            catch (InvalidProjectFileException ex)
            {
                if (_projectLoggingContext != null)
                {
                    _projectLoggingContext.LogInvalidProjectFileError(ex);
                }
                else
                {
                    _nodeLoggingContext.LogInvalidProjectFileError(ex);
                }
 
                thrownException = ex;
            }
            // This is a workaround for https://github.com/dotnet/msbuild/issues/2064. It catches the exception case and turns it into a more understandable warning.
            catch (UnbuildableProjectTypeException ex)
            {
                thrownException = ex;
                if (_projectLoggingContext is null)
                {
                    _nodeLoggingContext.LogWarning("SolutionParseUnknownProjectType", ex.Message);
                }
                else
                {
                    _projectLoggingContext.LogWarning("SolutionParseUnknownProjectType", ex.Message);
                }
            }
            catch (Exception ex)
            {
                thrownException = ex;
                if (ex is BuildAbortedException)
                {
                    // The build was likely cancelled. We do not need to log an error in this case.
                }
                else if (ex is InternalLoggerException)
                {
                    string realMessage = TaskLoggingHelper.GetInnerExceptionMessageString(ex);
                    LoggingContext loggingContext = ((LoggingContext)_projectLoggingContext) ?? _nodeLoggingContext;
                    loggingContext.LogError(
                        BuildEventFileInfo.Empty,
                        "FatalErrorWhileLoggingWithInnerException",
                        realMessage);
 
                    loggingContext.LogCommentFromText(MessageImportance.Low, ex.ToString());
                }
                else if (ex is ThreadAbortException)
                {
                    // Do nothing.  This will happen when the thread is forcibly terminated because we are shutting down, for example
                    // when the unit test framework terminates.
                    throw;
                }
                else if (ex is not CriticalTaskException)
                {
                    (((LoggingContext)_projectLoggingContext) ?? _nodeLoggingContext).LogError(BuildEventFileInfo.Empty, "UnhandledMSBuildError", ex.ToString());
                }
 
                if (ExceptionHandling.IsCriticalException(ex))
                {
                    // Dump all engine exceptions to a temp file
                    // so that we have something to go on in the
                    // event of a failure
                    ExceptionHandling.DumpExceptionToFile(ex);
 
                    // This includes InternalErrorException, which we definitely want a callstack for.
                    // Fortunately the default console UnhandledExceptionHandler will log the callstack even
                    // for unhandled exceptions thrown from threads other than the main thread, like here.
                    // Less fortunately NUnit doesn't.
 
                    // This is fatal: process will terminate: make sure the
                    // debugger launches
                    ErrorUtilities.ThrowInternalError(ex.Message, ex);
                    throw;
                }
            }
            finally
            {
                _blockType = BlockType.Unblocked;
 
                if (thrownException != null)
                {
                    ErrorUtilities.VerifyThrow(result == null, "Result already set when exception was thrown.");
                    result = new BuildResult(_requestEntry.Request, thrownException);
                }
 
                ReportResultAndCleanUp(result);
            }
        }
 
        /// <summary>
        /// Reports this result to the engine and cleans up.
        /// </summary>
        private void ReportResultAndCleanUp(BuildResult result)
        {
            if (_projectLoggingContext != null)
            {
                try
                {
                    _projectLoggingContext.LogProjectFinished(result.OverallResult == BuildResultCode.Success);
                }
                catch (Exception ex) when (!ExceptionHandling.IsCriticalException(ex))
                {
                    if (result.Exception == null)
                    {
                        result.Exception = ex;
                    }
                }
            }
 
            // Clear out our state now in case any of these callbacks cause the engine to try and immediately
            // reuse this builder.
            BuildRequestEntry entryToComplete = _requestEntry;
            _nodeLoggingContext = null;
            _requestEntry = null;
            if (_targetBuilder != null)
            {
                ((IBuildComponent)_targetBuilder).ShutdownComponent();
            }
 
            if (_componentHost.BuildParameters.SaveOperatingEnvironment)
            {
                entryToComplete.RequestConfiguration.SavedCurrentDirectory = NativeMethodsShared.GetCurrentDirectory();
                entryToComplete.RequestConfiguration.SavedEnvironmentVariables = CommunicationsUtilities.GetEnvironmentVariables();
            }
 
            entryToComplete.Complete(result);
            RaiseBuildRequestCompleted(entryToComplete);
        }
 
        /// <summary>
        /// This is called back when this request needs to issue new requests and possible wait on them.  This method will
        /// block the builder's thread if any of the requests require us to wait for their results.
        /// </summary>
        /// <param name="requests">The list of build requests to be built.</param>
        /// <returns>The results, or null if the build should terminate.</returns>
        private async Task<BuildResult[]> StartNewBuildRequests(FullyQualifiedBuildRequest[] requests)
        {
            // Determine if we need to wait for results from any of these requests.
            // UNDONE: Currently we never set ResultsNeeded to anything but true.  The purpose of this flag would be
            // to issue another top-level build request which no other request depends on, but which must finish in order for
            // the build to be considered complete.  This would be brand new semantics.
            bool waitForResults = false;
            foreach (FullyQualifiedBuildRequest request in requests)
            {
                if (request.ResultsNeeded)
                {
                    waitForResults = true;
                    break;
                }
            }
 
            _blockType = BlockType.BlockedOnChildRequests;
 
            // Save the current operating environment, if necessary
            if (waitForResults)
            {
                SaveOperatingEnvironment();
            }
 
            // Issue the requests to the engine
            RaiseOnNewBuildRequests(requests);
 
            // TODO: OPTIMIZATION: By returning null here, we commit to having to unwind the stack all the
            // way back to RequestThreadProc and then shutting down the thread before we can receive the
            // results and continue with them.  It is not always the case that this will be desirable, however,
            // particularly if the results we need are immediately available.  In those cases, it would be
            // useful to wait here for a short period in case those results become available - one second
            // might be enough.  This means we may occasionally get more than one builder thread lying around
            // waiting for something to happen, but that would be short lived.  At the same time it would
            // allow these already-available results to be utilized immediately without the unwind
            // semantics.
 
            // Now wait for results if we are supposed to.
            BuildResult[] results;
            if (waitForResults)
            {
                WaitHandle[] handles = [_terminateEvent, _continueEvent];
 
                int handle;
                if (IsBuilderUsingLegacyThreadingSemantics(_componentHost, _requestEntry))
                {
                    handle = RequestBuilder.WaitWithBuilderThreadStart(handles, true, _componentHost.LegacyThreadingData, _requestEntry.Request.SubmissionId);
                }
                else if (_inMSBuildCallback)
                {
                    CultureInfo savedCulture = CultureInfo.CurrentCulture;
                    CultureInfo savedUICulture = CultureInfo.CurrentUICulture;
 
                    handle = await handles.ToTask();
 
                    CultureInfo.CurrentCulture = savedCulture;
                    CultureInfo.CurrentUICulture = savedUICulture;
                }
                else
                {
                    handle = WaitHandle.WaitAny(handles);
                }
 
                // If this is not a shutdown case, then the entry should be in the active state.
                if (handle == 1)
                {
                    // Restore the operating environment.
                    RestoreOperatingEnvironment();
                    VerifyEntryInActiveState();
                }
 
                results = GetResultsForContinuation(requests, handle == 1);
            }
            else
            {
                results = Array.Empty<BuildResult>();
            }
 
            ErrorUtilities.VerifyThrow(requests.Length == results.Length, "# results != # requests");
 
            _blockType = BlockType.Unblocked;
            return results;
        }
 
        /// <summary>
        /// Gets the results uses to continue the current build request.
        /// </summary>
        private BuildResult[] GetResultsForContinuation(FullyQualifiedBuildRequest[] requests, bool isContinue)
        {
            IDictionary<int, BuildResult> results = _continueResults;
            _continueResults = null;
            if (results == null)
            {
                // This catches the case where an MSBuild call is making a series of non-parallel build requests after Cancel has been
                // invoked.  In this case, the wait above will immediately fall through and there will be no results.  We just need to be
                // sure that we return a complete set of results which indicate we are aborting.
                ErrorUtilities.VerifyThrow(!isContinue, "Unexpected null results during continue");
                results = new Dictionary<int, BuildResult>();
                for (int i = 0; i < requests.Length; i++)
                {
                    results[i] = new BuildResult(new BuildRequest(), new BuildAbortedException());
                }
            }
 
            // See if we got any exceptions we should throw.
            foreach (BuildResult result in results.Values)
            {
                if (result.CircularDependency)
                {
                    throw new CircularDependencyException();
                }
            }
 
            // The build results will have node request IDs in the same order as the requests were issued,
            // which is in the array order above.
            List<BuildResult> resultsList = new List<BuildResult>(results.Values);
            resultsList.Sort(delegate (BuildResult left, BuildResult right)
            {
                if (left.NodeRequestId < right.NodeRequestId)
                {
                    return -1;
                }
                else if (left.NodeRequestId == right.NodeRequestId)
                {
                    return 0;
                }
 
                return 1;
            });
 
            return resultsList.ToArray();
        }
 
        /// <summary>
        /// Invokes the OnNewBuildRequests event
        /// </summary>
        /// <param name="requests">The requests to be fulfilled.</param>
        private void RaiseOnNewBuildRequests(FullyQualifiedBuildRequest[] requests)
        {
            OnNewBuildRequests?.Invoke(_requestEntry, requests);
        }
 
        /// <summary>
        /// Invokes the OnBuildRequestCompleted event
        /// </summary>
        private void RaiseBuildRequestCompleted(BuildRequestEntry entryToComplete)
        {
            OnBuildRequestCompleted?.Invoke(entryToComplete);
        }
 
        /// <summary>
        /// Invokes the OnBlockedRequest event
        /// </summary>
        private void RaiseOnBlockedRequest(int blockingGlobalRequestId, string blockingTarget, BuildResult partialBuildResult = null)
        {
            OnBuildRequestBlocked?.Invoke(_requestEntry, blockingGlobalRequestId, blockingTarget, partialBuildResult);
        }
 
        /// <summary>
        /// Invokes the OnResourceRequest event
        /// </summary>
        /// <param name="request"></param>
        private void RaiseResourceRequest(ResourceRequest request)
        {
            OnResourceRequest?.Invoke(request);
        }
 
        /// <summary>
        /// This method is called to reset the current directory to the one appropriate for this project.  It should be called any time
        /// the project is resumed.
        /// If the directory does not exist, does nothing.
        /// This is because if the project has not been saved, this directory may not exist, yet it is often useful to still be able to build the project.
        /// No errors are masked by doing this: errors loading the project from disk are reported at load time, if necessary.
        /// </summary>
        private void SetProjectCurrentDirectory()
        {
            if (_componentHost.BuildParameters.SaveOperatingEnvironment)
            {
                NativeMethodsShared.SetCurrentDirectory(_requestEntry.ProjectRootDirectory);
            }
        }
 
        /// <summary>
        /// Kicks off the build of the project file.  Doesn't return until the build is complete (or aborted.)
        /// </summary>
        private async Task<BuildResult> BuildProject()
        {
            ErrorUtilities.VerifyThrow(_targetBuilder != null, "Target builder is null");
 
            // We consider this the entrypoint for the project build for purposes of BuildCheck processing
            bool isRestoring = _requestEntry.RequestConfiguration.GlobalProperties[MSBuildConstants.MSBuildIsRestoring] is not null;
 
            var buildCheckManager = isRestoring
                ? null
                : (_componentHost.GetComponent(BuildComponentType.BuildCheckManagerProvider) as IBuildCheckManagerProvider)!.Instance;
 
            buildCheckManager?.SetDataSource(BuildCheckDataSource.BuildExecution);
 
            // Make sure it is null before loading the configuration into the request, because if there is a problem
            // we do not wand to have an invalid projectLoggingContext floating around. Also if this is null the error will be
            // logged with the node logging context
            _projectLoggingContext = null;
 
            MSBuildEventSource.Log.BuildProjectStart(_requestEntry.RequestConfiguration.ProjectFullPath);
 
            try
            {
                // Load the project
                if (!_requestEntry.RequestConfiguration.IsLoaded)
                {
                    buildCheckManager?.ProjectFirstEncountered(
                        BuildCheckDataSource.BuildExecution,
                        new CheckLoggingContext(_nodeLoggingContext.LoggingService, _requestEntry.Request.BuildEventContext),
                        _requestEntry.RequestConfiguration.ProjectFullPath);
 
                    _requestEntry.RequestConfiguration.LoadProjectIntoConfiguration(
                        _componentHost,
                        RequestEntry.Request.BuildRequestDataFlags,
                        RequestEntry.Request.SubmissionId,
                        _nodeLoggingContext.BuildEventContext.NodeId);
                }
            }
            catch
            {
                // make sure that any errors thrown by a child project are logged in the context of their parent project: create a temporary projectLoggingContext
                _projectLoggingContext = new ProjectLoggingContext(
                    _nodeLoggingContext,
                    _requestEntry.Request,
                    _requestEntry.RequestConfiguration.ProjectFullPath,
                    _requestEntry.RequestConfiguration.ToolsVersion);
 
                throw;
            }
            finally
            {
                buildCheckManager?.EndProjectEvaluation(
                    _requestEntry.Request.BuildEventContext);
            }
 
 
            try
            {
                HandleProjectStarted(buildCheckManager);
 
                // Make sure to extract known immutable folders from properties and register them for fast up-to-date check
                ConfigureKnownImmutableFolders();
 
                // See comment on Microsoft.Build.Internal.Utilities.GenerateToolsVersionToUse
                _requestEntry.RequestConfiguration.RetrieveFromCache();
                if (_requestEntry.RequestConfiguration.Project.UsingDifferentToolsVersionFromProjectFile)
                {
                    _projectLoggingContext.LogComment(MessageImportance.Low,
                        "UsingDifferentToolsVersionFromProjectFile",
                        _requestEntry.RequestConfiguration.Project.OriginalProjectToolsVersion,
                        _requestEntry.RequestConfiguration.Project.ToolsVersion);
                }
 
                _requestEntry.Request.BuildEventContext = _projectLoggingContext.BuildEventContext;
 
                // Determine the set of targets we need to build
                (string name, TargetBuiltReason reason)[] allTargets = _requestEntry.RequestConfiguration
                    .GetTargetsUsedToBuildRequest(_requestEntry.Request).ToArray();
 
                ProjectErrorUtilities.VerifyThrowInvalidProject(allTargets.Length > 0,
                    _requestEntry.RequestConfiguration.Project.ProjectFileLocation, "NoTargetSpecified");
 
                // Set the current directory to that required by the project.
                SetProjectCurrentDirectory();
 
                // Transfer results and state from the previous node, if necessary.
                // In order for the check for target completeness for this project to be valid, all of the target results from the project must be present
                // in the results cache.  It is possible that this project has been moved from its original node and when it was its results did not come
                // with it.  This would be signified by the ResultsNode value in the configuration pointing to a different node than the current one.  In that
                // case we will need to request those results be moved from their original node to this one.
                if ((_requestEntry.RequestConfiguration.ResultsNodeId != Scheduler.InvalidNodeId) &&
                    (_requestEntry.RequestConfiguration.ResultsNodeId != _componentHost.BuildParameters.NodeId))
                {
                    // This indicates to the system that we will block waiting for a results transfer.  We will block here until those results become available.
                    await BlockOnTargetInProgress(Microsoft.Build.BackEnd.BuildRequest.InvalidGlobalRequestId, null);
 
                    // All of the results should now be on this node.
                    ErrorUtilities.VerifyThrow(
                        _requestEntry.RequestConfiguration.ResultsNodeId == _componentHost.BuildParameters.NodeId,
                        "Results for configuration {0} were not retrieved from node {1}",
                        _requestEntry.RequestConfiguration.ConfigurationId,
                        _requestEntry.RequestConfiguration.ResultsNodeId);
                }
 
                // Build the targets
                BuildResult result = await _targetBuilder.BuildTargets(_projectLoggingContext, _requestEntry, this,
                    allTargets, _requestEntry.RequestConfiguration.BaseLookup, _cancellationTokenSource.Token);
 
                result = _requestEntry.Request.ProxyTargets == null
                    ? result
                    : CopyTargetResultsFromProxyTargetsToRealTargets(result);
 
                if (MSBuildEventSource.Log.IsEnabled())
                {
                    MSBuildEventSource.Log.BuildProjectStop(_requestEntry.RequestConfiguration.ProjectFullPath,
                        string.Join(", ", allTargets));
                }
 
                return result;
            }
            finally
            {
                buildCheckManager?.EndProjectRequest(
                    new CheckLoggingContext(_nodeLoggingContext.LoggingService, _projectLoggingContext.BuildEventContext),
                    _requestEntry.RequestConfiguration.ProjectFullPath);
            }
 
            BuildResult CopyTargetResultsFromProxyTargetsToRealTargets(BuildResult resultFromTargetBuilder)
            {
                var proxyTargetMapping = _requestEntry.Request.ProxyTargets.ProxyTargetToRealTargetMap;
 
                var resultsCache = (IResultsCache)_componentHost.GetComponent(BuildComponentType.ResultsCache);
                var cachedResult = resultsCache.GetResultsForConfiguration(_requestEntry.Request.ConfigurationId);
 
                // Some proxy targets do not point to real targets. Exclude those.
                foreach (var proxyMapping in proxyTargetMapping.Where(kvp => kvp.Value != null))
                {
                    var proxyTarget = proxyMapping.Key;
                    var realTarget = proxyMapping.Value;
 
                    var proxyTargetResult = resultFromTargetBuilder.ResultsByTarget[proxyTarget];
 
                    // Update the results cache.
                    cachedResult.AddResultsForTarget(
                        realTarget,
                        proxyTargetResult);
 
                    // Update and return this one because TargetBuilder.BuildTargets did some mutations on it not present in the cached result.
                    resultFromTargetBuilder.AddResultsForTarget(
                        realTarget,
                        proxyTargetResult);
                }
 
                return resultFromTargetBuilder;
            }
        }
 
        /// <summary>
        /// Saves the current operating environment.
        /// </summary>
        private void SaveOperatingEnvironment()
        {
            if (_componentHost.BuildParameters.SaveOperatingEnvironment)
            {
                _requestEntry.RequestConfiguration.SavedCurrentDirectory = NativeMethodsShared.GetCurrentDirectory();
                _requestEntry.RequestConfiguration.SavedEnvironmentVariables = CommunicationsUtilities.GetEnvironmentVariables();
            }
        }
 
        private void HandleProjectStarted(IBuildCheckManager buildCheckManager)
        {
            (ProjectStartedEventArgs args, ProjectLoggingContext ctx) = _nodeLoggingContext.CreateProjectLoggingContext(_requestEntry);
 
            _projectLoggingContext = ctx;
            ConfigureWarningsAsErrorsAndMessages();
            ILoggingService loggingService = _projectLoggingContext?.LoggingService;
            BuildEventContext projectBuildEventContext = _projectLoggingContext?.BuildEventContext;
 
            // We can set the warning as errors and messages only after the project logging context has been created (as it creates the new ProjectContextId)
            if (loggingService != null && projectBuildEventContext != null)
            {
                args.WarningsAsErrors = loggingService.GetWarningsAsErrors(projectBuildEventContext).ToHashSet(StringComparer.OrdinalIgnoreCase);
                args.WarningsAsMessages = loggingService.GetWarningsAsMessages(projectBuildEventContext).ToHashSet(StringComparer.OrdinalIgnoreCase);
                args.WarningsNotAsErrors = loggingService.GetWarningsNotAsErrors(projectBuildEventContext).ToHashSet(StringComparer.OrdinalIgnoreCase);
            }
 
            // We can log the event only after the warning as errors and messages have been set and added
            loggingService?.LogProjectStarted(args);
 
            buildCheckManager?.StartProjectRequest(
                new CheckLoggingContext(_nodeLoggingContext.LoggingService, _projectLoggingContext!.BuildEventContext),
                _requestEntry.RequestConfiguration.ProjectFullPath);
        }
 
        /// <summary>
        /// Sets the operationg environment to the initial build environment.
        /// </summary>
        private void InitializeOperatingEnvironment()
        {
            if (_requestEntry.RequestConfiguration.SavedEnvironmentVariables != null && _componentHost.BuildParameters.SaveOperatingEnvironment)
            {
                // Restore the saved environment variables.
                SetEnvironmentVariableBlock(_requestEntry.RequestConfiguration.SavedEnvironmentVariables);
            }
            else
            {
                // Restore the original build environment variables.
                SetEnvironmentVariableBlock(_componentHost.BuildParameters.BuildProcessEnvironment);
            }
        }
 
        /// <summary>
        /// Restores a previously saved operating environment.
        /// </summary>
        private void RestoreOperatingEnvironment()
        {
            if (_componentHost.BuildParameters.SaveOperatingEnvironment)
            {
                ErrorUtilities.VerifyThrow(_requestEntry.RequestConfiguration.SavedCurrentDirectory != null, "Current directory not previously saved.");
                ErrorUtilities.VerifyThrow(_requestEntry.RequestConfiguration.SavedEnvironmentVariables != null, "Current environment not previously saved.");
 
                // Restore the saved environment variables.
                SetEnvironmentVariableBlock(_requestEntry.RequestConfiguration.SavedEnvironmentVariables);
                NativeMethodsShared.SetCurrentDirectory(_requestEntry.RequestConfiguration.SavedCurrentDirectory);
            }
        }
 
        /// <summary>
        /// Sets the environment block to the set of saved variables.
        /// </summary>
        private void SetEnvironmentVariableBlock(IDictionary<string, string> savedEnvironment)
        {
            IDictionary<string, string> currentEnvironment = CommunicationsUtilities.GetEnvironmentVariables();
            ClearVariablesNotInEnvironment(savedEnvironment, currentEnvironment);
            UpdateEnvironmentVariables(savedEnvironment, currentEnvironment);
        }
 
        /// <summary>
        /// Clears from the current environment any variables which do not exist in the saved environment
        /// </summary>
        private void ClearVariablesNotInEnvironment(IDictionary<string, string> savedEnvironment, IDictionary<string, string> currentEnvironment)
        {
            foreach (KeyValuePair<string, string> entry in currentEnvironment)
            {
                if (!savedEnvironment.ContainsKey(entry.Key))
                {
                    Environment.SetEnvironmentVariable(entry.Key, null);
                }
            }
        }
 
        /// <summary>
        /// Updates the current environment with values in the saved environment which differ or are not yet set.
        /// </summary>
        private void UpdateEnvironmentVariables(IDictionary<string, string> savedEnvironment, IDictionary<string, string> currentEnvironment)
        {
            foreach (KeyValuePair<string, string> entry in savedEnvironment)
            {
                // If the environment doesn't have the variable set, or if its value differs from what we have saved, set it
                // to the saved value.  Doing the comparison before setting is faster than unconditionally setting it using
                // the API.
                string value;
                if (!currentEnvironment.TryGetValue(entry.Key, out value) || !String.Equals(entry.Value, value, StringComparison.Ordinal))
                {
                    Environment.SetEnvironmentVariable(entry.Key, entry.Value);
                }
            }
        }
 
        /// <summary>
        /// Throws if the RequestBuilder has been zombied.
        /// </summary>
        private void VerifyIsNotZombie()
        {
            ErrorUtilities.VerifyThrow(!_isZombie, "RequestBuilder has been zombied.");
        }
 
        /// <summary>
        /// Configure warnings as messages and errors based on properties defined in the project.
        /// </summary>
        private void ConfigureWarningsAsErrorsAndMessages()
        {
            // Gather needed objects
            ProjectInstance project = _requestEntry?.RequestConfiguration?.Project;
            BuildEventContext buildEventContext = _projectLoggingContext?.BuildEventContext;
            ILoggingService loggingService = _projectLoggingContext?.LoggingService;
 
            // Ensure everything that is required is available at this time
            if (project != null && buildEventContext != null && loggingService != null && buildEventContext.ProjectInstanceId != BuildEventContext.InvalidProjectInstanceId)
            {
                if (String.Equals(project.GetEngineRequiredPropertyValue(MSBuildConstants.MSBuildPrefix + MSBuildConstants.TreatWarningsAsErrors)?.Trim(), "true", StringComparison.OrdinalIgnoreCase) ||
                    (String.Equals(project.GetEngineRequiredPropertyValue(MSBuildConstants.TreatWarningsAsErrors)?.Trim(), "true", StringComparison.OrdinalIgnoreCase) &&
                     ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_14)))
                {
                    // If <MSBuildTreatWarningsAsErrors was specified then an empty ISet<string> signals the IEventSourceSink to treat all warnings as errors
                    loggingService.AddWarningsAsErrors(buildEventContext, new HashSet<string>());
                }
                else
                {
                    ISet<string> warningsAsErrors = ParseWarningCodes(project.GetEngineRequiredPropertyValue(MSBuildConstants.MSBuildPrefix + MSBuildConstants.WarningsAsErrors),
                                                                      project.GetEngineRequiredPropertyValue(MSBuildConstants.WarningsAsErrors));
 
                    if (warningsAsErrors?.Count > 0)
                    {
                        loggingService.AddWarningsAsErrors(buildEventContext, warningsAsErrors);
                    }
                }
 
                ISet<string> warningsNotAsErrors = ParseWarningCodes(project.GetEngineRequiredPropertyValue(MSBuildConstants.MSBuildPrefix + MSBuildConstants.WarningsNotAsErrors),
                                                                     project.GetEngineRequiredPropertyValue(MSBuildConstants.WarningsNotAsErrors));
 
 
                if (warningsNotAsErrors?.Count > 0)
                {
                    loggingService.AddWarningsNotAsErrors(buildEventContext, warningsNotAsErrors);
                }
 
                ISet<string> warningsAsMessages = ParseWarningCodes(project.GetEngineRequiredPropertyValue(MSBuildConstants.MSBuildPrefix + MSBuildConstants.WarningsAsMessages),
                                                                    project.GetEngineRequiredPropertyValue(MSBuildConstants.WarningsAsMessages));
 
                if (warningsAsMessages?.Count > 0)
                {
                    loggingService.AddWarningsAsMessages(buildEventContext, warningsAsMessages);
                }
            }
        }
 
        private void ConfigureKnownImmutableFolders()
        {
            ProjectInstance project = _requestEntry?.RequestConfiguration?.Project;
            if (project != null)
            {
                FileClassifier.Shared.RegisterKnownImmutableLocations(project.GetPropertyValue);
            }
        }
 
        private static ISet<string> ParseWarningCodes(string warnings, string warningsNoPrefix)
        {
            // When this changewave is rotated out and this gets deleted, please consider removing
            // the <MSBuildWarningsAsMessages Condition="'$(MSBuildWarningsAsMessages)'==''">$(NoWarn)</MSBuildWarningsAsMessages>
            // and the two following lines from the msbuild/src/Tasks/Microsoft.Common.CurrentVersion.targets
            if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_14))
            {
                warningsNoPrefix = null;
            }
 
            HashSet<string> result1 = null;
            if (!String.IsNullOrWhiteSpace(warnings))
            {
                result1 = new HashSet<string>(ExpressionShredder.SplitSemiColonSeparatedList(warnings), StringComparer.OrdinalIgnoreCase);
            }
            HashSet<string> result2 = null;
            if (!String.IsNullOrWhiteSpace(warningsNoPrefix))
            {
                result2 = new HashSet<string>(ExpressionShredder.SplitSemiColonSeparatedList(warningsNoPrefix), StringComparer.OrdinalIgnoreCase);
            }
 
            if (result1 != null)
            {
                if (result2 != null)
                {
                    result1.UnionWith(result2);
                }
                return result1;
            }
 
            return result2;
        }
 
        private sealed class DedicatedThreadsTaskScheduler : TaskScheduler
        {
            private readonly BlockingCollection<Task> _tasks = new BlockingCollection<Task>();
            private int _availableThreads = 0;
 
            protected override void QueueTask(Task task)
            {
                RequestThread();
                _tasks.Add(task);
            }
 
            protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) => false;
 
            protected override IEnumerable<Task> GetScheduledTasks() => _tasks;
 
            private void RequestThread()
            {
                // Decrement available thread count; don't drop below zero
                // Prior value is stored in count
                var count = Volatile.Read(ref _availableThreads);
                while (count > 0)
                {
                    var prev = Interlocked.CompareExchange(ref _availableThreads, count - 1, count);
                    if (prev == count)
                    {
                        break;
                    }
                    count = prev;
                }
 
                if (count == 0)
                {
                    // No threads were available for request
                    InjectThread();
                }
            }
 
            private void InjectThread()
            {
                var thread = new Thread(() =>
                {
                    foreach (Task t in _tasks.GetConsumingEnumerable())
                    {
                        TryExecuteTask(t);
                        Interlocked.Increment(ref _availableThreads);
                    }
                });
                thread.IsBackground = true;
                thread.Start();
            }
        }
    }
}