File: .packages\microsoft.dotnet.hotreload.agent\10.0.100-preview.5.25258.39\contentFiles\cs\netstandard2.1\MetadataUpdateHandlerInvoker.cs
Web Access
Project: src\src\Components\WebAssembly\WebAssembly\src\Microsoft.AspNetCore.Components.WebAssembly.csproj (Microsoft.AspNetCore.Components.WebAssembly)
// 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.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Threading;
using System.Runtime.CompilerServices;
 
namespace Microsoft.DotNet.HotReload;
 
/// <summary>
/// Finds and invokes metadata update handlers.
/// </summary>
#if NET
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Hot reload is only expected to work when trimming is disabled.")]
[UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "Hot reload is only expected to work when trimming is disabled.")]
#endif
internal sealed class MetadataUpdateHandlerInvoker(AgentReporter reporter)
{
    internal delegate void ContentUpdateAction(StaticAssetUpdate update);
    internal delegate void MetadataUpdateAction(Type[]? updatedTypes);
 
    internal readonly struct UpdateHandler<TAction>(TAction action, MethodInfo method)
        where TAction : Delegate
    {
        public TAction Action { get; } = action;
        public MethodInfo Method { get; } = method;
 
        public void ReportInvocation(AgentReporter reporter)
            => reporter.Report(GetHandlerDisplayString(Method), AgentMessageSeverity.Verbose);
    }
 
    internal sealed class RegisteredActions(
        IReadOnlyList<UpdateHandler<MetadataUpdateAction>> clearCacheHandlers,
        IReadOnlyList<UpdateHandler<MetadataUpdateAction>> updateApplicationHandlers,
        List<UpdateHandler<ContentUpdateAction>> updateContentHandlers)
    {
        public void MetadataUpdated(AgentReporter reporter, Type[] updatedTypes)
        {
            foreach (var handler in clearCacheHandlers)
            {
                handler.ReportInvocation(reporter);
                handler.Action(updatedTypes);
            }
 
            foreach (var handler in updateApplicationHandlers)
            {
                handler.ReportInvocation(reporter);
                handler.Action(updatedTypes);
            }
        }
 
        public void UpdateContent(AgentReporter reporter, StaticAssetUpdate update)
        {
            foreach (var handler in updateContentHandlers)
            {
                handler.ReportInvocation(reporter);
                handler.Action(update);
            }
        }
 
        /// <summary>
        /// For testing.
        /// </summary>
        internal IEnumerable<UpdateHandler<MetadataUpdateAction>> ClearCacheHandlers => clearCacheHandlers;
 
        /// <summary>
        /// For testing.
        /// </summary>
        internal IEnumerable<UpdateHandler<MetadataUpdateAction>> UpdateApplicationHandlers => updateApplicationHandlers;
 
        /// <summary>
        /// For testing.
        /// </summary>
        internal IEnumerable<UpdateHandler<ContentUpdateAction>> UpdateContentHandlers => updateContentHandlers;
    }
 
    private const string ClearCacheHandlerName = "ClearCache";
    private const string UpdateApplicationHandlerName = "UpdateApplication";
    private const string UpdateContentHandlerName = "UpdateContent";
    private const BindingFlags HandlerMethodBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
 
    private static readonly Type[] s_contentUpdateSignature = [typeof(string), typeof(bool), typeof(string), typeof(byte[])];
    private static readonly Type[] s_metadataUpdateSignature = [typeof(Type[])];
 
    private RegisteredActions? _actions;
 
    /// <summary>
    /// Call when a new assembly is loaded.
    /// </summary>
    internal void Clear()
        => Interlocked.Exchange(ref _actions, null);
 
    private RegisteredActions GetActions()
    {
        // Defer discovering metadata updata handlers until after hot reload deltas have been applied.
        // This should give enough opportunity for AppDomain.GetAssemblies() to be sufficiently populated.
        var actions = _actions;
        if (actions == null)
        {
            Interlocked.CompareExchange(ref _actions, GetUpdateHandlerActions(), null);
            actions = _actions;
        }
 
        return actions;
    }
 
    /// <summary>
    /// Invokes all registered mtadata update handlers.
    /// </summary>
    internal void MetadataUpdated(Type[] updatedTypes)
    {
        try
        {
            reporter.Report("Invoking metadata update handlers.", AgentMessageSeverity.Verbose);
 
            GetActions().MetadataUpdated(reporter, updatedTypes);
        }
        catch (Exception e)
        {
            reporter.Report(e.ToString(), AgentMessageSeverity.Warning);
        }
    }
 
    /// <summary>
    /// Invokes all registered content update handlers.
    /// </summary>
    internal void ContentUpdated(StaticAssetUpdate update)
    {
        try
        {
            reporter.Report("Invoking content update handlers.", AgentMessageSeverity.Verbose);
 
            GetActions().UpdateContent(reporter, update);
        }
        catch (Exception e)
        {
            reporter.Report(e.ToString(), AgentMessageSeverity.Warning);
        }
    }
 
    private IEnumerable<Type> GetHandlerTypes()
    {
        // We need to execute MetadataUpdateHandlers in a well-defined order. For v1, the strategy that is used is to topologically
        // sort assemblies so that handlers in a dependency are executed before the dependent (e.g. the reflection cache action
        // in System.Private.CoreLib is executed before System.Text.Json clears its own cache.)
        // This would ensure that caches and updates more lower in the application stack are up to date
        // before ones higher in the stack are recomputed.
        var sortedAssemblies = TopologicalSort(AppDomain.CurrentDomain.GetAssemblies());
 
        foreach (var assembly in sortedAssemblies)
        {
            foreach (var attr in TryGetCustomAttributesData(assembly))
            {
                // Look up the attribute by name rather than by type. This would allow netstandard targeting libraries to
                // define their own copy without having to cross-compile.
                if (attr.AttributeType.FullName != "System.Reflection.Metadata.MetadataUpdateHandlerAttribute")
                {
                    continue;
                }
 
                IList<CustomAttributeTypedArgument> ctorArgs = attr.ConstructorArguments;
                if (ctorArgs.Count != 1 ||
                    ctorArgs[0].Value is not Type handlerType)
                {
                    reporter.Report($"'{attr}' found with invalid arguments.", AgentMessageSeverity.Warning);
                    continue;
                }
 
                yield return handlerType;
            }
        }
    }
 
    public RegisteredActions GetUpdateHandlerActions()
        => GetUpdateHandlerActions(GetHandlerTypes());
 
    /// <summary>
    /// Internal for testing.
    /// </summary>
    internal RegisteredActions GetUpdateHandlerActions(IEnumerable<Type> handlerTypes)
    {
        var clearCacheHandlers = new List<UpdateHandler<MetadataUpdateAction>>();
        var applicationUpdateHandlers = new List<UpdateHandler<MetadataUpdateAction>>();
        var contentUpdateHandlers = new List<UpdateHandler<ContentUpdateAction>>();
 
        foreach (var handlerType in handlerTypes)
        {
            bool methodFound = false;
 
            if (GetMetadataUpdateMethod(handlerType, ClearCacheHandlerName) is MethodInfo clearCache)
            {
                clearCacheHandlers.Add(CreateMetadataUpdateAction(clearCache));
                methodFound = true;
            }
 
            if (GetMetadataUpdateMethod(handlerType, UpdateApplicationHandlerName) is MethodInfo updateApplication)
            {
                applicationUpdateHandlers.Add(CreateMetadataUpdateAction(updateApplication));
                methodFound = true;
            }
 
            if (GetContentUpdateMethod(handlerType, UpdateContentHandlerName) is MethodInfo updateContent)
            {
                contentUpdateHandlers.Add(CreateContentUpdateAction(updateContent));
                methodFound = true;
            }
 
            if (!methodFound)
            {
                reporter.Report(
                    $"Expected to find a static method '{ClearCacheHandlerName}', '{UpdateApplicationHandlerName}' or '{UpdateContentHandlerName}' on type '{handlerType.AssemblyQualifiedName}' but neither exists.",
                    AgentMessageSeverity.Warning);
            }
        }
 
        return new RegisteredActions(clearCacheHandlers, applicationUpdateHandlers, contentUpdateHandlers);
 
        UpdateHandler<MetadataUpdateAction> CreateMetadataUpdateAction(MethodInfo method)
        {
            var action = (MetadataUpdateAction)method.CreateDelegate(typeof(MetadataUpdateAction));
            return new(types =>
            {
                try
                {
                    action(types);
                }
                catch (Exception e)
                {
                    ReportException(e, method);
                }
            }, method);
        }
 
        UpdateHandler<ContentUpdateAction> CreateContentUpdateAction(MethodInfo method)
        {
            var action = (Action<string, bool, string, byte[]>)method.CreateDelegate(typeof(Action<string, bool, string, byte[]>));
            return new(update =>
            {
                try
                {
                    action(update.AssemblyName, update.IsApplicationProject, update.RelativePath, update.Contents);
                }
                catch (Exception e)
                {
                    ReportException(e, method);
                }
            }, method);
        }
 
        void ReportException(Exception e, MethodInfo method)
            => reporter.Report($"Exception from '{GetHandlerDisplayString(method)}': {e}", AgentMessageSeverity.Warning);
 
        MethodInfo? GetMetadataUpdateMethod(Type handlerType, string name)
        {
            if (handlerType.GetMethod(name, HandlerMethodBindingFlags, binder: null, s_metadataUpdateSignature, modifiers: null) is MethodInfo updateMethod &&
                updateMethod.ReturnType == typeof(void))
            {
                return updateMethod;
            }
 
            ReportSignatureMismatch(handlerType, name);
            return null;
        }
 
        MethodInfo? GetContentUpdateMethod(Type handlerType, string name)
        {
            if (handlerType.GetMethod(name, HandlerMethodBindingFlags, binder: null, s_contentUpdateSignature, modifiers: null) is MethodInfo updateMethod &&
                updateMethod.ReturnType == typeof(void))
            {
                return updateMethod;
            }
 
            ReportSignatureMismatch(handlerType, name);
            return null;
        }
 
        void ReportSignatureMismatch(Type handlerType, string name)
        {
            foreach (MethodInfo method in handlerType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
            {
                if (method.Name == name)
                {
                    reporter.Report($"Type '{handlerType}' has method '{method}' that does not match the required signature.", AgentMessageSeverity.Warning);
                    break;
                }
            }
        }
    }
 
    private static string GetHandlerDisplayString(MethodInfo method)
        => $"{method.DeclaringType!.FullName}.{method.Name}";
 
    private IList<CustomAttributeData> TryGetCustomAttributesData(Assembly assembly)
    {
        try
        {
            return assembly.GetCustomAttributesData();
        }
        catch (Exception e)
        {
            // In cross-platform scenarios, such as debugging in VS through WSL, Roslyn
            // runs on Windows, and the agent runs on Linux. Assemblies accessible to Windows
            // may not be available or loaded on linux (such as WPF's assemblies).
            // In such case, we can ignore the assemblies and continue enumerating handlers for
            // the rest of the assemblies of current domain.
            reporter.Report($"'{assembly.FullName}' is not loaded ({e.Message})", AgentMessageSeverity.Verbose);
            return [];
        }
    }
 
    /// <summary>
    /// Internal for testing.
    /// </summary>
    internal static List<Assembly> TopologicalSort(Assembly[] assemblies)
    {
        var sortedAssemblies = new List<Assembly>(assemblies.Length);
 
        var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 
        foreach (var assembly in assemblies)
        {
            Visit(assemblies, assembly, sortedAssemblies, visited);
        }
 
        static void Visit(Assembly[] assemblies, Assembly assembly, List<Assembly> sortedAssemblies, HashSet<string> visited)
        {
            string? assemblyIdentifier;
 
            try
            {
                assemblyIdentifier = assembly.GetName().Name;
            }
            catch
            {
                return;
            }
 
            if (assemblyIdentifier == null || !visited.Add(assemblyIdentifier))
            {
                return;
            }
 
            AssemblyName[] referencedAssemblies;
            try
            {
                referencedAssemblies = assembly.GetReferencedAssemblies();
            }
            catch
            {
                referencedAssemblies = [];
            }
 
            foreach (var dependencyName in referencedAssemblies)
            {
                var dependency = Array.Find(assemblies, a =>
                {
                    try
                    {
                        return string.Equals(a.GetName().Name, dependencyName.Name, StringComparison.OrdinalIgnoreCase);
                    }
                    catch
                    {
                        return false;
                    }
                });
 
                if (dependency is not null)
                {
                    Visit(assemblies, dependency, sortedAssemblies, visited);
                }
            }
 
            sortedAssemblies.Add(assembly);
        }
 
        return sortedAssemblies;
    }
}