Skip to content

Commit 5c3ef58

Browse files
authored
Refactor WebAssemblyHotReload to use the agent code from the SDK (#32496)
* React to MetadataUpdateHandler renames Contributes to dotnet/runtime#51545
1 parent 23d7fb4 commit 5c3ef58

File tree

7 files changed

+287
-169
lines changed

7 files changed

+287
-169
lines changed

src/Components/Components/src/HotReload/HotReloadManager.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Reflection;
65
using System.Reflection.Metadata;
76
using Microsoft.AspNetCore.Components.HotReload;
87

9-
[assembly: AssemblyMetadata("ReceiveHotReloadDeltaNotification", "Microsoft.AspNetCore.Components.HotReload.HotReloadManager")]
108
[assembly: MetadataUpdateHandler(typeof(HotReloadManager))]
119

1210
namespace Microsoft.AspNetCore.Components.HotReload
@@ -20,6 +18,6 @@ public static void DeltaApplied()
2018
OnDeltaApplied?.Invoke();
2119
}
2220

23-
public static void AfterUpdate(Type[]? _) => OnDeltaApplied?.Invoke();
21+
public static void UpdateApplication(Type[]? _) => OnDeltaApplied?.Invoke();
2422
}
2523
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
// Based on the implementation in https://raw.githubusercontent.com/dotnet/sdk/f67f46eba6d3bcf2b3054381851a975096652454/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs
5+
6+
using System;
7+
using System.Collections.Concurrent;
8+
using System.Collections.Generic;
9+
using System.Linq;
10+
using System.Reflection;
11+
12+
namespace Microsoft.Extensions.HotReload
13+
{
14+
internal sealed class HotReloadAgent : IDisposable
15+
{
16+
private readonly Action<string> _log;
17+
private readonly AssemblyLoadEventHandler _assemblyLoad;
18+
private readonly ConcurrentDictionary<Guid, IReadOnlyList<UpdateDelta>> _deltas = new();
19+
private readonly ConcurrentDictionary<Assembly, Assembly> _appliedAssemblies = new();
20+
private volatile UpdateHandlerActions? _handlerActions;
21+
22+
public HotReloadAgent(Action<string> log)
23+
{
24+
_log = log;
25+
_assemblyLoad = OnAssemblyLoad;
26+
AppDomain.CurrentDomain.AssemblyLoad += _assemblyLoad;
27+
}
28+
29+
private void OnAssemblyLoad(object? _, AssemblyLoadEventArgs eventArgs)
30+
{
31+
_handlerActions = null;
32+
var loadedAssembly = eventArgs.LoadedAssembly;
33+
var moduleId = loadedAssembly.Modules.FirstOrDefault()?.ModuleVersionId;
34+
if (moduleId is null)
35+
{
36+
return;
37+
}
38+
39+
if (_deltas.TryGetValue(moduleId.Value, out var updateDeltas) && _appliedAssemblies.TryAdd(loadedAssembly, loadedAssembly))
40+
{
41+
// A delta for this specific Module exists and we haven't called ApplyUpdate on this instance of Assembly as yet.
42+
ApplyDeltas(updateDeltas);
43+
}
44+
}
45+
46+
internal sealed class UpdateHandlerActions
47+
{
48+
public List<Action<Type[]?>> ClearCache { get; } = new();
49+
public List<Action<Type[]?>> UpdateApplication { get; } = new();
50+
}
51+
52+
private UpdateHandlerActions GetMetadataUpdateHandlerActions()
53+
{
54+
// We need to execute MetadataUpdateHandlers in a well-defined order. For v1, the strategy that is used is to topologically
55+
// sort assemblies so that handlers in a dependency are executed before the dependent (e.g. the reflection cache action
56+
// in System.Private.CoreLib is executed before System.Text.Json clears it's own cache.)
57+
// This would ensure that caches and updates more lower in the application stack are up to date
58+
// before ones higher in the stack are recomputed.
59+
var sortedAssemblies = TopologicalSort(AppDomain.CurrentDomain.GetAssemblies());
60+
var handlerActions = new UpdateHandlerActions();
61+
foreach (var assembly in sortedAssemblies)
62+
{
63+
foreach (var attr in assembly.GetCustomAttributesData())
64+
{
65+
// Look up the attribute by name rather than by type. This would allow netstandard targeting libraries to
66+
// define their own copy without having to cross-compile.
67+
if (attr.AttributeType.FullName != "System.Reflection.Metadata.MetadataUpdateHandlerAttribute")
68+
{
69+
continue;
70+
}
71+
72+
IList<CustomAttributeTypedArgument> ctorArgs = attr.ConstructorArguments;
73+
if (ctorArgs.Count != 1 ||
74+
ctorArgs[0].Value is not Type handlerType)
75+
{
76+
_log($"'{attr}' found with invalid arguments.");
77+
continue;
78+
}
79+
80+
GetHandlerActions(handlerActions, handlerType);
81+
}
82+
}
83+
84+
return handlerActions;
85+
}
86+
87+
internal void GetHandlerActions(UpdateHandlerActions handlerActions, Type handlerType)
88+
{
89+
bool methodFound = false;
90+
91+
if (GetUpdateMethod(handlerType, "ClearCache") is MethodInfo clearCache)
92+
{
93+
handlerActions.ClearCache.Add(CreateAction(clearCache));
94+
methodFound = true;
95+
}
96+
97+
if (GetUpdateMethod(handlerType, "UpdateApplication") is MethodInfo updateApplication)
98+
{
99+
handlerActions.UpdateApplication.Add(CreateAction(updateApplication));
100+
methodFound = true;
101+
}
102+
103+
if (!methodFound)
104+
{
105+
_log($"No invokable methods found on metadata handler type '{handlerType}'. " +
106+
$"Allowed methods are ClearCache, UpdateApplication");
107+
}
108+
109+
Action<Type[]?> CreateAction(MethodInfo update)
110+
{
111+
Action<Type[]?> action = update.CreateDelegate<Action<Type[]?>>();
112+
return types =>
113+
{
114+
try
115+
{
116+
action(types);
117+
}
118+
catch (Exception ex)
119+
{
120+
_log($"Exception from '{action}': {ex}");
121+
}
122+
};
123+
}
124+
125+
MethodInfo? GetUpdateMethod(Type handlerType, string name)
126+
{
127+
if (handlerType.GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, new[] { typeof(Type[]) }) is MethodInfo updateMethod &&
128+
updateMethod.ReturnType == typeof(void))
129+
{
130+
return updateMethod;
131+
}
132+
133+
foreach (MethodInfo method in handlerType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
134+
{
135+
if (method.Name == name)
136+
{
137+
_log($"Type '{handlerType}' has method '{method}' that does not match the required signature.");
138+
break;
139+
}
140+
}
141+
142+
return null;
143+
}
144+
}
145+
146+
internal static List<Assembly> TopologicalSort(Assembly[] assemblies)
147+
{
148+
var sortedAssemblies = new List<Assembly>(assemblies.Length);
149+
150+
var visited = new HashSet<string>(StringComparer.Ordinal);
151+
152+
foreach (var assembly in assemblies)
153+
{
154+
Visit(assemblies, assembly, sortedAssemblies, visited);
155+
}
156+
157+
static void Visit(Assembly[] assemblies, Assembly assembly, List<Assembly> sortedAssemblies, HashSet<string> visited)
158+
{
159+
var assemblyIdentifier = assembly.GetName().Name!;
160+
if (!visited.Add(assemblyIdentifier))
161+
{
162+
return;
163+
}
164+
165+
foreach (var dependencyName in assembly.GetReferencedAssemblies())
166+
{
167+
var dependency = Array.Find(assemblies, a => a.GetName().Name == dependencyName.Name);
168+
if (dependency is not null)
169+
{
170+
Visit(assemblies, dependency, sortedAssemblies, visited);
171+
}
172+
}
173+
174+
sortedAssemblies.Add(assembly);
175+
}
176+
177+
return sortedAssemblies;
178+
}
179+
180+
public void ApplyDeltas(IReadOnlyList<UpdateDelta> deltas)
181+
{
182+
try
183+
{
184+
// Defer discovering the receiving deltas until the first hot reload delta.
185+
// This should give enough opportunity for AppDomain.GetAssemblies() to be sufficiently populated.
186+
_handlerActions ??= GetMetadataUpdateHandlerActions();
187+
var handlerActions = _handlerActions;
188+
189+
// TODO: Get types to pass in
190+
Type[]? updatedTypes = null;
191+
192+
for (var i = 0; i < deltas.Count; i++)
193+
{
194+
var item = deltas[i];
195+
var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.Modules.FirstOrDefault() is Module m && m.ModuleVersionId == item.ModuleId);
196+
if (assembly is not null)
197+
{
198+
System.Reflection.Metadata.AssemblyExtensions.ApplyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan<byte>.Empty);
199+
}
200+
}
201+
202+
handlerActions.ClearCache.ForEach(a => a(updatedTypes));
203+
handlerActions.UpdateApplication.ForEach(a => a(updatedTypes));
204+
205+
_log("Deltas applied.");
206+
}
207+
catch (Exception ex)
208+
{
209+
_log(ex.ToString());
210+
}
211+
}
212+
213+
public void Dispose()
214+
{
215+
AppDomain.CurrentDomain.AssemblyLoad -= _assemblyLoad;
216+
}
217+
}
218+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
6+
namespace Microsoft.Extensions.HotReload
7+
{
8+
internal sealed class UpdateDelta
9+
{
10+
public Guid ModuleId { get; set; }
11+
12+
public byte[] MetadataDelta { get; set; } = default!;
13+
14+
public byte[] ILDelta { get; set; } = default!;
15+
}
16+
}

0 commit comments

Comments
 (0)