Skip to content
Open
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
83 changes: 56 additions & 27 deletions src/BuiltInTools/DotNetDeltaApplier/PipeListener.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.IO.Pipes;
using System.Reflection;
using System.Runtime.Loader;
Expand All @@ -9,6 +10,17 @@ namespace Microsoft.DotNet.HotReload;

internal sealed class PipeListener(string pipeName, IHotReloadAgent agent, Action<string> log, int connectionTimeoutMS = 5000)
{
/// <summary>
/// Messages to the client sent after the initial <see cref="ClientInitializationResponse"/> is sent
/// need to be sent while holding this lock in order to synchronize
/// 1) responses to requests received from the client (e.g. <see cref="UpdateResponse"/>) or
/// 2) notifications sent to the client that may be triggered at arbitrary times (e.g. <see cref="HotReloadExceptionCreatedNotification"/>).
/// </summary>
private readonly SemaphoreSlim _messageToClientLock = new(initialCount: 1);

// Not-null once initialized:
private NamedPipeClientStream? _pipeClient;

public Task Listen(CancellationToken cancellationToken)
{
// Connect to the pipe synchronously.
Expand All @@ -21,23 +33,23 @@ public Task Listen(CancellationToken cancellationToken)

log($"Connecting to hot-reload server via pipe {pipeName}");

var pipeClient = new NamedPipeClientStream(serverName: ".", pipeName, PipeDirection.InOut, PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous);
_pipeClient = new NamedPipeClientStream(serverName: ".", pipeName, PipeDirection.InOut, PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous);
try
{
pipeClient.Connect(connectionTimeoutMS);
_pipeClient.Connect(connectionTimeoutMS);
log("Connected.");
}
catch (TimeoutException)
{
log($"Failed to connect in {connectionTimeoutMS}ms.");
pipeClient.Dispose();
_pipeClient.Dispose();
return Task.CompletedTask;
}

try
{
// block execution of the app until initial updates are applied:
InitializeAsync(pipeClient, cancellationToken).GetAwaiter().GetResult();
InitializeAsync(cancellationToken).GetAwaiter().GetResult();
}
catch (Exception e)
{
Expand All @@ -46,7 +58,7 @@ public Task Listen(CancellationToken cancellationToken)
log(e.Message);
}

pipeClient.Dispose();
_pipeClient.Dispose();
agent.Dispose();

return Task.CompletedTask;
Expand All @@ -56,48 +68,52 @@ public Task Listen(CancellationToken cancellationToken)
{
try
{
await ReceiveAndApplyUpdatesAsync(pipeClient, initialUpdates: false, cancellationToken);
await ReceiveAndApplyUpdatesAsync(initialUpdates: false, cancellationToken);
}
catch (Exception e) when (e is not OperationCanceledException)
{
log(e.Message);
}
finally
{
pipeClient.Dispose();
_pipeClient.Dispose();
agent.Dispose();
}
}, cancellationToken);
}

private async Task InitializeAsync(NamedPipeClientStream pipeClient, CancellationToken cancellationToken)
private async Task InitializeAsync(CancellationToken cancellationToken)
{
Debug.Assert(_pipeClient != null);

agent.Reporter.Report("Writing capabilities: " + agent.Capabilities, AgentMessageSeverity.Verbose);

var initPayload = new ClientInitializationResponse(agent.Capabilities);
await initPayload.WriteAsync(pipeClient, cancellationToken);
await initPayload.WriteAsync(_pipeClient, cancellationToken);

// Apply updates made before this process was launched to avoid executing unupdated versions of the affected modules.

// We should only receive ManagedCodeUpdate when when the debugger isn't attached,
// otherwise the initialization should send InitialUpdatesCompleted immediately.
// The debugger itself applies these updates when launching process with the debugger attached.
await ReceiveAndApplyUpdatesAsync(pipeClient, initialUpdates: true, cancellationToken);
await ReceiveAndApplyUpdatesAsync(initialUpdates: true, cancellationToken);
}

private async Task ReceiveAndApplyUpdatesAsync(NamedPipeClientStream pipeClient, bool initialUpdates, CancellationToken cancellationToken)
private async Task ReceiveAndApplyUpdatesAsync(bool initialUpdates, CancellationToken cancellationToken)
{
while (pipeClient.IsConnected)
Debug.Assert(_pipeClient != null);

while (_pipeClient.IsConnected)
{
var payloadType = (RequestType)await pipeClient.ReadByteAsync(cancellationToken);
var payloadType = (RequestType)await _pipeClient.ReadByteAsync(cancellationToken);
switch (payloadType)
{
case RequestType.ManagedCodeUpdate:
await ReadAndApplyManagedCodeUpdateAsync(pipeClient, cancellationToken);
await ReadAndApplyManagedCodeUpdateAsync(cancellationToken);
break;

case RequestType.StaticAssetUpdate:
await ReadAndApplyStaticAssetUpdateAsync(pipeClient, cancellationToken);
await ReadAndApplyStaticAssetUpdateAsync(cancellationToken);
break;

case RequestType.InitialUpdatesCompleted when initialUpdates:
Expand All @@ -110,11 +126,11 @@ private async Task ReceiveAndApplyUpdatesAsync(NamedPipeClientStream pipeClient,
}
}

private async ValueTask ReadAndApplyManagedCodeUpdateAsync(
NamedPipeClientStream pipeClient,
CancellationToken cancellationToken)
private async ValueTask ReadAndApplyManagedCodeUpdateAsync(CancellationToken cancellationToken)
{
var request = await ManagedCodeUpdateRequest.ReadAsync(pipeClient, cancellationToken);
Debug.Assert(_pipeClient != null);

var request = await ManagedCodeUpdateRequest.ReadAsync(_pipeClient, cancellationToken);

bool success;
try
Expand All @@ -131,15 +147,14 @@ private async ValueTask ReadAndApplyManagedCodeUpdateAsync(

var logEntries = agent.Reporter.GetAndClearLogEntries(request.ResponseLoggingLevel);

var response = new UpdateResponse(logEntries, success);
await response.WriteAsync(pipeClient, cancellationToken);
await SendResponseAsync(new UpdateResponse(logEntries, success), cancellationToken);
}

private async ValueTask ReadAndApplyStaticAssetUpdateAsync(
NamedPipeClientStream pipeClient,
CancellationToken cancellationToken)
private async ValueTask ReadAndApplyStaticAssetUpdateAsync(CancellationToken cancellationToken)
{
var request = await StaticAssetUpdateRequest.ReadAsync(pipeClient, cancellationToken);
Debug.Assert(_pipeClient != null);

var request = await StaticAssetUpdateRequest.ReadAsync(_pipeClient, cancellationToken);

try
{
Expand All @@ -155,8 +170,22 @@ private async ValueTask ReadAndApplyStaticAssetUpdateAsync(
// Updating static asset only invokes ContentUpdate metadata update handlers.
// Failures of these handlers are reported to the log and ignored.
// Therefore, this request always succeeds.
var response = new UpdateResponse(logEntries, success: true);
await SendResponseAsync(new UpdateResponse(logEntries, success: true), cancellationToken);
}

await response.WriteAsync(pipeClient, cancellationToken);
internal async ValueTask SendResponseAsync<T>(T response, CancellationToken cancellationToken)
where T : IResponse
{
Debug.Assert(_pipeClient != null);
try
{
await _messageToClientLock.WaitAsync(cancellationToken);
await _pipeClient.WriteAsync((byte)response.Type, cancellationToken);
await response.WriteAsync(_pipeClient, cancellationToken);
}
finally
{
_messageToClientLock.Release();
}
}
}
49 changes: 42 additions & 7 deletions src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,49 @@ public static void Initialize()

RegisterSignalHandlers();

var agent = new HotReloadAgent(assemblyResolvingHandler: (_, args) =>
{
Log($"Resolving '{args.Name}, Version={args.Version}'");
var path = Path.Combine(processDir, args.Name + ".dll");
return File.Exists(path) ? AssemblyLoadContext.Default.LoadFromAssemblyPath(path) : null;
});
PipeListener? listener = null;

var agent = new HotReloadAgent(
assemblyResolvingHandler: (_, args) =>
{
Log($"Resolving '{args.Name}, Version={args.Version}'");
var path = Path.Combine(processDir, args.Name + ".dll");
return File.Exists(path) ? AssemblyLoadContext.Default.LoadFromAssemblyPath(path) : null;
},
hotReloadExceptionCreateHandler: (code, message) =>
{
// Continue executing the code if the debugger is attached.
// It will throw the exception and the debugger will handle it.
if (Debugger.IsAttached)
{
return;
}

Debug.Assert(listener != null);
Log($"Runtime rude edit detected: '{message}'");

SendAndForgetAsync().Wait();

// Handle Ctrl+C to terminate gracefully:
Console.CancelKeyPress += (_, _) => Environment.Exit(0);

// wait for the process to be terminated by the Hot Reload client (other threads might still execute):
Thread.Sleep(Timeout.Infinite);

async Task SendAndForgetAsync()
{
try
{
await listener.SendResponseAsync(new HotReloadExceptionCreatedNotification(code, message), CancellationToken.None);
}
catch
{
// do not crash the app
}
}
});

var listener = new PipeListener(s_namedPipeName, agent, Log);
listener = new PipeListener(s_namedPipeName, agent, Log);

// fire and forget:
_ = listener.Listen(CancellationToken.None);
Expand Down
50 changes: 45 additions & 5 deletions src/BuiltInTools/HotReloadAgent.PipeRpc/NamedPipeContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,39 @@

namespace Microsoft.DotNet.HotReload;

internal interface IRequest
internal interface IMessage
{
RequestType Type { get; }
ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken);
}

internal interface IRequest : IMessage
{
RequestType Type { get; }
}

internal interface IResponse : IMessage
{
ResponseType Type { get; }
}

internal interface IUpdateRequest : IRequest
{
}

internal enum RequestType
internal enum RequestType : byte
{
ManagedCodeUpdate = 1,
StaticAssetUpdate = 2,
InitialUpdatesCompleted = 3,
}

internal enum ResponseType : byte
{
InitializationResponse = 1,
UpdateResponse = 2,
HotReloadExceptionNotification = 3,
}

internal readonly struct ManagedCodeUpdateRequest(IReadOnlyList<RuntimeManagedCodeUpdate> updates, ResponseLoggingLevel responseLoggingLevel) : IUpdateRequest
{
private const byte Version = 4;
Expand Down Expand Up @@ -81,8 +97,10 @@ public static async ValueTask<ManagedCodeUpdateRequest> ReadAsync(Stream stream,
}
}

internal readonly struct UpdateResponse(IReadOnlyCollection<(string message, AgentMessageSeverity severity)> log, bool success)
internal readonly struct UpdateResponse(IReadOnlyCollection<(string message, AgentMessageSeverity severity)> log, bool success) : IResponse
{
public ResponseType Type => ResponseType.UpdateResponse;

public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken)
{
await stream.WriteAsync(success, cancellationToken);
Expand Down Expand Up @@ -116,10 +134,12 @@ public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationT
}
}

internal readonly struct ClientInitializationResponse(string capabilities)
internal readonly struct ClientInitializationResponse(string capabilities) : IResponse
{
private const byte Version = 0;

public ResponseType Type => ResponseType.InitializationResponse;

public string Capabilities { get; } = capabilities;

public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken)
Expand All @@ -141,6 +161,26 @@ public static async ValueTask<ClientInitializationResponse> ReadAsync(Stream str
}
}

internal readonly struct HotReloadExceptionCreatedNotification(int code, string message) : IResponse
{
public ResponseType Type => ResponseType.HotReloadExceptionNotification;
public int Code => code;
public string Message => message;

public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken)
{
await stream.WriteAsync(code, cancellationToken);
await stream.WriteAsync(message, cancellationToken);
}

public static async ValueTask<HotReloadExceptionCreatedNotification> ReadAsync(Stream stream, CancellationToken cancellationToken)
{
var code = await stream.ReadInt32Async(cancellationToken);
var message = await stream.ReadStringAsync(cancellationToken);
return new HotReloadExceptionCreatedNotification(code, message);
}
}

internal readonly struct StaticAssetUpdateRequest(
RuntimeStaticAssetUpdate update,
ResponseLoggingLevel responseLoggingLevel) : IUpdateRequest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ public static async Task InitializeAsync(string baseUri)
{
s_initialized = true;

var agent = new HotReloadAgent(assemblyResolvingHandler: null);
// TODO: Implement hotReloadExceptionCreateHandler: https://github.com/dotnet/sdk/issues/51056
var agent = new HotReloadAgent(assemblyResolvingHandler: null, hotReloadExceptionCreateHandler: null);

var existingAgent = Interlocked.CompareExchange(ref s_hotReloadAgent, agent, null);
if (existingAgent != null)
Expand Down
Loading
Loading