// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Xml;
using Microsoft.Build.BackEnd;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Collections;
using Microsoft.Build.Construction;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Internal;
using Microsoft.Build.ObjectModelRemoting;
using Microsoft.Build.Shared;
using ForwardingLoggerRecord = Microsoft.Build.Logging.ForwardingLoggerRecord;
using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService;
using InternalLoggerException = Microsoft.Build.Exceptions.InternalLoggerException;
using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException;
using LoggerMode = Microsoft.Build.BackEnd.Logging.LoggerMode;
using ObjectModel = System.Collections.ObjectModel;

#nullable disable

namespace Microsoft.Build.Evaluation
{
    using Utilities = Microsoft.Build.Internal.Utilities;

    /// <summary>
    /// Flags for controlling the toolset initialization.
    /// </summary>
    [Flags]
    public enum ToolsetDefinitionLocations
    {
        /// <summary>
        /// Do not read toolset information from any external location.
        /// </summary>
        None = 0,

        /// <summary>
        /// Read toolset information from the exe configuration file.
        /// </summary>
        ConfigurationFile = 1,

        /// <summary>
        /// Read toolset information from the registry (HKLM\Software\Microsoft\MSBuild\ToolsVersions).
        /// </summary>
        Registry = 2,

        /// <summary>
        /// Read toolset information from the current exe path
        /// </summary>
        Local = 4,

        /// <summary>
        /// Use the default location or locations.
        /// </summary>
        Default = None
#if FEATURE_SYSTEM_CONFIGURATION
                | ConfigurationFile
#endif
#if FEATURE_REGISTRY_TOOLSETS
                | Registry
#endif
#if !FEATURE_SYSTEM_CONFIGURATION && !FEATURE_REGISTRY_TOOLSETS
                | Local
#endif
    }

    /// <summary>
    /// This class encapsulates a set of related projects, their toolsets, a default set of global properties,
    /// and the loggers that should be used to build them.
    /// A global version of this class acts as the default ProjectCollection.
    /// Multiple ProjectCollections can exist within an appdomain. However, these must not build concurrently.
    /// </summary>
    [SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix", Justification = "This is a collection of projects API review has approved this")]
    public class ProjectCollection : IToolsetProvider, IBuildComponent, IDisposable
    {
        /// <summary>
        /// The object to synchronize with when accessing certain fields.
        /// </summary>
        /// <remarks>
        /// ProjectCollection is highly reentrant - project creation, toolset and logger changes, and so on
        /// all need lock protection, but there are a lot of read cases as well, and calls to create Projects
        /// call back to the ProjectCollection under locks. Use a RW lock with recursion support to avoid
        /// adding reentrancy bugs.
        /// </remarks>
        private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

        /// <summary>
        /// The global singleton project collection used as a default for otherwise
        /// unassociated projects.
        /// </summary>
        private static ProjectCollection s_globalProjectCollection;

        /// <summary>
        /// Gets the file version of the file in which the Engine assembly lies.
        /// </summary>
        /// <remarks>
        /// This is the Windows file version (specifically the value of the ProductVersion
        /// resource), not necessarily the assembly version.
        /// If you want the assembly version, use Constants.AssemblyVersion.
        /// </remarks>
        private static Version s_engineVersion;

        /// <summary>
        /// The display version of the file in which the Engine assembly lies.
        /// </summary>
        private static string s_assemblyDisplayVersion;

        private static ProjectRootElementCacheBase s_projectRootElementCache = null;

        /// <summary>
        /// The projects loaded into this collection.
        /// </summary>
        private readonly LoadedProjectCollection _loadedProjects;

        /// <summary>
        /// External projects support
        /// </summary>
        private ExternalProjectsProvider _link;

        /// <summary>
        /// Single logging service used for all builds of projects in this project collection
        /// </summary>
        private ILoggingService _loggingService;

        /// <summary>
        /// Any object exposing host services.
        /// May be null.
        /// </summary>
        private HostServices _hostServices;

        /// <summary>
        /// A mapping of tools versions to Toolsets, which contain the public Toolsets.
        /// This is the collection we use internally.
        /// </summary>
        private Dictionary<string, Toolset> _toolsets;

        /// <summary>
        /// The default global properties.
        /// </summary>
        private readonly PropertyDictionary<ProjectPropertyInstance> _globalProperties;

        /// <summary>
        /// The properties representing the environment.
        /// </summary>
        private PropertyDictionary<ProjectPropertyInstance> _environmentProperties;

        /// <summary>
        /// The default tools version obtained by examining all of the toolsets.
        /// </summary>
        private string _defaultToolsVersion;

        /// <summary>
        /// A counter incremented every time the toolsets change which would necessitate a re-evaluation of
        /// associated projects.
        /// </summary>
        private int _toolsetsVersion;

        /// <summary>
        /// This is the default value used by newly created projects for whether or not the building
        /// of projects is enabled.  This is for security purposes in case a host wants to closely
        /// control which projects it allows to run targets/tasks.
        /// </summary>
        private bool _isBuildEnabled = true;

        /// <summary>
        /// We may only wish to log critical events, record that fact so we can apply it to build requests
        /// </summary>
        private bool _onlyLogCriticalEvents;

        private bool _enableTargetOutputLogging;

        /// <summary>
        /// Whether reevaluation is temporarily disabled on projects in this collection.
        /// This is useful when the host expects to make a number of reads and writes
        /// to projects, and wants to temporarily sacrifice correctness for performance.
        /// </summary>
        private bool _skipEvaluation;

        /// <summary>
        /// Whether <see cref="Project.MarkDirty()">MarkDirty()</see> is temporarily disabled on
        /// projects in this collection.
        /// This allows, for example, global properties to be set without projects getting
        /// marked dirty for reevaluation as a consequence.
        /// </summary>
        private bool _disableMarkDirty;

        /// <summary>
        /// The maximum number of nodes which can be started during the build
        /// </summary>
        private int _maxNodeCount;

        /// <summary>
        /// LoggingService Logger mode.
        /// If Asynchronous mode is used
        /// </summary>
        private LoggerMode _loggerMode;

        /// <summary>
        /// Instantiates a project collection with no global properties or loggers that reads toolset
        /// information from the configuration file and registry.
        /// </summary>
        public ProjectCollection()
            : this(null)
        {
        }

        /// <summary>
        /// Instantiates a project collection using toolsets from the specified locations,
        /// and no global properties or loggers.
        /// May throw InvalidToolsetDefinitionException.
        /// </summary>
        /// <param name="toolsetLocations">The locations from which to load toolsets.</param>
        public ProjectCollection(ToolsetDefinitionLocations toolsetLocations)
            : this(null, null, toolsetLocations)
        {
        }

        /// <summary>
        /// Instantiates a project collection with specified global properties, no loggers,
        /// and that reads toolset information from the configuration file and registry.
        /// May throw InvalidToolsetDefinitionException.
        /// </summary>
        /// <param name="globalProperties">The default global properties to use. May be null.</param>
        public ProjectCollection(IDictionary<string, string> globalProperties)
            : this(globalProperties, null, ToolsetDefinitionLocations.Default)
        {
        }

        /// <summary>
        /// Instantiates a project collection with specified global properties and loggers and using the
        /// specified toolset locations.
        /// May throw InvalidToolsetDefinitionException.
        /// </summary>
        /// <param name="globalProperties">The default global properties to use. May be null.</param>
        /// <param name="loggers">The loggers to register. May be null.</param>
        /// <param name="toolsetDefinitionLocations">The locations from which to load toolsets.</param>
        public ProjectCollection(IDictionary<string, string> globalProperties, IEnumerable<ILogger> loggers, ToolsetDefinitionLocations toolsetDefinitionLocations)
            : this(globalProperties, loggers, null, toolsetDefinitionLocations, 1 /* node count */, false /* do not only log critical events */)
        {
        }

        /// <summary>
        /// Instantiates a project collection with specified global properties and loggers and using the
        /// specified toolset locations, node count, and setting of onlyLogCriticalEvents.
        /// Global properties and loggers may be null.
        /// Throws InvalidProjectFileException if any of the global properties are reserved.
        /// May throw InvalidToolsetDefinitionException.
        /// </summary>
        /// <param name="globalProperties">The default global properties to use. May be null.</param>
        /// <param name="loggers">The loggers to register. May be null and specified to any build instead.</param>
        /// <param name="remoteLoggers">Any remote loggers to register. May be null and specified to any build instead.</param>
        /// <param name="toolsetDefinitionLocations">The locations from which to load toolsets.</param>
        /// <param name="maxNodeCount">The maximum number of nodes to use for building.</param>
        /// <param name="onlyLogCriticalEvents">If set to true, only critical events will be logged.</param>
        public ProjectCollection(IDictionary<string, string> globalProperties, IEnumerable<ILogger> loggers, IEnumerable<ForwardingLoggerRecord> remoteLoggers, ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents)
            : this(globalProperties, loggers, null, toolsetDefinitionLocations, maxNodeCount, onlyLogCriticalEvents, loadProjectsReadOnly: false)
        {
        }

        /// <summary>
        /// Instantiates a project collection with specified global properties and loggers and using the
        /// specified toolset locations, node count, and setting of onlyLogCriticalEvents.
        /// Global properties and loggers may be null.
        /// Throws InvalidProjectFileException if any of the global properties are reserved.
        /// May throw InvalidToolsetDefinitionException.
        /// </summary>
        /// <param name="globalProperties">The default global properties to use. May be null.</param>
        /// <param name="loggers">The loggers to register. May be null and specified to any build instead.</param>
        /// <param name="remoteLoggers">Any remote loggers to register. May be null and specified to any build instead.</param>
        /// <param name="toolsetDefinitionLocations">The locations from which to load toolsets.</param>
        /// <param name="maxNodeCount">The maximum number of nodes to use for building.</param>
        /// <param name="onlyLogCriticalEvents">If set to true, only critical events will be logged.</param>
        /// <param name="loadProjectsReadOnly">If set to true, load all projects as read-only.</param>
        public ProjectCollection(IDictionary<string, string> globalProperties, IEnumerable<ILogger> loggers, IEnumerable<ForwardingLoggerRecord> remoteLoggers, ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly)
            : this(globalProperties, loggers, remoteLoggers, toolsetDefinitionLocations, maxNodeCount, onlyLogCriticalEvents, loadProjectsReadOnly, useAsynchronousLogging: false, reuseProjectRootElementCache: false, enableTargetOutputLogging: false)
        {
        }

        /// <summary>
        /// Instantiates a project collection with specified global properties and loggers and using the
        /// specified toolset locations, node count, and setting of onlyLogCriticalEvents.
        /// Global properties and loggers may be null.
        /// Throws InvalidProjectFileException if any of the global properties are reserved.
        /// May throw InvalidToolsetDefinitionException.
        /// </summary>
        /// <param name="globalProperties">The default global properties to use. May be null.</param>
        /// <param name="loggers">The loggers to register. May be null and specified to any build instead.</param>
        /// <param name="remoteLoggers">Any remote loggers to register. May be null and specified to any build instead.</param>
        /// <param name="toolsetDefinitionLocations">The locations from which to load toolsets.</param>
        /// <param name="maxNodeCount">The maximum number of nodes to use for building.</param>
        /// <param name="onlyLogCriticalEvents">If set to true, only critical events will be logged.</param>
        /// <param name="loadProjectsReadOnly">If set to true, load all projects as read-only.</param>
        /// <param name="useAsynchronousLogging">If set to true, asynchronous logging will be used. <see cref="Dispose()"/> has to called to clear resources used by async logging.</param>
        /// <param name="reuseProjectRootElementCache">If set to true, it will try to reuse <see cref="ProjectRootElementCacheBase"/> singleton.</param>
        /// <remarks>
        /// This constructor disables target output logging, so TerminalLogger and other loggers may not work well. Prefer <see cref="ProjectCollection(IDictionary{string, string}, IEnumerable{ILogger}, IEnumerable{ForwardingLoggerRecord}, ToolsetDefinitionLocations, int, bool, bool, bool, bool, bool)"/> instead to control this behavior.
        /// </remarks>
        public ProjectCollection(IDictionary<string, string> globalProperties, IEnumerable<ILogger> loggers, IEnumerable<ForwardingLoggerRecord> remoteLoggers, ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool useAsynchronousLogging, bool reuseProjectRootElementCache) :
            this(globalProperties: globalProperties, loggers: loggers, remoteLoggers: remoteLoggers, toolsetDefinitionLocations: toolsetDefinitionLocations, maxNodeCount: maxNodeCount, onlyLogCriticalEvents: onlyLogCriticalEvents, loadProjectsReadOnly: loadProjectsReadOnly, useAsynchronousLogging: useAsynchronousLogging, reuseProjectRootElementCache: reuseProjectRootElementCache, enableTargetOutputLogging: false)
        {
        }

        /// <summary>
        /// Instantiates a project collection with specified global properties and loggers and using the
        /// specified toolset locations, node count, and setting of onlyLogCriticalEvents.
        /// Global properties and loggers may be null.
        /// Throws InvalidProjectFileException if any of the global properties are reserved.
        /// May throw InvalidToolsetDefinitionException.
        /// </summary>
        /// <param name="globalProperties">The default global properties to use. May be null.</param>
        /// <param name="loggers">The loggers to register. May be null and specified to any build instead.</param>
        /// <param name="remoteLoggers">Any remote loggers to register. May be null and specified to any build instead.</param>
        /// <param name="toolsetDefinitionLocations">The locations from which to load toolsets.</param>
        /// <param name="maxNodeCount">The maximum number of nodes to use for building.</param>
        /// <param name="onlyLogCriticalEvents">If set to true, only critical events will be logged.</param>
        /// <param name="loadProjectsReadOnly">If set to true, load all projects as read-only.</param>
        /// <param name="useAsynchronousLogging">If set to true, asynchronous logging will be used. <see cref="Dispose()"/> has to called to clear resources used by async logging.</param>
        /// <param name="reuseProjectRootElementCache">If set to true, it will try to reuse <see cref="ProjectRootElementCacheBase"/> singleton.</param>
        /// <param name="enableTargetOutputLogging">If set to true, loggers will collect and send Target outputs when targets are finished executing.</param>
        public ProjectCollection(IDictionary<string, string> globalProperties, IEnumerable<ILogger> loggers, IEnumerable<ForwardingLoggerRecord> remoteLoggers, ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool useAsynchronousLogging, bool reuseProjectRootElementCache, bool enableTargetOutputLogging)
        {
            _loadedProjects = new LoadedProjectCollection();
            ToolsetLocations = toolsetDefinitionLocations;
            MaxNodeCount = maxNodeCount;

            if (Traits.Instance.UseSimpleProjectRootElementCacheConcurrency)
            {
                ProjectRootElementCache = new SimpleProjectRootElementCache();
            }
            else if (reuseProjectRootElementCache && s_projectRootElementCache != null)
            {
                ProjectRootElementCache = s_projectRootElementCache;
            }
            else
            {
                // When we are reusing ProjectRootElementCache we need to reload XMLs if it has changed between MSBuild Server sessions/builds.
                // If we are not reusing, cache will be released at end of build and as we do not support project files will changes during build
                // we do not need to auto reload.
                bool autoReloadFromDisk = reuseProjectRootElementCache;
                ProjectRootElementCache = new ProjectRootElementCache(autoReloadFromDisk, loadProjectsReadOnly);
                if (reuseProjectRootElementCache)
                {
                    s_projectRootElementCache = ProjectRootElementCache;
                }
            }

            OnlyLogCriticalEvents = onlyLogCriticalEvents;
            EnableTargetOutputLogging = enableTargetOutputLogging;

            try
            {
                _loggerMode = useAsynchronousLogging ? LoggerMode.Asynchronous : LoggerMode.Synchronous;
                CreateLoggingService(maxNodeCount, onlyLogCriticalEvents, enableTargetOutputLogging);

                RegisterLoggers(loggers);
                RegisterForwardingLoggers(remoteLoggers);

                if (globalProperties != null)
                {
                    _globalProperties = new PropertyDictionary<ProjectPropertyInstance>(globalProperties.Count);

                    foreach (KeyValuePair<string, string> pair in globalProperties)
                    {
                        try
                        {
                            _globalProperties.Set(ProjectPropertyInstance.Create(pair.Key, pair.Value));
                        }
                        catch (ArgumentException ex)
                        {
                            // Reserved or invalid property name
                            try
                            {
                                ProjectErrorUtilities.ThrowInvalidProject(ElementLocation.Create("MSBUILD"), "InvalidProperty", ex.Message);
                            }
                            catch (InvalidProjectFileException ex2)
                            {
                                BuildEventContext buildEventContext = new BuildEventContext(0 /* node ID */, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId);
                                LoggingService.LogInvalidProjectFileError(buildEventContext, ex2);
                                throw;
                            }
                        }
                    }
                }
                else
                {
                    _globalProperties = new PropertyDictionary<ProjectPropertyInstance>();
                }

                InitializeToolsetCollection();
            }
            catch (Exception)
            {
                ShutDownLoggingService();
                throw;
            }

            ProjectRootElementCache.ProjectRootElementAddedHandler += ProjectRootElementCache_ProjectRootElementAddedHandler;
            ProjectRootElementCache.ProjectRootElementDirtied += ProjectRootElementCache_ProjectRootElementDirtiedHandler;
            ProjectRootElementCache.ProjectDirtied += ProjectRootElementCache_ProjectDirtiedHandler;
        }

        /// <summary>
        /// Handler to receive which project got added to the project collection.
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "This has been API reviewed")]
        public delegate void ProjectAddedEventHandler(object sender, ProjectAddedToProjectCollectionEventArgs e);

        /// <summary>
        /// Event that is fired when a project is added to the ProjectRootElementCache of this project collection.
        /// </summary>
        public event ProjectAddedEventHandler ProjectAdded;

        /// <summary>
        /// Raised when state is changed on this instance.
        /// </summary>
        /// <remarks>
        /// This event is NOT raised for changes in individual projects.
        /// </remarks>
        public event EventHandler<ProjectCollectionChangedEventArgs> ProjectCollectionChanged;

        /// <summary>
        /// Raised when a <see cref="ProjectRootElement"/> contained by this instance is changed.
        /// </summary>
        /// <remarks>
        /// This event is NOT raised for changes to global properties, or any other change that doesn't actually dirty the XML.
        /// </remarks>
        public event EventHandler<ProjectXmlChangedEventArgs> ProjectXmlChanged;

        /// <summary>
        /// Raised when a <see cref="Project"/> contained by this instance is directly changed.
        /// </summary>
        /// <remarks>
        /// This event is NOT raised for direct project XML changes via the construction model.
        /// </remarks>
        public event EventHandler<ProjectChangedEventArgs> ProjectChanged;

        /// <summary>
        /// Retrieves the global project collection object.
        /// This is a singleton project collection with no global properties or loggers that reads toolset
        /// information from the configuration file and registry.
        /// May throw InvalidToolsetDefinitionException.
        /// Thread safe.
        /// </summary>
        public static ProjectCollection GlobalProjectCollection
        {
            get
            {
                if (s_globalProjectCollection == null)
                {
                    // Take care to ensure that there is never more than one value observed
                    // from this property even in the case of race conditions while lazily initializing.
                    var local = new ProjectCollection(null, null, null, ToolsetDefinitionLocations.Default,
                        maxNodeCount: 1, onlyLogCriticalEvents: false, loadProjectsReadOnly: false, useAsynchronousLogging: true, reuseProjectRootElementCache: false, enableTargetOutputLogging: false);

                    if (Interlocked.CompareExchange(ref s_globalProjectCollection, local, null) != null)
                    {
                        // Other thread beat us to it; dispose of this project collection
                        local.Dispose();
                    }
                }

                return s_globalProjectCollection;
            }
        }

        /// <summary>
        /// Gets the file version of the file in which the Engine assembly lies.
        /// </summary>
        /// <remarks>
        /// This is the Windows file version (specifically the value of the FileVersion
        /// resource), not necessarily the assembly version.
        /// If you want the assembly version, use Constants.AssemblyVersion.
        /// This is not the <see cref="Toolset.ToolsVersion"/>.
        /// </remarks>
        public static Version Version
        {
            get
            {
                if (s_engineVersion == null)
                {
                    // Get the file version from the currently executing assembly.
                    // Use .CodeBase instead of .Location, because .Location doesn't
                    // work when Microsoft.Build.dll has been shadow-copied, for example
                    // in scenarios where NUnit is loading Microsoft.Build.
                    var versionInfo = FileVersionInfo.GetVersionInfo(FileUtilities.ExecutingAssemblyPath);
                    s_engineVersion = new Version(versionInfo.FileMajorPart, versionInfo.FileMinorPart, versionInfo.FileBuildPart, versionInfo.FilePrivatePart);
                }

                return s_engineVersion;
            }
        }

        /// <summary>
        /// Gets a version of the Engine suitable for display to a user.
        /// </summary>
        /// <remarks>
        /// This is in the form of a SemVer v2 version, Major.Minor.Patch-prerelease+metadata.
        /// </remarks>
        public static string DisplayVersion
        {
            get
            {
                if (s_assemblyDisplayVersion == null)
                {
                    var fullInformationalVersion = typeof(Constants).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;

                    // use a truncated version with only 9 digits of SHA
                    var plusIndex = fullInformationalVersion.IndexOf('+');
                    s_assemblyDisplayVersion = plusIndex < 0
                                                    ? fullInformationalVersion
                                                    : fullInformationalVersion.Substring(startIndex: 0, length: plusIndex + 10);
                }

                return s_assemblyDisplayVersion;
            }
        }

        /// <summary>
        /// Properties passed from the command line (e.g. by using /p:).
        /// </summary>
        public ICollection<string> PropertiesFromCommandLine { get; set; }

        /// <summary>
        /// The default tools version of this project collection. Projects use this tools version if they
        /// aren't otherwise told what tools version to use.
        /// This value is gotten from the .exe.config file, or else in the registry,
        /// or if neither specify a default tools version then it is hard-coded to the tools version "2.0".
        /// Setter throws InvalidOperationException if a toolset with the provided tools version has not been defined.
        /// Always defined.
        /// </summary>
        public string DefaultToolsVersion
        {
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    ErrorUtilities.VerifyThrow(_defaultToolsVersion != null, "Should have a default");
                    return _defaultToolsVersion;
                }
            }

            set
            {
                ErrorUtilities.VerifyThrowArgumentLength(value, nameof(DefaultToolsVersion));

                bool sendEvent = false;
                using (_locker.EnterDisposableWriteLock())
                {
                    if (!_toolsets.ContainsKey(value))
                    {
                        string toolsVersionList = Utilities.CreateToolsVersionListString(Toolsets);
                        ErrorUtilities.ThrowInvalidOperation("UnrecognizedToolsVersion", value, toolsVersionList);
                    }

                    if (_defaultToolsVersion != value)
                    {
                        _defaultToolsVersion = value;
                        sendEvent = true;
                    }
                }

                if (sendEvent)
                {
                    OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.DefaultToolsVersion));
                }
            }
        }

        /// <summary>
        /// Returns default global properties for all projects in this collection.
        /// Read-only dead dictionary.
        /// </summary>
        /// <remarks>
        /// This is the publicly exposed getter, that translates into a read-only dead IDictionary&lt;string, string&gt;.
        ///
        /// To be consistent with Project, setting and removing global properties is done with
        /// <see cref="SetGlobalProperty">SetGlobalProperty</see> and <see cref="RemoveGlobalProperty">RemoveGlobalProperty</see>.
        /// </remarks>
        public IDictionary<string, string> GlobalProperties
        {
            get
            {
                Dictionary<string, string> dictionary;

                using (_locker.EnterDisposableReadLock())
                {
                    if (_globalProperties.Count == 0)
                    {
                        return ReadOnlyEmptyDictionary<string, string>.Instance;
                    }

                    dictionary = new Dictionary<string, string>(_globalProperties.Count, MSBuildNameIgnoreCaseComparer.Default);

                    foreach (ProjectPropertyInstance property in _globalProperties)
                    {
                        dictionary[property.Name] = ((IProperty)property).EvaluatedValueEscaped;
                    }
                }

                return new ObjectModel.ReadOnlyDictionary<string, string>(dictionary);
            }
        }

        /// <summary>
        /// All the projects currently loaded into this collection.
        /// Each has a unique combination of path, global properties, and tools version.
        /// </summary>
        [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This is a reasonable choice. API review approved")]
        public ICollection<Project> LoadedProjects => GetLoadedProjects(true, null);

        /// <summary>
        /// Number of projects currently loaded into this collection.
        /// </summary>
        public int Count
        {
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _loadedProjects.Count;
                }
            }
        }

        /// <summary>
        /// Loggers that all contained projects will use for their builds.
        /// Loggers are added with the <see cref="RegisterLogger"/>.
        /// Returns an empty collection if there are no loggers.
        /// </summary>
        // UNDONE: Currently loggers cannot be removed.
        public ICollection<ILogger> Loggers
        {
            [DebuggerStepThrough]
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _loggingService.Loggers == null
                        ? (ICollection<ILogger>)ReadOnlyEmptyCollection<ILogger>.Instance
                        : new List<ILogger>(_loggingService.Loggers);
                }
            }
        }

        /// <summary>
        /// Returns the toolsets this ProjectCollection knows about.
        /// </summary>
        /// <comments>
        /// ValueCollection is already read-only
        /// </comments>
        public ICollection<Toolset> Toolsets
        {
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return new List<Toolset>(_toolsets.Values);
                }
            }
        }

        /// <summary>
        /// Returns the locations used to find the toolsets.
        /// </summary>
        public ToolsetDefinitionLocations ToolsetLocations { get; }

        /// <summary>
        /// This is the default value used by newly created projects for whether or not the building
        /// of projects is enabled.  This is for security purposes in case a host wants to closely
        /// control which projects it allows to run targets/tasks.
        /// </summary>
        public bool IsBuildEnabled
        {
            [DebuggerStepThrough]
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _isBuildEnabled;
                }
            }

            [DebuggerStepThrough]
            set
            {
                bool sendEvent = false;
                using (_locker.EnterDisposableWriteLock())
                {
                    if (_isBuildEnabled != value)
                    {
                        _isBuildEnabled = value;
                        sendEvent = true;
                    }
                }

                if (sendEvent)
                {
                    OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.IsBuildEnabled));
                }
            }
        }

        /// <summary>
        /// When true, only log critical events such as warnings and errors. Has to be in here for API compat
        /// </summary>
        public bool OnlyLogCriticalEvents
        {
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _onlyLogCriticalEvents;
                }
            }

            set
            {
                bool sendEvent = false;
                using (_locker.EnterDisposableWriteLock())
                {
                    if (_onlyLogCriticalEvents != value)
                    {
                        _onlyLogCriticalEvents = value;
                        sendEvent = true;
                    }
                }

                if (sendEvent)
                {
                    OnProjectCollectionChanged(
                        new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.OnlyLogCriticalEvents));
                }
            }
        }

        public bool EnableTargetOutputLogging
        {
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _enableTargetOutputLogging;
                }
            }
            set
            {
                bool sendEvent = false;
                using (_locker.EnterDisposableWriteLock())
                {
                    if (_enableTargetOutputLogging != value)
                    {
                        _enableTargetOutputLogging = value;
                        sendEvent = true;
                    }
                }

                if (sendEvent)
                {
                    OnProjectCollectionChanged(
                        new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.EnableTargetOutputLogging));
                }
            }
        }

        /// <summary>
        /// Object exposing host services to tasks during builds of projects
        /// contained by this project collection.
        /// By default, <see cref="HostServices">HostServices</see> is used.
        /// May be set to null, but the getter will create a new instance in that case.
        /// </summary>
        public HostServices HostServices
        {
            get
            {
                // Avoid write lock if possible, this getter is called a lot during Project construction.
                using (_locker.EnterDisposableReadLock())
                {
                    if (_hostServices != null)
                    {
                        return _hostServices;
                    }
                }
                using (_locker.EnterDisposableWriteLock())
                {
                    if (_hostServices == null)
                    {
                        _hostServices = new HostServices();
                    }
                    return _hostServices;
                }
            }

            set
            {
                bool sendEvent = false;
                using (_locker.EnterDisposableWriteLock())
                {
                    if (_hostServices != value)
                    {
                        _hostServices = value;
                        sendEvent = true;
                    }
                }

                if (sendEvent)
                {
                    OnProjectCollectionChanged(
                        new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.HostServices));
                }
            }
        }

        /// <summary>
        /// Whether reevaluation is temporarily disabled on projects in this collection.
        /// This is useful when the host expects to make a number of reads and writes
        /// to projects, and wants to temporarily sacrifice correctness for performance.
        /// </summary>
        public bool SkipEvaluation
        {
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _skipEvaluation;
                }
            }

            set
            {
                bool sendEvent = false;
                using (_locker.EnterDisposableWriteLock())
                {
                    if (_skipEvaluation != value)
                    {
                        _skipEvaluation = value;
                        sendEvent = true;
                    }
                }

                if (sendEvent)
                {
                    OnProjectCollectionChanged(
                        new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.SkipEvaluation));
                }
            }
        }

        /// <summary>
        /// Whether <see cref="Project.MarkDirty()">MarkDirty()</see> is temporarily disabled on
        /// projects in this collection.
        /// This allows, for example, global properties to be set without projects getting
        /// marked dirty for reevaluation as a consequence.
        /// </summary>
        public bool DisableMarkDirty
        {
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _disableMarkDirty;
                }
            }

            set
            {
                bool sendEvent = false;
                using (_locker.EnterDisposableWriteLock())
                {
                    if (_disableMarkDirty != value)
                    {
                        _disableMarkDirty = value;
                        sendEvent = true;
                    }
                }

                if (sendEvent)
                {
                    OnProjectCollectionChanged(
                        new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.DisableMarkDirty));
                }
            }
        }

        /// <summary>
        /// Global collection id.
        /// Can be used for external providers to optimize the cross-site link exchange
        /// </summary>
        internal Guid CollectionId { get; } = Guid.NewGuid();

        /// <summary>
        /// External project support.
        /// Establish a remote project link for this collection.
        /// </summary>
        internal ExternalProjectsProvider Link
        {
            get => _link;
            set => Interlocked.Exchange(ref _link, value)?.Disconnected(this);
        }

        /// <summary>
        /// Logging service that should be used for project load and for builds
        /// </summary>
        internal ILoggingService LoggingService
        {
            [DebuggerStepThrough]
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _loggingService;
                }
            }
        }

        /// <summary>
        /// Gets default global properties for all projects in this collection.
        /// Dead copy.
        /// </summary>
        internal PropertyDictionary<ProjectPropertyInstance> GlobalPropertiesCollection
        {
            [DebuggerStepThrough]
            get
            {
                var clone = new PropertyDictionary<ProjectPropertyInstance>();

                using (_locker.EnterDisposableReadLock())
                {
                    foreach (ProjectPropertyInstance property in _globalProperties)
                    {
                        clone.Set(property.DeepClone());
                    }

                    return clone;
                }
            }
        }

        /// <summary>
        /// Returns the property dictionary containing the properties representing the environment.
        /// </summary>
        internal PropertyDictionary<ProjectPropertyInstance> EnvironmentProperties
        {
            get
            {
                // Retrieves the environment properties.
                // This is only done once, when the project collection is created. Any subsequent
                // environment changes will be ignored. Child nodes will be passed this set
                // of properties in their build parameters.
                return new PropertyDictionary<ProjectPropertyInstance>(SharedReadOnlyEnvironmentProperties);
            }
        }

        /// <summary>
        /// Returns a shared immutable property dictionary containing the properties representing the environment.
        /// </summary>
        internal PropertyDictionary<ProjectPropertyInstance> SharedReadOnlyEnvironmentProperties
        {
            get
            {
                // Retrieves the environment properties.
                // This is only done once, when the project collection is created. Any subsequent
                // environment changes will be ignored. Child nodes will be passed this set
                // of properties in their build parameters.
                using (_locker.EnterDisposableReadLock())
                {
                    if (_environmentProperties != null)
                    {
                        return _environmentProperties;
                    }
                }

                using (_locker.EnterDisposableWriteLock())
                {
                    if (_environmentProperties == null)
                    {
                        _environmentProperties = Utilities.GetEnvironmentProperties(makeReadOnly: true);
                    }
                    return _environmentProperties;
                }
            }
        }

        /// <summary>
        /// Returns the internal version for this object's state.
        /// Updated when toolsets change, indicating all contained projects are potentially invalid.
        /// </summary>
        internal int ToolsetsVersion
        {
            [DebuggerStepThrough]
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _toolsetsVersion;
                }
            }
        }

        /// <summary>
        /// The maximum number of nodes which can be started during the build
        /// </summary>
        internal int MaxNodeCount
        {
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _maxNodeCount;
                }
            }

            set
            {
                using (_locker.EnterDisposableWriteLock())
                {
                    _maxNodeCount = value;
                }
            }
        }

        /// <summary>
        /// The cache of project root elements associated with this project collection.
        /// Each is associated with a specific project collection for two reasons:
        /// - To help protect one project collection from any XML edits through another one:
        /// until a reload from disk - when it's ready to accept changes - it won't see the edits;
        /// - So that the owner of this project collection can force the XML to be loaded again
        /// from disk, by doing <see cref="UnloadAllProjects"/>.
        /// </summary>
        internal ProjectRootElementCacheBase ProjectRootElementCache { get; }

        /// <summary>
        /// Escape a string using MSBuild escaping format. For example, "%3b" for ";".
        /// Only characters that are especially significant to MSBuild parsing are escaped.
        /// Callers can use this method to make a string safe to be parsed to other methods
        /// that would otherwise expand it; or to make a string safe to be written to a project file.
        /// </summary>
        [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "string", Justification = "Public API that has shipped")]
        public static string Escape(string unescapedString)
        {
            return EscapingUtilities.Escape(unescapedString);
        }

        /// <summary>
        /// Unescape a string using MSBuild escaping format. For example, "%3b" for ";".
        /// All escaped characters are unescaped.
        /// </summary>
        [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "string", Justification = "Public API that has shipped")]
        public static string Unescape(string escapedString)
        {
            return EscapingUtilities.UnescapeAll(escapedString);
        }

        /// <summary>
        /// Returns true if there is a toolset defined for the specified
        /// tools version, otherwise false.
        /// </summary>
        public bool ContainsToolset(string toolsVersion) => GetToolset(toolsVersion) != null;

        /// <summary>
        /// Add a new toolset.
        /// Replaces any existing toolset with the same tools version.
        /// </summary>
        public void AddToolset(Toolset toolset)
        {
            ErrorUtilities.VerifyThrowArgumentNull(toolset);
            using (_locker.EnterDisposableWriteLock())
            {
                _toolsets[toolset.ToolsVersion] = toolset;
                _toolsetsVersion++;
            }

            OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Toolsets));
        }

        /// <summary>
        /// Remove a toolset.
        /// Returns true if it was present, otherwise false.
        /// </summary>
        public bool RemoveToolset(string toolsVersion)
        {
            ErrorUtilities.VerifyThrowArgumentLength(toolsVersion);

            bool changed;
            using (_locker.EnterDisposableWriteLock())
            {
                changed = RemoveToolsetInternal(toolsVersion);
            }

            if (changed)
            {
                OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Toolsets));
            }

            return changed;
        }

        /// <summary>
        /// Removes all toolsets.
        /// </summary>
        public void RemoveAllToolsets()
        {
            bool changed = false;
            using (_locker.EnterDisposableWriteLock())
            {
                var toolsets = new List<Toolset>(Toolsets);

                foreach (Toolset toolset in toolsets)
                {
                    changed |= RemoveToolsetInternal(toolset.ToolsVersion);
                }
            }

            if (changed)
            {
                OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Toolsets));
            }
        }

        /// <summary>
        /// Get the toolset with the specified tools version.
        /// If it is not present, returns null.
        /// </summary>
        public Toolset GetToolset(string toolsVersion)
        {
            ErrorUtilities.VerifyThrowArgumentLength(toolsVersion);
            using (_locker.EnterDisposableWriteLock())
            {
                _toolsets.TryGetValue(toolsVersion, out var toolset);
                return toolset;
            }
        }

        /// <summary>
        /// Figure out what ToolsVersion to use to actually build the project with.
        /// </summary>
        /// <param name="explicitToolsVersion">The user-specified ToolsVersion (through e.g. /tv: on the command line). May be null</param>
        /// <param name="toolsVersionFromProject">The ToolsVersion from the project file. May be null</param>
        /// <returns>The ToolsVersion we should use to build this project.  Should never be null.</returns>
        public string GetEffectiveToolsVersion(string explicitToolsVersion, string toolsVersionFromProject)
        {
            return Utilities.GenerateToolsVersionToUse(explicitToolsVersion, toolsVersionFromProject, GetToolset, DefaultToolsVersion, out _);
        }

        /// <summary>
        /// Returns any and all loaded projects with the provided path.
        /// There may be more than one, if they are distinguished by global properties
        /// and/or tools version.
        /// </summary>
        public ICollection<Project> GetLoadedProjects(string fullPath)
        {
            return GetLoadedProjects(true, fullPath);
        }

        /// <summary>
        /// Returns any and all loaded projects with the provided path.
        /// There may be more than one, if they are distinguished by global properties
        /// and/or tools version.
        /// </summary>
        internal ICollection<Project> GetLoadedProjects(bool includeExternal, string fullPath = null)
        {
            List<Project> loaded;
            using (_locker.EnterDisposableWriteLock())
            {
                loaded = fullPath == null ? new List<Project>(_loadedProjects) : new List<Project>(_loadedProjects.GetMatchingProjectsIfAny(fullPath));
            }

            if (includeExternal)
            {
                var link = Link;
                if (link != null)
                {
                    loaded.AddRange(link.GetLoadedProjects(fullPath));
                }
            }

            return loaded;
        }

        /// <summary>
        /// Loads a project with the specified filename, using the collection's global properties and tools version.
        /// If a matching project is already loaded, it will be returned, otherwise a new project will be loaded.
        /// </summary>
        /// <param name="fileName">The project file to load</param>
        /// <returns>A loaded project.</returns>
        public Project LoadProject(string fileName)
        {
            return LoadProject(fileName, null);
        }

        /// <summary>
        /// Loads a project with the specified filename and tools version, using the collection's global properties.
        /// If a matching project is already loaded, it will be returned, otherwise a new project will be loaded.
        /// </summary>
        /// <param name="fileName">The project file to load</param>
        /// <param name="toolsVersion">The tools version to use. May be null.</param>
        /// <returns>A loaded project.</returns>
        public Project LoadProject(string fileName, string toolsVersion)
        {
            return LoadProject(fileName, null /* use project collection's global properties */, toolsVersion);
        }

        /// <summary>
        /// Loads a project with the specified filename, tools version and global properties.
        /// If a matching project is already loaded, it will be returned, otherwise a new project will be loaded.
        /// </summary>
        /// <param name="fileName">The project file to load</param>
        /// <param name="globalProperties">The global properties to use. May be null, in which case the containing project collection's global properties will be used.</param>
        /// <param name="toolsVersion">The tools version. May be null.</param>
        /// <returns>A loaded project.</returns>
        public Project LoadProject(string fileName, IDictionary<string, string> globalProperties, string toolsVersion)
        {
            ErrorUtilities.VerifyThrowArgumentLength(fileName);
            fileName = FileUtilities.NormalizePath(fileName);

            using (_locker.EnterDisposableWriteLock())
            {
                if (globalProperties == null)
                {
                    globalProperties = GlobalProperties;
                }
                else
                {
                    // We need to update the set of global properties to merge in the ProjectCollection global properties --
                    // otherwise we might end up declaring "not matching" a project that actually does ... and then throw
                    // an exception when we go to actually add the newly created project to the ProjectCollection.
                    // BUT remember that project global properties win -- don't override a property that already exists.
                    foreach (KeyValuePair<string, string> globalProperty in GlobalProperties)
                    {
                        if (!globalProperties.ContainsKey(globalProperty.Key))
                        {
                            globalProperties.Add(globalProperty);
                        }
                    }
                }

                // We do not control the current directory at this point, but assume that if we were
                // passed a relative path, the caller assumes we will prepend the current directory.
                string toolsVersionFromProject = null;

                if (toolsVersion == null)
                {
                    // Load the project XML to get any ToolsVersion attribute.
                    // If there isn't already an equivalent project loaded, the real load we'll do will be satisfied from the cache.
                    // If there is already an equivalent project loaded, we'll never need this XML -- but it'll already
                    // have been loaded by that project so it will have been satisfied from the ProjectRootElementCache.
                    // Either way, no time wasted.
                    try
                    {
                        ProjectRootElement xml = ProjectRootElement.OpenProjectOrSolution(fileName, globalProperties, toolsVersion, ProjectRootElementCache, true /*explicitlyloaded*/);
                        toolsVersionFromProject = (xml.ToolsVersion.Length > 0) ? xml.ToolsVersion : DefaultToolsVersion;
                    }
                    catch (InvalidProjectFileException ex)
                    {
                        var buildEventContext = new BuildEventContext(0 /* node ID */, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId);
                        LoggingService.LogInvalidProjectFileError(buildEventContext, ex);
                        throw;
                    }
                }

                string effectiveToolsVersion = Utilities.GenerateToolsVersionToUse(toolsVersion, toolsVersionFromProject, GetToolset, DefaultToolsVersion, out _);
                Project project = _loadedProjects.GetMatchingProjectIfAny(fileName, globalProperties, effectiveToolsVersion);

                if (project == null)
                {
                    // The Project constructor adds itself to our collection,
                    // it is not done by us
                    project = new Project(fileName, globalProperties, effectiveToolsVersion, this);
                }

                return project;
            }
        }

        /// <summary>
        /// Loads a project with the specified reader, using the collection's global properties and tools version.
        /// The project will be added to this project collection when it is named.
        /// </summary>
        /// <param name="xmlReader">Xml reader to read project from</param>
        /// <returns>A loaded project.</returns>
        public Project LoadProject(XmlReader xmlReader)
        {
            return LoadProject(xmlReader, null);
        }

        /// <summary>
        /// Loads a project with the specified reader and tools version, using the collection's global properties.
        /// The project will be added to this project collection when it is named.
        /// </summary>
        /// <param name="xmlReader">Xml reader to read project from</param>
        /// <param name="toolsVersion">The tools version to use. May be null.</param>
        /// <returns>A loaded project.</returns>
        public Project LoadProject(XmlReader xmlReader, string toolsVersion)
        {
            return LoadProject(xmlReader, null /* use project collection's global properties */, toolsVersion);
        }

        /// <summary>
        /// Loads a project with the specified reader, tools version and global properties.
        /// The project will be added to this project collection when it is named.
        /// </summary>
        /// <param name="xmlReader">Xml reader to read project from</param>
        /// <param name="globalProperties">The global properties to use. May be null in which case the containing project collection's global properties will be used.</param>
        /// <param name="toolsVersion">The tools version. May be null.</param>
        /// <returns>A loaded project.</returns>
        public Project LoadProject(XmlReader xmlReader, IDictionary<string, string> globalProperties, string toolsVersion)
        {
            return new Project(xmlReader, globalProperties, toolsVersion, this);
        }

        /// <summary>
        /// Adds a logger to the collection of loggers used for builds of projects in this collection.
        /// If the logger object is already in the collection, does nothing.
        /// </summary>
        public void RegisterLogger(ILogger logger)
        {
            using (_locker.EnterDisposableWriteLock())
            {
                RegisterLoggerInternal(logger);
            }

            OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Loggers));
        }

        /// <summary>
        /// Adds some loggers to the collection of loggers used for builds of projects in this collection.
        /// If any logger object is already in the collection, does nothing for that logger.
        /// May be null.
        /// </summary>
        public void RegisterLoggers(IEnumerable<ILogger> loggers)
        {
            bool changed = false;
            if (loggers != null)
            {
                using (_locker.EnterDisposableWriteLock())
                {
                    foreach (ILogger logger in loggers)
                    {
                        RegisterLoggerInternal(logger);
                        changed = true;
                    }
                }
            }

            if (changed)
            {
                OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Loggers));
            }
        }

        /// <summary>
        /// Adds some remote loggers to the collection of remote loggers used for builds of projects in this collection.
        /// May be null.
        /// </summary>
        public void RegisterForwardingLoggers(IEnumerable<ForwardingLoggerRecord> remoteLoggers)
        {
            using (_locker.EnterDisposableWriteLock())
            {
                if (remoteLoggers != null)
                {
                    foreach (ForwardingLoggerRecord remoteLoggerRecord in remoteLoggers)
                    {
                        _loggingService.RegisterDistributedLogger(new Logging.ReusableLogger(remoteLoggerRecord.CentralLogger), remoteLoggerRecord.ForwardingLoggerDescription);
                    }
                }
            }

            OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Loggers));
        }

        /// <summary>
        /// Removes all loggers from the collection of loggers used for builds of projects in this collection.
        /// </summary>
        public void UnregisterAllLoggers()
        {
            using (_locker.EnterDisposableWriteLock())
            {
                _loggingService.UnregisterAllLoggers();

                // UNDONE: Logging service should not shut down when all loggers are unregistered.
                // VS unregisters all loggers on the same project collection often. To workaround this, we have to create it again now!
                CreateLoggingService(MaxNodeCount, OnlyLogCriticalEvents, EnableTargetOutputLogging);
            }

            OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Loggers));
        }

        /// <summary>
        /// Unloads the specific project specified.
        /// Host should call this when they are completely done with the project.
        /// If project was not already loaded, throws InvalidOperationException.
        /// </summary>
        public void UnloadProject(Project project)
        {
            if (project.IsLinked)
            {
                project.Zombify();
                return;
            }

            using (_locker.EnterDisposableWriteLock())
            {
                bool existed = _loadedProjects.RemoveProject(project);
                ErrorUtilities.VerifyThrowInvalidOperation(existed, "OM_ProjectWasNotLoaded");

                project.Zombify();

                // If we've removed the last entry for the given project full path
                // then unregister any and all host objects for that project
                if (_hostServices != null && _loadedProjects.GetMatchingProjectsIfAny(project.FullPath).Count == 0)
                {
                    _hostServices.UnregisterProject(project.FullPath);
                }

                // Release our own cache's strong references to try to help
                // free memory. These may be the last references to the ProjectRootElements
                // in the cache, so the cache shouldn't hold strong references to them of its own.
                ProjectRootElementCache.DiscardStrongReferences();
            }
        }

        /// <summary>
        /// Unloads a project XML root element from the weak cache.
        /// </summary>
        /// <param name="projectRootElement">The project XML root element to unload.</param>
        /// <exception cref="InvalidOperationException">
        /// Thrown if the project XML root element to unload is still in use by a loaded project or its imports.
        /// </exception>
        /// <remarks>
        /// This method is useful for the case where the host knows that all projects using this XML element
        /// are unloaded, and desires to discard any unsaved changes.
        /// </remarks>
        public void UnloadProject(ProjectRootElement projectRootElement)
        {
            ErrorUtilities.VerifyThrowArgumentNull(projectRootElement);
            if (projectRootElement.Link != null)
            {
                return;
            }

            using (_locker.EnterDisposableWriteLock())
            {
                Project conflictingProject = GetLoadedProjects(false, null).FirstOrDefault(project => project.UsesProjectRootElement(projectRootElement));
                if (conflictingProject != null)
                {
                    ErrorUtilities.ThrowInvalidOperation("OM_ProjectXmlCannotBeUnloadedDueToLoadedProjects", projectRootElement.FullPath, conflictingProject.FullPath);
                }

                ProjectRootElementCache.DiscardAnyWeakReference(projectRootElement);
            }
        }

        /// <summary>
        /// Unloads all the projects contained by this ProjectCollection.
        /// Host should call this when they are completely done with all the projects.
        /// </summary>
        public void UnloadAllProjects()
        {
            using (_locker.EnterDisposableWriteLock())
            {
                foreach (Project project in _loadedProjects)
                {
                    project.Zombify();

                    // We're removing every entry from the project collection
                    // so unregister any and all host objects for each project
                    _hostServices?.UnregisterProject(project.FullPath);
                }

                _loadedProjects.RemoveAllProjects();

                ProjectRootElementCache.Clear();
            }
        }

        /// <summary>
        /// Get any global property on the project collection that has the specified name,
        /// otherwise returns null.
        /// </summary>
        public ProjectPropertyInstance GetGlobalProperty(string name)
        {
            using (_locker.EnterDisposableReadLock())
            {
                return _globalProperties[name];
            }
        }

        /// <summary>
        /// Set a global property at the collection-level,
        /// and on all projects in the project collection.
        /// </summary>
        public void SetGlobalProperty(string name, string value)
        {
            bool sendEvent = false;
            using (_locker.EnterDisposableWriteLock())
            {
                ProjectPropertyInstance propertyInGlobalProperties = _globalProperties.GetProperty(name);
                bool changed = propertyInGlobalProperties == null || !String.Equals(((IValued)propertyInGlobalProperties).EscapedValue, value, StringComparison.OrdinalIgnoreCase);
                if (changed)
                {
                    _globalProperties.Set(ProjectPropertyInstance.Create(name, value));
                    sendEvent = true;
                }

                // Copy LoadedProjectCollection as modifying a project's global properties will cause it to re-add
                var projects = new List<Project>(_loadedProjects);
                foreach (Project project in projects)
                {
                    project.SetGlobalProperty(name, value);
                }
            }

            if (sendEvent)
            {
                OnProjectCollectionChanged(
                    new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.GlobalProperties));
            }
        }

        /// <summary>
        /// Removes a global property from the collection-level set of global properties,
        /// and all projects in the project collection.
        /// If it was on this project collection, returns true.
        /// </summary>
        public bool RemoveGlobalProperty(string name)
        {
            bool set;
            using (_locker.EnterDisposableWriteLock())
            {
                set = _globalProperties.Remove(name);

                // Copy LoadedProjectCollection as modifying a project's global properties will cause it to re-add
                var projects = new List<Project>(_loadedProjects);
                foreach (Project project in projects)
                {
                    project.RemoveGlobalProperty(name);
                }
            }

            OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.GlobalProperties));

            return set;
        }

        /// <summary>
        /// Called when a host is completely done with the project collection.
        /// </summary>
        // UNDONE: This is a hack to make sure the logging thread shuts down if the build used the logging service
        // off the ProjectCollection. After CTP we need to rationalize this and see if we can remove the logging service from
        // the project collection entirely so this isn't necessary.
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        #region IBuildComponent Members

        /// <summary>
        /// Initializes the component with the component host.
        /// </summary>
        /// <param name="host">The component host.</param>
        void IBuildComponent.InitializeComponent(IBuildComponentHost host)
        {
        }

        /// <summary>
        /// Shuts down the component.
        /// </summary>
        void IBuildComponent.ShutdownComponent()
        {
        }

        #endregion

        /// <summary>
        /// Unloads a project XML root element from the cache entirely, if it is not
        /// in use by project loaded into this collection.
        /// Returns true if it was unloaded successfully, or was not already loaded.
        /// Returns false if it was not unloaded because it was still in use by a loaded <see cref="Project"/>.
        /// </summary>
        /// <param name="projectRootElement">The project XML root element to unload.</param>
        public bool TryUnloadProject(ProjectRootElement projectRootElement)
        {
            ErrorUtilities.VerifyThrowArgumentNull(projectRootElement);
            if (projectRootElement.Link != null)
            {
                return false;
            }

            using (_locker.EnterDisposableWriteLock())
            {
                ProjectRootElementCache.DiscardStrongReferences();

                Project conflictingProject = GetLoadedProjects(false, null).FirstOrDefault(project => project.UsesProjectRootElement(projectRootElement));
                if (conflictingProject == null)
                {
                    ProjectRootElementCache.DiscardAnyWeakReference(projectRootElement);
                    return true;
                }

                return false;
            }
        }

        /// <summary>
        /// Logs a BuildFinished event. This is used specifically when a ProjectCollection is created but never actually built, yet a BuildFinished event
        /// is still desired. As an example, if a Project is just meant to be evaluated, but a binlog is also collected, that binlog should be able to
        /// say the build succeeded or failed. This provides a mechanism to achieve that.
        /// </summary>
        public void LogBuildFinishedEvent(bool success) => _loggingService.LogBuildFinished(success);

        /// <summary>
        /// Called by a Project object to load itself into this collection.
        /// If the project was already loaded under a different name, it is unloaded.
        /// Stores the project in the list of loaded projects if it has a name.
        /// Does not store the project if it has no name because it has not been saved to disk yet.
        /// If the project previously had a name, but was not in the collection already, throws InvalidOperationException.
        /// If the project was not previously in the collection, sets the collection's global properties on it.
        /// </summary>
        internal void OnAfterRenameLoadedProject(string oldFullPathIfAny, Project project)
        {
            if (project.FullPath == null)
            {
                return;
            }

            using (_locker.EnterDisposableWriteLock())
            {
                if (oldFullPathIfAny != null)
                {
                    bool existed = _loadedProjects.RemoveProject(oldFullPathIfAny, project);
                    ErrorUtilities.VerifyThrowInvalidOperation(existed, "OM_ProjectWasNotLoaded");
                }

                // The only time this ever gets called with a null full path is when the project is first being
                // constructed.  The mere fact that this method is being called means that this project will belong
                // to this project collection.  As such, it has already had all necessary global properties applied
                // when being constructed -- we don't need to do anything special here.
                // If we did add global properties here, we would just end up either duplicating work or possibly
                // wiping out global properties set on the project meant to override the ProjectCollection copies.
                _loadedProjects.AddProject(project);

                if (_hostServices != null)
                {
                    HostServices.OnRenameProject(oldFullPathIfAny, project.FullPath);
                }
            }
        }

        /// <summary>
        /// Called after a loaded project's global properties are changed, so we can update
        /// our loaded project table.
        /// Project need not already be in the project collection yet, but it can't be in another one.
        /// </summary>
        /// <remarks>
        /// We have to remove and re-add so that there's an error if there's already an equivalent
        /// project loaded.
        /// </remarks>
        internal void AfterUpdateLoadedProjectGlobalProperties(Project project)
        {
            using (_locker.EnterDisposableWriteLock())
            {
                ErrorUtilities.VerifyThrowInvalidOperation(ReferenceEquals(project.ProjectCollection, this), "OM_IncorrectObjectAssociation", "Project", "ProjectCollection");

                if (project.FullPath == null)
                {
                    return;
                }

                bool existed = _loadedProjects.RemoveProject(project);
                if (existed)
                {
                    _loadedProjects.AddProject(project);
                }
            }
        }

        /// <summary>
        /// Following standard framework guideline dispose pattern.
        /// Shut down logging service if the project collection owns one, in order
        /// to shut down the logger thread and loggers.
        /// </summary>
        /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources..</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                ShutDownLoggingService();
                if (ProjectRootElementCache != null)
                {
                    ProjectRootElementCache.ProjectRootElementAddedHandler -= ProjectRootElementCache_ProjectRootElementAddedHandler;
                    ProjectRootElementCache.ProjectRootElementDirtied -= ProjectRootElementCache_ProjectRootElementDirtiedHandler;
                    ProjectRootElementCache.ProjectDirtied -= ProjectRootElementCache_ProjectDirtiedHandler;
                }
                Tracing.Dump();
            }
        }

        /// <summary>
        /// Remove a toolset and does not raise events. The caller should have acquired a write lock on this method's behalf.
        /// </summary>
        /// <param name="toolsVersion">The toolset to remove.</param>
        /// <returns><c>true</c> if the toolset was found and removed; <c>false</c> otherwise.</returns>
        private bool RemoveToolsetInternal(string toolsVersion)
        {
            Debug.Assert(_locker.IsWriteLockHeld);

            if (!_toolsets.Remove(toolsVersion))
            {
                return false;
            }

            _toolsetsVersion++;
            return true;
        }

        /// <summary>
        /// Adds a logger to the collection of loggers used for builds of projects in this collection.
        /// If the logger object is already in the collection, does nothing.
        /// </summary>
        private void RegisterLoggerInternal(ILogger logger)
        {
            ErrorUtilities.VerifyThrowArgumentNull(logger);
            Debug.Assert(_locker.IsWriteLockHeld);
            _loggingService.RegisterLogger(new Logging.ReusableLogger(logger));
        }

        /// <summary>
        /// Handler which is called when a project is added to the RootElementCache of this project collection. We then fire an event indicating that a project was added to the collection itself.
        /// </summary>
        private void ProjectRootElementCache_ProjectRootElementAddedHandler(object sender, ProjectRootElementCacheAddEntryEventArgs e)
        {
            ProjectAdded?.Invoke(this, new ProjectAddedToProjectCollectionEventArgs(e.RootElement));
        }

        /// <summary>
        /// Handler which is called when a project that is part of this collection is dirtied. We then fire an event indicating that a project has been dirtied.
        /// </summary>
        private void ProjectRootElementCache_ProjectRootElementDirtiedHandler(object sender, ProjectXmlChangedEventArgs e)
        {
            OnProjectXmlChanged(e);
        }

        /// <summary>
        /// Handler which is called when a project is dirtied.
        /// </summary>
        private void ProjectRootElementCache_ProjectDirtiedHandler(object sender, ProjectChangedEventArgs e)
        {
            OnProjectChanged(e);
        }

        /// <summary>
        /// Raises the <see cref="ProjectXmlChanged"/> event.
        /// </summary>
        /// <param name="e">The event arguments that indicate ProjectRootElement-specific details.</param>
        private void OnProjectXmlChanged(ProjectXmlChangedEventArgs e)
        {
            ProjectXmlChanged?.Invoke(this, e);
        }

        /// <summary>
        /// Raises the <see cref="ProjectChanged"/> event.
        /// </summary>
        /// <param name="e">The event arguments that indicate Project-specific details.</param>
        private void OnProjectChanged(ProjectChangedEventArgs e)
        {
            ProjectChanged?.Invoke(this, e);
        }

        /// <summary>
        /// Raises the <see cref="ProjectCollectionChanged"/> event.
        /// </summary>
        /// <param name="e">The event arguments that indicate details on what changed on the collection.</param>
        private void OnProjectCollectionChanged(ProjectCollectionChangedEventArgs e)
        {
            Debug.Assert(!_locker.IsWriteLockHeld, "We should never raise events while holding a private lock.");
            ProjectCollectionChanged?.Invoke(this, e);
        }

        /// <summary>
        /// Shutdown the logging service
        /// </summary>
        private void ShutDownLoggingService()
        {
            if (_loggingService != null)
            {
                try
                {
                    (LoggingService as LoggingService)?.WaitForLoggingToProcessEvents();
                    ((IBuildComponent)LoggingService).ShutdownComponent();
                }
                catch (LoggerException)
                {
                    throw;
                }
                catch (InternalLoggerException)
                {
                    throw;
                }
                catch (Exception ex)
                {
                    // According to Framework Guidelines, Dispose methods should never throw except in dire circumstances.
                    // However if we throw at all, its a bug. Throw InternalErrorException to emphasize that.
                    ErrorUtilities.ThrowInternalError("Throwing from logger shutdown", ex);
                    throw;
                }

                _loggingService = null;
            }
        }

        /// <summary>
        /// Create a new logging service
        /// </summary>
        private void CreateLoggingService(int maxCPUCount, bool onlyLogCriticalEvents, bool enableTargetOutputLogging)
        {
            _loggingService = BackEnd.Logging.LoggingService.CreateLoggingService(_loggerMode, 0 /*Evaluation can be done as if it was on node "0"*/);
            _loggingService.MaxCPUCount = maxCPUCount;
            _loggingService.OnlyLogCriticalEvents = onlyLogCriticalEvents;
            _loggingService.EnableTargetOutputLogging = enableTargetOutputLogging;
        }

        /// <summary>
        /// Reset the toolsets using the provided toolset reader, used by unit tests
        /// </summary>
        internal void ResetToolsetsForTests(ToolsetConfigurationReader configurationReaderForTestsOnly)
        {
            InitializeToolsetCollection(configReader: configurationReaderForTestsOnly);
        }

#if FEATURE_WIN32_REGISTRY
        /// <summary>
        /// Reset the toolsets using the provided toolset reader, used by unit tests
        /// </summary>
        internal void ResetToolsetsForTests(ToolsetRegistryReader registryReaderForTestsOnly)
        {
            InitializeToolsetCollection(registryReader: registryReaderForTestsOnly);
        }
#endif

        /// <summary>
        /// Populate Toolsets with a dictionary of (toolset version, Toolset)
        /// using information from the registry and config file, if any.
        /// </summary>
        private void InitializeToolsetCollection(
#if FEATURE_WIN32_REGISTRY
                ToolsetRegistryReader registryReader = null,
#endif
                ToolsetConfigurationReader configReader = null)
        {
            _toolsets = new Dictionary<string, Toolset>(StringComparer.OrdinalIgnoreCase);

            // We only want our local toolset (as defined in MSBuild.exe.config) when we're operating locally...
            _defaultToolsVersion = ToolsetReader.ReadAllToolsets(_toolsets,
#if FEATURE_WIN32_REGISTRY
                    registryReader,
#endif
                    configReader,
                    EnvironmentProperties, _globalProperties, ToolsetLocations);

            _toolsetsVersion++;
        }

        /// <summary>
        /// Event to provide information about what project just got added to the project collection.
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "This has been API reviewed")]
        public class ProjectAddedToProjectCollectionEventArgs : EventArgs
        {
            /// <summary>
            /// The root element which was added to the project collection.
            /// </summary>
            public ProjectAddedToProjectCollectionEventArgs(ProjectRootElement element)
            {
                ProjectRootElement = element;
            }

            /// <summary>
            /// Root element which was added to the project collection.
            /// </summary>
            public ProjectRootElement ProjectRootElement { get; }
        }

        /// <summary>
        /// Holder for the projects loaded into this collection.
        /// </summary>
        private class LoadedProjectCollection : IEnumerable<Project>
        {
            /// <summary>
            /// The collection of all projects already loaded into this collection.
            /// Key is the full path to the project, value is a list of projects with that path, each
            /// with different global properties and/or tools version.
            /// </summary>
            /// <remarks>
            /// If hosts tend to load lots of projects with the same path, the value will have to be
            /// changed to a more efficient type of collection.
            ///
            /// Lock on this object. Concurrent load must be thread safe.
            /// Not using ConcurrentDictionary because some of the add/update
            /// semantics would get convoluted.
            /// </remarks>
            private Dictionary<string, List<Project>> _loadedProjects = new Dictionary<string, List<Project>>(StringComparer.OrdinalIgnoreCase);

            /// <summary>
            /// Count of loaded projects
            /// </summary>
            private int _count;

            /// <summary>
            /// Returns the number of projects currently loaded
            /// </summary>
            internal int Count
            {
                get
                {
                    lock (_loadedProjects)
                    {
                        return _count;
                    }
                }
            }

            /// <summary>
            /// Enumerate all the projects
            /// </summary>
            public IEnumerator<Project> GetEnumerator()
            {
                lock (_loadedProjects)
                {
                    var projects = new List<Project>();

                    foreach (List<Project> projectList in _loadedProjects.Values)
                    {
                        foreach (Project project in projectList)
                        {
                            projects.Add(project);
                        }
                    }

                    return projects.GetEnumerator();
                }
            }

            /// <summary>
            /// Enumerate all the projects.
            /// </summary>
            IEnumerator IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }

            /// <summary>
            /// Get all projects with the provided path.
            /// Returns an empty list if there are none.
            /// </summary>
            internal IList<Project> GetMatchingProjectsIfAny(string fullPath)
            {
                lock (_loadedProjects)
                {
                    _loadedProjects.TryGetValue(fullPath, out List<Project> candidates);

                    return candidates ?? (IList<Project>)Array.Empty<Project>();
                }
            }

            /// <summary>
            /// Returns the project in the collection matching the path, global properties, and tools version provided.
            /// There can be no more than one match.
            /// If none is found, returns null.
            /// </summary>
            internal Project GetMatchingProjectIfAny(string fullPath, IDictionary<string, string> globalProperties, string toolsVersion)
            {
                lock (_loadedProjects)
                {
                    if (_loadedProjects.TryGetValue(fullPath, out List<Project> candidates))
                    {
                        foreach (Project candidate in candidates)
                        {
                            if (HasEquivalentGlobalPropertiesAndToolsVersion(candidate, globalProperties, toolsVersion))
                            {
                                return candidate;
                            }
                        }
                    }

                    return null;
                }
            }

            /// <summary>
            /// Adds the provided project to the collection.
            /// If there is already an equivalent project, throws InvalidOperationException.
            /// </summary>
            internal void AddProject(Project project)
            {
                lock (_loadedProjects)
                {
                    if (!_loadedProjects.TryGetValue(project.FullPath, out List<Project> projectList))
                    {
                        projectList = new List<Project>();
                        _loadedProjects.Add(project.FullPath, projectList);
                    }

                    foreach (Project existing in projectList)
                    {
                        if (HasEquivalentGlobalPropertiesAndToolsVersion(existing, project.GlobalProperties, project.ToolsVersion))
                        {
                            ErrorUtilities.ThrowInvalidOperation("OM_MatchingProjectAlreadyInCollection", existing.FullPath);
                        }
                    }

                    projectList.Add(project);
                    _count++;
                }
            }

            /// <summary>
            /// Removes the provided project from the collection.
            /// If project was not loaded, returns false.
            /// </summary>
            internal bool RemoveProject(Project project)
            {
                return RemoveProject(project.FullPath, project);
            }

            /// <summary>
            /// Removes a project, using the specified full path to use as the key to find it.
            /// This is specified separately in case the project was previously stored under a different path.
            /// </summary>
            internal bool RemoveProject(string projectFullPath, Project project)
            {
                lock (_loadedProjects)
                {
                    if (!_loadedProjects.TryGetValue(projectFullPath, out List<Project> projectList))
                    {
                        return false;
                    }

                    bool result = projectList.Remove(project);

                    if (result)
                    {
                        _count--;

                        if (projectList.Count == 0)
                        {
                            _loadedProjects.Remove(projectFullPath);
                        }
                    }

                    return result;
                }
            }

            /// <summary>
            /// Removes all projects from the collection.
            /// </summary>
            internal void RemoveAllProjects()
            {
                lock (_loadedProjects)
                {
                    _loadedProjects = new Dictionary<string, List<Project>>(StringComparer.OrdinalIgnoreCase);
                    _count = 0;
                }
            }

            /// <summary>
            /// Returns true if the global properties and tools version provided are equivalent to
            /// those in the provided project, otherwise false.
            /// </summary>
            private static bool HasEquivalentGlobalPropertiesAndToolsVersion(Project project, IDictionary<string, string> globalProperties, string toolsVersion)
            {
                if (!String.Equals(project.ToolsVersion, toolsVersion, StringComparison.OrdinalIgnoreCase))
                {
                    return false;
                }

                if (project.GlobalPropertiesCount != globalProperties.Count)
                {
                    return false;
                }

                foreach (KeyValuePair<string, string> leftProperty in project.GlobalPropertiesEnumerable)
                {
                    if (!globalProperties.TryGetValue(leftProperty.Key, out var rightValue))
                    {
                        return false;
                    }

                    if (!String.Equals(leftProperty.Value, rightValue, StringComparison.OrdinalIgnoreCase))
                    {
                        return false;
                    }
                }

                return true;
            }
        }
    }
}
