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.Diagnostics;
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)
{
    /// <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.
        //
        // 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}");
 
        _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(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(initialUpdates: false, cancellationToken);
            }
            catch (Exception e) when (e is not OperationCanceledException)
            {
                log(e.Message);
            }
            finally
            {
                _pipeClient.Dispose();
                agent.Dispose();
            }
        }, 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);
 
        // 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(initialUpdates: true, cancellationToken);
    }
 
    private async Task ReceiveAndApplyUpdatesAsync(bool initialUpdates, CancellationToken cancellationToken)
    {
        Debug.Assert(_pipeClient != null);
 
        while (_pipeClient.IsConnected)
        {
            var payloadType = (RequestType)await _pipeClient.ReadByteAsync(cancellationToken);
            switch (payloadType)
            {
                case RequestType.ManagedCodeUpdate:
                    await ReadAndApplyManagedCodeUpdateAsync(cancellationToken);
                    break;
 
                case RequestType.StaticAssetUpdate:
                    await ReadAndApplyStaticAssetUpdateAsync(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(CancellationToken cancellationToken)
    {
        Debug.Assert(_pipeClient != null);
 
        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);
 
        await SendResponseAsync(new UpdateResponse(logEntries, success), cancellationToken);
    }
 
    private async ValueTask ReadAndApplyStaticAssetUpdateAsync(CancellationToken cancellationToken)
    {
        Debug.Assert(_pipeClient != null);
 
        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.
        await SendResponseAsync(new UpdateResponse(logEntries, success: true), 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();
        }
    }
}