File: HotReload\HotReloadClients.cs
Web Access
Project: ..\..\..\src\BuiltInTools\dotnet-watch\dotnet-watch.csproj (dotnet-watch)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Immutable;
using Microsoft.Build.Graph;
using Microsoft.DotNet.Watch;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.DotNet.HotReload;
 
internal sealed class HotReloadClients(ImmutableArray<(HotReloadClient client, string name)> clients, BrowserRefreshServer? browserRefreshServer) : IDisposable
{
    public HotReloadClients(HotReloadClient client, BrowserRefreshServer? browserRefreshServer)
        : this([(client, "")], browserRefreshServer)
    {
    }
 
    public void Dispose()
    {
        foreach (var (client, _) in clients)
        {
            client.Dispose();
        }
    }
 
    public BrowserRefreshServer? BrowserRefreshServer
        => browserRefreshServer;
 
    /// <summary>
    /// All clients share the same loggers.
    /// </summary>
    public ILogger ClientLogger
        => clients.First().client.Logger;
 
    /// <summary>
    /// All clients share the same loggers.
    /// </summary>
    public ILogger AgentLogger
        => clients.First().client.AgentLogger;
 
    internal void ConfigureLaunchEnvironment(IDictionary<string, string> environmentBuilder)
    {
        foreach (var (client, _) in clients)
        {
            client.ConfigureLaunchEnvironment(environmentBuilder);
        }
 
        browserRefreshServer?.ConfigureLaunchEnvironment(environmentBuilder, enableHotReload: true);
    }
 
    internal void InitiateConnection(CancellationToken cancellationToken)
    {
        foreach (var (client, _) in clients)
        {
            client.InitiateConnection(cancellationToken);
        }
    }
 
    internal async ValueTask WaitForConnectionEstablishedAsync(CancellationToken cancellationToken)
    {
        await Task.WhenAll(clients.Select(c => c.client.WaitForConnectionEstablishedAsync(cancellationToken)));
    }
 
    public async ValueTask<ImmutableArray<string>> GetUpdateCapabilitiesAsync(CancellationToken cancellationToken)
    {
        if (clients is [var (singleClient, _)])
        {
            return await singleClient.GetUpdateCapabilitiesAsync(cancellationToken);
        }
 
        var results = await Task.WhenAll(clients.Select(c => c.client.GetUpdateCapabilitiesAsync(cancellationToken)));
 
        // Allow updates that are supported by at least one process.
        // When applying changes we will filter updates applied to a specific process based on their required capabilities.
        return [.. results.SelectMany(r => r).Distinct(StringComparer.Ordinal).OrderBy(c => c)];
    }
 
    public async ValueTask ApplyManagedCodeUpdatesAsync(ImmutableArray<HotReloadManagedCodeUpdate> updates, bool isProcessSuspended, CancellationToken cancellationToken)
    {
        var anyFailure = false;
 
        if (clients is [var (singleClient, _)])
        {
            anyFailure = await singleClient.ApplyManagedCodeUpdatesAsync(updates, isProcessSuspended, cancellationToken) == ApplyStatus.Failed;
        }
        else
        {
            // Apply to all processes.
            // The module the change is for does not need to be loaded to any of the processes, yet we still consider it successful if the application does not fail.
            // In each process we store the deltas for application when/if the module is loaded to the process later.
            // An error is only reported if the delta application fails, which would be a bug either in the runtime (applying valid delta incorrectly),
            // the compiler (producing wrong delta), or rude edit detection (the change shouldn't have been allowed).
 
            var results = await Task.WhenAll(clients.Select(c => c.client.ApplyManagedCodeUpdatesAsync(updates, isProcessSuspended, cancellationToken)));
 
            var index = 0;
            foreach (var status in results)
            {
                var (client, name) = clients[index++];
 
                switch (status)
                {
                    case ApplyStatus.Failed:
                        anyFailure = true;
                        break;
 
                    case ApplyStatus.AllChangesApplied:
                        break;
 
                    case ApplyStatus.SomeChangesApplied:
                        client.Logger.LogWarning("Some changes not applied to {Name} because they are not supported by the runtime.", name);
                        break;
 
                    case ApplyStatus.NoChangesApplied:
                        client.Logger.LogWarning("No changes applied to {Name} because they are not supported by the runtime.", name);
                        break;
                }
            }
        }
 
        if (!anyFailure)
        {
            // all clients share the same loggers, pick any:
            var logger = clients[0].client.Logger;
            logger.Log(LogEvents.HotReloadSucceeded);
 
            if (browserRefreshServer != null)
            {
                await browserRefreshServer.RefreshBrowserAsync(cancellationToken);
            }
        }
    }
 
    public async ValueTask InitialUpdatesAppliedAsync(CancellationToken cancellationToken)
    {
        if (clients is [var (singleClient, _)])
        {
            await singleClient.InitialUpdatesAppliedAsync(cancellationToken);
        }
        else
        {
            await Task.WhenAll(clients.Select(c => c.client.InitialUpdatesAppliedAsync(cancellationToken)));
        }
    }
 
    public async Task ApplyStaticAssetUpdatesAsync(IEnumerable<(string filePath, string relativeUrl, string assemblyName, bool isApplicationProject)> assets, CancellationToken cancellationToken)
    {
        if (browserRefreshServer != null)
        {
            await browserRefreshServer.UpdateStaticAssetsAsync(assets.Select(static a => a.relativeUrl), cancellationToken);
        }
        else
        {
            var updates = new List<HotReloadStaticAssetUpdate>();
 
            foreach (var (filePath, relativeUrl, assemblyName, isApplicationProject) in assets)
            {
                ImmutableArray<byte> content;
                try
                {
                    content = ImmutableCollectionsMarshal.AsImmutableArray(await File.ReadAllBytesAsync(filePath, cancellationToken));
                }
                catch (Exception e)
                {
                    ClientLogger.LogError("Failed to read file {FilePath}: {Message}", filePath, e.Message);
                    continue;
                }
 
                updates.Add(new HotReloadStaticAssetUpdate(
                    assemblyName: assemblyName,
                    relativePath: relativeUrl,
                    content: content,
                    isApplicationProject));
            }
 
            await ApplyStaticAssetUpdatesAsync([.. updates], isProcessSuspended: false, cancellationToken);
        }
    }
 
    public async ValueTask ApplyStaticAssetUpdatesAsync(ImmutableArray<HotReloadStaticAssetUpdate> updates, bool isProcessSuspended, CancellationToken cancellationToken)
    {
        if (clients is [var (singleClient, _)])
        {
            await singleClient.ApplyStaticAssetUpdatesAsync(updates, isProcessSuspended, cancellationToken);
        }
        else
        {
            await Task.WhenAll(clients.Select(c => c.client.ApplyStaticAssetUpdatesAsync(updates, isProcessSuspended, cancellationToken)));
        }
    }
 
    public ValueTask ReportCompilationErrorsInApplicationAsync(ImmutableArray<string> compilationErrors, CancellationToken cancellationToken)
        => browserRefreshServer?.ReportCompilationErrorsInBrowserAsync(compilationErrors, cancellationToken) ?? ValueTask.CompletedTask;
}