File: ExternalAccess\Watch\Api\WatchHotReloadService.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#if NET
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.EditAndContinue;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Contracts.EditAndContinue;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.ExternalAccess.Watch.Api;
 
internal sealed class WatchHotReloadService(SolutionServices services, Func<ValueTask<ImmutableArray<string>>> capabilitiesProvider)
{
    private sealed class DebuggerService(Func<ValueTask<ImmutableArray<string>>> capabilitiesProvider) : IManagedHotReloadService
    {
        public ValueTask<ImmutableArray<ManagedActiveStatementDebugInfo>> GetActiveStatementsAsync(CancellationToken cancellationToken)
            => ValueTaskFactory.FromResult(ImmutableArray<ManagedActiveStatementDebugInfo>.Empty);
 
        public ValueTask<ManagedHotReloadAvailability> GetAvailabilityAsync(Guid module, CancellationToken cancellationToken)
            => ValueTaskFactory.FromResult(new ManagedHotReloadAvailability(ManagedHotReloadAvailabilityStatus.Available));
 
        public ValueTask<ImmutableArray<string>> GetCapabilitiesAsync(CancellationToken cancellationToken)
            => capabilitiesProvider();
 
        public ValueTask PrepareModuleForUpdateAsync(Guid module, CancellationToken cancellationToken)
            => ValueTaskFactory.CompletedTask;
    }
 
    public readonly struct Update
    {
        public readonly Guid ModuleId;
        public readonly ProjectId ProjectId;
        public readonly ImmutableArray<byte> ILDelta;
        public readonly ImmutableArray<byte> MetadataDelta;
        public readonly ImmutableArray<byte> PdbDelta;
        public readonly ImmutableArray<int> UpdatedTypes;
        public readonly ImmutableArray<string> RequiredCapabilities;
 
        internal Update(
            Guid moduleId,
            ProjectId projectId,
            ImmutableArray<byte> ilDelta,
            ImmutableArray<byte> metadataDelta,
            ImmutableArray<byte> pdbDelta,
            ImmutableArray<int> updatedTypes,
            ImmutableArray<string> requiredCapabilities)
        {
            ModuleId = moduleId;
            ProjectId = projectId;
            ILDelta = ilDelta;
            MetadataDelta = metadataDelta;
            PdbDelta = pdbDelta;
            UpdatedTypes = updatedTypes;
            RequiredCapabilities = requiredCapabilities;
        }
    }
 
    public readonly struct Updates(
        ModuleUpdateStatus status,
        ImmutableArray<Diagnostic> diagnostics,
        ImmutableArray<Update> projectUpdates,
        IReadOnlySet<Project> projectsToRestart,
        IReadOnlySet<Project> projectsToRebuild)
    {
        /// <summary>
        /// Status of the updates.
        /// </summary>
        public readonly ModuleUpdateStatus Status { get; } = status;
 
        /// <summary>
        /// Hot Reload specific diagnostics to be reported (includes rude edits and emit errors).
        /// </summary>
        public ImmutableArray<Diagnostic> Diagnostics { get; } = diagnostics;
 
        /// <summary>
        /// Updates to be applied to modules. Empty if there are blocking rude edits.
        /// Only updates to projects that are not included in <see cref="ProjectsToRebuild"/> are listed.
        /// </summary>
        public ImmutableArray<Update> ProjectUpdates { get; } = projectUpdates;
 
        /// <summary>
        /// Running projects that need to be restarted due to rude edits in order to apply changes.
        /// </summary>
        public IReadOnlySet<Project> ProjectsToRestart { get; } = projectsToRestart;
 
        /// <summary>
        /// Projects with changes that need to be rebuilt in order to apply changes.
        /// </summary>
        public IReadOnlySet<Project> ProjectsToRebuild { get; } = projectsToRebuild;
    }
 
    private static readonly ActiveStatementSpanProvider s_solutionActiveStatementSpanProvider =
        (_, _, _) => ValueTaskFactory.FromResult(ImmutableArray<ActiveStatementSpan>.Empty);
 
    private readonly IEditAndContinueService _encService = services.GetRequiredService<IEditAndContinueWorkspaceService>().Service;
 
    private DebuggingSessionId _sessionId;
 
    public WatchHotReloadService(HostWorkspaceServices services, ImmutableArray<string> capabilities)
        : this(services.SolutionServices, () => ValueTaskFactory.FromResult(AddImplicitDotNetCapabilities(capabilities)))
    {
    }
 
    private DebuggingSessionId GetDebuggingSession()
    {
        var sessionId = _sessionId;
        Contract.ThrowIfFalse(sessionId != default, "Session has not started");
        return sessionId;
    }
 
    /// <summary>
    /// Adds capabilities that are available by default on runtimes supported by dotnet-watch: .NET and Mono
    /// and not on .NET Framework (they are not in <see cref="EditAndContinueCapabilities.Baseline"/>.
    /// </summary>
    private static ImmutableArray<string> AddImplicitDotNetCapabilities(ImmutableArray<string> capabilities)
        => capabilities.Add(nameof(EditAndContinueCapabilities.AddExplicitInterfaceImplementation));
 
    /// <summary>
    /// Starts the watcher.
    /// </summary>
    /// <param name="solution">Solution that represents sources that match the built binaries on disk.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    public async Task StartSessionAsync(Solution solution, CancellationToken cancellationToken)
    {
        var newSessionId = await _encService.StartDebuggingSessionAsync(
            solution,
            new DebuggerService(capabilitiesProvider),
            NullPdbMatchingSourceTextProvider.Instance,
            captureMatchingDocuments: [],
            captureAllMatchingDocuments: true,
            reportDiagnostics: false,
            cancellationToken).ConfigureAwait(false);
        Contract.ThrowIfFalse(_sessionId == default, "Session already started");
        _sessionId = newSessionId;
    }
 
    /// <summary>
    /// Invoke when capabilities have changed.
    /// </summary>
    public void CapabilitiesChanged()
    {
        _encService.BreakStateOrCapabilitiesChanged(GetDebuggingSession(), inBreakState: null);
    }
 
    public async Task<(ImmutableArray<Update> updates, ImmutableArray<Diagnostic> diagnostics)> EmitSolutionUpdateAsync(Solution solution, CancellationToken cancellationToken)
    {
        var result = await GetUpdatesAsync(solution, isRunningProject: static _ => false, cancellationToken).ConfigureAwait(false);
        return (result.ProjectUpdates, result.Diagnostics);
    }
 
    /// <summary>
    /// Emits updates for all projects that differ between the given <paramref name="solution"/> snapshot and the one given to the previous successful call or
    /// the one passed to <see cref="StartSessionAsync(Solution, CancellationToken)"/> for the first invocation.
    /// </summary>
    /// <param name="solution">Solution snapshot.</param>
    /// <param name="isRunningProject">Identifies projects that launched a process.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>
    /// Updates (one for each changed project) and Rude Edit diagnostics. Does not include syntax or semantic diagnostics.
    /// </returns>
    public async Task<Updates> GetUpdatesAsync(Solution solution, Func<Project, bool> isRunningProject, CancellationToken cancellationToken)
    {
        var sessionId = GetDebuggingSession();
 
        var results = await _encService.EmitSolutionUpdateAsync(sessionId, solution, s_solutionActiveStatementSpanProvider, cancellationToken).ConfigureAwait(false);
 
        if (results.ModuleUpdates.Status == ModuleUpdateStatus.Ready)
        {
            _encService.CommitSolutionUpdate(sessionId);
        }
 
        var diagnostics = results.GetAllDiagnostics();
 
        var projectsToRestart = new HashSet<Project>();
        var projectsToRebuild = new HashSet<Project>();
        results.GetProjectsToRebuildAndRestart(solution, isRunningProject, projectsToRestart, projectsToRebuild);
 
        var projectUpdates =
            from update in results.ModuleUpdates.Updates
            let project = solution.GetRequiredProject(update.ProjectId)
            where !projectsToRestart.Contains(project)
            select new Update(
                update.Module,
                project.Id,
                update.ILDelta,
                update.MetadataDelta,
                update.PdbDelta,
                update.UpdatedTypes,
                update.RequiredCapabilities);
 
        return new Updates(results.ModuleUpdates.Status, diagnostics, projectUpdates.ToImmutableArray(), projectsToRestart, projectsToRebuild);
    }
 
    public void EndSession()
    {
        _encService.EndDebuggingSession(GetDebuggingSession());
        _sessionId = default;
    }
 
    // access to internal API:
    public static Solution WithProjectInfo(Solution solution, ProjectInfo info)
        => solution.WithProjectInfo(info);
 
    internal TestAccessor GetTestAccessor()
        => new(this);
 
    internal readonly struct TestAccessor(WatchHotReloadService instance)
    {
        public DebuggingSessionId SessionId
            => instance._sessionId;
    }
}
#endif