File: BackEnd\LogMessagePacketBase.cs
Web Access
Project: ..\..\..\src\MSBuildTaskHost\MSBuildTaskHost.csproj (MSBuildTaskHost)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Microsoft.Build.Framework;
using Microsoft.Build.TaskHost.Exceptions;
using Microsoft.Build.TaskHost.Utilities;
 
namespace Microsoft.Build.TaskHost.BackEnd;
 
// On .NET Framework 3.5, Microsoft.Build.Framework includes the following concrete event args types:
//
// - BuildErrorEventArgs
// - BuildFinishedEventArgs
// - BuildMessageEventArgs
// - BuildStartedEventArgs
// - BuildWarningEventArgs
// - ExternalProjectFinishedEventArgs
// - ExternalProjectStartedEventArgs
// - ProjectFinishedEventArgs
// - ProjectStartedEventArgs
// - TargetFinishedEventArgs
// - TargetStartedEventArgs
// - TaskCommandLineEventArgs
// - TaskFinishedEventArgs
// - TaskStartedEventArgs
 
/// <summary>
/// An enumeration of all the types of BuildEventArgs that can be
/// packaged by this logMessagePacket
/// </summary>
internal enum LoggingEventType : int
{
    /// <summary>
    /// An invalid eventId, used during initialization of a <see cref="LoggingEventType"/>.
    /// </summary>
    Invalid = -1,
 
    /// <summary>
    /// Event is a CustomEventArgs.
    /// </summary>
    CustomEvent = 0,
 
    /// <summary>
    /// Event is a <see cref="BuildErrorEventArgs"/>.
    /// </summary>
    BuildErrorEvent = 1,
 
    /// <summary>
    /// Event is a <see cref="BuildFinishedEventArgs"/>.
    /// </summary>
    BuildFinishedEvent = 2,
 
    /// <summary>
    /// Event is a <see cref="BuildMessageEventArgs"/>.
    /// </summary>
    BuildMessageEvent = 3,
 
    /// <summary>
    /// Event is a <see cref="BuildStartedEventArgs"/>.
    /// </summary>
    BuildStartedEvent = 4,
 
    /// <summary>
    /// Event is a <see cref="BuildWarningEventArgs"/>.
    /// </summary>
    BuildWarningEvent = 5,
 
    /// <summary>
    /// Event is a <see cref="ProjectFinishedEventArgs"/>.
    /// </summary>
    ProjectFinishedEvent = 6,
 
    /// <summary>
    /// Event is a <see cref="ProjectStartedEventArgs"/>.
    /// </summary>
    ProjectStartedEvent = 7,
 
    /// <summary>
    /// Event is a <see cref="TargetStartedEventArgs"/>.
    /// </summary>
    TargetStartedEvent = 8,
 
    /// <summary>
    /// Event is a <see cref="TargetFinishedEventArgs"/>.
    /// </summary>
    TargetFinishedEvent = 9,
 
    /// <summary>
    /// Event is a <see cref="TaskStartedEventArgs"/>.
    /// </summary>
    TaskStartedEvent = 10,
 
    /// <summary>
    /// Event is a <see cref="TaskFinishedEventArgs"/>.
    /// </summary>
    TaskFinishedEvent = 11,
 
    /// <summary>
    /// Event is a <see cref="TaskCommandLineEventArgs"/>.
    /// </summary>
    TaskCommandLineEvent = 12,
 
    /// <summary>
    /// Event is <see cref="ExternalProjectStartedEventArgs"/>.
    /// </summary>
    ExternalProjectStartedEvent = 22,
 
    /// <summary>
    /// Event is <see cref="ExternalProjectFinishedEventArgs"/>.
    /// </summary>
    ExternalProjectFinishedEvent = 23,
}
 
/// <summary>
/// A packet to encapsulate a BuildEventArg logging message.
/// Contents:
/// Build Event Type
/// Build Event Args
/// </summary>
internal sealed class LogMessagePacketBase : INodePacket
{
    private const string WriteToStreamMethodName = "WriteToStream";
    private const string CreateFromStreamMethodName = "CreateFromStream";
 
    private const BindingFlags NonPublicInstance = BindingFlags.NonPublic | BindingFlags.Instance;
 
    /// <summary>
    /// The packet version, which is based on the CLR version. Cached because querying Environment.Version each time becomes an allocation bottleneck.
    /// </summary>
    private static readonly int s_defaultPacketVersion = (Environment.Version.Major * 10) + Environment.Version.Minor;
 
    /// <summary>
    /// Dictionary of methods used to read BuildEventArgs.
    /// </summary>
    private static readonly Dictionary<LoggingEventType, MethodInfo> s_readMethodCache = [];
 
    /// <summary>
    /// Dictionary of methods used to write BuildEventArgs.
    /// </summary>
    private static readonly Dictionary<LoggingEventType, MethodInfo> s_writeMethodCache = [];
 
    /// <summary>
    /// The event type of the buildEventArg based on the
    /// LoggingEventType enumeration
    /// </summary>
    private LoggingEventType _eventType = LoggingEventType.Invalid;
 
    /// <summary>
    /// The buildEventArg which is encapsulated by the packet.
    /// </summary>
    private BuildEventArgs? _buildEvent;
 
    /// <summary>
    /// The sink id
    /// </summary>
    private int _sinkId;
 
    /// <summary>
    /// Encapsulates the buildEventArg in this packet.
    /// </summary>
    public LogMessagePacketBase(KeyValuePair<int, BuildEventArgs>? nodeBuildEvent)
    {
        ErrorUtilities.VerifyThrow(nodeBuildEvent != null, "nodeBuildEvent was null");
        _buildEvent = nodeBuildEvent.Value.Value;
        _sinkId = nodeBuildEvent.Value.Key;
        _eventType = GetLoggingEventId(_buildEvent);
    }
 
    /// <summary>
    /// Delegate representing a method on the BuildEventArgs classes used to write to a stream.
    /// </summary>
    private delegate void WriteToStreamMethod(BinaryWriter writer);
 
    /// <summary>
    /// Delegate representing a method on the BuildEventArgs classes used to read from a stream.
    /// </summary>
    private delegate void CreateFromStreamMethod(BinaryReader reader);
 
    /// <summary>
    /// Gets the nodePacket Type, in this case the packet is a Logging Message.
    /// </summary>
    public NodePacketType Type => NodePacketType.LogMessage;
 
    /// <summary>
    /// Reads/writes this packet.
    /// </summary>
    public void Translate(ITranslator translator)
    {
        translator.TranslateEnum(ref _eventType, (int)_eventType);
        translator.Translate(ref _sinkId);
        if (translator.Mode == TranslationDirection.ReadFromStream)
        {
            ReadFromStream(translator);
        }
        else
        {
            WriteToStream(translator);
        }
    }
 
    /// <summary>
    /// Writes the logging packet to the translator.
    /// </summary>
    private void WriteToStream(ITranslator translator)
    {
        ErrorUtilities.VerifyThrow(_eventType != LoggingEventType.CustomEvent, "_eventType should not be a custom event");
 
        MethodInfo? methodInfo = null;
        lock (s_writeMethodCache)
        {
            if (!s_writeMethodCache.TryGetValue(_eventType, out methodInfo))
            {
                Type eventDerivedType = _buildEvent!.GetType();
                methodInfo = eventDerivedType.GetMethod(WriteToStreamMethodName, NonPublicInstance);
                s_writeMethodCache.Add(_eventType, methodInfo);
            }
        }
 
        int packetVersion = s_defaultPacketVersion;
 
        // Make sure the other side knows what sort of serialization is coming
        translator.Translate(ref packetVersion);
 
        // Note: This should always be true in MSBuildTaskHost (and methodInfo should never be null).
        // The .NET 3.5 event args have correct "WriteToStream" methods.
        bool eventCanSerializeItself = methodInfo != null;
 
        translator.Translate(ref eventCanSerializeItself);
 
        if (eventCanSerializeItself)
        {
            WriteToStreamMethod writerMethod = CreateDelegateRobust<WriteToStreamMethod>(_buildEvent!, methodInfo!);
            writerMethod(translator.Writer);
        }
        else
        {
            WriteEventToStream(_buildEvent!, _eventType, translator);
        }
    }
 
    /// <summary>
    /// Reads the logging packet from the translator.
    /// </summary>
    private void ReadFromStream(ITranslator translator)
    {
        ErrorUtilities.VerifyThrow(_eventType != LoggingEventType.CustomEvent, "_eventType should not be a custom event");
 
        _buildEvent = GetBuildEventArgFromId();
 
        // The other side is telling us whether the event knows how to log itself, or whether we're going to have
        // to do it manually
        int packetVersion = s_defaultPacketVersion;
        translator.Translate(ref packetVersion);
 
        bool eventCanSerializeItself = true;
        translator.Translate(ref eventCanSerializeItself);
 
        // Note: This should always be true in MSBuildTaskHost (and methodInfo should never be null).
        // The .NET 3.5 event args have correct "CreateFromStream" methods.
        if (eventCanSerializeItself)
        {
            MethodInfo? methodInfo = null;
            lock (s_readMethodCache)
            {
                if (!s_readMethodCache.TryGetValue(_eventType, out methodInfo))
                {
                    Type eventDerivedType = _buildEvent.GetType();
                    methodInfo = eventDerivedType.GetMethod(CreateFromStreamMethodName, NonPublicInstance);
                    s_readMethodCache.Add(_eventType, methodInfo);
                }
            }
 
            CreateFromStreamMethod readerMethod = CreateDelegateRobust<CreateFromStreamMethod>(_buildEvent, methodInfo);
 
            readerMethod(translator.Reader);
        }
        else
        {
            _buildEvent = ReadEventFromStream(_eventType, translator);
            ErrorUtilities.VerifyThrow(_buildEvent is not null, $"Unsupported LoggingEventType {_eventType}");
        }
 
        _eventType = GetLoggingEventId(_buildEvent);
    }
 
    /// <summary>
    /// Wrapper for Delegate.CreateDelegate with retries.
    /// </summary>
    private static T CreateDelegateRobust<T>(object firstArgument, MethodInfo methodInfo)
        where T : class, Delegate
    {
        Type type = typeof(T);
 
        for (int i = 0; i < 5; i++)
        {
            try
            {
                return (T)Delegate.CreateDelegate(type, firstArgument, methodInfo);
            }
            catch (FileLoadException)
            {
                // Sometimes, in 64-bit processes, the fusion load of Microsoft.Build.Framework.dll
                // spontaneously fails when trying to bind to the delegate.  However, it seems to
                // not repeat on additional tries -- so we'll try again a few times.  However, if
                // it keeps happening, it's probably a real problem, so we want to go ahead and
                // throw to let the user know what's up.
            }
        }
 
        ErrorUtilities.ThrowInternalErrorUnreachable();
        return null;
    }
 
    /// <summary>
    /// Takes in a id (LoggingEventType as an int) and creates the correct specific logging class
    /// </summary>
    private BuildEventArgs GetBuildEventArgFromId() => _eventType switch
    {
        LoggingEventType.BuildErrorEvent => new BuildErrorEventArgs(null, null, null, -1, -1, -1, -1, null, null, null),
        LoggingEventType.BuildFinishedEvent => new BuildFinishedEventArgs(null, null, false),
        LoggingEventType.BuildMessageEvent => new BuildMessageEventArgs(null, null, null, MessageImportance.Normal),
        LoggingEventType.BuildStartedEvent => new BuildStartedEventArgs(null, null),
        LoggingEventType.BuildWarningEvent => new BuildWarningEventArgs(null, null, null, -1, -1, -1, -1, null, null, null),
        LoggingEventType.ProjectFinishedEvent => new ProjectFinishedEventArgs(null, null, null, false),
        LoggingEventType.ProjectStartedEvent => new ProjectStartedEventArgs(null, null, null, null, null, null),
        LoggingEventType.TargetStartedEvent => new TargetStartedEventArgs(null, null, null, null, null),
        LoggingEventType.TargetFinishedEvent => new TargetFinishedEventArgs(null, null, null, null, null, false),
        LoggingEventType.TaskStartedEvent => new TaskStartedEventArgs(null, null, null, null, null),
        LoggingEventType.TaskFinishedEvent => new TaskFinishedEventArgs(null, null, null, null, null, false),
        LoggingEventType.TaskCommandLineEvent => new TaskCommandLineEventArgs(null, null, MessageImportance.Normal),
        LoggingEventType.ExternalProjectStartedEvent => new ExternalProjectStartedEventArgs(null, null, null, null, null),
        LoggingEventType.ExternalProjectFinishedEvent => new ExternalProjectFinishedEventArgs(null, null, null, null, false),
 
        _ => throw new InternalErrorException($"Should not get to the default of GetBuildEventArgFromId ID: {_eventType}")
    };
 
    /// <summary>
    /// Based on the type of the BuildEventArg to be wrapped
    /// generate an Id which identifies which concrete type the
    /// BuildEventArg is.
    /// </summary>
    /// <param name="eventArg">Argument to get the type Id for</param>
    /// <returns>An enumeration entry which represents the type</returns>
    private LoggingEventType GetLoggingEventId(BuildEventArgs eventArg)
    {
        Type eventType = eventArg.GetType();
        if (eventType == typeof(BuildMessageEventArgs))
        {
            return LoggingEventType.BuildMessageEvent;
        }
        else if (eventType == typeof(TaskCommandLineEventArgs))
        {
            return LoggingEventType.TaskCommandLineEvent;
        }
        else if (eventType == typeof(ProjectFinishedEventArgs))
        {
            return LoggingEventType.ProjectFinishedEvent;
        }
        else if (eventType == typeof(ProjectStartedEventArgs))
        {
            return LoggingEventType.ProjectStartedEvent;
        }
        else if (eventType == typeof(ExternalProjectStartedEventArgs))
        {
            return LoggingEventType.ExternalProjectStartedEvent;
        }
        else if (eventType == typeof(ExternalProjectFinishedEventArgs))
        {
            return LoggingEventType.ExternalProjectFinishedEvent;
        }
        else if (eventType == typeof(TargetStartedEventArgs))
        {
            return LoggingEventType.TargetStartedEvent;
        }
        else if (eventType == typeof(TargetFinishedEventArgs))
        {
            return LoggingEventType.TargetFinishedEvent;
        }
        else if (eventType == typeof(TaskStartedEventArgs))
        {
            return LoggingEventType.TaskStartedEvent;
        }
        else if (eventType == typeof(TaskFinishedEventArgs))
        {
            return LoggingEventType.TaskFinishedEvent;
        }
        else if (eventType == typeof(BuildFinishedEventArgs))
        {
            return LoggingEventType.BuildFinishedEvent;
        }
        else if (eventType == typeof(BuildStartedEventArgs))
        {
            return LoggingEventType.BuildStartedEvent;
        }
        else if (eventType == typeof(BuildWarningEventArgs))
        {
            return LoggingEventType.BuildWarningEvent;
        }
        else if (eventType == typeof(BuildErrorEventArgs))
        {
            return LoggingEventType.BuildErrorEvent;
        }
        else
        {
            return LoggingEventType.CustomEvent;
        }
    }
 
    /// <summary>
    /// Given a build event that is presumed to be 2.0 (due to its lack of a "WriteToStream" method) and its
    /// LoggingEventType, serialize that event to the stream.
    /// </summary>
    /// <remarks>
    /// Override to customize serialization per-assembly without relying on compile directives.
    /// </remarks>
    private void WriteEventToStream(BuildEventArgs buildEvent, LoggingEventType eventType, ITranslator translator)
    {
        string? message = buildEvent.Message;
        string? helpKeyword = buildEvent.HelpKeyword;
        string? senderName = buildEvent.SenderName;
 
        translator.Translate(ref message);
        translator.Translate(ref helpKeyword);
        translator.Translate(ref senderName);
 
        // It is essential that you translate in the same order during writing and reading
        switch (eventType)
        {
            case LoggingEventType.BuildMessageEvent:
                WriteBuildMessageEventToStream((BuildMessageEventArgs)buildEvent, translator);
                break;
            case LoggingEventType.TaskCommandLineEvent:
                WriteTaskCommandLineEventToStream((TaskCommandLineEventArgs)buildEvent, translator);
                break;
            case LoggingEventType.BuildErrorEvent:
                WriteBuildErrorEventToStream((BuildErrorEventArgs)buildEvent, translator);
                break;
            case LoggingEventType.BuildWarningEvent:
                WriteBuildWarningEventToStream((BuildWarningEventArgs)buildEvent, translator);
                break;
            default:
                ErrorUtilities.ThrowInternalError($"Not Supported LoggingEventType {eventType}");
                break;
        }
    }
 
    /// <summary>
    /// Write Build Warning Log message into the translator
    /// </summary>
    private void WriteBuildWarningEventToStream(BuildWarningEventArgs buildWarningEventArgs, ITranslator translator)
    {
        string? code = buildWarningEventArgs.Code;
        translator.Translate(ref code);
 
        int columnNumber = buildWarningEventArgs.ColumnNumber;
        translator.Translate(ref columnNumber);
 
        int endColumnNumber = buildWarningEventArgs.EndColumnNumber;
        translator.Translate(ref endColumnNumber);
 
        int endLineNumber = buildWarningEventArgs.EndLineNumber;
        translator.Translate(ref endLineNumber);
 
        string? file = buildWarningEventArgs.File;
        translator.Translate(ref file);
 
        int lineNumber = buildWarningEventArgs.LineNumber;
        translator.Translate(ref lineNumber);
 
        string? subCategory = buildWarningEventArgs.Subcategory;
        translator.Translate(ref subCategory);
    }
 
    /// <summary>
    /// Write a Build Error message into the translator
    /// </summary>
    private void WriteBuildErrorEventToStream(BuildErrorEventArgs buildErrorEventArgs, ITranslator translator)
    {
        string? code = buildErrorEventArgs.Code;
        translator.Translate(ref code);
 
        int columnNumber = buildErrorEventArgs.ColumnNumber;
        translator.Translate(ref columnNumber);
 
        int endColumnNumber = buildErrorEventArgs.EndColumnNumber;
        translator.Translate(ref endColumnNumber);
 
        int endLineNumber = buildErrorEventArgs.EndLineNumber;
        translator.Translate(ref endLineNumber);
 
        string? file = buildErrorEventArgs.File;
        translator.Translate(ref file);
 
        int lineNumber = buildErrorEventArgs.LineNumber;
        translator.Translate(ref lineNumber);
 
        string? subCategory = buildErrorEventArgs.Subcategory;
        translator.Translate(ref subCategory);
    }
 
    /// <summary>
    /// Write Task Command Line log message into the translator.
    /// </summary>
    private void WriteTaskCommandLineEventToStream(TaskCommandLineEventArgs taskCommandLineEventArgs, ITranslator translator)
    {
        MessageImportance importance = taskCommandLineEventArgs.Importance;
        translator.TranslateEnum(ref importance, (int)importance);
 
        string? commandLine = taskCommandLineEventArgs.CommandLine;
        translator.Translate(ref commandLine);
 
        string? taskName = taskCommandLineEventArgs.TaskName;
        translator.Translate(ref taskName);
    }
 
    /// <summary>
    /// Write a "standard" Message Log the translator
    /// </summary>
    private void WriteBuildMessageEventToStream(BuildMessageEventArgs buildMessageEventArgs, ITranslator translator)
    {
        MessageImportance importance = buildMessageEventArgs.Importance;
        translator.TranslateEnum(ref importance, (int)importance);
    }
 
    /// <summary>
    /// Given a build event that is presumed to be 2.0 (due to its lack of a "ReadFromStream" method) and its
    /// LoggingEventType, read that event from the stream.
    /// </summary>
    /// <remarks>
    /// Override to customize serialization per-assembly without relying on compile directives.
    /// </remarks>
    private BuildEventArgs? ReadEventFromStream(LoggingEventType eventType, ITranslator translator)
    {
        string? message = null;
        string? helpKeyword = null;
        string? senderName = null;
 
        translator.Translate(ref message);
        translator.Translate(ref helpKeyword);
        translator.Translate(ref senderName);
 
        return eventType switch
        {
            LoggingEventType.TaskCommandLineEvent => ReadTaskCommandLineEventFromStream(translator),
            LoggingEventType.BuildErrorEvent => ReadTaskBuildErrorEventFromStream(translator, message, helpKeyword, senderName),
            LoggingEventType.BuildMessageEvent => ReadBuildMessageEventFromStream(translator, message, helpKeyword, senderName),
            LoggingEventType.BuildWarningEvent => ReadBuildWarningEventFromStream(translator, message, helpKeyword, senderName),
            _ => null,
        };
    }
 
    /// <summary>
    /// Read and reconstruct a BuildWarningEventArgs from the stream.
    /// </summary>
    private BuildWarningEventArgs ReadBuildWarningEventFromStream(ITranslator translator, string? message, string? helpKeyword, string? senderName)
    {
        string? code = null;
        translator.Translate(ref code);
 
        int columnNumber = -1;
        translator.Translate(ref columnNumber);
 
        int endColumnNumber = -1;
        translator.Translate(ref endColumnNumber);
 
        int endLineNumber = -1;
        translator.Translate(ref endLineNumber);
 
        string? file = null;
        translator.Translate(ref file);
 
        int lineNumber = -1;
        translator.Translate(ref lineNumber);
 
        string? subCategory = null;
        translator.Translate(ref subCategory);
 
        return new BuildWarningEventArgs(
            subCategory,
            code,
            file,
            lineNumber,
            columnNumber,
            endLineNumber,
            endColumnNumber,
            message,
            helpKeyword,
            senderName);
    }
 
    /// <summary>
    /// Read and reconstruct a BuildErrorEventArgs from the stream.
    /// </summary>
    private BuildErrorEventArgs ReadTaskBuildErrorEventFromStream(ITranslator translator, string? message, string? helpKeyword, string? senderName)
    {
        string? code = null;
        translator.Translate(ref code);
 
        int columnNumber = -1;
        translator.Translate(ref columnNumber);
 
        int endColumnNumber = -1;
        translator.Translate(ref endColumnNumber);
 
        int endLineNumber = -1;
        translator.Translate(ref endLineNumber);
 
        string? file = null;
        translator.Translate(ref file);
 
        int lineNumber = -1;
        translator.Translate(ref lineNumber);
 
        string? subCategory = null;
        translator.Translate(ref subCategory);
 
        return new BuildErrorEventArgs(
            subCategory,
            code,
            file,
            lineNumber,
            columnNumber,
            endLineNumber,
            endColumnNumber,
            message,
            helpKeyword,
            senderName);
    }
 
    /// <summary>
    /// Read and reconstruct a TaskCommandLineEventArgs from the stream.
    /// </summary>
    private TaskCommandLineEventArgs ReadTaskCommandLineEventFromStream(ITranslator translator)
    {
        MessageImportance importance = MessageImportance.Normal;
        translator.TranslateEnum(ref importance, (int)importance);
 
        string? commandLine = null;
        translator.Translate(ref commandLine);
 
        string? taskName = null;
        translator.Translate(ref taskName);
 
        return new TaskCommandLineEventArgs(commandLine, taskName, importance);
    }
 
    /// <summary>
    /// Read and reconstruct a BuildMessageEventArgs from the stream.
    /// </summary>
    private BuildMessageEventArgs ReadBuildMessageEventFromStream(ITranslator translator, string? message, string? helpKeyword, string? senderName)
    {
        MessageImportance importance = MessageImportance.Normal;
 
        translator.TranslateEnum(ref importance, (int)importance);
 
        return new BuildMessageEventArgs(message, helpKeyword, senderName, importance);
    }
}