diff --git a/src/Assets/TestProjects/WatchApp60/Program.cs b/src/Assets/TestProjects/WatchApp60/Program.cs new file mode 100644 index 000000000000..f3da51ab089b --- /dev/null +++ b/src/Assets/TestProjects/WatchApp60/Program.cs @@ -0,0 +1,24 @@ +// 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.Diagnostics; +using System.Threading; + +namespace ConsoleApplication +{ + public class Program + { + public static void Main(string[] args) + { + Console.WriteLine("Started"); + // Process ID is insufficient because PID's may be reused. + Console.WriteLine($"Process identifier = {Process.GetCurrentProcess().Id}, {Process.GetCurrentProcess().StartTime:hh:mm:ss.FF}"); + if (args.Length > 0 && args[0] == "--no-exit") + { + Thread.Sleep(Timeout.Infinite); + } + Console.WriteLine("Exiting"); + } + } +} diff --git a/src/Assets/TestProjects/WatchApp60/WatchApp60.csproj b/src/Assets/TestProjects/WatchApp60/WatchApp60.csproj new file mode 100644 index 000000000000..841d5ad95c8d --- /dev/null +++ b/src/Assets/TestProjects/WatchApp60/WatchApp60.csproj @@ -0,0 +1,8 @@ + + + + net60 + exe + + + diff --git a/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs b/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs index eeab5275d5cc..0959abeed6ea 100644 --- a/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs +++ b/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs @@ -4,27 +4,53 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; -using System.Reflection.Metadata; namespace Microsoft.Extensions.HotReload { internal sealed class HotReloadAgent : IDisposable { + private delegate void ApplyUpdateDelegate(Assembly assembly, ReadOnlySpan metadataDelta, ReadOnlySpan ilDelta, ReadOnlySpan pdbDelta); + private readonly Action _log; private readonly AssemblyLoadEventHandler _assemblyLoad; private readonly ConcurrentDictionary> _deltas = new(); private readonly ConcurrentDictionary _appliedAssemblies = new(); + private readonly ApplyUpdateDelegate? _applyUpdate; + private readonly string? _capabilities; private volatile UpdateHandlerActions? _handlerActions; public HotReloadAgent(Action log) { + var metadataUpdater = Type.GetType("System.Reflection.Metadata.MetadataUpdater, System.Runtime.Loader", throwOnError: false); + + if (metadataUpdater != null) + { + _applyUpdate = (ApplyUpdateDelegate?)metadataUpdater.GetMethod("ApplyUpdate", BindingFlags.Public | BindingFlags.Static, binder: null, + new[] { typeof(Assembly), typeof(ReadOnlySpan), typeof(ReadOnlySpan), typeof(ReadOnlySpan) }, modifiers: null)?.CreateDelegate(typeof(ApplyUpdateDelegate)); + + if (_applyUpdate != null) + { + try + { + _capabilities = metadataUpdater.GetMethod("GetCapabilities", BindingFlags.NonPublic | BindingFlags.Static, binder: null, Type.EmptyTypes, modifiers: null)?. + Invoke(obj: null, parameters: null) as string; + } + catch + { + } + } + } + _log = log; _assemblyLoad = OnAssemblyLoad; AppDomain.CurrentDomain.AssemblyLoad += _assemblyLoad; } + public string Capabilities => _capabilities ?? string.Empty; + private void OnAssemblyLoad(object? _, AssemblyLoadEventArgs eventArgs) { _handlerActions = null; @@ -107,7 +133,7 @@ internal void GetHandlerActions(UpdateHandlerActions handlerActions, Type handle Action CreateAction(MethodInfo update) { - Action action = update.CreateDelegate>(); + var action = (Action)update.CreateDelegate(typeof(Action)); return types => { try @@ -123,7 +149,7 @@ internal void GetHandlerActions(UpdateHandlerActions handlerActions, Type handle MethodInfo? GetUpdateMethod(Type handlerType, string name) { - if (handlerType.GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, new[] { typeof(Type[]) }) is MethodInfo updateMethod && + if (handlerType.GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, binder: null, new[] { typeof(Type[]) }, modifiers: null) is MethodInfo updateMethod && updateMethod.ReturnType == typeof(void)) { return updateMethod; @@ -178,6 +204,9 @@ static void Visit(Assembly[] assemblies, Assembly assembly, List sorte public void ApplyDeltas(IReadOnlyList deltas) { + Debug.Assert(Capabilities.Length > 0); + Debug.Assert(_applyUpdate != null); + for (var i = 0; i < deltas.Count; i++) { var item = deltas[i]; @@ -185,7 +214,7 @@ public void ApplyDeltas(IReadOnlyList deltas) { if (TryGetModuleId(assembly) is Guid moduleId && moduleId == item.ModuleId) { - MetadataUpdater.ApplyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan.Empty); + _applyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan.Empty); } } @@ -244,11 +273,13 @@ private Type[] GetMetadataUpdateTypes(IReadOnlyList deltas) public void ApplyDeltas(Assembly assembly, IReadOnlyList deltas) { + Debug.Assert(_applyUpdate != null); + try { foreach (var item in deltas) { - MetadataUpdater.ApplyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan.Empty); + _applyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan.Empty); } _log("Deltas applied."); diff --git a/src/BuiltInTools/DotNetDeltaApplier/Microsoft.Extensions.DotNetDeltaApplier.csproj b/src/BuiltInTools/DotNetDeltaApplier/Microsoft.Extensions.DotNetDeltaApplier.csproj index 3c2236e38d14..edb6aea1d2c0 100644 --- a/src/BuiltInTools/DotNetDeltaApplier/Microsoft.Extensions.DotNetDeltaApplier.csproj +++ b/src/BuiltInTools/DotNetDeltaApplier/Microsoft.Extensions.DotNetDeltaApplier.csproj @@ -1,7 +1,10 @@  - - net7.0 + + netstandard2.1 MicrosoftAspNetCore false diff --git a/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs b/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs index d0bfb940826f..a944275f4b12 100644 --- a/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs +++ b/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs @@ -13,6 +13,9 @@ internal sealed class StartupHook { private static readonly bool LogDeltaClientMessages = Environment.GetEnvironmentVariable("HOTRELOAD_DELTA_CLIENT_LOG_MESSAGES") == "1"; + /// + /// Invoked by the runtime when the containing assembly is listed in DOTNET_STARTUP_HOOKS. + /// public static void Initialize() { ClearHotReloadEnvironmentVariables(Environment.GetEnvironmentVariable, Environment.SetEnvironmentVariable); @@ -77,7 +80,7 @@ public static async Task ReceiveDeltas(HotReloadAgent hotReloadAgent) return; } - var initPayload = new ClientInitializationPayload { Capabilities = GetApplyUpdateCapabilities() }; + var initPayload = new ClientInitializationPayload(hotReloadAgent.Capabilities); Log("Writing capabilities: " + initPayload.Capabilities); initPayload.Write(pipeClient); @@ -88,19 +91,9 @@ public static async Task ReceiveDeltas(HotReloadAgent hotReloadAgent) hotReloadAgent.ApplyDeltas(update.Deltas); pipeClient.WriteByte((byte)ApplyResult.Success); - } - Log("Stopped received delta updates. Server is no longer connected."); - } - private 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)!; + Log("Stopped received delta updates. Server is no longer connected."); } private static void Log(string message) diff --git a/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs b/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs index 9637b4248ed8..ca8f7c4ae462 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs @@ -74,16 +74,11 @@ public async ValueTask Apply(DotNetWatchContext context, ImmutableArray new UpdateDelta - { - ModuleId = c.ModuleId, - ILDelta = c.ILDelta.ToArray(), - MetadataDelta = c.MetadataDelta.ToArray(), - UpdatedTypes = c.UpdatedTypes.ToArray(), - }), - }; + var payload = new UpdatePayload(ImmutableArray.CreateRange(solutionUpdate, c => new UpdateDelta( + c.ModuleId, + metadataDelta: c.MetadataDelta.ToArray(), + ilDelta: c.ILDelta.ToArray(), + c.UpdatedTypes.ToArray()))); await payload.WriteAsync(_pipe, cancellationToken); await _pipe.FlushAsync(cancellationToken); diff --git a/src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs b/src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs index f4bcf66f4589..c114a7129005 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -14,8 +15,16 @@ internal readonly struct UpdatePayload { private static readonly byte Version = 1; - public IReadOnlyList Deltas { get; init; } + public IReadOnlyList Deltas { get; } + public UpdatePayload(IReadOnlyList deltas) + { + Deltas = deltas; + } + + /// + /// Called by the dotnet-watch. + /// public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken) { await using var binaryWriter = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true); @@ -54,6 +63,9 @@ static void WriteIntArray(BinaryWriter binaryWriter, int[] values) } } + /// + /// Called by delta applier. + /// public static async ValueTask ReadAsync(Stream stream, CancellationToken cancellationToken) { using var binaryReader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); @@ -68,21 +80,15 @@ public static async ValueTask ReadAsync(Stream stream, Cancellati var deltas = new UpdateDelta[count]; for (var i = 0; i < count; i++) { - var delta = new UpdateDelta - { - ModuleId = Guid.Parse(binaryReader.ReadString()), - MetadataDelta = await ReadBytesAsync(binaryReader, cancellationToken), - ILDelta = await ReadBytesAsync(binaryReader, cancellationToken), - UpdatedTypes = ReadIntArray(binaryReader), - }; + var moduleId = Guid.Parse(binaryReader.ReadString()); + var metadataDelta = await ReadBytesAsync(binaryReader, cancellationToken); + var ilDelta = await ReadBytesAsync(binaryReader, cancellationToken); + var updatedTypes = ReadIntArray(binaryReader); - deltas[i] = delta; + deltas[i] = new UpdateDelta(moduleId, metadataDelta: metadataDelta, ilDelta: ilDelta, updatedTypes); } - return new UpdatePayload - { - Deltas = deltas, - }; + return new UpdatePayload(deltas); static async ValueTask ReadBytesAsync(BinaryReader binaryReader, CancellationToken cancellationToken) { @@ -121,10 +127,18 @@ static int[] ReadIntArray(BinaryReader binaryReader) internal readonly struct UpdateDelta { - public Guid ModuleId { get; init; } - public byte[] MetadataDelta { get; init; } - public byte[] ILDelta { get; init; } - public int[] UpdatedTypes { get; init; } + public Guid ModuleId { get; } + public byte[] MetadataDelta { get; } + public byte[] ILDelta { get; } + public int[] UpdatedTypes { get; } + + public UpdateDelta(Guid moduleId, byte[] metadataDelta, byte[] ilDelta, int[] updatedTypes) + { + ModuleId = moduleId; + MetadataDelta = metadataDelta; + ILDelta = ilDelta; + UpdatedTypes = updatedTypes; + } } internal enum ApplyResult @@ -137,8 +151,16 @@ internal readonly struct ClientInitializationPayload { private const byte Version = 0; - public string Capabilities { get; init; } + public string Capabilities { get; } + + public ClientInitializationPayload(string capabilities) + { + Capabilities = capabilities; + } + /// + /// Called by delta applier. + /// public void Write(Stream stream) { using var binaryWriter = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true); @@ -147,6 +169,9 @@ public void Write(Stream stream) binaryWriter.Flush(); } + /// + /// Called by dotnet-watch. + /// public static ClientInitializationPayload Read(Stream stream) { using var binaryReader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); @@ -157,7 +182,7 @@ public static ClientInitializationPayload Read(Stream stream) } var capabilities = binaryReader.ReadString(); - return new ClientInitializationPayload { Capabilities = capabilities }; + return new ClientInitializationPayload(capabilities); } } } diff --git a/src/Layout/redist/targets/GenerateLayout.targets b/src/Layout/redist/targets/GenerateLayout.targets index d399fe6f2985..566590bcedb3 100644 --- a/src/Layout/redist/targets/GenerateLayout.targets +++ b/src/Layout/redist/targets/GenerateLayout.targets @@ -196,7 +196,7 @@ - + diff --git a/src/Layout/redist/targets/OverlaySdkOnLKG.targets b/src/Layout/redist/targets/OverlaySdkOnLKG.targets index 4052c1581b1a..f0823c76bbef 100644 --- a/src/Layout/redist/targets/OverlaySdkOnLKG.targets +++ b/src/Layout/redist/targets/OverlaySdkOnLKG.targets @@ -77,7 +77,7 @@ - + diff --git a/src/Tests/dotnet-watch.Tests/DotNetWatcherTests.cs b/src/Tests/dotnet-watch.Tests/DotNetWatcherTests.cs index b1e492402114..159e8b01fc6a 100644 --- a/src/Tests/dotnet-watch.Tests/DotNetWatcherTests.cs +++ b/src/Tests/dotnet-watch.Tests/DotNetWatcherTests.cs @@ -195,5 +195,18 @@ public async Task Run_WithHotReloadEnabled_DoesNotReadConsoleIn_InNonInteractive await standardInput.WriteLineAsync(inputString); await app.Process.GetOutputLineAsync($"Echo: {inputString}"); } + + [CoreMSBuildOnlyFact] + public async Task TargetNet60() + { + var testAsset = _testAssetsManager.CopyTestAsset("WatchApp60") + .WithSource() + .Path; + + using var app = new WatchableApp(testAsset, _logger); + + await app.StartWatcherAsync(); + await app.GetProcessIdentifier(); + } } } diff --git a/src/Tests/dotnet-watch.Tests/HotReload/UpdatePayloadTest.cs b/src/Tests/dotnet-watch.Tests/HotReload/UpdatePayloadTest.cs index 2de4a1074281..763f324374ba 100644 --- a/src/Tests/dotnet-watch.Tests/HotReload/UpdatePayloadTest.cs +++ b/src/Tests/dotnet-watch.Tests/HotReload/UpdatePayloadTest.cs @@ -15,24 +15,20 @@ public class UpdatePayloadtest [Fact] public async Task UpdatePayload_CanRoundTrip() { - var initial = new UpdatePayload - { - Deltas = new[] + var initial = new UpdatePayload( + new[] { - new UpdateDelta - { - ModuleId = Guid.NewGuid(), - ILDelta = new byte[] { 0, 0, 1 }, - MetadataDelta = new byte[] { 0, 1, 1 }, - }, - new UpdateDelta - { - ModuleId = Guid.NewGuid(), - ILDelta = new byte[] { 1, 0, 0 }, - MetadataDelta = new byte[] { 1, 0, 1 }, - } - }, - }; + new UpdateDelta( + moduleId: Guid.NewGuid(), + ilDelta: new byte[] { 0, 0, 1 }, + metadataDelta: new byte[] { 0, 1, 1 }, + updatedTypes: Array.Empty()), + new UpdateDelta( + moduleId: Guid.NewGuid(), + ilDelta: new byte[] { 1, 0, 0 }, + metadataDelta: new byte[] { 1, 0, 1 }, + updatedTypes: Array.Empty()) + }); using var stream = new MemoryStream(); await initial.WriteAsync(stream, default); @@ -46,26 +42,20 @@ public async Task UpdatePayload_CanRoundTrip() [Fact] public async Task UpdatePayload_CanRoundTripUpdatedTypes() { - var initial = new UpdatePayload - { - Deltas = new[] + var initial = new UpdatePayload( + new[] { - new UpdateDelta - { - ModuleId = Guid.NewGuid(), - ILDelta = new byte[] { 0, 0, 1 }, - MetadataDelta = new byte[] { 0, 1, 1 }, - UpdatedTypes = new int[] { 60, 74, 22323 }, - }, - new UpdateDelta - { - ModuleId = Guid.NewGuid(), - ILDelta = new byte[] { 1, 0, 0 }, - MetadataDelta = new byte[] { 1, 0, 1 }, - UpdatedTypes = new int[] { -18 }, - } - }, - }; + new UpdateDelta( + moduleId: Guid.NewGuid(), + ilDelta: new byte[] { 0, 0, 1 }, + metadataDelta: new byte[] { 0, 1, 1 }, + updatedTypes: new int[] { 60, 74, 22323 }), + new UpdateDelta( + moduleId: Guid.NewGuid(), + ilDelta: new byte[] { 1, 0, 0 }, + metadataDelta: new byte[] { 1, 0, 1 }, + updatedTypes: new int[] { -18 }) + }); using var stream = new MemoryStream(); await initial.WriteAsync(stream, default); @@ -79,18 +69,15 @@ public async Task UpdatePayload_CanRoundTripUpdatedTypes() [Fact] public async Task UpdatePayload_WithLargeDeltas_CanRoundtrip() { - var initial = new UpdatePayload - { - Deltas = new[] + var initial = new UpdatePayload( + new[] { - new UpdateDelta - { - ModuleId = Guid.NewGuid(), - ILDelta = Enumerable.Range(0, 68200).Select(c => (byte)(c%2)).ToArray(), - MetadataDelta = new byte[] { 0, 1, 1 }, - }, - }, - }; + new UpdateDelta( + moduleId: Guid.NewGuid(), + ilDelta: Enumerable.Range(0, 68200).Select(c => (byte)(c%2)).ToArray(), + metadataDelta: new byte[] { 0, 1, 1 }, + updatedTypes: Array.Empty()) + }); using var stream = new MemoryStream(); await initial.WriteAsync(stream, default);