| File: HotReload\ManagedCodeWorkspace.cs | Web Access |
| Project: src\sdk\src\Dotnet.Watch\Watch\Microsoft.DotNet.HotReload.Watch.csproj (Microsoft.DotNet.HotReload.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.Execution; using Microsoft.Build.Graph; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch; internal sealed class ManagedCodeWorkspace : IDisposable { private readonly ILogger _logger; private readonly RunningProjectsManager _runningProjectsManager; private readonly HotReloadMSBuildWorkspace _workspace; public readonly HotReloadService HotReloadService; private int _solutionUpdateId; /// <summary> /// Current set of project instances indexed by <see cref="ProjectInstance.FullPath"/>. /// Updated whenever the project graph changes. /// </summary> private ImmutableDictionary<string, ImmutableArray<ProjectInstance>> _projectInstances = ImmutableDictionary<string, ImmutableArray<ProjectInstance>>.Empty.WithComparers(PathUtilities.OSSpecificPathComparer); public ManagedCodeWorkspace(ILogger logger, RunningProjectsManager runningProjectsManager) { _logger = logger; _runningProjectsManager = runningProjectsManager; _workspace = new HotReloadMSBuildWorkspace(logger, projectFile => (instances: _projectInstances.GetValueOrDefault(projectFile, []), project: null)); HotReloadService = new HotReloadService(_workspace.CurrentSolution.Services, () => ValueTask.FromResult(GetAggregateCapabilities())); } public void Dispose() { _workspace.Dispose(); } public ManagedCodeSnapshot CurrentSnapshot => new() { Solution = _workspace.CurrentSolution, ProjectInstances = _projectInstances }; private ImmutableArray<string> GetAggregateCapabilities() { var capabilities = _runningProjectsManager.CurrentRunningProjects .SelectMany(p => p.Value) .SelectMany(p => p.ManagedCodeUpdateCapabilities) .Distinct(StringComparer.Ordinal) .Order() .ToImmutableArray(); _logger.Log(MessageDescriptor.HotReloadCapabilities, string.Join(" ", capabilities)); return capabilities; } public void PrepareCompilation(string projectPath, CancellationToken cancellationToken) { // Warm up the compilation. This would help make the deltas for first edit appear much more quickly. // Preparing the compilation is a perf optimization. We can skip it if the session hasn't been started yet. foreach (var project in _workspace.CurrentSolution.Projects) { if (project.FilePath == projectPath) { // fire and forget: _ = project.GetCompilationAsync(cancellationToken); } } } public async ValueTask StartSessionAsync(ProjectGraph graph, CancellationToken cancellationToken) { var solution = await UpdateProjectGraphAsync(graph, cancellationToken); await HotReloadService.StartSessionAsync(solution, cancellationToken); _logger.Log(MessageDescriptor.HotReloadSessionStarted); } public async Task<Solution> UpdateProjectGraphAsync(ProjectGraph projectGraph, CancellationToken cancellationToken) { _projectInstances = CreateProjectInstanceMap(projectGraph); var solution = await _workspace.UpdateProjectGraphAsync([.. projectGraph.EntryPointNodes.Select(n => n.ProjectInstance.FullPath)], cancellationToken); await SolutionUpdatedAsync(solution, "project update", cancellationToken); return solution; } private static ImmutableDictionary<string, ImmutableArray<ProjectInstance>> CreateProjectInstanceMap(ProjectGraph graph) => graph.ProjectNodes .GroupBy(static node => node.ProjectInstance.FullPath) .ToImmutableDictionary( keySelector: static group => group.Key, elementSelector: static group => group.Select(static node => node.ProjectInstance).ToImmutableArray()); public async Task UpdateFileContentAsync(IReadOnlyList<ChangedFile> changedFiles, CancellationToken cancellationToken) { var solution = await _workspace.UpdateFileContentAsync(changedFiles.Select(static f => (f.Item.FilePath, f.Kind.Convert())), cancellationToken); await SolutionUpdatedAsync(solution, "document update", cancellationToken); } private Task SolutionUpdatedAsync(Solution newSolution, string operationDisplayName, CancellationToken cancellationToken) => ReportSolutionFilesAsync(newSolution, Interlocked.Increment(ref _solutionUpdateId), operationDisplayName, cancellationToken); private async Task ReportSolutionFilesAsync(Solution solution, int updateId, string operationDisplayName, CancellationToken cancellationToken) { _logger.LogDebug("Solution after {Operation}: v{Version}", operationDisplayName, updateId); if (!_logger.IsEnabled(LogLevel.Trace)) { return; } foreach (var project in solution.Projects) { _logger.LogDebug(" Project: {Path}", project.FilePath); foreach (var document in project.Documents) { await InspectDocumentAsync(document, "Document").ConfigureAwait(false); } foreach (var document in project.AdditionalDocuments) { await InspectDocumentAsync(document, "Additional").ConfigureAwait(false); } foreach (var document in project.AnalyzerConfigDocuments) { await InspectDocumentAsync(document, "Config").ConfigureAwait(false); } } async ValueTask InspectDocumentAsync(TextDocument document, string kind) { var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); _logger.LogDebug(" {Kind}: {FilePath} [{Checksum}]", kind, document.FilePath, Convert.ToBase64String(text.GetChecksum().ToArray())); } } }