File: Engine\EngineProxy.cs
Web Access
Project: ..\..\..\src\Deprecated\Engine\Microsoft.Build.Engine.csproj (Microsoft.Build.Engine)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
// THE ASSEMBLY BUILT FROM THIS SOURCE FILE HAS BEEN DEPRECATED FOR YEARS. IT IS BUILT ONLY TO PROVIDE
// BACKWARD COMPATIBILITY FOR API USERS WHO HAVE NOT YET MOVED TO UPDATED APIS. PLEASE DO NOT SEND PULL
// REQUESTS THAT CHANGE THIS FILE WITHOUT FIRST CHECKING WITH THE MAINTAINERS THAT THE FIX IS REQUIRED.
 
using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.Build.Framework;
using Microsoft.Build.BuildEngine.Shared;
using System.Runtime.Remoting.Lifetime;
using System.Runtime.Remoting;
 
namespace Microsoft.Build.BuildEngine
{
    /// <summary>
    /// This class serves as a surrogate for the build engine. It limits access to the build engine by implementing only a subset
    /// of all public methods on the Engine class.
    /// </summary>
    internal sealed class EngineProxy : MarshalByRefObject, IBuildEngine3
    {
        #region Data
        // The logging interface
        private EngineLoggingServices loggingServices;
 
        // We've already computed and cached the line/column number of the task node in the project file.
        private bool haveProjectFileLocation = false;
 
        // The line number of the task node in the calling project file.
        private int lineNumber;
 
        // The column number of the task node in the calling project file.
        private int columnNumber;
 
        /// <summary>
        /// The full path to the project that's currently building.
        /// </summary>
        private string parentProjectFullFileName;
 
        /// <summary>
        /// The project file that contains the XML for task. This may be an import file and not the primary
        /// project file
        /// </summary>
        private string projectFileOfTaskNode;
 
        /// <summary>
        /// The token identifing the context of this evaluation
        /// </summary>
        private int handleId;
 
        /// <summary>
        /// Continue on error value per batch exposed via IBuildEngine
        /// </summary>
        private bool continueOnError;
 
        /// <summary>
        /// The module within which this class has been created. Used for all callbacks to
        /// engine.
        /// </summary>
        private TaskExecutionModule parentModule;
 
        /// <summary>
        /// Event contextual information, this tells the loggers where the task events were fired from
        /// </summary>
        private BuildEventContext buildEventContext;
 
        /// <summary>
        /// True if the task connected to this proxy is alive
        /// </summary>
        private bool activeProxy;
 
        /// <summary>
        /// This reference type is used to block access to a single entry methods of the interface
        /// </summary>
        private object callbackMonitor;
 
        /// <summary>
        /// A client sponsor is a class
        /// which will respond to a lease renewal request and will
        /// increase the lease time allowing the object to stay in memory
        /// </summary>
        private ClientSponsor sponsor;
 
        /// <summary>
        /// Will hold cached copy of typeof(BuildErrorEventArgs) used by each call to LogError
        /// </summary>
        private static Type buildErrorEventArgsType = null;
 
        /// <summary>
        /// Will hold cached copy of typeof(BuildErrorEventArgs) used by each call to LogError
        /// </summary>
        private static Type buildWarningEventArgsType = null;
 
        #endregion
 
        /// <summary>
        /// Private default constructor disallows parameterless instantiation.
        /// </summary>
        private EngineProxy()
        {
            // do nothing
        }
 
        /// <summary>
        /// Create an instance of this class to represent the IBuildEngine2 interface to the task
        /// including the event location where the log messages are raised
        /// </summary>
        /// <param name="parentModule">Parent Task Execution Module</param>
        /// <param name="handleId"></param>
        /// <param name="parentProjectFullFileName">the full path to the currently building project</param>
        /// <param name="projectFileOfTaskNode">the path to the actual file (project or targets) where the task invocation is located</param>
        /// <param name="loggingServices"></param>
        /// <param name="buildEventContext">Event Context where events will be seen to be raised from. Task messages will get this as their event context</param>
        internal EngineProxy
        (
            TaskExecutionModule parentModule,
            int handleId,
            string parentProjectFullFileName,
            string projectFileOfTaskNode,
            EngineLoggingServices loggingServices,
            BuildEventContext buildEventContext
        )
        {
            ErrorUtilities.VerifyThrow(parentModule != null, "No parent module.");
            ErrorUtilities.VerifyThrow(loggingServices != null, "No logging services.");
            ErrorUtilities.VerifyThrow(projectFileOfTaskNode != null, "Need project file path string");
 
            this.parentModule = parentModule;
            this.handleId = handleId;
            this.parentProjectFullFileName = parentProjectFullFileName;
            this.projectFileOfTaskNode = projectFileOfTaskNode;
            this.loggingServices = loggingServices;
            this.buildEventContext = buildEventContext;
            this.callbackMonitor = new object();
 
            activeProxy = true;
        }
 
        /// <summary>
        /// Stub implementation -- forwards to engine being proxied.
        /// </summary>
        public void LogErrorEvent(BuildErrorEventArgs e)
        {
            ErrorUtilities.VerifyThrowArgumentNull(e, nameof(e));
            ErrorUtilities.VerifyThrowInvalidOperation(activeProxy, "AttemptingToLogFromInactiveTask");
 
            if (parentModule.IsRunningMultipleNodes && !e.GetType().IsSerializable)
            {
                loggingServices.LogWarning(buildEventContext, new BuildEventFileInfo(string.Empty), "ExpectedEventToBeSerializable", e.GetType().Name);
                return;
            }
 
            string message = GetUpdatedMessage(e.File, e.Message, parentProjectFullFileName);
 
            if (ContinueOnError)
            {
                // Convert the error into a warning.  We do this because the whole point of
                // ContinueOnError is that a project author expects that the task might fail,
                // but wants to ignore the failures.  This implies that we shouldn't be logging
                // errors either, because you should never have a successful build with errors.
                BuildWarningEventArgs warningEvent = new BuildWarningEventArgs
                        (e.Subcategory,
                            e.Code,
                            e.File,
                            e.LineNumber,
                            e.ColumnNumber,
                            e.EndLineNumber,
                            e.EndColumnNumber,
                            message,  // this is the new message from above
                            e.HelpKeyword,
                            e.SenderName);
 
                warningEvent.BuildEventContext = buildEventContext;
                loggingServices.LogWarningEvent(warningEvent);
 
                // Log a message explaining why we converted the previous error into a warning.
                loggingServices.LogComment(buildEventContext, MessageImportance.Normal, "ErrorConvertedIntoWarning");
            }
            else
            {
                if (e.GetType().Equals(BuildErrorEventArgsType))
                {
                    // We'd like to add the project file to the subcategory, but since this property
                    // is read-only on the BuildErrorEventArgs type, this requires creating a new
                    // instance.  However, if some task logged a custom error type, we don't want to
                    // impolitely (as we already do above on ContinueOnError) throw the custom type
                    // data away.
                    e = new BuildErrorEventArgs
                        (
                            e.Subcategory,
                            e.Code,
                            e.File,
                            e.LineNumber,
                            e.ColumnNumber,
                            e.EndLineNumber,
                            e.EndColumnNumber,
                            message,  // this is the new message from above
                            e.HelpKeyword,
                            e.SenderName
                        );
                }
 
                e.BuildEventContext = buildEventContext;
                loggingServices.LogErrorEvent(e);
            }
        }
 
        /// <summary>
        /// Stub implementation -- forwards to engine being proxied.
        /// </summary>
        public void LogWarningEvent(BuildWarningEventArgs e)
        {
            ErrorUtilities.VerifyThrowArgumentNull(e, nameof(e));
            ErrorUtilities.VerifyThrowInvalidOperation(activeProxy, "AttemptingToLogFromInactiveTask");
 
            if (parentModule.IsRunningMultipleNodes && !e.GetType().IsSerializable)
            {
                loggingServices.LogWarning(buildEventContext, new BuildEventFileInfo(string.Empty), "ExpectedEventToBeSerializable", e.GetType().Name);
                return;
            }
 
            if (e.GetType().Equals(BuildWarningEventArgsType))
            {
                // We'd like to add the project file to the message, but since this property
                // is read-only on the BuildWarningEventArgs type, this requires creating a new
                // instance.  However, if some task logged a custom warning type, we don't want
                // to impolitely throw the custom type data away.
 
                string message = GetUpdatedMessage(e.File, e.Message, parentProjectFullFileName);
 
                e = new BuildWarningEventArgs
                (
                    e.Subcategory,
                    e.Code,
                    e.File,
                    e.LineNumber,
                    e.ColumnNumber,
                    e.EndLineNumber,
                    e.EndColumnNumber,
                    message, // this is the new message from above
                    e.HelpKeyword,
                    e.SenderName
                );
            }
 
            e.BuildEventContext = buildEventContext;
            loggingServices.LogWarningEvent(e);
        }
 
        /// <summary>
        ///
        /// </summary>
        /// <param name="file">File field from the original BuildEventArgs</param>
        /// <param name="message">Message field from the original BuildEventArgs</param>
        /// <param name="parentProjectFullFileName">Full file name of the parent (building) project.</param>
        /// <returns></returns>
        private static string GetUpdatedMessage(string file, string message, string parentProjectFullFileName)
        {
#if BUILDING_DF_LKG
            // In the dogfood LKG, add the project path to the end, because we need it to help diagnose builds.
 
            // Don't bother doing anything if we don't have a project path (e.g., we loaded from XML directly)
            if (String.IsNullOrEmpty(parentProjectFullFileName))
            {
                return message;
            }
 
            // Don't bother adding the project file path if it's already in the file part
            if (String.Equals(file, parentProjectFullFileName, StringComparison.OrdinalIgnoreCase))
            {
                return message;
            }
 
            string updatedMessage = String.IsNullOrEmpty(message) ?
                                        String.Format(CultureInfo.InvariantCulture, "[{0}]", parentProjectFullFileName) :
                                        String.Format(CultureInfo.InvariantCulture, "{0} [{1}]", message, parentProjectFullFileName);
 
            return updatedMessage;
#else
            // In the regular product, don't modify the message. We want to do this properly, with a field on the event args, in a future version.
            return message;
#endif
        }
 
        /// <summary>
        /// Stub implementation -- forwards to engine being proxied.
        /// </summary>
        public void LogMessageEvent(BuildMessageEventArgs e)
        {
            ErrorUtilities.VerifyThrowArgumentNull(e, nameof(e));
            ErrorUtilities.VerifyThrowInvalidOperation(activeProxy, "AttemptingToLogFromInactiveTask");
 
            if (parentModule.IsRunningMultipleNodes && !e.GetType().IsSerializable)
            {
                loggingServices.LogWarning(buildEventContext, new BuildEventFileInfo(string.Empty), "ExpectedEventToBeSerializable", e.GetType().Name);
                return;
            }
            e.BuildEventContext = buildEventContext;
            loggingServices.LogMessageEvent(e);
        }
 
        /// <summary>
        /// Stub implementation -- forwards to engine being proxied.
        /// </summary>
        public void LogCustomEvent(CustomBuildEventArgs e)
        {
            ErrorUtilities.VerifyThrowArgumentNull(e, nameof(e));
            ErrorUtilities.VerifyThrowInvalidOperation(activeProxy, "AttemptingToLogFromInactiveTask");
 
            if (parentModule.IsRunningMultipleNodes && !e.GetType().IsSerializable)
            {
                loggingServices.LogWarning(buildEventContext, new BuildEventFileInfo(string.Empty), "ExpectedEventToBeSerializable", e.GetType().Name);
                return;
            }
 
            e.BuildEventContext = buildEventContext;
            loggingServices.LogCustomEvent(e);
        }
 
        /// <summary>
        /// Returns true if the ContinueOnError flag was set to true for this particular task
        /// in the project file.
        /// </summary>
        public bool ContinueOnError
        {
            get
            {
                ErrorUtilities.VerifyThrowInvalidOperation(activeProxy, "AttemptingToLogFromInactiveTask");
 
                return this.continueOnError;
            }
        }
 
        /// <summary>
        /// Called by the task engine to update the value for each batch
        /// </summary>
        /// <param name="shouldContinueOnError"></param>
        internal void UpdateContinueOnError(bool shouldContinueOnError)
        {
            this.continueOnError = shouldContinueOnError;
        }
 
        /// <summary>
        /// Retrieves the line number of the task node withing the project file that called it.
        /// </summary>
        /// <remarks>This method is expensive in terms of perf.  Do not call it in mainline scenarios.</remarks>
        /// <owner>RGoel</owner>
        public int LineNumberOfTaskNode
        {
            get
            {
                ErrorUtilities.VerifyThrowInvalidOperation(activeProxy, "AttemptingToLogFromInactiveTask");
 
                ComputeProjectFileLocationOfTaskNode();
                return this.lineNumber;
            }
        }
 
        /// <summary>
        /// Retrieves the line number of the task node withing the project file that called it.
        /// </summary>
        /// <remarks>This method is expensive in terms of perf.  Do not call it in mainline scenarios.</remarks>
        /// <owner>RGoel</owner>
        public int ColumnNumberOfTaskNode
        {
            get
            {
                ErrorUtilities.VerifyThrowInvalidOperation(activeProxy, "AttemptingToLogFromInactiveTask");
 
                ComputeProjectFileLocationOfTaskNode();
                return this.columnNumber;
            }
        }
 
        /// <summary>
        /// Returns the full path to the project file that contained the call to this task.
        /// </summary>
        public string ProjectFileOfTaskNode
        {
            get
            {
                ErrorUtilities.VerifyThrowInvalidOperation(activeProxy, "AttemptingToLogFromInactiveTask");
 
                return projectFileOfTaskNode;
            }
        }
 
        /// <summary>
        /// Computes the line/column number of the task node in the project file (or .TARGETS file)
        /// that called it.
        /// </summary>
        private void ComputeProjectFileLocationOfTaskNode()
        {
            if (!haveProjectFileLocation)
            {
                parentModule.GetLineColumnOfXmlNode(handleId, out this.lineNumber, out this.columnNumber);
                haveProjectFileLocation = true;
            }
        }
 
        /// <summary>
        /// Stub implementation -- forwards to engine being proxied.
        /// </summary>
        /// <param name="projectFileName"></param>
        /// <param name="targetNames"></param>
        /// <param name="globalProperties"></param>
        /// <param name="targetOutputs"></param>
        /// <returns>result of call to engine</returns>
        public bool BuildProjectFile
            (
            string projectFileName,
            string[] targetNames,
            IDictionary globalProperties,
            IDictionary targetOutputs
            )
        {
            return BuildProjectFile(projectFileName, targetNames, globalProperties, targetOutputs, null);
        }
 
        /// <summary>
        /// Stub implementation -- forwards to engine being proxied.
        /// </summary>
        /// <param name="projectFileName"></param>
        /// <param name="targetNames"></param>
        /// <param name="globalProperties"></param>
        /// <param name="targetOutputs"></param>
        /// <param name="toolsVersion">Tools Version to override on the project. May be null</param>
        /// <returns>result of call to engine</returns>
        public bool BuildProjectFile
            (
            string projectFileName,
            string[] targetNames,
            IDictionary globalProperties,
            IDictionary targetOutputs,
            string toolsVersion
            )
        {
            lock (callbackMonitor)
            {
                ErrorUtilities.VerifyThrowInvalidOperation(activeProxy, "AttemptingToLogFromInactiveTask");
 
                // Wrap the project name into an array
                string[] projectFileNames = new string[1];
                projectFileNames[0] = projectFileName;
                string[] toolsVersions = new string[1];
                toolsVersions[0] = toolsVersion;
                IDictionary[] targetOutputsPerProject = new IDictionary[1];
                targetOutputsPerProject[0] = targetOutputs;
                IDictionary[] globalPropertiesPerProject = new IDictionary[1];
                globalPropertiesPerProject[0] = globalProperties;
                return parentModule.BuildProjectFile(handleId, projectFileNames, targetNames, globalPropertiesPerProject, targetOutputsPerProject,
                                                     loggingServices, toolsVersions, false, false, buildEventContext);
            }
        }
 
        /// <summary>
        /// Stub implementation -- forwards to engine being proxied.
        /// </summary>
        /// <param name="projectFileNames"></param>
        /// <param name="targetNames"></param>
        /// <param name="globalProperties"></param>
        /// <param name="targetOutputsPerProject"></param>
        /// <param name="toolsVersions">Tools Version to overrides per project. May contain null values</param>
        /// <param name="unloadProjectsOnCompletion"></param>
        /// <returns>result of call to engine</returns>
        public bool BuildProjectFilesInParallel
            (
            string[] projectFileNames,
            string[] targetNames,
            IDictionary[] globalProperties,
            IDictionary[] targetOutputsPerProject,
            string[] toolsVersions,
            bool useResultsCache,
            bool unloadProjectsOnCompletion
            )
        {
            lock (callbackMonitor)
            {
                return parentModule.BuildProjectFile(handleId, projectFileNames, targetNames, globalProperties,
                                                         targetOutputsPerProject, loggingServices,
                                                         toolsVersions, useResultsCache, unloadProjectsOnCompletion, buildEventContext);
            }
        }
 
        /// <summary>
        /// Not implemented for the proxy
        /// </summary>
        public void Yield()
        {
        }
 
        /// <summary>
        /// Not implemented for the proxy
        /// </summary>
        public void Reacquire()
        {
        }
 
        /// <summary>
        /// Stub implementation -- forwards to engine being proxied.
        /// </summary>
        /// <remarks>
        /// 1) it is acceptable to pass null for both <c>targetNames</c> and <c>targetOutputs</c>
        /// 2) if no targets are specified, the default targets are built
        ///
        /// </remarks>
        /// <param name="projectFileNames">The project to build.</param>
        /// <param name="targetNames">The targets in the project to build (can be null).</param>
        /// <param name="globalProperties">An array of hashtables of additional global properties to apply
        ///     to the child project (array entries can be null).
        ///     The key and value in the hashtable should both be strings.</param>
        /// <param name="removeGlobalProperties">A list of global properties which should be removed.</param>
        /// <param name="toolsVersions">A tools version recognized by the Engine that will be used during this build (can be null).</param>
        /// <param name="returnTargetOutputs">Should the target outputs be returned in the BuildEngineResults</param>
        /// <returns>Returns a structure containing the success or failures of the build and the target outputs by project.</returns>
        public BuildEngineResult BuildProjectFilesInParallel
            (
            string[] projectFileNames,
            string[] targetNames,
            IDictionary[] globalProperties,
            IList<string>[] removeGlobalProperties,
            string[] toolsVersions,
            bool returnTargetOutputs
            )
        {
            lock (callbackMonitor)
            {
                ErrorUtilities.VerifyThrowInvalidOperation(activeProxy, "AttemptingToLogFromInactiveTask");
 
                ErrorUtilities.VerifyThrowArgumentNull(projectFileNames, nameof(projectFileNames));
                ErrorUtilities.VerifyThrowArgumentNull(globalProperties, "globalPropertiesPerProject");
 
                Dictionary<string, ITaskItem[]>[] targetOutputsPerProject = null;
 
                if (returnTargetOutputs)
                {
                    targetOutputsPerProject = new Dictionary<string, ITaskItem[]>[projectFileNames.Length];
                    for (int i = 0; i < targetOutputsPerProject.Length; i++)
                    {
                        targetOutputsPerProject[i] = new Dictionary<string, ITaskItem[]>(StringComparer.OrdinalIgnoreCase);
                    }
                }
 
                bool result = parentModule.BuildProjectFile(handleId, projectFileNames, targetNames, globalProperties,
                                                     targetOutputsPerProject, loggingServices,
                                                     toolsVersions, false, false, buildEventContext);
 
                return new BuildEngineResult(result, new List<IDictionary<string, ITaskItem[]>>(targetOutputsPerProject));
            }
        }
 
        /// <summary>
        /// InitializeLifetimeService is called when the remote object is activated.
        /// This method will determine how long the lifetime for the object will be.
        /// </summary>
        public override object InitializeLifetimeService()
        {
            // Each MarshalByRef object has a reference to the service which
            // controls how long the remote object will stay around
            ILease lease = (ILease)base.InitializeLifetimeService();
 
            // Set how long a lease should be initially. Once a lease expires
            // the remote object will be disconnected and it will be marked as being availiable
            // for garbage collection
            int initialLeaseTime = 1;
 
            string initialLeaseTimeFromEnvironment = Environment.GetEnvironmentVariable("MSBUILDENGINEPROXYINITIALLEASETIME");
 
            if (!String.IsNullOrEmpty(initialLeaseTimeFromEnvironment))
            {
                int leaseTimeFromEnvironment;
                if (int.TryParse(initialLeaseTimeFromEnvironment, out leaseTimeFromEnvironment) && leaseTimeFromEnvironment > 0)
                {
                    initialLeaseTime = leaseTimeFromEnvironment;
                }
            }
 
            lease.InitialLeaseTime = TimeSpan.FromMinutes(initialLeaseTime);
 
            // Make a new client sponsor. A client sponsor is a class
            // which will respond to a lease renewal request and will
            // increase the lease time allowing the object to stay in memory
            sponsor = new ClientSponsor();
 
            // When a new lease is requested lets make it last 1 minutes longer.
            int leaseExtensionTime = 1;
 
            string leaseExtensionTimeFromEnvironment = Environment.GetEnvironmentVariable("MSBUILDENGINEPROXYLEASEEXTENSIONTIME");
            if (!String.IsNullOrEmpty(leaseExtensionTimeFromEnvironment))
            {
                int leaseExtensionFromEnvironment;
                if (int.TryParse(leaseExtensionTimeFromEnvironment, out leaseExtensionFromEnvironment) && leaseExtensionFromEnvironment > 0)
                {
                    leaseExtensionTime = leaseExtensionFromEnvironment;
                }
            }
 
            sponsor.RenewalTime = TimeSpan.FromMinutes(leaseExtensionTime);
 
            // Register the sponsor which will increase lease timeouts when the lease expires
            lease.Register(sponsor);
 
            return lease;
        }
 
        /// <summary>
        /// Indicates to the EngineProxy that it is no longer needed.
        /// Called by TaskEngine when the task using the EngineProxy is done.
        /// </summary>
        internal void MarkAsInActive()
        {
            activeProxy = false;
 
            // Since the task has a pointer to this class it may store it in a static field. Null out
            // internal data so the leak of this object doesn't lead to a major memory leak.
            loggingServices = null;
            parentModule = null;
            buildEventContext = null;
 
            // Clear out the sponsor (who is responsible for keeping the EngineProxy remoting lease alive until the task is done)
            // this will be null if the engineproxy was never sent accross an appdomain boundry.
            if (sponsor != null)
            {
                ILease lease = (ILease)RemotingServices.GetLifetimeService(this);
                lease?.Unregister(sponsor);
 
                sponsor.Close();
                sponsor = null;
            }
        }
 
        #region Properties
        /// <summary>
        /// Provide a way to change the BuildEventContext of the engine proxy. This is important in batching where each batch will need its own buildEventContext.
        /// </summary>
        internal BuildEventContext BuildEventContext
        {
            get { return buildEventContext; }
            set { buildEventContext = value; }
        }
 
        /// <summary>
        /// This property allows a task to query whether or not the system is running in single process mode or multi process mode.
        /// Single process mode is where the engine is initialized with the number of cpus = 1 and the engine is not a child engine.
        /// The engine is in multi process mode when the engine is initialized with a number of cpus > 1 or the engine is a child engine.
        /// </summary>
        public bool IsRunningMultipleNodes
        {
            get { return parentModule.IsRunningMultipleNodes; }
        }
 
        /// <summary>
        /// Cached copy of typeof(BuildErrorEventArgs) used during each call to LogError
        /// </summary>
        private static Type BuildErrorEventArgsType
        {
            get
            {
                if (buildErrorEventArgsType == null)
                {
                    buildErrorEventArgsType = typeof(BuildErrorEventArgs);
                }
                return buildErrorEventArgsType;
            }
        }
 
        /// <summary>
        /// Cached copy of typeof(BuildWarningEventArgs) used during each call to LogWarning
        /// </summary>
        private static Type BuildWarningEventArgsType
        {
            get
            {
                if (buildWarningEventArgsType == null)
                {
                    buildWarningEventArgsType = typeof(BuildWarningEventArgs);
                }
                return buildWarningEventArgsType;
            }
        }
 
        #endregion
    }
}