File: HotReloadClient.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.
 
#nullable enable
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.DotNet.HotReload;
 
internal abstract class HotReloadClient(ILogger logger, ILogger agentLogger) : IDisposable
{
    /// <summary>
    /// List of modules that can't receive changes anymore.
    /// A module is added when a change is requested for it that is not supported by the runtime.
    /// </summary>
    private readonly HashSet<Guid> _frozenModules = [];
 
    public readonly ILogger Logger = logger;
    public readonly ILogger AgentLogger = agentLogger;
 
    private int _updateBatchId;
 
    /// <summary>
    /// Updates that were sent over to the agent while the process has been suspended.
    /// </summary>
    private readonly object _pendingUpdatesGate = new();
    private Task _pendingUpdates = Task.CompletedTask;
 
    // for testing
    internal Task PendingUpdates
        => _pendingUpdates;
 
    public abstract void ConfigureLaunchEnvironment(IDictionary<string, string> environmentBuilder);
 
    /// <summary>
    /// Initiates connection with the agent in the target process.
    /// </summary>
    public abstract void InitiateConnection(CancellationToken cancellationToken);
 
    /// <summary>
    /// Waits until the connection with the agent is established.
    /// </summary>
    public abstract Task WaitForConnectionEstablishedAsync(CancellationToken cancellationToken);
 
    public abstract Task<ImmutableArray<string>> GetUpdateCapabilitiesAsync(CancellationToken cancellationToken);
 
    public abstract Task<ApplyStatus> ApplyManagedCodeUpdatesAsync(ImmutableArray<HotReloadManagedCodeUpdate> updates, bool isProcessSuspended, CancellationToken cancellationToken);
    public abstract Task<ApplyStatus> ApplyStaticAssetUpdatesAsync(ImmutableArray<HotReloadStaticAssetUpdate> updates, bool isProcessSuspended, CancellationToken cancellationToken);
 
    /// <summary>
    /// Notifies the agent that the initial set of updates has been applied and the user code in the process can start executing.
    /// </summary>
    public abstract Task InitialUpdatesAppliedAsync(CancellationToken cancellationToken);
 
    public abstract void Dispose();
 
    public static void ReportLogEntry(ILogger logger, string message, AgentMessageSeverity severity)
    {
        var level = severity switch
        {
            AgentMessageSeverity.Error => LogLevel.Error,
            AgentMessageSeverity.Warning => LogLevel.Warning,
            _ => LogLevel.Debug
        };
 
        logger.Log(level, message);
    }
 
    public async Task<IReadOnlyList<HotReloadManagedCodeUpdate>> FilterApplicableUpdatesAsync(ImmutableArray<HotReloadManagedCodeUpdate> updates, CancellationToken cancellationToken)
    {
        var availableCapabilities = await GetUpdateCapabilitiesAsync(cancellationToken);
        var applicableUpdates = new List<HotReloadManagedCodeUpdate>();
 
        foreach (var update in updates)
        {
            if (_frozenModules.Contains(update.ModuleId))
            {
                // can't update frozen module:
                continue;
            }
 
            if (update.RequiredCapabilities.Except(availableCapabilities).Any())
            {
                // required capability not available:
                _frozenModules.Add(update.ModuleId);
            }
            else
            {
                applicableUpdates.Add(update);
            }
        }
 
        return applicableUpdates;
    }
 
    protected async ValueTask<TResult> SendAndReceiveUpdateAsync<TResult>(
        Func<int, CancellationToken, ValueTask<TResult>> send,
        bool isProcessSuspended,
        TResult suspendedResult,
        CancellationToken cancellationToken)
        where TResult : struct
    {
        var batchId = _updateBatchId++;
 
        Task previous;
        lock (_pendingUpdatesGate)
        {
            previous = _pendingUpdates;
 
            if (isProcessSuspended)
            {
                _pendingUpdates = Task.Run(async () =>
                {
                    await previous;
                    _ = await send(batchId, cancellationToken);
                }, cancellationToken);
 
                return suspendedResult;
            }
        }
 
        await previous;
        return await send(batchId, cancellationToken);
    }
}