From a7e4728798cee70eca9a3a257ea32838b71b4311 Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Thu, 17 Nov 2022 23:01:43 -0500 Subject: [PATCH 1/2] chore: Bump solution to C# 11 (cherry picked from commit 9b59ae78d21e66f1deb307f18347cfe89efc46ce) --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 25690243e..474ab78fd 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -13,7 +13,7 @@ $(AssemblyName) ($(TargetFramework)) en-US - 8.0 + 11.0 From 2e1c4d7bffb528e99ec5c8d87fc5bdfa91465b04 Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Thu, 17 Nov 2022 23:02:27 -0500 Subject: [PATCH 2/2] fix: restore metadata updater, include new pdb delta (cherry picked from commit a1ead0bfce27d28f4f79d1a5cb55de7c6a67dc2b) --- .../ts/Uno/WebAssembly/Bootstrapper.ts | 4 +- .../ts/Uno/WebAssembly/HotReloadSupport.ts | 13 +- .../HotReloadAgent.cs | 400 +++++++++--------- src/Uno.Wasm.MetadataUpdater/UpdateDelta.cs | 17 +- .../WebAssemblyHotReload.cs | 3 +- 5 files changed, 225 insertions(+), 212 deletions(-) diff --git a/src/Uno.Wasm.Bootstrap/ts/Uno/WebAssembly/Bootstrapper.ts b/src/Uno.Wasm.Bootstrap/ts/Uno/WebAssembly/Bootstrapper.ts index c2144348c..a51010a15 100644 --- a/src/Uno.Wasm.Bootstrap/ts/Uno/WebAssembly/Bootstrapper.ts +++ b/src/Uno.Wasm.Bootstrap/ts/Uno/WebAssembly/Bootstrapper.ts @@ -154,12 +154,11 @@ namespace Uno.WebAssembly.Bootstrap { this.setupRequire(); this.setupEmscriptenPreRun(); - this.setupHotReload(); } private setupHotReload() { if (this._context.Module.ENVIRONMENT_IS_WEB && this.hasDebuggingEnabled()) { - this._hotReloadSupport = new HotReloadSupport(this._context); + this._hotReloadSupport = new HotReloadSupport(this._context, this._unoConfig); } } @@ -271,6 +270,7 @@ namespace Uno.WebAssembly.Bootstrap { private async mainInit(): Promise { try { this.attachDebuggerHotkey(); + this.setupHotReload(); this.timezoneSetup(); if (this._hotReloadSupport) { diff --git a/src/Uno.Wasm.Bootstrap/ts/Uno/WebAssembly/HotReloadSupport.ts b/src/Uno.Wasm.Bootstrap/ts/Uno/WebAssembly/HotReloadSupport.ts index 912242762..9568d61b7 100644 --- a/src/Uno.Wasm.Bootstrap/ts/Uno/WebAssembly/HotReloadSupport.ts +++ b/src/Uno.Wasm.Bootstrap/ts/Uno/WebAssembly/HotReloadSupport.ts @@ -2,13 +2,17 @@ export class HotReloadSupport { private _context?: DotnetPublicAPI; + private _unoConfig?: UnoConfig; - constructor(context: DotnetPublicAPI) { + constructor(context: DotnetPublicAPI, unoConfig: UnoConfig) { this._context = context; + this._unoConfig = unoConfig; } public async initializeHotReload(): Promise { + const webAppBasePath = this._unoConfig.environmentVariables["UNO_BOOTSTRAP_WEBAPP_BASE_PATH"]; + // Take the place of the internal .NET for WebAssembly APIs for metadata updates coming // from the "BrowserLink" feature. @@ -25,7 +29,6 @@ }, applyExisting: async function (): Promise { - const webAppBasePath = this._unoConfig.environmentVariables["UNO_BOOTSTRAP_WEBAPP_BASE_PATH"]; var hotreloadConfigResponse = await fetch(`/_framework/unohotreload`); @@ -54,13 +57,13 @@ return this.getApplyUpdateCapabilitiesMethod(); }, - applyHotReload: function (moduleId: any, metadataDelta: any, ilDelta: any) { + applyHotReload: function (moduleId: any, metadataDelta: any, ilDelta: any, pdbDelta: any) { this.initialize(); - return this.applyHotReloadDeltaMethod(moduleId, metadataDelta, ilDelta); + return this.applyHotReloadDeltaMethod(moduleId, metadataDelta, ilDelta, pdbDelta || ""); } }; })((window).Blazor || ((window).Blazor = {})); - + // Apply pending updates caused by a browser refresh (window).Blazor._internal.initialize(this._context.BINDING); await (window).Blazor._internal.applyExisting(); diff --git a/src/Uno.Wasm.MetadataUpdater/HotReloadAgent.cs b/src/Uno.Wasm.MetadataUpdater/HotReloadAgent.cs index 9f4a24262..075d77d5f 100644 --- a/src/Uno.Wasm.MetadataUpdater/HotReloadAgent.cs +++ b/src/Uno.Wasm.MetadataUpdater/HotReloadAgent.cs @@ -1,7 +1,7 @@ // 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://github.com/dotnet/aspnetcore/blob/b569ccf308f5c95b0e1d2a5df2163e0716822db9/src/Components/WebAssembly/WebAssembly/src/HotReload/HotReloadAgent.cs +// Based on the implementation in https://github.com/dotnet/aspnetcore/blob/26e3dfc7f3f3a91ba445ec0f8b1598d12542fb9f/src/Components/WebAssembly/WebAssembly/src/HotReload/HotReloadAgent.cs using System; using System.Collections.Concurrent; @@ -12,274 +12,282 @@ using System.Reflection.Metadata; using Uno; -namespace Uno.Wasm.MetadataUpdate +namespace Uno.Wasm.MetadataUpdate; + +internal sealed class HotReloadAgent : IDisposable { - internal sealed class HotReloadAgent : IDisposable + /// Flags for hot reload handler Types like MVC's HotReloadService. + private const DynamicallyAccessedMemberTypes HotReloadHandlerLinkerFlags = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; + + private readonly Action _log; + private readonly AssemblyLoadEventHandler _assemblyLoad; + private readonly ConcurrentDictionary> _deltas = new(); + private readonly ConcurrentDictionary _appliedAssemblies = new(); + private volatile UpdateHandlerActions? _handlerActions; + + public HotReloadAgent(Action log) { - private readonly Action _log; - private readonly AssemblyLoadEventHandler _assemblyLoad; - private readonly ConcurrentDictionary> _deltas = new ConcurrentDictionary>(); - private readonly ConcurrentDictionary _appliedAssemblies = new ConcurrentDictionary(); - private volatile UpdateHandlerActions? _handlerActions; + _log = log; + _assemblyLoad = OnAssemblyLoad; + AppDomain.CurrentDomain.AssemblyLoad += _assemblyLoad; + } - public HotReloadAgent(Action log) + private void OnAssemblyLoad(object? _, AssemblyLoadEventArgs eventArgs) + { + _handlerActions = null; + var loadedAssembly = eventArgs.LoadedAssembly; + var moduleId = TryGetModuleId(loadedAssembly); + if (moduleId is null) { - _log = log; - _assemblyLoad = OnAssemblyLoad; - AppDomain.CurrentDomain.AssemblyLoad += _assemblyLoad; + return; } - private void OnAssemblyLoad(object? _, AssemblyLoadEventArgs eventArgs) + if (_deltas.TryGetValue(moduleId.Value, out var updateDeltas) && _appliedAssemblies.TryAdd(loadedAssembly, loadedAssembly)) { - _handlerActions = null; - var loadedAssembly = eventArgs.LoadedAssembly; - var moduleId = TryGetModuleId(loadedAssembly); - if (moduleId is null) - { - return; - } - - if (_deltas.TryGetValue(moduleId.Value, out var updateDeltas) && _appliedAssemblies.TryAdd(loadedAssembly, loadedAssembly)) - { - // A delta for this specific Module exists and we haven't called ApplyUpdate on this instance of Assembly as yet. - ApplyDeltas(loadedAssembly, updateDeltas); - } + // A delta for this specific Module exists and we haven't called ApplyUpdate on this instance of Assembly as yet. + ApplyDeltas(loadedAssembly, updateDeltas); } + } - internal sealed class UpdateHandlerActions - { - public List> ClearCache { get; } = new List>(); - public List> UpdateApplication { get; } = new List>(); - } + internal sealed class UpdateHandlerActions + { + public List> ClearCache { get; } = new(); + public List> UpdateApplication { get; } = new(); + } - private UpdateHandlerActions GetMetadataUpdateHandlerActions() + [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) { - // 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 attr in assembly.GetCustomAttributesData()) { - 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") { - // 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 ctorArgs = attr.ConstructorArguments; - if (ctorArgs.Count != 1 || - !(ctorArgs[0].Value is Type handlerType)) - { - _log($"'{attr}' found with invalid arguments."); - continue; - } - - GetHandlerActions(handlerActions, handlerType); + continue; } + + IList 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; + 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; } - internal void GetHandlerActions(UpdateHandlerActions handlerActions, Type handlerType) + if (GetUpdateMethod(handlerType, "UpdateApplication") is MethodInfo updateApplication) { - bool methodFound = false; + handlerActions.UpdateApplication.Add(CreateAction(updateApplication)); + methodFound = true; + } - if (GetUpdateMethod(handlerType, "ClearCache") is MethodInfo clearCache) - { - handlerActions.ClearCache.Add(CreateAction(clearCache)); - methodFound = true; - } + if (!methodFound) + { + _log($"No invokable methods found on metadata handler type '{handlerType}'. " + + $"Allowed methods are ClearCache, UpdateApplication"); + } - if (GetUpdateMethod(handlerType, "UpdateApplication") is MethodInfo updateApplication) + Action CreateAction(MethodInfo update) + { + Action action = update.CreateDelegate>(); + return types => { - handlerActions.UpdateApplication.Add(CreateAction(updateApplication)); - methodFound = true; - } + try + { + action(types); + } + catch (Exception ex) + { + _log($"Exception from '{action}': {ex}"); + } + }; + } - if (!methodFound) + 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)) { - _log($"No invokable methods found on metadata handler type '{handlerType}'. " + - $"Allowed methods are ClearCache, UpdateApplication"); + return updateMethod; } - Action CreateAction(MethodInfo update) + foreach (MethodInfo method in handlerType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)) { - var action = (Action)update.CreateDelegate(typeof(Action)); - return types => + if (method.Name == name) { - try - { - action(types); - } - catch (Exception ex) - { - _log($"Exception from '{action}': {ex}"); - } - }; + _log($"Type '{handlerType}' has method '{method}' that does not match the required signature."); + break; + } } - MethodInfo? GetUpdateMethod(Type handlerType, string name) - { - if (handlerType.GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static) is MethodInfo updateMethod && - updateMethod.ReturnType == typeof(void)) - { - return updateMethod; - } + return null; + } + } - 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; - } - } + internal static List TopologicalSort(Assembly[] assemblies) + { + var sortedAssemblies = new List(assemblies.Length); - return null; - } - } + var visited = new HashSet(StringComparer.Ordinal); - internal static List TopologicalSort(Assembly[] assemblies) + foreach (var assembly in assemblies) { - var sortedAssemblies = new List(assemblies.Length); - - var visited = new HashSet(StringComparer.Ordinal); + Visit(assemblies, assembly, sortedAssemblies, visited); + } - foreach (var assembly in assemblies) + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Hot reload is only expected to work when trimming is disabled.")] + static void Visit(Assembly[] assemblies, Assembly assembly, List sortedAssemblies, HashSet visited) + { + var assemblyIdentifier = assembly.GetName().Name!; + if (!visited.Add(assemblyIdentifier)) { - Visit(assemblies, assembly, sortedAssemblies, visited); + return; } - static void Visit(Assembly[] assemblies, Assembly assembly, List sortedAssemblies, HashSet visited) + foreach (var dependencyName in assembly.GetReferencedAssemblies()) { - 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) { - var dependency = Array.Find(assemblies, a => a.GetName().Name == dependencyName.Name); - if (!(dependency is null)) - { - Visit(assemblies, dependency, sortedAssemblies, visited); - } + Visit(assemblies, dependency, sortedAssemblies, visited); } - - sortedAssemblies.Add(assembly); } - return sortedAssemblies; + sortedAssemblies.Add(assembly); } - public void ApplyDeltas(IReadOnlyList deltas) + return sortedAssemblies; + } + + public void ApplyDeltas(IReadOnlyList deltas) + { + for (var i = 0; i < deltas.Count; i++) { - for (var i = 0; i < deltas.Count; i++) + var item = deltas[i]; + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { - 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 == item.ModuleId) - { - _log($"Applying delta to {assembly} / {moduleId}"); + _log($"Applying delta to {assembly} / {moduleId}"); - MetadataUpdater.ApplyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan.Empty); - } + MetadataUpdater.ApplyUpdate(assembly, item.MetadataDelta, item.ILDelta, item.PdbBytes ?? ReadOnlySpan.Empty); } - - // Additionally stash the deltas away so it may be applied to assemblies loaded later. - var cachedDeltas = _deltas.GetOrAdd(item.ModuleId, _ => new List()); - cachedDeltas.Add(item); } - 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; + // 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); + } - Type[]? updatedTypes = GetMetadataUpdateTypes(deltas); + 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; - handlerActions.ClearCache.ForEach(a => a(updatedTypes)); - handlerActions.UpdateApplication.ForEach(a => a(updatedTypes)); + Type[]? updatedTypes = GetMetadataUpdateTypes(deltas); - _log("Deltas applied."); - } - catch (Exception ex) - { - _log(ex.ToString()); - } - } + handlerActions.ClearCache.ForEach(a => a(updatedTypes)); + handlerActions.UpdateApplication.ForEach(a => a(updatedTypes)); - private Type[] GetMetadataUpdateTypes(IReadOnlyList deltas) + _log("Deltas applied."); + } + catch (Exception ex) { - List? types = null; - - foreach (var delta in deltas) - { - var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(assembly => TryGetModuleId(assembly) is Guid moduleId && moduleId == delta.ModuleId); - if (assembly is null) - { - continue; - } + _log(ex.ToString()); + } + } - var assemblyTypes = assembly.GetTypes(); + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Hot reload is only expected to work when trimming is disabled.")] + private static Type[] GetMetadataUpdateTypes(IReadOnlyList deltas) + { + List? types = null; - foreach (var updatedType in delta.UpdatedTypes ?? Array.Empty()) - { - var type = assemblyTypes.FirstOrDefault(t => t.MetadataToken == updatedType); - if (type != null) - { - types ??= new List(); - types.Add(type); - } - } + foreach (var delta in deltas) + { + var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(assembly => TryGetModuleId(assembly) is Guid moduleId && moduleId == delta.ModuleId); + if (assembly is null) + { + continue; } - return types?.ToArray() ?? Type.EmptyTypes; - } + var assemblyTypes = assembly.GetTypes(); - public void ApplyDeltas(Assembly assembly, IReadOnlyList deltas) - { - try + foreach (var updatedType in delta.UpdatedTypes ?? Array.Empty()) { - foreach (var item in deltas) + var type = assemblyTypes.FirstOrDefault(t => t.MetadataToken == updatedType); + if (type != null) { - MetadataUpdater.ApplyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan.Empty); + types ??= new(); + types.Add(type); } - - _log("Deltas applied."); - } - catch (Exception ex) - { - _log(ex.ToString()); } } - public void Dispose() - => AppDomain.CurrentDomain.AssemblyLoad -= _assemblyLoad; + return types?.ToArray() ?? Type.EmptyTypes; + } - private static Guid? TryGetModuleId(Assembly loadedAssembly) + public void ApplyDeltas(Assembly assembly, IReadOnlyList deltas) + { + try { - try + foreach (var item in deltas) { - return loadedAssembly.Modules.FirstOrDefault()?.ModuleVersionId; - } - catch - { - // Assembly.Modules might throw. See https://github.com/dotnet/aspnetcore/issues/33152 + MetadataUpdater.ApplyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan.Empty); } - return default; + _log("Deltas applied."); + } + catch (Exception ex) + { + _log(ex.ToString()); } } + + public void Dispose() + => AppDomain.CurrentDomain.AssemblyLoad -= _assemblyLoad; + + private static Guid? TryGetModuleId(Assembly loadedAssembly) + { + try + { + return loadedAssembly.Modules.FirstOrDefault()?.ModuleVersionId; + } + catch + { + // Assembly.Modules might throw. See https://github.com/dotnet/aspnetcore/issues/33152 + } + + return default; + } } diff --git a/src/Uno.Wasm.MetadataUpdater/UpdateDelta.cs b/src/Uno.Wasm.MetadataUpdater/UpdateDelta.cs index 51ab466ba..5a2bac473 100644 --- a/src/Uno.Wasm.MetadataUpdater/UpdateDelta.cs +++ b/src/Uno.Wasm.MetadataUpdater/UpdateDelta.cs @@ -1,15 +1,16 @@ using System; -namespace Uno.Wasm.MetadataUpdate +namespace Uno.Wasm.MetadataUpdate; + +internal sealed class UpdateDelta { - internal sealed class UpdateDelta - { - public Guid ModuleId { get; set; } + public Guid ModuleId { get; set; } + + public byte[] MetadataDelta { get; set; } = default!; - public byte[] MetadataDelta { get; set; } = default!; + public byte[] ILDelta { get; set; } = default!; - public byte[] ILDelta { get; set; } = default!; + public byte[]? PdbBytes { get; set; } - public int[]? UpdatedTypes { get; set; } - } + public int[]? UpdatedTypes { get; set; } } diff --git a/src/Uno.Wasm.MetadataUpdater/WebAssemblyHotReload.cs b/src/Uno.Wasm.MetadataUpdater/WebAssemblyHotReload.cs index d3b8812cf..d12e8fda7 100644 --- a/src/Uno.Wasm.MetadataUpdater/WebAssemblyHotReload.cs +++ b/src/Uno.Wasm.MetadataUpdater/WebAssemblyHotReload.cs @@ -59,7 +59,7 @@ internal static void Initialize() /// /// For framework use only. /// - public static void ApplyHotReloadDelta(string moduleIdString, string metadataDelta, string ilDeta) + public static void ApplyHotReloadDelta(string moduleIdString, string metadataDelta, string ilDeta, string pdbDelta) { if (_linkerEnabled) { @@ -80,6 +80,7 @@ public static void ApplyHotReloadDelta(string moduleIdString, string metadataDel _updateDeltas[0].ModuleId = moduleId; _updateDeltas[0].MetadataDelta = Convert.FromBase64String(metadataDelta); _updateDeltas[0].ILDelta = Convert.FromBase64String(ilDeta); + _updateDeltas[0].PdbBytes = Convert.FromBase64String(pdbDelta); _hotReloadAgent!.ApplyDeltas(_updateDeltas); }