diff --git a/sdk.sln b/sdk.sln index d2720233aa8b..c40475a2db48 100644 --- a/sdk.sln +++ b/sdk.sln @@ -335,7 +335,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SourceGenerators", "SourceG EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.NET.Sdk.Razor.SourceGenerators", "src\RazorSdk\SourceGenerators\Microsoft.NET.Sdk.Razor.SourceGenerators.csproj", "{56C34654-DE8F-4F14-B2F8-6C37285B786E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.AspNetCoreDeltaApplier", "src\BuiltInTools\AspNetCoreDeltaApplier\Microsoft.Extensions.AspNetCoreDeltaApplier.csproj", "{1BBFA19C-03F0-4D27-9D0D-0F8172642107}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.DotNetDeltaApplier", "src\BuiltInTools\DotNetDeltaApplier\Microsoft.Extensions.DotNetDeltaApplier.csproj", "{1BBFA19C-03F0-4D27-9D0D-0F8172642107}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.NativeWrapper", "src\Resolvers\Microsoft.DotNet.NativeWrapper\Microsoft.DotNet.NativeWrapper.csproj", "{E97E9E7F-11B4-42F7-8B55-D0451F5E82A0}" EndProject diff --git a/src/BuiltInTools/AspNetCoreDeltaApplier/StartupHook.cs b/src/BuiltInTools/AspNetCoreDeltaApplier/StartupHook.cs deleted file mode 100644 index f5b81e4b39b5..000000000000 --- a/src/BuiltInTools/AspNetCoreDeltaApplier/StartupHook.cs +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.IO.Pipes; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using Microsoft.DotNet.Watcher.Tools; - -internal sealed class StartupHook -{ - private static readonly bool LogDeltaClientMessages = Environment.GetEnvironmentVariable("HOTRELOAD_DELTA_CLIENT_LOG_MESSAGES") == "1"; - private static volatile UpdateHandlerActions? s_beforeAfterUpdates; - - public static void Initialize() - { - Task.Run(async () => - { - AssemblyLoadEventHandler handler = (s, e) => s_beforeAfterUpdates = null; - try - { - AppDomain.CurrentDomain.AssemblyLoad += handler; - await ReceiveDeltas(); - } - catch (Exception ex) - { - Log(ex.Message); - } - finally - { - AppDomain.CurrentDomain.AssemblyLoad -= handler; - } - }); - } - - private sealed class UpdateHandlerActions - { - public UpdateHandlerActions(List> before, List> after) - { - Before = before; - After = after; - } - - public List> Before { get; } - public List> After { get; } - } - - private static UpdateHandlerActions GetMetadataUpdateHandlerActions() - { - var before = new List>(); - var after = new List>(); - - foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - foreach (CustomAttributeData attr in assembly.GetCustomAttributesData()) - { - if (attr.AttributeType.FullName != "System.Reflection.Metadata.MetadataUpdateHandlerAttribute") - { - continue; - } - - IList ctorArgs = attr.ConstructorArguments; - if (ctorArgs.Count != 1 || - ctorArgs[0].Value is not Type handlerType) - { - Log($"'{attr}' found with invalid arguments."); - continue; - } - - bool methodFound = false; - - if (GetUpdateMethod(handlerType, "BeforeUpdate") is MethodInfo beforeUpdate) - { - before.Add(CreateAction(beforeUpdate)); - methodFound = true; - } - - if (GetUpdateMethod(handlerType, "AfterUpdate") is MethodInfo afterUpdate) - { - after.Add(CreateAction(afterUpdate)); - methodFound = true; - } - - if (!methodFound) - { - Log($"No BeforeUpdate or AfterUpdate method found on '{handlerType}'."); - } - - static Action CreateAction(MethodInfo update) - { - Action action = update.CreateDelegate>(); - return types => - { - try - { - action(types); - } - catch (Exception ex) - { - Log($"Exception from '{action}': {ex}"); - } - }; - } - - static MethodInfo? GetUpdateMethod(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; - } - } - } - - return new UpdateHandlerActions(before, after); - } - - public static async Task ReceiveDeltas() - { - Log("Attempting to receive deltas."); - - // This value is configured by dotnet-watch when the app is to be launched. - var namedPipeName = Environment.GetEnvironmentVariable("DOTNET_HOTRELOAD_NAMEDPIPE_NAME") ?? - throw new InvalidOperationException("DOTNET_HOTRELOAD_NAMEDPIPE_NAME was not specified."); - - using var pipeClient = new NamedPipeClientStream(".", namedPipeName, PipeDirection.InOut, PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous); - try - { - await pipeClient.ConnectAsync(5000); - Log("Connected."); - } - catch (TimeoutException) - { - Log("Unable to connect to hot-reload server."); - return; - } - - while (pipeClient.IsConnected) - { - var update = await UpdatePayload.ReadAsync(pipeClient, default); - Log("Attempting to apply deltas."); - - try - { - UpdateHandlerActions beforeAfterUpdates = s_beforeAfterUpdates ??= GetMetadataUpdateHandlerActions(); - - beforeAfterUpdates.Before.ForEach(b => b(null)); // TODO: Get types to pass in - - foreach (var item in update.Deltas) - { - var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.Modules.FirstOrDefault() is Module m && m.ModuleVersionId == item.ModuleId); - if (assembly is not null) - { - System.Reflection.Metadata.AssemblyExtensions.ApplyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan.Empty); - } - } - - // We want to base this off of mvids, but we'll figure that out eventually. - var applyResult = update.ChangedFile is string changedFile && changedFile.EndsWith(".razor", StringComparison.Ordinal) ? - ApplyResult.Success : - ApplyResult.Success_RefreshBrowser; - pipeClient.WriteByte((byte)applyResult); - - // Defer discovering the receiving deltas until the first hot reload delta. - // This should give enough opportunity for AppDomain.GetAssemblies() to be sufficiently populated. - - beforeAfterUpdates.After.ForEach(a => a(null)); // TODO: Get types to pass in - - Log("Deltas applied."); - } - catch (Exception ex) - { - Log(ex.ToString()); - } - } - Log("Stopped received delta updates. Server is no longer connected."); - } - - private static void Log(string message) - { - if (LogDeltaClientMessages) - { - Console.WriteLine(message); - } - } -} diff --git a/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs b/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs new file mode 100644 index 000000000000..cbbf5b9f8631 --- /dev/null +++ b/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs @@ -0,0 +1,165 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Metadata; +using Microsoft.DotNet.Watcher.Tools; + +namespace Microsoft.Extensions.HotReload +{ + internal class HotReloadAgent : IDisposable + { + private readonly Action _log; + private readonly AssemblyLoadEventHandler _assemblyLoad; + private readonly ConcurrentDictionary> _deltas = new(); + private readonly ConcurrentDictionary _appliedAssemblies = new(); + private volatile UpdateHandlerActions? _beforeAfterUpdates; + + public HotReloadAgent(Action log) + { + _log = log; + _assemblyLoad = OnAssemblyLoad; + AppDomain.CurrentDomain.AssemblyLoad += _assemblyLoad; + } + + private void OnAssemblyLoad(object? _, AssemblyLoadEventArgs eventArgs) + { + _beforeAfterUpdates = null; + var loadedAssembly = eventArgs.LoadedAssembly; + var moduleId = loadedAssembly.Modules.FirstOrDefault()?.ModuleVersionId; + 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(updateDeltas); + } + } + + private sealed class UpdateHandlerActions + { + public UpdateHandlerActions(List> before, List> after) + { + Before = before; + After = after; + } + + public List> Before { get; } + public List> After { get; } + } + + private UpdateHandlerActions GetMetadataUpdateHandlerActions() + { + var before = new List>(); + var after = new List>(); + + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + foreach (var attr in assembly.GetCustomAttributes()) + { + bool methodFound = false; + var handlerType = attr.HandlerType; + + if (GetUpdateMethod(handlerType, "BeforeUpdate") is MethodInfo beforeUpdate) + { + before.Add(CreateAction(beforeUpdate)); + methodFound = true; + } + + if (GetUpdateMethod(handlerType, "AfterUpdate") is MethodInfo afterUpdate) + { + after.Add(CreateAction(afterUpdate)); + methodFound = true; + } + + if (!methodFound) + { + _log($"No BeforeUpdate or AfterUpdate method found on '{handlerType}'."); + } + + Action CreateAction(MethodInfo update) + { + Action action = update.CreateDelegate>(); + return types => + { + try + { + action(types); + } + catch (Exception ex) + { + _log($"Exception from '{action}': {ex}"); + } + }; + } + + MethodInfo? GetUpdateMethod(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; + } + } + } + + return new UpdateHandlerActions(before, after); + } + + public void ApplyDeltas(IReadOnlyList deltas) + { + try + { + UpdateHandlerActions beforeAfterUpdates = _beforeAfterUpdates ??= GetMetadataUpdateHandlerActions(); + + beforeAfterUpdates.Before.ForEach(b => b(null)); // TODO: Get types to pass in + + foreach (var item in deltas) + { + var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.Modules.FirstOrDefault() is Module m && m.ModuleVersionId == item.ModuleId); + if (assembly is not null) + { + System.Reflection.Metadata.AssemblyExtensions.ApplyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan.Empty); + } + } + + // Defer discovering the receiving deltas until the first hot reload delta. + // This should give enough opportunity for AppDomain.GetAssemblies() to be sufficiently populated. + + beforeAfterUpdates.After.ForEach(a => a(null)); // TODO: Get types to pass in + + _log("Deltas applied."); + } + catch (Exception ex) + { + _log(ex.ToString()); + } + } + + public void Dispose() + { + AppDomain.CurrentDomain.AssemblyLoad -= _assemblyLoad; + } + } +} diff --git a/src/BuiltInTools/AspNetCoreDeltaApplier/Microsoft.Extensions.AspNetCoreDeltaApplier.csproj b/src/BuiltInTools/DotNetDeltaApplier/Microsoft.Extensions.DotNetDeltaApplier.csproj similarity index 83% rename from src/BuiltInTools/AspNetCoreDeltaApplier/Microsoft.Extensions.AspNetCoreDeltaApplier.csproj rename to src/BuiltInTools/DotNetDeltaApplier/Microsoft.Extensions.DotNetDeltaApplier.csproj index ceb309199aaa..40927ae1f263 100644 --- a/src/BuiltInTools/AspNetCoreDeltaApplier/Microsoft.Extensions.AspNetCoreDeltaApplier.csproj +++ b/src/BuiltInTools/DotNetDeltaApplier/Microsoft.Extensions.DotNetDeltaApplier.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs b/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs new file mode 100644 index 000000000000..6e78cf953f78 --- /dev/null +++ b/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO.Pipes; +using System.Threading.Tasks; +using Microsoft.DotNet.Watcher.Tools; +using Microsoft.Extensions.HotReload; + +internal sealed class StartupHook +{ + private static readonly bool LogDeltaClientMessages = Environment.GetEnvironmentVariable("HOTRELOAD_DELTA_CLIENT_LOG_MESSAGES") == "1"; + + public static void Initialize() + { + Task.Run(async () => + { + using var hotReloadAgent = new HotReloadAgent(Log); + try + { + await ReceiveDeltas(hotReloadAgent); + } + catch (Exception ex) + { + Log(ex.Message); + } + }); + } + + public static async Task ReceiveDeltas(HotReloadAgent hotReloadAgent) + { + Log("Attempting to receive deltas."); + + // This value is configured by dotnet-watch when the app is to be launched. + var namedPipeName = Environment.GetEnvironmentVariable("DOTNET_HOTRELOAD_NAMEDPIPE_NAME") ?? + throw new InvalidOperationException("DOTNET_HOTRELOAD_NAMEDPIPE_NAME was not specified."); + + using var pipeClient = new NamedPipeClientStream(".", namedPipeName, PipeDirection.InOut, PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous); + try + { + await pipeClient.ConnectAsync(5000); + Log("Connected."); + } + catch (TimeoutException) + { + Log("Unable to connect to hot-reload server."); + return; + } + + while (pipeClient.IsConnected) + { + var update = await UpdatePayload.ReadAsync(pipeClient, default); + Log("Attempting to apply deltas."); + + hotReloadAgent.ApplyDeltas(update.Deltas); + + // We want to base this off of mvids, but we'll figure that out eventually. + var applyResult = update.ChangedFile is string changedFile && changedFile.EndsWith(".razor", StringComparison.Ordinal) ? + ApplyResult.Success : + ApplyResult.Success_RefreshBrowser; + pipeClient.WriteByte((byte)ApplyResult.Success); + + } + Log("Stopped received delta updates. Server is no longer connected."); + } + + private static void Log(string message) + { + if (LogDeltaClientMessages) + { + Console.WriteLine(message); + } + } +} diff --git a/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs b/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs index 33886f17a335..54381ee6e45c 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs @@ -42,7 +42,7 @@ public async ValueTask InitializeAsync(DotNetWatchContext context, CancellationT if (context.Iteration == 0) { - var deltaApplier = Path.Combine(AppContext.BaseDirectory, "hotreload", "Microsoft.Extensions.AspNetCoreDeltaApplier.dll"); + var deltaApplier = Path.Combine(AppContext.BaseDirectory, "hotreload", "Microsoft.Extensions.DotNetDeltaApplier.dll"); context.ProcessSpec.EnvironmentVariables.DotNetStartupHooks.Add(deltaApplier); // Configure the app for EnC diff --git a/src/BuiltInTools/dotnet-watch/HotReload/AspNetCoreContract.cs b/src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/HotReload/AspNetCoreContract.cs rename to src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs diff --git a/src/BuiltInTools/dotnet-watch/dotnet-watch.slnf b/src/BuiltInTools/dotnet-watch/dotnet-watch.slnf index 97ee57361676..05b870c21e16 100644 --- a/src/BuiltInTools/dotnet-watch/dotnet-watch.slnf +++ b/src/BuiltInTools/dotnet-watch/dotnet-watch.slnf @@ -2,7 +2,7 @@ "solution": { "path": "..\\..\\..\\sdk.sln", "projects": [ - "src\\BuiltInTools\\AspNetCoreDeltaApplier\\Microsoft.Extensions.AspNetCoreDeltaApplier.csproj", + "src\\BuiltInTools\\DotNetDeltaApplier\\Microsoft.Extensions.DotNetDeltaApplier.csproj", "src\\BuiltInTools\\BrowserRefresh\\Microsoft.AspNetCore.Watch.BrowserRefresh.csproj", "src\\BuiltInTools\\DotNetWatchTasks\\DotNetWatchTasks.csproj", "src\\BuiltInTools\\dotnet-watch\\dotnet-watch.csproj", diff --git a/src/Layout/redist/targets/GenerateLayout.targets b/src/Layout/redist/targets/GenerateLayout.targets index 83325abecf3b..0360d93e980a 100644 --- a/src/Layout/redist/targets/GenerateLayout.targets +++ b/src/Layout/redist/targets/GenerateLayout.targets @@ -141,7 +141,7 @@ - + diff --git a/src/Layout/redist/targets/OverlaySdkOnLKG.targets b/src/Layout/redist/targets/OverlaySdkOnLKG.targets index b42deac17f80..71da7f5a2a22 100644 --- a/src/Layout/redist/targets/OverlaySdkOnLKG.targets +++ b/src/Layout/redist/targets/OverlaySdkOnLKG.targets @@ -45,7 +45,7 @@ - + diff --git a/src/RazorSdk/Razor.slnf b/src/RazorSdk/Razor.slnf index 0cfea7b0c655..77001c70748f 100644 --- a/src/RazorSdk/Razor.slnf +++ b/src/RazorSdk/Razor.slnf @@ -2,7 +2,6 @@ "solution": { "path": "..\\..\\sdk.sln", "projects": [ - "src\\BuiltInTools\\AspNetCoreDeltaApplier\\Microsoft.Extensions.AspNetCoreDeltaApplier.csproj", "src\\RazorSdk\\SourceGenerators\\Microsoft.NET.Sdk.Razor.SourceGenerators.csproj", "src\\RazorSdk\\Tasks\\Microsoft.NET.Sdk.Razor.Tasks.csproj", "src\\RazorSdk\\Tool\\Microsoft.NET.Sdk.Razor.Tool.csproj",