File: PipeListener.cs
Web Access
Project: ..\..\..\src\BuiltInTools\DotNetDeltaApplier\Microsoft.Extensions.DotNetDeltaApplier.csproj (Microsoft.Extensions.DotNetDeltaApplier)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.IO.Pipes;
using System.Reflection;
using System.Runtime.Loader;
 
namespace Microsoft.DotNet.HotReload;
 
internal sealed class PipeListener(string pipeName, IHotReloadAgent agent, Action<string> log, int connectionTimeoutMS = 5000)
{
    public Task Listen(CancellationToken cancellationToken)
    {
        // Connect to the pipe synchronously.
        //
        // If a debugger is attached and there is a breakpoint in the startup code connecting asynchronously would
        // set up a race between this code connecting to the server, and the breakpoint being hit. If the breakpoint
        // hits first, applying changes will throw an error that the client is not connected.
        //
        // Updates made before the process is launched need to be applied before loading the affected modules.
 
        log($"Connecting to hot-reload server via pipe {pipeName}");
 
        var pipeClient = new NamedPipeClientStream(serverName: ".", pipeName, PipeDirection.InOut, PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous);
        try
        {
            pipeClient.Connect(connectionTimeoutMS);
            log("Connected.");
        }
        catch (TimeoutException)
        {
            log($"Failed to connect in {connectionTimeoutMS}ms.");
            pipeClient.Dispose();
            return Task.CompletedTask;
        }
 
        try
        {
            // block execution of the app until initial updates are applied:
            InitializeAsync(pipeClient, cancellationToken).GetAwaiter().GetResult();
        }
        catch (Exception e)
        {
            if (e is not OperationCanceledException)
            {
                log(e.Message);
            }
 
            pipeClient.Dispose();
            agent.Dispose();
 
            return Task.CompletedTask;
        }
 
        return Task.Run(async () =>
        {
            try
            {
                await ReceiveAndApplyUpdatesAsync(pipeClient, initialUpdates: false, cancellationToken);
            }
            catch (Exception e) when (e is not OperationCanceledException)
            {
                log(e.Message);
            }
            finally
            {
                pipeClient.Dispose();
                agent.Dispose();
            }
        }, cancellationToken);
    }
 
    private async Task InitializeAsync(NamedPipeClientStream pipeClient, CancellationToken cancellationToken)
    {
        agent.Reporter.Report("Writing capabilities: " + agent.Capabilities, AgentMessageSeverity.Verbose);
 
        var initPayload = new ClientInitializationResponse(agent.Capabilities);
        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);
    }
 
    private async Task ReceiveAndApplyUpdatesAsync(NamedPipeClientStream pipeClient, bool initialUpdates, CancellationToken cancellationToken)
    {
        while (pipeClient.IsConnected)
        {
            var payloadType = (RequestType)await pipeClient.ReadByteAsync(cancellationToken);
            switch (payloadType)
            {
                case RequestType.ManagedCodeUpdate:
                    await ReadAndApplyManagedCodeUpdateAsync(pipeClient, cancellationToken);
                    break;
 
                case RequestType.StaticAssetUpdate:
                    await ReadAndApplyStaticAssetUpdateAsync(pipeClient, cancellationToken);
                    break;
 
                case RequestType.InitialUpdatesCompleted when initialUpdates:
                    return;
 
                default:
                    // can't continue, the pipe content is in an unknown state
                    throw new InvalidOperationException($"Unexpected payload type: {payloadType}");
            }
        }
    }
 
    private async ValueTask ReadAndApplyManagedCodeUpdateAsync(
        NamedPipeClientStream pipeClient,
        CancellationToken cancellationToken)
    {
        var request = await ManagedCodeUpdateRequest.ReadAsync(pipeClient, cancellationToken);
 
        bool success;
        try
        {
            agent.ApplyManagedCodeUpdates(request.Updates);
            success = true;
        }
        catch (Exception e)
        {
            agent.Reporter.Report($"The runtime failed to applying the change: {e.Message}", AgentMessageSeverity.Error);
            agent.Reporter.Report("Further changes won't be applied to this process.", AgentMessageSeverity.Warning);
            success = false;
        }
 
        var logEntries = agent.Reporter.GetAndClearLogEntries(request.ResponseLoggingLevel);
 
        var response = new UpdateResponse(logEntries, success);
        await response.WriteAsync(pipeClient, cancellationToken);
    }
 
    private async ValueTask ReadAndApplyStaticAssetUpdateAsync(
        NamedPipeClientStream pipeClient,
        CancellationToken cancellationToken)
    {
        var request = await StaticAssetUpdateRequest.ReadAsync(pipeClient, cancellationToken);
 
        try
        {
            agent.ApplyStaticAssetUpdate(request.Update);
        }
        catch (Exception e)
        {
            agent.Reporter.Report($"Failed to apply static asset update: {e.Message}", AgentMessageSeverity.Error);
        }
 
        var logEntries = agent.Reporter.GetAndClearLogEntries(request.ResponseLoggingLevel);
 
        // 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 response.WriteAsync(pipeClient, cancellationToken);
    }
}