File: TaskLoader.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.Reflection;
using Microsoft.Build.Framework;
using Microsoft.Build.TaskHost.Resources;
using Microsoft.Build.TaskHost.Utilities;
 
namespace Microsoft.Build.TaskHost;
 
/// <summary>
/// Class for loading tasks.
/// </summary>
internal static class TaskLoader
{
    /// <summary>
    /// For saving the assembly that was loaded by the TypeLoader
    /// We only use this when the assembly failed to load properly into the appdomain.
    /// </summary>
    private static LoadedType? s_resolverLoadedType;
 
    /// <summary>
    /// Delegate for logging task loading errors.
    /// </summary>
    internal delegate void LogError(string taskLocation, int taskLine, int taskColumn, string message);
 
    /// <summary>
    /// Checks if the given type is a task factory.
    /// </summary>
    /// <remarks>This method is used as a type filter delegate.</remarks>
    /// <returns>true, if specified type is a task</returns>
    internal static bool IsTaskClass(Type type, object unused)
        => type.IsClass && !type.IsAbstract && type.GetInterface("Microsoft.Build.Framework.ITask") != null;
 
    /// <summary>
    /// Creates an ITask instance and returns it.
    /// </summary>
    internal static ITask? CreateTask(
        LoadedType loadedType,
        string taskName,
        string taskLocation,
        int taskLine,
        int taskColumn,
        LogError logError,
        AppDomainSetup appDomainSetup,
        Action<AppDomain>? appDomainCreated,
        bool isOutOfProc,
        out AppDomain? taskAppDomain)
    {
        bool separateAppDomain = loadedType.HasLoadInSeparateAppDomainAttribute;
        s_resolverLoadedType = null;
        taskAppDomain = null;
        ITask? taskInstanceInOtherAppDomain = null;
 
        try
        {
            if (separateAppDomain)
            {
                if (!loadedType.IsMarshalByRef)
                {
                    logError(taskLocation, taskLine, taskColumn, string.Format(SR.TaskNotMarshalByRef, taskName));
                    return null;
                }
                else
                {
                    // Our task depend on this name to be precisely that, so if you change it make sure
                    // you also change the checks in the tasks run in separate AppDomains. Better yet, just don't change it.
 
                    // Make sure we copy the appdomain configuration and send it to the appdomain we create so that if the creator of the current appdomain
                    // has done the binding redirection in code, that we will get those settings as well.
                    AppDomainSetup appDomainInfo = new AppDomainSetup();
 
                    // Get the current app domain setup settings
                    byte[] currentAppdomainBytes = appDomainSetup.GetConfigurationBytes();
 
                    // Apply the appdomain settings to the new appdomain before creating it
                    appDomainInfo.SetConfigurationBytes(currentAppdomainBytes);
 
                    AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolver;
                    s_resolverLoadedType = loadedType;
 
                    taskAppDomain = AppDomain.CreateDomain(isOutOfProc ? "taskAppDomain (out-of-proc)" : "taskAppDomain (in-proc)", null, appDomainInfo);
 
                    if (loadedType.LoadedAssembly != null)
                    {
                        taskAppDomain.Load(loadedType.LoadedAssemblyName);
                    }
 
                    // Hook up last minute dumping of any exceptions
                    taskAppDomain.UnhandledException += ExceptionHandling.UnhandledExceptionHandler;
                    appDomainCreated?.Invoke(taskAppDomain);
                }
            }
            else
            {
                // perf improvement for the same appdomain case - we already have the type object
                // and don't want to go through reflection to recreate it from the name.
                return (ITask?)Activator.CreateInstance(loadedType.Type);
            }
 
            if (loadedType.AssemblyFilePath != null)
            {
                taskInstanceInOtherAppDomain = (ITask)taskAppDomain.CreateInstanceFromAndUnwrap(loadedType.AssemblyFilePath, loadedType.Type.FullName);
 
                // this will force evaluation of the task class type and try to load the task assembly
                Type taskType = taskInstanceInOtherAppDomain.GetType();
 
                // If the types don't match, we have a problem. It means that our AppDomain was able to load
                // a task assembly using Load, and loaded a different one. I don't see any other choice than
                // to fail here.
                if (taskType != loadedType.Type)
                {
                    logError(taskLocation, taskLine, taskColumn, string.Format(SR.ConflictingTaskAssembly, loadedType.AssemblyFilePath, loadedType.Type.Assembly.Location));
                    taskInstanceInOtherAppDomain = null;
                }
            }
            else
            {
                taskInstanceInOtherAppDomain = (ITask)taskAppDomain.CreateInstanceAndUnwrap(loadedType.Type.Assembly.FullName, loadedType.Type.FullName);
            }
 
            return taskInstanceInOtherAppDomain;
        }
        finally
        {
            // Don't leave appdomains open
            if (taskAppDomain != null && taskInstanceInOtherAppDomain == null)
            {
                AppDomain.Unload(taskAppDomain);
                RemoveAssemblyResolver();
            }
        }
    }
 
    /// <summary>
    /// This is a resolver to help created AppDomains when they are unable to load an assembly into their domain we will help
    /// them succeed by providing the already loaded one in the currentdomain so that they can derive AssemblyName info from it
    /// </summary>
    private static Assembly? AssemblyResolver(object sender, ResolveEventArgs args)
    {
        if (args.Name.Equals(s_resolverLoadedType?.LoadedAssemblyName.FullName, StringComparison.OrdinalIgnoreCase))
        {
            if (s_resolverLoadedType == null || s_resolverLoadedType.Path == null)
            {
                return null;
            }
 
            return s_resolverLoadedType.LoadedAssembly ?? Assembly.Load(s_resolverLoadedType.Path);
        }
 
        return null;
    }
 
    /// <summary>
    /// Check if we added a resolver and remove it
    /// </summary>
    internal static void RemoveAssemblyResolver()
    {
        if (s_resolverLoadedType != null)
        {
            AppDomain.CurrentDomain.AssemblyResolve -= AssemblyResolver;
            s_resolverLoadedType = null;
        }
    }
}