* Hot Reload agent improvements

Refactor agent code so that it can be shared with dotnet-watch implementation via a shared project.

Add new API applyHotReloadDeltas to apply updates.

* Feedback and cleanup
This commit is contained in:
Tomáš Matoušek 2024-11-19 13:52:48 -08:00 коммит произвёл GitHub
Родитель be19faf14e
Коммит 0a5f4deafc
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
14 изменённых файлов: 566 добавлений и 275 удалений

2
src/Components/Web.JS/dist/Release/blazor.server.js сгенерированный поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

2
src/Components/Web.JS/dist/Release/blazor.web.js сгенерированный поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

2
src/Components/Web.JS/dist/Release/blazor.webassembly.js сгенерированный поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -97,10 +97,15 @@ async function startCore(components: RootComponentManager<WebAssemblyComponentDe
}
});
// obsolete:
Blazor._internal.applyHotReload = (id: string, metadataDelta: string, ilDelta: string, pdbDelta: string | undefined, updatedTypes?: number[]) => {
dispatcher.invokeDotNetStaticMethod('Microsoft.AspNetCore.Components.WebAssembly', 'ApplyHotReloadDelta', id, metadataDelta, ilDelta, pdbDelta, updatedTypes ?? null);
};
Blazor._internal.applyHotReloadDeltas = (deltas: { moduleId: string, metadataDelta: string, ilDelta: string, pdbDelta: string, updatedTypes: number[] }[], loggingLevel: number) => {
return dispatcher.invokeDotNetStaticMethod('Microsoft.AspNetCore.Components.WebAssembly', 'ApplyHotReloadDeltas', deltas, loggingLevel);
};
Blazor._internal.getApplyUpdateCapabilities = () => dispatcher.invokeDotNetStaticMethod('Microsoft.AspNetCore.Components.WebAssembly', 'GetApplyUpdateCapabilities');
// Configure JS interop

Просмотреть файл

@ -89,7 +89,11 @@ export interface IBlazor {
}
// APIs invoked by hot reload
// obsolete:
applyHotReload?: (id: string, metadataDelta: string, ilDelta: string, pdbDelta: string | undefined, updatedTypes?: number[]) => void;
applyHotReloadDeltas?: (deltas: { moduleId: string, metadataDelta: string, ilDelta: string, pdbDelta: string, updatedTypes: number[] }[], loggingLevel: number) => {message: string, severity: number}[];
getApplyUpdateCapabilities?: () => string;
hotReloadApplied?: () => void;
}

Просмотреть файл

@ -0,0 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.DotNet.HotReload;
internal enum AgentMessageSeverity : byte
{
Verbose = 0,
Warning = 1,
Error = 2,
}

Просмотреть файл

@ -0,0 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Linq;
namespace Microsoft.DotNet.HotReload;
internal sealed class AgentReporter
{
private readonly List<(string message, AgentMessageSeverity severity)> _log = [];
public void Report(string message, AgentMessageSeverity severity)
{
_log.Add((message, severity));
}
public IReadOnlyCollection<(string message, AgentMessageSeverity severity)> GetAndClearLogEntries(ResponseLoggingLevel level)
{
lock (_log)
{
var filteredLog = (level != ResponseLoggingLevel.Verbose)
? _log.Where(static entry => entry.severity != AgentMessageSeverity.Verbose)
: _log;
var log = filteredLog.ToArray();
_log.Clear();
return log;
}
}
}

Просмотреть файл

@ -1,37 +1,105 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// Based on the implementation in https://raw.githubusercontent.com/dotnet/sdk/aad0424c0bfaa60c8bd136a92fd131e53d14561a/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
namespace Microsoft.Extensions.HotReload;
namespace Microsoft.DotNet.HotReload;
#if NET
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Hot reload is only expected to work when trimming is disabled.")]
#endif
internal sealed class HotReloadAgent : IDisposable
{
/// Flags for hot reload handler Types like MVC's HotReloadService.
private const DynamicallyAccessedMemberTypes HotReloadHandlerLinkerFlags = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods;
private const string MetadataUpdaterTypeName = "System.Reflection.Metadata.MetadataUpdater";
private const string ApplyUpdateMethodName = "ApplyUpdate";
private const string GetCapabilitiesMethodName = "GetCapabilities";
private delegate void ApplyUpdateDelegate(Assembly assembly, ReadOnlySpan<byte> metadataDelta, ReadOnlySpan<byte> ilDelta, ReadOnlySpan<byte> pdbDelta);
private readonly Action<string> _log;
private readonly AssemblyLoadEventHandler _assemblyLoad;
private readonly ConcurrentDictionary<Guid, List<UpdateDelta>> _deltas = new();
private readonly ConcurrentDictionary<Assembly, Assembly> _appliedAssemblies = new();
private volatile UpdateHandlerActions? _handlerActions;
private readonly ApplyUpdateDelegate _applyUpdate;
private readonly MetadataUpdateHandlerInvoker _metadataUpdateHandlerInvoker;
public HotReloadAgent(Action<string> log)
public AgentReporter Reporter { get; }
public string Capabilities { get; }
private HotReloadAgent(AgentReporter reporter, ApplyUpdateDelegate applyUpdate, string capabilities)
{
_log = log;
_assemblyLoad = OnAssemblyLoad;
AppDomain.CurrentDomain.AssemblyLoad += _assemblyLoad;
Reporter = reporter;
_metadataUpdateHandlerInvoker = new(reporter);
_applyUpdate = applyUpdate;
Capabilities = capabilities;
AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoad;
}
public static bool TryCreate(AgentReporter reporter, [NotNullWhen(true)] out HotReloadAgent? agent)
{
GetUpdaterMethodsAndCapabilities(reporter, out var applyUpdate, out var capabilities);
if (applyUpdate != null && !string.IsNullOrEmpty(capabilities))
{
agent = new HotReloadAgent(reporter, applyUpdate, capabilities);
return true;
}
agent = null;
return false;
}
public void Dispose()
{
AppDomain.CurrentDomain.AssemblyLoad -= OnAssemblyLoad;
}
private static void GetUpdaterMethodsAndCapabilities(AgentReporter reporter, out ApplyUpdateDelegate? applyUpdate, out string? capabilities)
{
applyUpdate = null;
capabilities = null;
var metadataUpdater = Type.GetType(MetadataUpdaterTypeName + ", System.Runtime.Loader", throwOnError: false);
if (metadataUpdater == null)
{
reporter.Report($"Type not found: {MetadataUpdaterTypeName}", AgentMessageSeverity.Error);
return;
}
var applyUpdateMethod = metadataUpdater.GetMethod(ApplyUpdateMethodName, BindingFlags.Public | BindingFlags.Static, binder: null, [typeof(Assembly), typeof(ReadOnlySpan<byte>), typeof(ReadOnlySpan<byte>), typeof(ReadOnlySpan<byte>)], modifiers: null);
if (applyUpdateMethod == null)
{
reporter.Report($"{MetadataUpdaterTypeName}.{ApplyUpdateMethodName} not found.", AgentMessageSeverity.Error);
return;
}
applyUpdate = (ApplyUpdateDelegate)applyUpdateMethod.CreateDelegate(typeof(ApplyUpdateDelegate));
var getCapabilities = metadataUpdater.GetMethod(GetCapabilitiesMethodName, BindingFlags.NonPublic | BindingFlags.Static, binder: null, Type.EmptyTypes, modifiers: null);
if (getCapabilities == null)
{
reporter.Report($"{MetadataUpdaterTypeName}.{GetCapabilitiesMethodName} not found.", AgentMessageSeverity.Error);
return;
}
try
{
capabilities = getCapabilities.Invoke(obj: null, parameters: null) as string;
}
catch (Exception e)
{
reporter.Report($"Error retrieving capabilities: {e.Message}", AgentMessageSeverity.Error);
}
}
private void OnAssemblyLoad(object? _, AssemblyLoadEventArgs eventArgs)
{
_handlerActions = null;
_metadataUpdateHandlerInvoker.Clear();
var loadedAssembly = eventArgs.LoadedAssembly;
var moduleId = TryGetModuleId(loadedAssembly);
if (moduleId is null)
@ -46,185 +114,29 @@ internal sealed class HotReloadAgent : IDisposable
}
}
internal sealed class UpdateHandlerActions
public void ApplyDeltas(IEnumerable<UpdateDelta> deltas)
{
public List<Action<Type[]?>> ClearCache { get; } = new();
public List<Action<Type[]?>> UpdateApplication { get; } = new();
}
[UnconditionalSuppressMessage("Trimmer", "IL2072",
Justification = "The handlerType passed to GetHandlerActions is preserved by MetadataUpdateHandlerAttribute with DynamicallyAccessedMemberTypes.All.")]
private UpdateHandlerActions GetMetadataUpdateHandlerActions()
{
// 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 it's 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());
var handlerActions = new UpdateHandlerActions();
foreach (var assembly in sortedAssemblies)
foreach (var delta in deltas)
{
foreach (var attr in assembly.GetCustomAttributesData())
{
// 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;
}
Reporter.Report($"Applying delta to module {delta.ModuleId}.", AgentMessageSeverity.Verbose);
IList<CustomAttributeTypedArgument> ctorArgs = attr.ConstructorArguments;
if (ctorArgs.Count != 1 ||
ctorArgs[0].Value is not Type handlerType)
{
_log($"'{attr}' found with invalid arguments.");
continue;
}
GetHandlerActions(handlerActions, handlerType);
}
}
return handlerActions;
}
internal void GetHandlerActions(
UpdateHandlerActions handlerActions,
[DynamicallyAccessedMembers(HotReloadHandlerLinkerFlags)] Type handlerType)
{
bool methodFound = false;
if (GetUpdateMethod(handlerType, "ClearCache") is MethodInfo clearCache)
{
handlerActions.ClearCache.Add(CreateAction(clearCache));
methodFound = true;
}
if (GetUpdateMethod(handlerType, "UpdateApplication") is MethodInfo updateApplication)
{
handlerActions.UpdateApplication.Add(CreateAction(updateApplication));
methodFound = true;
}
if (!methodFound)
{
_log($"No invokable methods found on metadata handler type '{handlerType}'. " +
$"Allowed methods are ClearCache, UpdateApplication");
}
Action<Type[]?> CreateAction(MethodInfo update)
{
Action<Type[]?> action = update.CreateDelegate<Action<Type[]?>>();
return types =>
{
try
{
action(types);
}
catch (Exception ex)
{
_log($"Exception from '{action}': {ex}");
}
};
}
MethodInfo? GetUpdateMethod([DynamicallyAccessedMembers(HotReloadHandlerLinkerFlags)] Type handlerType, string name)
{
if (handlerType.GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, new[] { typeof(Type[]) }) is MethodInfo updateMethod &&
updateMethod.ReturnType == typeof(void))
{
return updateMethod;
}
foreach (MethodInfo method in handlerType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
{
if (method.Name == name)
{
_log($"Type '{handlerType}' has method '{method}' that does not match the required signature.");
break;
}
}
return null;
}
}
internal static List<Assembly> TopologicalSort(Assembly[] assemblies)
{
var sortedAssemblies = new List<Assembly>(assemblies.Length);
var visited = new HashSet<string>(StringComparer.Ordinal);
foreach (var assembly in assemblies)
{
Visit(assemblies, assembly, sortedAssemblies, visited);
}
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Hot reload is only expected to work when trimming is disabled.")]
static void Visit(Assembly[] assemblies, Assembly assembly, List<Assembly> sortedAssemblies, HashSet<string> visited)
{
var assemblyIdentifier = assembly.GetName().Name!;
if (!visited.Add(assemblyIdentifier))
{
return;
}
foreach (var dependencyName in assembly.GetReferencedAssemblies())
{
var dependency = Array.Find(assemblies, a => a.GetName().Name == dependencyName.Name);
if (dependency is not null)
{
Visit(assemblies, dependency, sortedAssemblies, visited);
}
}
sortedAssemblies.Add(assembly);
}
return sortedAssemblies;
}
public void ApplyDeltas(IReadOnlyList<UpdateDelta> deltas)
{
for (var i = 0; i < deltas.Count; i++)
{
var item = deltas[i];
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
if (TryGetModuleId(assembly) is Guid moduleId && moduleId == item.ModuleId)
if (TryGetModuleId(assembly) is Guid moduleId && moduleId == delta.ModuleId)
{
MetadataUpdater.ApplyUpdate(assembly, item.MetadataDelta, item.ILDelta, item.PdbBytes ?? ReadOnlySpan<byte>.Empty);
_applyUpdate(assembly, delta.MetadataDelta, delta.ILDelta, delta.PdbDelta);
}
}
// Additionally stash the deltas away so it may be applied to assemblies loaded later.
var cachedDeltas = _deltas.GetOrAdd(item.ModuleId, static _ => new());
cachedDeltas.Add(item);
var cachedDeltas = _deltas.GetOrAdd(delta.ModuleId, static _ => new());
cachedDeltas.Add(delta);
}
try
{
// 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.
_handlerActions ??= GetMetadataUpdateHandlerActions();
var handlerActions = _handlerActions;
Type[]? updatedTypes = GetMetadataUpdateTypes(deltas);
handlerActions.ClearCache.ForEach(a => a(updatedTypes));
handlerActions.UpdateApplication.ForEach(a => a(updatedTypes));
_log("Deltas applied.");
}
catch (Exception ex)
{
_log(ex.ToString());
}
_metadataUpdateHandlerInvoker.Invoke(GetMetadataUpdateTypes(deltas));
}
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Hot reload is only expected to work when trimming is disabled.")]
private static Type[] GetMetadataUpdateTypes(IReadOnlyList<UpdateDelta> deltas)
private Type[] GetMetadataUpdateTypes(IEnumerable<UpdateDelta> deltas)
{
List<Type>? types = null;
@ -236,44 +148,45 @@ internal sealed class HotReloadAgent : IDisposable
continue;
}
var assemblyTypes = assembly.GetTypes();
foreach (var updatedType in delta.UpdatedTypes ?? Array.Empty<int>())
foreach (var updatedType in delta.UpdatedTypes)
{
var type = assemblyTypes.FirstOrDefault(t => t.MetadataToken == updatedType);
if (type != null)
// Must be a TypeDef.
Debug.Assert(MetadataTokens.EntityHandle(updatedType) is { Kind: HandleKind.TypeDefinition, IsNil: false });
// The type has to be in the manifest module since Hot Reload does not support multi-module assemblies:
try
{
var type = assembly.ManifestModule.ResolveType(updatedType);
types ??= new();
types.Add(type);
}
catch (Exception e)
{
Reporter.Report($"Failed to load type 0x{updatedType:X8}: {e.Message}", AgentMessageSeverity.Warning);
}
}
}
return types?.ToArray() ?? Type.EmptyTypes;
}
public void ApplyDeltas(Assembly assembly, IReadOnlyList<UpdateDelta> deltas)
private void ApplyDeltas(Assembly assembly, IReadOnlyList<UpdateDelta> deltas)
{
try
{
foreach (var item in deltas)
{
MetadataUpdater.ApplyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan<byte>.Empty);
_applyUpdate(assembly, item.MetadataDelta, item.ILDelta, item.PdbDelta);
}
_log("Deltas applied.");
Reporter.Report("Deltas applied.", AgentMessageSeverity.Verbose);
}
catch (Exception ex)
{
_log(ex.ToString());
Reporter.Report(ex.ToString(), AgentMessageSeverity.Warning);
}
}
public void Dispose()
{
AppDomain.CurrentDomain.AssemblyLoad -= _assemblyLoad;
}
private static Guid? TryGetModuleId(Assembly loadedAssembly)
{
try

Просмотреть файл

@ -0,0 +1,244 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
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 sealed class RegisteredActions(IReadOnlyList<Action<Type[]?>> clearCache, IReadOnlyList<Action<Type[]?>> updateApplication)
{
public void Invoke(Type[] updatedTypes)
{
foreach (var action in clearCache)
{
action(updatedTypes);
}
foreach (var action in updateApplication)
{
action(updatedTypes);
}
}
/// <summary>
/// For testing.
/// </summary>
internal IEnumerable<Action<Type[]?>> ClearCache => clearCache;
/// <summary>
/// For testing.
/// </summary>
internal IEnumerable<Action<Type[]?>> UpdateApplication => updateApplication;
}
private const string ClearCacheHandlerName = "ClearCache";
private const string UpdateApplicationHandlerName = "UpdateApplication";
private RegisteredActions? _actions;
/// <summary>
/// Call when a new assembly is loaded.
/// </summary>
internal void Clear()
=> Interlocked.Exchange(ref _actions, null);
/// <summary>
/// Invokes all registerd handlers.
/// </summary>
internal void Invoke(Type[] updatedTypes)
{
try
{
// 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, GetMetadataUpdateHandlerActions(), null);
actions = _actions;
}
reporter.Report($"Invoking metadata update handlers. {updatedTypes.Length} type(s) updated.", AgentMessageSeverity.Verbose);
actions.Invoke(updatedTypes);
reporter.Report("Deltas applied.", AgentMessageSeverity.Verbose);
}
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 GetMetadataUpdateHandlerActions()
=> GetMetadataUpdateHandlerActions(GetHandlerTypes());
/// <summary>
/// Internal for testing.
/// </summary>
internal RegisteredActions GetMetadataUpdateHandlerActions(IEnumerable<Type> handlerTypes)
{
var clearCacheActions = new List<Action<Type[]?>>();
var updateApplicationActions = new List<Action<Type[]?>>();
foreach (var handlerType in handlerTypes)
{
bool methodFound = false;
if (GetUpdateMethod(handlerType, ClearCacheHandlerName) is MethodInfo clearCache)
{
clearCacheActions.Add(CreateAction(clearCache));
methodFound = true;
}
if (GetUpdateMethod(handlerType, UpdateApplicationHandlerName) is MethodInfo updateApplication)
{
updateApplicationActions.Add(CreateAction(updateApplication));
methodFound = true;
}
if (!methodFound)
{
reporter.Report(
$"Expected to find a static method '{ClearCacheHandlerName}' or '{UpdateApplicationHandlerName}' on type '{handlerType.AssemblyQualifiedName}' but neither exists.",
AgentMessageSeverity.Warning);
}
}
return new RegisteredActions(clearCacheActions, updateApplicationActions);
Action<Type[]?> CreateAction(MethodInfo update)
{
var action = (Action<Type[]?>)update.CreateDelegate(typeof(Action<Type[]?>));
return types =>
{
try
{
action(types);
}
catch (Exception ex)
{
reporter.Report($"Exception from '{action}': {ex}", AgentMessageSeverity.Warning);
}
};
}
MethodInfo? GetUpdateMethod(Type handlerType, string name)
{
if (handlerType.GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, binder: null, [typeof(Type[])], modifiers: null) is MethodInfo updateMethod &&
updateMethod.ReturnType == typeof(void))
{
return updateMethod;
}
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;
}
}
return null;
}
}
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.Ordinal);
foreach (var assembly in assemblies)
{
Visit(assemblies, assembly, sortedAssemblies, visited);
}
static void Visit(Assembly[] assemblies, Assembly assembly, List<Assembly> sortedAssemblies, HashSet<string> visited)
{
var assemblyIdentifier = assembly.GetName().Name!;
if (!visited.Add(assemblyIdentifier))
{
return;
}
foreach (var dependencyName in assembly.GetReferencedAssemblies())
{
var dependency = Array.Find(assemblies, a => a.GetName().Name == dependencyName.Name);
if (dependency is not null)
{
Visit(assemblies, dependency, sortedAssemblies, visited);
}
}
sortedAssemblies.Add(assembly);
}
return sortedAssemblies;
}
}

Просмотреть файл

@ -0,0 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.DotNet.HotReload;
internal enum ResponseLoggingLevel : byte
{
WarningsAndErrors = 0,
Verbose = 1,
}

Просмотреть файл

@ -1,17 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.Extensions.HotReload;
namespace Microsoft.DotNet.HotReload;
internal sealed class UpdateDelta
internal readonly struct UpdateDelta(Guid moduleId, byte[] metadataDelta, byte[] ilDelta, byte[] pdbDelta, int[] updatedTypes)
{
public Guid ModuleId { get; set; }
public byte[] MetadataDelta { get; set; } = default!;
public byte[] ILDelta { get; set; } = default!;
public byte[]? PdbBytes { get; set; }
public int[]? UpdatedTypes { get; set; }
public Guid ModuleId { get; } = moduleId;
public byte[] MetadataDelta { get; } = metadataDelta;
public byte[] ILDelta { get; } = ilDelta;
public byte[] PdbDelta { get; } = pdbDelta;
public int[] UpdatedTypes { get; } = updatedTypes;
}

Просмотреть файл

@ -2,13 +2,17 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Runtime.InteropServices.JavaScript;
using Microsoft.Extensions.HotReload;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using Microsoft.AspNetCore.Components.WebAssembly.Services;
using Microsoft.DotNet.HotReload;
using Microsoft.JSInterop;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
namespace Microsoft.AspNetCore.Components.WebAssembly.HotReload;
/// <summary>
@ -16,49 +20,143 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.HotReload;
/// code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
[UnconditionalSuppressMessage(
"Trimming",
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "Hot Reload does not support trimming")]
public static partial class WebAssemblyHotReload
{
private const string BlazorHotReloadModuleName = "blazor-hotreload";
private static HotReloadAgent? _hotReloadAgent;
private static readonly UpdateDelta[] _updateDeltas = new[]
/// <summary>
/// For framework use only.
/// </summary>
public readonly struct LogEntry
{
new UpdateDelta(),
};
public string Message { get; init; }
public int Severity { get; init; }
}
/// <summary>
/// For framework use only.
/// </summary>
internal sealed class Update
{
public int Id { get; set; }
public Delta[] Deltas { get; set; } = default!;
}
/// <summary>
/// For framework use only.
/// </summary>
public readonly struct Delta
{
public string ModuleId { get; init; }
public byte[] MetadataDelta { get; init; }
public byte[] ILDelta { get; init; }
public byte[] PdbDelta { get; init; }
public int[] UpdatedTypes { get; init; }
}
private static readonly AgentReporter s_reporter = new();
private static readonly JsonSerializerOptions s_jsonSerializerOptions = new(JsonSerializerDefaults.Web);
private static bool s_initialized;
private static HotReloadAgent? s_hotReloadAgent;
internal static async Task InitializeAsync()
{
if (Environment.GetEnvironmentVariable("__ASPNETCORE_BROWSER_TOOLS") == "true" &&
OperatingSystem.IsBrowser())
{
// Attempt to read previously applied hot reload deltas if the ASP.NET Core browser tools are available (indicated by the presence of the Environment variable).
// The agent is injected in to the hosted app and can serve this script that can provide results from local-storage.
// See https://github.com/dotnet/aspnetcore/issues/37357#issuecomment-941237000
await JSHost.ImportAsync(BlazorHotReloadModuleName, "/_framework/blazor-hotreload.js");
await ReceiveHotReloadAsync();
s_initialized = true;
if (!HotReloadAgent.TryCreate(s_reporter, out var agent))
{
return;
}
var existingAgent = Interlocked.CompareExchange(ref s_hotReloadAgent, agent, null);
if (existingAgent != null)
{
throw new InvalidOperationException("Hot Reload agent already initialized");
}
await ApplyPreviousDeltasAsync(agent);
}
}
private static async ValueTask ApplyPreviousDeltasAsync(HotReloadAgent agent)
{
string errorMessage;
using var client = new HttpClient()
{
BaseAddress = new Uri(WebAssemblyNavigationManager.Instance.BaseUri, UriKind.Absolute)
};
try
{
var response = await client.GetAsync("/_framework/blazor-hotreload");
if (response.IsSuccessStatusCode)
{
var deltasJson = await response.Content.ReadAsStringAsync();
var updates = deltasJson != "" ? JsonSerializer.Deserialize<Update[]>(deltasJson, s_jsonSerializerOptions) : null;
if (updates == null)
{
s_reporter.Report($"No previous updates to apply.", AgentMessageSeverity.Verbose);
return;
}
var i = 1;
foreach (var update in updates)
{
s_reporter.Report($"Reapplying update {i}/{updates.Length}.", AgentMessageSeverity.Verbose);
agent.ApplyDeltas(
update.Deltas.Select(d => new UpdateDelta(Guid.Parse(d.ModuleId, CultureInfo.InvariantCulture), d.MetadataDelta, d.ILDelta, d.PdbDelta, d.UpdatedTypes)));
i++;
}
return;
}
errorMessage = $"HTTP GET '/_framework/blazor-hotreload' returned {response.StatusCode}";
}
catch (Exception e)
{
errorMessage = e.ToString();
}
s_reporter.Report($"Failed to retrieve and apply previous deltas from the server: ${errorMessage}", AgentMessageSeverity.Error);
}
private static HotReloadAgent? GetAgent()
=> s_hotReloadAgent ?? (s_initialized ? throw new InvalidOperationException("Hot Reload agent not initialized") : null);
/// <summary>
/// For framework use only.
/// </summary>
[Obsolete("Use ApplyHotReloadDeltas instead")]
[JSInvokable(nameof(ApplyHotReloadDelta))]
public static void ApplyHotReloadDelta(string moduleIdString, byte[] metadataDelta, byte[] ilDelta, byte[] pdbBytes, int[]? updatedTypes)
{
GetAgent()?.ApplyDeltas(
[new UpdateDelta(Guid.Parse(moduleIdString, CultureInfo.InvariantCulture), metadataDelta, ilDelta, pdbBytes, updatedTypes ?? [])]);
}
/// <summary>
/// For framework use only.
/// </summary>
[JSInvokable(nameof(ApplyHotReloadDelta))]
public static void ApplyHotReloadDelta(string moduleIdString, byte[] metadataDelta, byte[] ilDelta, byte[] pdbBytes, int[]? updatedTypes)
[JSInvokable(nameof(ApplyHotReloadDeltas))]
public static LogEntry[] ApplyHotReloadDeltas(Delta[] deltas, int loggingLevel)
{
// Analyzer has a bug where it doesn't handle ConditionalAttribute: https://github.com/dotnet/roslyn/issues/63464
#pragma warning disable IDE0200 // Remove unnecessary lambda expression
Interlocked.CompareExchange(ref _hotReloadAgent, new HotReloadAgent(m => Debug.WriteLine(m)), null);
#pragma warning restore IDE0200 // Remove unnecessary lambda expression
var agent = GetAgent();
var moduleId = Guid.Parse(moduleIdString, CultureInfo.InvariantCulture);
agent?.ApplyDeltas(
deltas.Select(d => new UpdateDelta(Guid.Parse(d.ModuleId, CultureInfo.InvariantCulture), d.MetadataDelta, d.ILDelta, d.PdbDelta, d.UpdatedTypes)));
_updateDeltas[0].ModuleId = moduleId;
_updateDeltas[0].MetadataDelta = metadataDelta;
_updateDeltas[0].ILDelta = ilDelta;
_updateDeltas[0].PdbBytes = pdbBytes;
_updateDeltas[0].UpdatedTypes = updatedTypes;
_hotReloadAgent.ApplyDeltas(_updateDeltas);
return s_reporter.GetAndClearLogEntries((ResponseLoggingLevel)loggingLevel)
.Select(log => new LogEntry() { Message = log.message, Severity = (int)log.severity }).ToArray();
}
/// <summary>
@ -66,15 +164,5 @@ public static partial class WebAssemblyHotReload
/// </summary>
[JSInvokable(nameof(GetApplyUpdateCapabilities))]
public static string GetApplyUpdateCapabilities()
{
var method = typeof(System.Reflection.Metadata.MetadataUpdater).GetMethod("GetCapabilities", BindingFlags.NonPublic | BindingFlags.Static, Type.EmptyTypes);
if (method is null)
{
return string.Empty;
}
return (string)method.Invoke(obj: null, parameters: null)!;
}
[JSImport("receiveHotReloadAsync", BlazorHotReloadModuleName)]
private static partial Task ReceiveHotReloadAsync();
=> GetAgent()?.Capabilities ?? "";
}

Просмотреть файл

@ -81,6 +81,24 @@ Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostConfiguration
Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostConfiguration.WebAssemblyHostConfiguration() -> void
Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostEnvironmentExtensions
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.Delta() -> void
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.ILDelta.get -> byte[]!
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.ILDelta.init -> void
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.MetadataDelta.get -> byte[]!
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.MetadataDelta.init -> void
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.ModuleId.get -> string!
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.ModuleId.init -> void
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.PdbDelta.get -> byte[]!
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.PdbDelta.init -> void
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.UpdatedTypes.get -> int[]!
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.UpdatedTypes.init -> void
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry.LogEntry() -> void
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry.Message.get -> string!
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry.Message.init -> void
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry.Severity.get -> int
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry.Severity.init -> void
Microsoft.AspNetCore.Components.WebAssembly.Http.BrowserRequestCache
Microsoft.AspNetCore.Components.WebAssembly.Http.BrowserRequestCache.Default = 0 -> Microsoft.AspNetCore.Components.WebAssembly.Http.BrowserRequestCache
Microsoft.AspNetCore.Components.WebAssembly.Http.BrowserRequestCache.ForceCache = 4 -> Microsoft.AspNetCore.Components.WebAssembly.Http.BrowserRequestCache
@ -108,6 +126,7 @@ static Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostEnviro
static Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostEnvironmentExtensions.IsProduction(this Microsoft.AspNetCore.Components.WebAssembly.Hosting.IWebAssemblyHostEnvironment! hostingEnvironment) -> bool
static Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostEnvironmentExtensions.IsStaging(this Microsoft.AspNetCore.Components.WebAssembly.Hosting.IWebAssemblyHostEnvironment! hostingEnvironment) -> bool
static Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.ApplyHotReloadDelta(string! moduleIdString, byte[]! metadataDelta, byte[]! ilDelta, byte[]! pdbBytes, int[]? updatedTypes) -> void
static Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.ApplyHotReloadDeltas(Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta[]! deltas, int loggingLevel) -> Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry[]!
static Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.GetApplyUpdateCapabilities() -> string!
static Microsoft.AspNetCore.Components.WebAssembly.Http.WebAssemblyHttpRequestMessageExtensions.SetBrowserRequestCache(this System.Net.Http.HttpRequestMessage! requestMessage, Microsoft.AspNetCore.Components.WebAssembly.Http.BrowserRequestCache requestCache) -> System.Net.Http.HttpRequestMessage!
static Microsoft.AspNetCore.Components.WebAssembly.Http.WebAssemblyHttpRequestMessageExtensions.SetBrowserRequestCredentials(this System.Net.Http.HttpRequestMessage! requestMessage, Microsoft.AspNetCore.Components.WebAssembly.Http.BrowserRequestCredentials requestCredentials) -> System.Net.Http.HttpRequestMessage!

Просмотреть файл

@ -1,29 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.Extensions.HotReload;
namespace Microsoft.AspNetCore.Components.WebAssembly.HotReload;
public class WebAssemblyHotReloadTest
{
[Fact]
public void WebAssemblyHotReload_DiscoversMetadataHandlers_FromHot()
{
// Arrange
var hotReloadManager = typeof(Renderer).Assembly.GetType("Microsoft.AspNetCore.Components.HotReload.HotReloadManager");
Assert.NotNull(hotReloadManager);
var handlerActions = new HotReloadAgent.UpdateHandlerActions();
var logs = new List<string>();
var hotReloadAgent = new HotReloadAgent(logs.Add);
// Act
hotReloadAgent.GetHandlerActions(handlerActions, hotReloadManager);
// Assert
Assert.Empty(handlerActions.ClearCache);
Assert.Single(handlerActions.UpdateApplication);
}
}