File: ProjectStartedEventArgs.cs
Web Access
Project: ..\..\..\src\Framework\Microsoft.Build.Framework.csproj (Microsoft.Build.Framework)
// 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.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using Microsoft.Build.Experimental.BuildCheck;
using Microsoft.Build.Shared;
 
namespace Microsoft.Build.Framework
{
    /// <summary>
    /// Arguments for project started events
    /// </summary>
    // WARNING: marking a type [Serializable] without implementing
    // ISerializable imposes a serialization contract -- it is a
    // promise to never change the type's fields i.e. the type is
    // immutable; adding new fields in the next version of the type
    // without following certain special FX guidelines, can break both
    // forward and backward compatibility
    [Serializable]
    public class ProjectStartedEventArgs : BuildStatusEventArgs
    {
        #region Constants
        /// <summary>
        /// Indicates an invalid project identifier.
        /// </summary>
        public const int InvalidProjectId = -1;
        #endregion
 
        /// <summary>
        /// Default constructor
        /// </summary>
        protected ProjectStartedEventArgs()
            : base()
        {
            // do nothing
        }
 
        /// <summary>
        /// This constructor allows event data to be initialized.
        /// Sender is assumed to be "MSBuild".
        /// </summary>
        /// <param name="message">text message</param>
        /// <param name="helpKeyword">help keyword </param>
        /// <param name="projectFile">project name</param>
        /// <param name="targetNames">targets we are going to build (empty indicates default targets)</param>
        /// <param name="properties">list of properties</param>
        /// <param name="items">list of items</param>
        public ProjectStartedEventArgs(
            string message,
            string helpKeyword,
            string projectFile,
            string targetNames,
            IEnumerable properties,
            IEnumerable items)
            : this(message, helpKeyword, projectFile, targetNames, properties, items, DateTime.UtcNow)
        {
        }
 
        /// <summary>
        /// This constructor allows event data to be initialized.
        /// Sender is assumed to be "MSBuild".
        /// </summary>
        /// <param name="projectId">project id</param>
        /// <param name="message">text message</param>
        /// <param name="helpKeyword">help keyword </param>
        /// <param name="projectFile">project name</param>
        /// <param name="targetNames">targets we are going to build (empty indicates default targets)</param>
        /// <param name="properties">list of properties</param>
        /// <param name="items">list of items</param>
        /// <param name="parentBuildEventContext">event context info for the parent project</param>
        public ProjectStartedEventArgs(
            int projectId,
            string message,
            string helpKeyword,
            string? projectFile,
            string? targetNames,
            IEnumerable? properties,
            IEnumerable? items,
            BuildEventContext? parentBuildEventContext)
            : this(projectId, message, helpKeyword, projectFile, targetNames, properties, items, parentBuildEventContext, DateTime.UtcNow)
        {
        }
 
        /// <summary>
        /// This constructor allows event data to be initialized.
        /// Sender is assumed to be "MSBuild".
        /// </summary>
        /// <param name="projectId">project id</param>
        /// <param name="message">text message</param>
        /// <param name="helpKeyword">help keyword </param>
        /// <param name="projectFile">project name</param>
        /// <param name="targetNames">targets we are going to build (empty indicates default targets)</param>
        /// <param name="properties">list of properties</param>
        /// <param name="items">list of items</param>
        /// <param name="parentBuildEventContext">event context info for the parent project</param>
        /// <param name="globalProperties">An <see cref="IDictionary{String, String}"/> containing global properties.</param>
        /// <param name="toolsVersion">The tools version.</param>
        public ProjectStartedEventArgs(
            int projectId,
            string message,
            string helpKeyword,
            string? projectFile,
            string? targetNames,
            IEnumerable? properties,
            IEnumerable? items,
            BuildEventContext? parentBuildEventContext,
            IDictionary<string, string>? globalProperties,
            string? toolsVersion)
            : this(projectId, message, helpKeyword, projectFile, targetNames, properties, items, parentBuildEventContext)
        {
            this.GlobalProperties = globalProperties;
            this.ToolsVersion = toolsVersion;
        }
 
        /// <summary>
        /// This constructor allows event data to be initialized. Also the timestamp can be set
        /// Sender is assumed to be "MSBuild".
        /// </summary>
        /// <param name="message">text message</param>
        /// <param name="helpKeyword">help keyword </param>
        /// <param name="projectFile">project name</param>
        /// <param name="targetNames">targets we are going to build (empty indicates default targets)</param>
        /// <param name="properties">list of properties</param>
        /// <param name="items">list of items</param>
        /// <param name="eventTimestamp">The <see cref="DateTime"/> of the event.</param>
        public ProjectStartedEventArgs(
            string message,
            string helpKeyword,
            string? projectFile,
            string? targetNames,
            IEnumerable? properties,
            IEnumerable? items,
            DateTime eventTimestamp)
            : base(message, helpKeyword, "MSBuild", eventTimestamp)
        {
            this.projectFile = projectFile;
            this.targetNames = targetNames ?? String.Empty;
            this.properties = properties;
            this.items = items;
        }
 
        /// <summary>
        /// This constructor allows event data to be initialized.
        /// Sender is assumed to be "MSBuild".
        /// </summary>
        /// <param name="projectId">project id</param>
        /// <param name="message">text message</param>
        /// <param name="helpKeyword">help keyword </param>
        /// <param name="projectFile">project name</param>
        /// <param name="targetNames">targets we are going to build (empty indicates default targets)</param>
        /// <param name="properties">list of properties</param>
        /// <param name="items">list of items</param>
        /// <param name="parentBuildEventContext">event context info for the parent project</param>
        /// <param name="eventTimestamp">The <see cref="DateTime"/> of the event.</param>
        public ProjectStartedEventArgs(
            int projectId,
            string message,
            string helpKeyword,
            string? projectFile,
            string? targetNames,
            IEnumerable? properties,
            IEnumerable? items,
            BuildEventContext? parentBuildEventContext,
            DateTime eventTimestamp)
            : this(message, helpKeyword, projectFile, targetNames, properties, items, eventTimestamp)
        {
            parentProjectBuildEventContext = parentBuildEventContext;
            this.projectId = projectId;
        }
 
        // ProjectId is only contained in the project started event.
        // This number indicated the instance id of the project and can be
        // used when debugging to determine if two projects with the same name
        // are the same project instance or different instances
        [OptionalField(VersionAdded = 2)]
        private int projectId;
 
        /// <summary>
        /// Gets the identifier of the project.
        /// </summary>
        public int ProjectId
        {
            get
            {
                return projectId;
            }
        }
 
        [OptionalField(VersionAdded = 2)]
        private BuildEventContext? parentProjectBuildEventContext;
 
        /// <summary>
        /// Event context information, where the event was fired from in terms of the build location
        /// </summary>
        public BuildEventContext? ParentProjectBuildEventContext
        {
            get
            {
                return parentProjectBuildEventContext;
            }
        }
 
        /// <summary>
        /// The name of the project file
        /// </summary>
        private string? projectFile;
 
        /// <summary>
        /// Project name
        /// </summary>
        public string? ProjectFile
        {
            get
            {
                return projectFile;
            }
        }
 
        /// <summary>
        /// Targets that we will build in the project
        /// </summary>
        private string? targetNames;
 
        /// <summary>
        /// Targets that we will build in the project
        /// </summary>
        public string? TargetNames
        {
            get
            {
                return targetNames;
            }
        }
 
        /// <summary>
        /// Gets the set of global properties used to evaluate this project.
        /// </summary>
        [OptionalField(VersionAdded = 2)]
        private IDictionary<string, string>? globalProperties;
 
        /// <summary>
        /// Gets the set of global properties used to evaluate this project.
        /// </summary>
        public IDictionary<string, string>? GlobalProperties
        {
            get
            {
                return globalProperties ?? (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_12)
                    ? ImmutableDictionary<string, string>.Empty
                    : null);
            }
 
            internal set
            {
                globalProperties = value;
            }
        }
 
        [OptionalField(VersionAdded = 2)]
        private string? toolsVersion;
 
        /// <summary>
        /// Gets the tools version used to evaluate this project.
        /// </summary>
        public string? ToolsVersion
        {
            get
            {
                return toolsVersion;
            }
 
            internal set
            {
                toolsVersion = value;
            }
        }
 
        // IEnumerable is not a serializable type. That is okay because
        // (a) this event will not be thrown by tasks, so it should not generally cross AppDomain boundaries
        // (b) this event still makes sense when this field is "null"
        [NonSerialized]
        private IEnumerable? properties;
 
        /// <summary>
        /// List of properties in this project. This is a live, read-only list.
        /// </summary>
        public IEnumerable? Properties
        {
            get
            {
                // UNDONE: (Serialization.) Rather than storing the properties directly in this class, we could
                // grab them from the BuildRequestConfiguration associated with this projectId (which is now poorly
                // named because it is actually the BuildRequestConfigurationId.)  For central loggers in the
                // multi-proc case, this could pull up just the global properties used to start the project.  For
                // distributed loggers in the multi-proc case and all loggers in the single-proc case, this could pull
                // up the live list of properties from the loaded project, which is stored in the configuration as well.
                // By doing this, we no longer need to transmit properties using this message because they've already
                // been transmitted as part of the BuildRequestConfiguration.
                return properties ?? (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_12)
                    ? (DictionaryEntry[])[]
                    : null);
            }
        }
 
        // IEnumerable is not a serializable type. That is okay because
        // (a) this event will not be thrown by tasks, so it should not generally cross AppDomain boundaries
        // (b) this event still makes sense when this field is "null"
        [NonSerialized]
        private IEnumerable? items;
 
        /// <summary>
        /// List of items in this project. This is a live, read-only list.
        /// </summary>
        public IEnumerable? Items
        {
            get
            {
                // UNDONE: (Serialization.) Currently there is a "bug" in the old OM in that items are not transported to
                // the central logger in the multi-proc case.  No one uses this though, so it's probably no big deal.  In
                // the new OM, this list of items could come directly from the BuildRequestConfiguration, which has access
                // to the loaded project.  For distributed loggers in the multi-proc case and all loggers in the single-proc
                // case, this access is to the live list.  For the central logger in the multi-proc case, the main node
                // has likely not loaded this project, and therefore the live items would not be available to them, which is
                // the same as the current functionality.
                return items ?? (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_12)
                    ? (DictionaryEntry[])[]
                    : null);
            }
        }
 
        // Following 3 properties are intended only for internal transfer - to properly communicate the warn as error/msg
        //  from the worker node, to the main node.
        // They are not going to be in a binlog (at least not as of now).
 
        internal ISet<string>? WarningsAsErrors { get; set; }
        internal ISet<string>? WarningsNotAsErrors { get; set; }
        internal ISet<string>? WarningsAsMessages { get; set; }
 
        #region CustomSerializationToStream
 
        /// <summary>
        /// Serializes to a stream through a binary writer
        /// </summary>
        /// <param name="writer">Binary writer which is attached to the stream the event will be serialized into</param>
        internal override void WriteToStream(BinaryWriter writer)
        {
            base.WriteToStream(writer);
            writer.Write((Int32)projectId);
 
            if (parentProjectBuildEventContext == null)
            {
                writer.Write((byte)0);
            }
            else
            {
                writer.Write((byte)1);
                writer.Write((Int32)parentProjectBuildEventContext.NodeId);
                writer.Write((Int32)parentProjectBuildEventContext.ProjectContextId);
                writer.Write((Int32)parentProjectBuildEventContext.TargetId);
                writer.Write((Int32)parentProjectBuildEventContext.TaskId);
                writer.Write((Int32)parentProjectBuildEventContext.SubmissionId);
                writer.Write((Int32)parentProjectBuildEventContext.ProjectInstanceId);
            }
 
            writer.WriteOptionalString(projectFile);
 
            // TargetNames cannot be null as per the constructor
            writer.Write(targetNames!);
 
            // If no properties were added to the property list
            // then we have nothing to create when it is deserialized
            // This can happen if properties is null or if none of the
            // five properties were found in the property object.
            if (properties == null)
            {
                writer.Write((byte)0);
            }
            else
            {
                var validProperties = properties.Cast<DictionaryEntry>().Where(entry => entry.Key != null && entry.Value != null);
                // ReSharper disable once PossibleMultipleEnumeration - We need to get the count of non-null first
                var propertyCount = validProperties.Count();
 
                writer.Write((byte)1);
                writer.Write(propertyCount);
 
                // Write the actual property name value pairs into the stream
                // ReSharper disable once PossibleMultipleEnumeration
                foreach (var propertyPair in validProperties)
                {
                    writer.Write((string)propertyPair.Key);
                    writer.Write((string?)propertyPair.Value ?? "");
                }
            }
 
            WriteCollection(writer, WarningsAsErrors);
            WriteCollection(writer, WarningsNotAsErrors);
            WriteCollection(writer, WarningsAsMessages);
        }
 
        /// <summary>
        /// Deserializes from a stream through a binary reader
        /// </summary>
        /// <param name="reader">Binary reader which is attached to the stream the event will be deserialized from</param>
        /// <param name="version">The version of the runtime the message packet was created from</param>
        internal override void CreateFromStream(BinaryReader reader, int version)
        {
            base.CreateFromStream(reader, version);
            projectId = reader.ReadInt32();
 
            if (reader.ReadByte() == 0)
            {
                parentProjectBuildEventContext = null;
            }
            else
            {
                int nodeId = reader.ReadInt32();
                int projectContextId = reader.ReadInt32();
                int targetId = reader.ReadInt32();
                int taskId = reader.ReadInt32();
 
                if (version > 20)
                {
                    int submissionId = reader.ReadInt32();
                    int projectInstanceId = reader.ReadInt32();
                    parentProjectBuildEventContext = new BuildEventContext(submissionId, nodeId, projectInstanceId, projectContextId, targetId, taskId);
                }
                else
                {
                    parentProjectBuildEventContext = new BuildEventContext(nodeId, targetId, projectContextId, taskId);
                }
            }
 
            projectFile = reader.ReadByte() == 0 ? null : reader.ReadString();
 
            // TargetNames cannot be null as per the constructor
            targetNames = reader.ReadString();
 
            // Check to see if properties was null
            if (reader.ReadByte() == 0)
            {
                properties = null;
            }
            else
            {
                // Get number of properties put on the stream
                int numberOfProperties = reader.ReadInt32();
 
                // We need to use a dictionaryEntry as that is what the old behavior was
                ArrayList dictionaryList = new ArrayList(numberOfProperties);
 
                // Read off each of the key value pairs and put them into the dictionaryList
                for (int i = 0; i < numberOfProperties; i++)
                {
                    string key = reader.ReadString();
                    string value = reader.ReadString();
 
                    if (key != null && value != null)
                    {
                        DictionaryEntry entry = new DictionaryEntry(key, value);
                        dictionaryList.Add(entry);
                    }
                }
 
                properties = dictionaryList;
            }
 
            WarningsAsErrors = ReadStringSet(reader);
            WarningsNotAsErrors = ReadStringSet(reader);
            WarningsAsMessages = ReadStringSet(reader);
        }
 
        private static void WriteCollection(BinaryWriter writer, ICollection<string>? collection)
        {
            if (collection == null)
            {
                writer.Write((byte)0);
            }
            else
            {
                writer.Write((byte)1);
                writer.Write(collection.Count);
                foreach (string item in collection)
                {
                    writer.Write(item);
                }
            }
        }
 
        private static ISet<string>? ReadStringSet(BinaryReader reader)
        {
            if (reader.ReadByte() == 0)
            {
                return null;
            }
            else
            {
                int count = reader.ReadInt32();
                HashSet<string> set = EnumerableExtensions.NewHashSet<string>(count, StringComparer.OrdinalIgnoreCase);
                for (int i = 0; i < count; i++)
                {
                    set.Add(reader.ReadString());
                }
 
                return set;
            }
        }
 
        #endregion
 
        #region SerializationSection
        [OnDeserializing] // Will happen before the object is deserialized
        private void SetDefaultsBeforeSerialization(StreamingContext sc)
        {
            projectId = InvalidProjectId;
            // Don't want to set the default before deserialization is completed to a new event context because
            // that would most likely be a lot of wasted allocations
            parentProjectBuildEventContext = null;
        }
 
        [OnDeserialized]
        private void SetDefaultsAfterSerialization(StreamingContext sc)
        {
            if (parentProjectBuildEventContext == null)
            {
                parentProjectBuildEventContext = BuildEventContext.Invalid;
            }
        }
        #endregion
 
        public override string Message
        {
            get
            {
                if (RawMessage == null)
                {
                    string? projectFilePath = Path.GetFileName(ProjectFile);
 
                    // Check to see if the there are any specific target names to be built.
                    // If targetNames is null or empty then we will be building with the
                    // default targets.
                    if (!string.IsNullOrEmpty(TargetNames))
                    {
                        RawMessage = FormatResourceStringIgnoreCodeAndKeyword("ProjectStartedPrefixForTopLevelProjectWithTargetNames", projectFilePath, TargetNames!);
                    }
                    else
                    {
                        RawMessage = FormatResourceStringIgnoreCodeAndKeyword("ProjectStartedPrefixForTopLevelProjectWithDefaultTargets", projectFilePath);
                    }
                }
 
                return RawMessage;
            }
        }
    }
}