|
// 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.Reflection;
#if FEATURE_APPDOMAIN
using System.Threading.Tasks;
#endif
using Microsoft.Build.BackEnd.Components.RequestBuilder;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using System.IO;
using ElementLocation = Microsoft.Build.Construction.ElementLocation;
using TargetLoggingContext = Microsoft.Build.BackEnd.Logging.TargetLoggingContext;
using TaskLoggingContext = Microsoft.Build.BackEnd.Logging.TaskLoggingContext;
#nullable disable
namespace Microsoft.Build.BackEnd
{
/// <summary>
/// The assembly task factory is used to wrap and construct tasks which are from .net assemblies.
/// </summary>
internal class AssemblyTaskFactory : ITaskFactory2
{
#region Data
/// <summary>
/// The type loader to load types which derrive from ITask or ITask2
/// </summary>
private readonly TypeLoader _typeLoader = new TypeLoader(TaskLoader.IsTaskClass);
/// <summary>
/// Name of the task wrapped by the task factory
/// </summary>
private string _taskName = null;
/// <summary>
/// The loaded type (type, assembly name / file) of the task wrapped by the factory
/// </summary>
private LoadedType _loadedType;
#if FEATURE_APPDOMAIN
/// <summary>
/// A cache of tasks and the AppDomains they are loaded in.
/// </summary>
private Dictionary<ITask, AppDomain> _tasksAndAppDomains = new Dictionary<ITask, AppDomain>();
#endif
/// <summary>
/// the set of parameters owned by this particular task host
/// </summary>
private IDictionary<string, string> _factoryIdentityParameters;
/// <summary>
/// Tracks whether, in the UsingTask invocation, we were specifically asked to use
/// the task host. If so, that overrides all other concerns, and we will launch
/// the task host even if the requested runtime / architecture match that of the
/// current MSBuild process.
/// </summary>
private bool _taskHostFactoryExplicitlyRequested;
/// <summary>
/// Need to store away the taskloggingcontext used by CreateTaskInstance so that
/// TaskLoader will be able to call back with errors.
/// </summary>
private TaskLoggingContext _taskLoggingContext;
#endregion
/// <summary>
/// Initializes a new instance of the <see cref="AssemblyTaskFactory"/> class.
/// </summary>
internal AssemblyTaskFactory()
{
}
#region Public Members
/// <summary>
/// Name of the factory. In this case the name is the assembly name which is wrapped by the factory
/// </summary>
public string FactoryName
{
get
{
return _loadedType.Assembly.AssemblyLocation;
}
}
/// <summary>
/// Gets the type of task this factory creates.
/// </summary>
public Type TaskType
{
get { return _loadedType.Type; }
}
/// <summary>
/// Initializes this factory for instantiating tasks with a particular inline task block.
/// </summary>
/// <param name="taskName">Name of the task.</param>
/// <param name="parameterGroup">The parameter group.</param>
/// <param name="taskBody">The task body.</param>
/// <param name="taskFactoryLoggingHost">The task factory logging host.</param>
/// <returns>A value indicating whether initialization was successful.</returns>
/// <remarks>
/// <para>MSBuild engine will call this to initialize the factory. This should initialize the factory enough so that the factory can be asked
/// whether or not task names can be created by the factory.</para>
/// <para>
/// The taskFactoryLoggingHost will log messages in the context of the target where the task is first used.
/// </para>
/// </remarks>
public bool Initialize(string taskName, IDictionary<string, TaskPropertyInfo> parameterGroup, string taskBody, IBuildEngine taskFactoryLoggingHost)
{
ErrorUtilities.ThrowInternalError("Use internal call to properly initialize the assembly task factory");
return false;
}
/// <summary>
/// Initializes this factory for instantiating tasks with a particular inline task block and a set of UsingTask parameters.
/// </summary>
/// <param name="taskName">Name of the task.</param>
/// <param name="factoryIdentityParameters">Special parameters that the task factory can use to modify how it executes tasks,
/// such as Runtime and Architecture. The key is the name of the parameter and the value is the parameter's value. This
/// is the set of parameters that was set on the UsingTask using e.g. the UsingTask Runtime and Architecture parameters.</param>
/// <param name="parameterGroup">The parameter group.</param>
/// <param name="taskBody">The task body.</param>
/// <param name="taskFactoryLoggingHost">The task factory logging host.</param>
/// <returns>A value indicating whether initialization was successful.</returns>
/// <remarks>
/// <para>MSBuild engine will call this to initialize the factory. This should initialize the factory enough so that the
/// factory can be asked whether or not task names can be created by the factory. If a task factory implements ITaskFactory2,
/// this Initialize method will be called in place of ITaskFactory.Initialize.</para>
/// <para>
/// The taskFactoryLoggingHost will log messages in the context of the target where the task is first used.
/// </para>
/// </remarks>
public bool Initialize(string taskName, IDictionary<string, string> factoryIdentityParameters, IDictionary<string, TaskPropertyInfo> parameterGroup, string taskBody, IBuildEngine taskFactoryLoggingHost)
{
ErrorUtilities.ThrowInternalError("Use internal call to properly initialize the assembly task factory");
return false;
}
/// <summary>
/// Get a list of parameters for the task.
/// </summary>
public TaskPropertyInfo[] GetTaskParameters()
{
return _loadedType.Properties;
}
/// <summary>
/// Create an instance of the task to be used.
/// The task factory logging host will log messages in the context of the task.
/// </summary>
/// <param name="taskFactoryLoggingHost">
/// The task factory logging host will log messages in the context of the task.
/// </param>
/// <returns>
/// The generated task, or <c>null</c> if the task failed to be created.
/// </returns>
public ITask CreateTask(IBuildEngine taskFactoryLoggingHost)
{
ErrorUtilities.ThrowInternalError("Use internal call to properly create a task instance from the assembly task factory");
return null;
}
/// <summary>
/// Create an instance of the task to be used.
/// </summary>
/// <param name="taskFactoryLoggingHost">
/// The task factory logging host will log messages in the context of the task.
/// </param>
/// <param name="taskIdentityParameters">
/// Special parameters that the task factory can use to modify how it executes tasks, such as Runtime and Architecture.
/// The key is the name of the parameter and the value is the parameter's value. This is the set of parameters that was
/// set to the task invocation itself, via e.g. the special MSBuildRuntime and MSBuildArchitecture parameters.
/// </param>
/// <remarks>
/// If a task factory implements ITaskFactory2, MSBuild will call this method instead of ITaskFactory.CreateTask.
/// </remarks>
/// <returns>
/// The generated task, or <c>null</c> if the task failed to be created.
/// </returns>
public ITask CreateTask(IBuildEngine taskFactoryLoggingHost, IDictionary<string, string> taskIdentityParameters)
{
ErrorUtilities.ThrowInternalError("Use internal call to properly create a task instance from the assembly task factory");
return null;
}
/// <summary>
/// Cleans up any context or state that may have been built up for a given task.
/// </summary>
/// <param name="task">The task to clean up.</param>
/// <remarks>
/// For many factories, this method is a no-op. But some factories may have built up
/// an AppDomain as part of an individual task instance, and this is their opportunity
/// to shutdown the AppDomain.
/// </remarks>
public void CleanupTask(ITask task)
{
ErrorUtilities.VerifyThrowArgumentNull(task);
#if FEATURE_APPDOMAIN
AppDomain appDomain;
if (_tasksAndAppDomains.TryGetValue(task, out appDomain))
{
_tasksAndAppDomains.Remove(task);
if (appDomain != null)
{
AssemblyLoadsTracker.StopTracking(appDomain);
// Unload the AppDomain asynchronously to avoid a deadlock that can happen because
// AppDomain.Unload blocks for the process's one Finalizer thread to finalize all
// objects. Some objects are RCWs for STA COM objects and as such would need the
// VS main thread to be processing messages in order to finalize. But if the main thread
// is blocked in a non-pumping wait waiting for this build request to complete, we would
// deadlock. By unloading asynchronously, the AppDomain unload can block till the main
// thread is available, even if it isn't available until after this MSBuild Task has
// finished executing.
Task.Run(() => AppDomain.Unload(appDomain));
}
}
#endif
TaskHostTask taskAsTaskHostTask = task as TaskHostTask;
if (taskAsTaskHostTask != null)
{
taskAsTaskHostTask.Cleanup();
}
else
{
#if FEATURE_APPDOMAIN
// It's really not necessary to do it for TaskHostTasks
TaskLoader.RemoveAssemblyResolver();
#endif
}
}
#endregion
#region Internal Members
/// <summary>
/// Initialize the factory from the task registry
/// </summary>
internal LoadedType InitializeFactory(
AssemblyLoadInfo loadInfo,
string taskName,
IDictionary<string, TaskPropertyInfo> taskParameters,
string taskElementContents,
IDictionary<string, string> taskFactoryIdentityParameters,
bool taskHostFactoryExplicitlyRequested,
TargetLoggingContext targetLoggingContext,
ElementLocation elementLocation,
string taskProjectFile)
{
ErrorUtilities.VerifyThrowArgumentNull(loadInfo);
VerifyThrowIdentityParametersValid(taskFactoryIdentityParameters, elementLocation, taskName, "Runtime", "Architecture");
if (taskFactoryIdentityParameters != null)
{
_factoryIdentityParameters = new Dictionary<string, string>(taskFactoryIdentityParameters, StringComparer.OrdinalIgnoreCase);
}
_taskHostFactoryExplicitlyRequested = taskHostFactoryExplicitlyRequested;
try
{
ErrorUtilities.VerifyThrowArgumentLength(taskName);
_taskName = taskName;
string assemblyName = loadInfo.AssemblyName ?? Path.GetFileName(loadInfo.AssemblyFile);
using var assemblyLoadsTracker = AssemblyLoadsTracker.StartTracking(targetLoggingContext, AssemblyLoadingContext.TaskRun, assemblyName);
_loadedType = _typeLoader.Load(taskName, loadInfo, _taskHostFactoryExplicitlyRequested);
ProjectErrorUtilities.VerifyThrowInvalidProject(_loadedType != null, elementLocation, "TaskLoadFailure", taskName, loadInfo.AssemblyLocation, String.Empty);
}
catch (TargetInvocationException e)
{
// Exception thrown by the called code itself
// Log the stack, so the task vendor can fix their code
ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "TaskLoadFailure", taskName, loadInfo.AssemblyLocation, Environment.NewLine + e.InnerException.ToString());
}
catch (ReflectionTypeLoadException e)
{
// ReflectionTypeLoadException.LoaderExceptions may contain nulls
foreach (Exception exception in e.LoaderExceptions)
{
if (exception != null)
{
targetLoggingContext.LogError(new BuildEventFileInfo(taskProjectFile), "TaskLoadFailure", taskName, loadInfo.AssemblyLocation, exception.Message);
}
}
ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "TaskLoadFailure", taskName, loadInfo.AssemblyLocation, e.Message);
}
catch (ArgumentNullException e)
{
// taskName may be null
ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "TaskLoadFailure", taskName, loadInfo.AssemblyLocation, e.Message);
}
catch (Exception e) when (!ExceptionHandling.NotExpectedReflectionException(e))
{
ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "TaskLoadFailure", taskName, loadInfo.AssemblyLocation, e.Message);
}
return _loadedType;
}
/// <summary>
/// Create an instance of the wrapped ITask for a batch run of the task.
/// </summary>
internal ITask CreateTaskInstance(ElementLocation taskLocation, TaskLoggingContext taskLoggingContext, IBuildComponentHost buildComponentHost, IDictionary<string, string> taskIdentityParameters,
#if FEATURE_APPDOMAIN
AppDomainSetup appDomainSetup,
#endif
bool isOutOfProc)
{
bool useTaskFactory = false;
IDictionary<string, string> mergedParameters = null;
_taskLoggingContext = taskLoggingContext;
// Optimization for the common (vanilla AssemblyTaskFactory) case -- only calculate
// the task factory parameters if we have any to calculate; otherwise even if we
// still launch the task factory, it will be with parameters corresponding to the
// current process.
if ((_factoryIdentityParameters?.Count > 0) || (taskIdentityParameters?.Count > 0))
{
VerifyThrowIdentityParametersValid(taskIdentityParameters, taskLocation, _taskName, "MSBuildRuntime", "MSBuildArchitecture");
mergedParameters = MergeTaskFactoryParameterSets(_factoryIdentityParameters, taskIdentityParameters);
useTaskFactory = _taskHostFactoryExplicitlyRequested || !TaskHostParametersMatchCurrentProcess(mergedParameters);
}
else
{
// if we don't have any task host parameters specified on either the using task or the
// task invocation, then we will run in-proc UNLESS "TaskHostFactory" is explicitly specified
// as the task factory.
useTaskFactory = _taskHostFactoryExplicitlyRequested;
}
if (useTaskFactory)
{
ErrorUtilities.VerifyThrowInternalNull(buildComponentHost);
mergedParameters ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (!mergedParameters.ContainsKey(XMakeAttributes.runtime))
{
mergedParameters[XMakeAttributes.runtime] = XMakeAttributes.GetCurrentMSBuildRuntime();
}
if (!mergedParameters.ContainsKey(XMakeAttributes.architecture))
{
mergedParameters[XMakeAttributes.architecture] = XMakeAttributes.GetCurrentMSBuildArchitecture();
}
#pragma warning disable SA1111, SA1009 // Closing parenthesis should be on line of last parameter
TaskHostTask task = new TaskHostTask(
taskLocation,
taskLoggingContext,
buildComponentHost,
mergedParameters,
_loadedType
#if FEATURE_APPDOMAIN
, appDomainSetup
#endif
);
#pragma warning restore SA1111, SA1009 // Closing parenthesis should be on line of last parameter
return task;
}
else
{
#if FEATURE_APPDOMAIN
AppDomain taskAppDomain = null;
#endif
#pragma warning disable SA1111, SA1009 // Closing parenthesis should be on line of last parameter
ITask taskInstance = TaskLoader.CreateTask(
_loadedType,
_taskName,
taskLocation.File,
taskLocation.Line,
taskLocation.Column,
new TaskLoader.LogError(ErrorLoggingDelegate),
#if FEATURE_APPDOMAIN
appDomainSetup,
appDomain => AssemblyLoadsTracker.StartTracking(taskLoggingContext, AssemblyLoadingContext.TaskRun, _loadedType.Type, appDomain),
#endif
isOutOfProc
#if FEATURE_APPDOMAIN
, out taskAppDomain
#endif
);
#pragma warning restore SA1111, SA1009 // Closing parenthesis should be on line of last parameter
#if FEATURE_APPDOMAIN
if (taskAppDomain != null && taskInstance != null)
{
_tasksAndAppDomains[taskInstance] = taskAppDomain;
}
else if (taskAppDomain != null)
{
AssemblyLoadsTracker.StopTracking(taskAppDomain);
}
#endif
return taskInstance;
}
}
/// <summary>
/// Is the given task name able to be created by the task factory. In the case of an assembly task factory
/// this question is answered by checking the assembly wrapped by the task factory to see if it exists.
/// </summary>
internal bool TaskNameCreatableByFactory(string taskName, IDictionary<string, string> taskIdentityParameters, string taskProjectFile, TargetLoggingContext targetLoggingContext, ElementLocation elementLocation)
{
if (!TaskIdentityParametersMatchFactory(_factoryIdentityParameters, taskIdentityParameters))
{
return false;
}
try
{
ErrorUtilities.VerifyThrowArgumentLength(taskName, "TaskName");
// Parameters match, so now we check to see if the task exists.
return _typeLoader.ReflectionOnlyLoad(taskName, _loadedType.Assembly) != null;
}
catch (TargetInvocationException e)
{
// Exception thrown by the called code itself
// Log the stack, so the task vendor can fix their code
ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "TaskLoadFailure", taskName, _loadedType.Assembly.AssemblyLocation, Environment.NewLine + e.InnerException.ToString());
}
catch (ReflectionTypeLoadException e)
{
// ReflectionTypeLoadException.LoaderExceptions may contain nulls
foreach (Exception exception in e.LoaderExceptions)
{
if (exception != null)
{
targetLoggingContext.LogError(new BuildEventFileInfo(taskProjectFile), "TaskLoadFailure", taskName, _loadedType.Assembly.AssemblyLocation, exception.Message);
}
}
ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "TaskLoadFailure", taskName, _loadedType.Assembly.AssemblyLocation, e.Message);
}
catch (ArgumentNullException e)
{
// taskName may be null
ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "TaskLoadFailure", taskName, _loadedType.Assembly.AssemblyLocation, e.Message);
}
catch (Exception e) when (!ExceptionHandling.NotExpectedReflectionException(e))
{
ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "TaskLoadFailure", taskName, _loadedType.Assembly.AssemblyLocation, e.Message);
}
return false;
}
#endregion
#region Private members
/// <summary>
/// Validates the given set of parameters, logging the appropriate errors as necessary.
/// </summary>
private static void VerifyThrowIdentityParametersValid(IDictionary<string, string> identityParameters, IElementLocation errorLocation, string taskName, string runtimeName, string architectureName)
{
// validate the task factory parameters
if (identityParameters?.Count > 0)
{
string runtime;
if (identityParameters.TryGetValue(XMakeAttributes.runtime, out runtime))
{
if (!XMakeAttributes.IsValidMSBuildRuntimeValue(runtime))
{
ProjectErrorUtilities.ThrowInvalidProject(
errorLocation,
"TaskLoadFailureInvalidTaskHostFactoryParameter",
taskName,
runtime,
runtimeName,
XMakeAttributes.MSBuildRuntimeValues.clr2,
XMakeAttributes.MSBuildRuntimeValues.clr4,
XMakeAttributes.MSBuildRuntimeValues.currentRuntime,
XMakeAttributes.MSBuildRuntimeValues.any);
}
}
string architecture;
if (identityParameters.TryGetValue(XMakeAttributes.architecture, out architecture))
{
if (!XMakeAttributes.IsValidMSBuildArchitectureValue(architecture))
{
ProjectErrorUtilities.ThrowInvalidProject(
errorLocation,
"TaskLoadFailureInvalidTaskHostFactoryParameter",
taskName,
architecture,
architectureName,
XMakeAttributes.MSBuildArchitectureValues.x86,
XMakeAttributes.MSBuildArchitectureValues.x64,
XMakeAttributes.MSBuildArchitectureValues.currentArchitecture,
XMakeAttributes.MSBuildArchitectureValues.any);
}
}
}
}
/// <summary>
/// Given the set of parameters that are set to the factory, and the set of parameters coming from the task invocation that we're searching for
/// a matching record to, determine whether the parameters match this record.
/// </summary>
private static bool TaskIdentityParametersMatchFactory(IDictionary<string, string> factoryIdentityParameters, IDictionary<string, string> taskIdentityParameters)
{
if (taskIdentityParameters == null || taskIdentityParameters.Count == 0 || factoryIdentityParameters == null || factoryIdentityParameters.Count == 0)
{
// either the task or the using task doesn't care about anything, in which case we match by default.
return true;
}
string taskArchitecture;
string taskRuntime;
taskIdentityParameters.TryGetValue(XMakeAttributes.runtime, out taskRuntime);
string usingTaskRuntime;
factoryIdentityParameters.TryGetValue(XMakeAttributes.runtime, out usingTaskRuntime);
if (XMakeAttributes.RuntimeValuesMatch(taskRuntime, usingTaskRuntime))
{
taskIdentityParameters.TryGetValue(XMakeAttributes.architecture, out taskArchitecture);
string usingTaskArchitecture;
factoryIdentityParameters.TryGetValue(XMakeAttributes.architecture, out usingTaskArchitecture);
if (XMakeAttributes.ArchitectureValuesMatch(taskArchitecture, usingTaskArchitecture))
{
// both match
return true;
}
}
// one or more does not match, so we don't match.
return false;
}
/// <summary>
/// Given a set of task parameters from the UsingTask and from the task invocation, generate a dictionary that combines the two, or throws if the merge
/// is impossible (we shouldn't ever get to this point if it is ...)
/// </summary>
private static IDictionary<string, string> MergeTaskFactoryParameterSets(IDictionary<string, string> factoryIdentityParameters, IDictionary<string, string> taskIdentityParameters)
{
IDictionary<string, string> mergedParameters = null;
if (factoryIdentityParameters == null || factoryIdentityParameters.Count == 0)
{
mergedParameters = new Dictionary<string, string>(taskIdentityParameters, StringComparer.OrdinalIgnoreCase);
}
else if (taskIdentityParameters == null || taskIdentityParameters.Count == 0)
{
mergedParameters = new Dictionary<string, string>(factoryIdentityParameters, StringComparer.OrdinalIgnoreCase);
}
string mergedRuntime;
string mergedArchitecture;
if (mergedParameters != null)
{
mergedParameters.TryGetValue(XMakeAttributes.runtime, out mergedRuntime);
mergedParameters.TryGetValue(XMakeAttributes.architecture, out mergedArchitecture);
mergedParameters[XMakeAttributes.runtime] = XMakeAttributes.GetExplicitMSBuildRuntime(mergedRuntime);
mergedParameters[XMakeAttributes.architecture] = XMakeAttributes.GetExplicitMSBuildArchitecture(mergedArchitecture);
}
else
{
mergedParameters = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
taskIdentityParameters.TryGetValue(XMakeAttributes.runtime, out string taskRuntime);
factoryIdentityParameters.TryGetValue(XMakeAttributes.runtime, out string usingTaskRuntime);
if (!XMakeAttributes.TryMergeRuntimeValues(taskRuntime, usingTaskRuntime, out mergedRuntime))
{
ErrorUtilities.ThrowInternalError("How did we get two runtime values that were unmergeable?");
}
else
{
mergedParameters.Add(XMakeAttributes.runtime, mergedRuntime);
}
taskIdentityParameters.TryGetValue(XMakeAttributes.architecture, out string taskArchitecture);
factoryIdentityParameters.TryGetValue(XMakeAttributes.architecture, out string usingTaskArchitecture);
if (!XMakeAttributes.TryMergeArchitectureValues(taskArchitecture, usingTaskArchitecture, out mergedArchitecture))
{
ErrorUtilities.ThrowInternalError("How did we get two runtime values that were unmergeable?");
}
else
{
mergedParameters.Add(XMakeAttributes.architecture, mergedArchitecture);
}
}
return mergedParameters;
}
/// <summary>
/// Returns true if the provided set of task host parameters matches the current process,
/// and false otherwise.
/// </summary>
private static bool TaskHostParametersMatchCurrentProcess(IDictionary<string, string> mergedParameters)
{
if (mergedParameters == null || mergedParameters.Count == 0)
{
// We don't care, so they match by default.
return true;
}
string runtime;
if (mergedParameters.TryGetValue(XMakeAttributes.runtime, out runtime))
{
string currentRuntime = XMakeAttributes.GetExplicitMSBuildRuntime(XMakeAttributes.MSBuildRuntimeValues.currentRuntime);
if (!currentRuntime.Equals(XMakeAttributes.GetExplicitMSBuildRuntime(runtime), StringComparison.OrdinalIgnoreCase))
{
// runtime doesn't match
return false;
}
}
string architecture;
if (mergedParameters.TryGetValue(XMakeAttributes.architecture, out architecture))
{
string currentArchitecture = XMakeAttributes.GetCurrentMSBuildArchitecture();
if (!currentArchitecture.Equals(XMakeAttributes.GetExplicitMSBuildArchitecture(architecture), StringComparison.OrdinalIgnoreCase))
{
// architecture doesn't match
return false;
}
}
// if it doesn't not match, then it matches
return true;
}
/// <summary>
/// Log errors from TaskLoader.
/// </summary>
private void ErrorLoggingDelegate(string taskLocation, int taskLine, int taskColumn, string message, params object[] messageArgs)
{
_taskLoggingContext.LogError(new BuildEventFileInfo(taskLocation, taskLine, taskColumn), message, messageArgs);
}
#endregion
}
}
|