Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/Assets/TestProjects/WatchApp60/Program.cs
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
8 changes: 8 additions & 0 deletions src/Assets/TestProjects/WatchApp60/WatchApp60.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net60</TargetFramework>
<OutputType>exe</OutputType>
</PropertyGroup>

</Project>
41 changes: 36 additions & 5 deletions src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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 readonly ApplyUpdateDelegate? _applyUpdate;
private readonly string? _capabilities;
private volatile UpdateHandlerActions? _handlerActions;

public HotReloadAgent(Action<string> 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<byte>), typeof(ReadOnlySpan<byte>), typeof(ReadOnlySpan<byte>) }, 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;
Expand Down Expand Up @@ -107,7 +133,7 @@ internal void GetHandlerActions(UpdateHandlerActions handlerActions, Type handle

Action<Type[]?> CreateAction(MethodInfo update)
{
Action<Type[]?> action = update.CreateDelegate<Action<Type[]?>>();
var action = (Action<Type[]?>)update.CreateDelegate(typeof(Action<Type[]?>));
return types =>
{
try
Expand All @@ -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;
Expand Down Expand Up @@ -178,14 +204,17 @@ static void Visit(Assembly[] assemblies, Assembly assembly, List<Assembly> sorte

public void ApplyDeltas(IReadOnlyList<UpdateDelta> deltas)
{
Debug.Assert(Capabilities.Length > 0);
Debug.Assert(_applyUpdate != null);

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)
{
MetadataUpdater.ApplyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan<byte>.Empty);
_applyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan<byte>.Empty);
}
}

Expand Down Expand Up @@ -244,11 +273,13 @@ private Type[] GetMetadataUpdateTypes(IReadOnlyList<UpdateDelta> deltas)

public void ApplyDeltas(Assembly assembly, IReadOnlyList<UpdateDelta> deltas)
{
Debug.Assert(_applyUpdate != null);

try
{
foreach (var item in deltas)
{
MetadataUpdater.ApplyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan<byte>.Empty);
_applyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan<byte>.Empty);
}

_log("Deltas applied.");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Intentionally pinned. This feature is supported in projects targeting 6.0 or newer.-->
<TargetFramework>net7.0</TargetFramework>
<!--
dotnet-watch may inject this assembly to .NET 6.0+ app, so we can't target a newer version.
At the same time source build requires us to not target 6.0, so we fall back to netstandard.
-->
<TargetFramework>netstandard2.1</TargetFramework>
<StrongNameKeyId>MicrosoftAspNetCore</StrongNameKeyId>

<IsPackable>false</IsPackable>
Expand Down
17 changes: 5 additions & 12 deletions src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ internal sealed class StartupHook
{
private static readonly bool LogDeltaClientMessages = Environment.GetEnvironmentVariable("HOTRELOAD_DELTA_CLIENT_LOG_MESSAGES") == "1";

/// <summary>
/// Invoked by the runtime when the containing assembly is listed in DOTNET_STARTUP_HOOKS.
/// </summary>
public static void Initialize()
{
ClearHotReloadEnvironmentVariables(Environment.GetEnvironmentVariable, Environment.SetEnvironmentVariable);
Expand Down Expand Up @@ -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);

Expand All @@ -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)
Expand Down
15 changes: 5 additions & 10 deletions src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,11 @@ public async ValueTask<bool> Apply(DotNetWatchContext context, ImmutableArray<Wa
return false;
}

var payload = new UpdatePayload
{
Deltas = ImmutableArray.CreateRange(solutionUpdate, c => 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);
Expand Down
63 changes: 44 additions & 19 deletions src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,8 +15,16 @@ internal readonly struct UpdatePayload
{
private static readonly byte Version = 1;

public IReadOnlyList<UpdateDelta> Deltas { get; init; }
public IReadOnlyList<UpdateDelta> Deltas { get; }

public UpdatePayload(IReadOnlyList<UpdateDelta> deltas)
{
Deltas = deltas;
}

/// <summary>
/// Called by the dotnet-watch.
/// </summary>
public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken)
{
await using var binaryWriter = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true);
Expand Down Expand Up @@ -54,6 +63,9 @@ static void WriteIntArray(BinaryWriter binaryWriter, int[] values)
}
}

/// <summary>
/// Called by delta applier.
/// </summary>
public static async ValueTask<UpdatePayload> ReadAsync(Stream stream, CancellationToken cancellationToken)
{
using var binaryReader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
Expand All @@ -68,21 +80,15 @@ public static async ValueTask<UpdatePayload> 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<byte[]> ReadBytesAsync(BinaryReader binaryReader, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -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
Expand All @@ -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;
}

/// <summary>
/// Called by delta applier.
/// </summary>
public void Write(Stream stream)
{
using var binaryWriter = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true);
Expand All @@ -147,6 +169,9 @@ public void Write(Stream stream)
binaryWriter.Flush();
}

/// <summary>
/// Called by dotnet-watch.
/// </summary>
public static ClientInitializationPayload Read(Stream stream)
{
using var binaryReader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
Expand All @@ -157,7 +182,7 @@ public static ClientInitializationPayload Read(Stream stream)
}

var capabilities = binaryReader.ReadString();
return new ClientInitializationPayload { Capabilities = capabilities };
return new ClientInitializationPayload(capabilities);
}
}
}
2 changes: 1 addition & 1 deletion src/Layout/redist/targets/GenerateLayout.targets
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@
<ItemGroup>
<DotNetWatchFile Include="$(ArtifactsDir)bin\dotnet-watch\$(Configuration)\$(SdkTargetFramework)\**" />
<DotNetWatchFile Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.Watch.BrowserRefresh\$(Configuration)\netcoreapp3.1\*.dll" DeploymentSubpath="middleware" />
<DotNetWatchFile Include="$(ArtifactsDir)bin\Microsoft.Extensions.DotNetDeltaApplier\$(Configuration)\net7.0\*.dll" DeploymentSubpath="hotreload" />
<DotNetWatchFile Include="$(ArtifactsDir)bin\Microsoft.Extensions.DotNetDeltaApplier\$(Configuration)\netstandard2.1\*.dll" DeploymentSubpath="hotreload" />
<DotNetWatchFile Include="$(ArtifactsDir)bin\DotNetWatchTasks\$(Configuration)\netstandard2.0\DotNetWatchTasks.dll" />
</ItemGroup>

Expand Down
2 changes: 1 addition & 1 deletion src/Layout/redist/targets/OverlaySdkOnLKG.targets
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
<DotNetWatchOverlay Remove="$(ArtifactsDir)bin\dotnet-watch\$(Configuration)\$(SdkTargetFramework)\Microsoft.CodeAnalysis.CSharp.dll" />
<DotNetWatchOverlay Remove="$(ArtifactsDir)bin\dotnet-watch\$(Configuration)\$(SdkTargetFramework)\Microsoft.CodeAnalysis.dll" />
<DotNetWatchOverlay Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.Watch.BrowserRefresh\$(Configuration)\netcoreapp3.1\*.dll" TargetDir="middleware" />
<DotNetWatchOverlay Include="$(ArtifactsDir)bin\Microsoft.Extensions.DotNetDeltaApplier\$(Configuration)\net7.0\*.dll" TargetDir="hotreload" />
<DotNetWatchOverlay Include="$(ArtifactsDir)bin\Microsoft.Extensions.DotNetDeltaApplier\$(Configuration)\netstandard2.1\*.dll" TargetDir="hotreload" />
<DotNetWatchOverlay Include="$(ArtifactsDir)bin\DotNetWatchTasks\$(Configuration)\netstandard2.0\DotNetWatchTasks.dll" />
</ItemGroup>

Expand Down
13 changes: 13 additions & 0 deletions src/Tests/dotnet-watch.Tests/DotNetWatcherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}
Loading