| File: EditAndContinue\EditAndContinueService.cs | Web Access |
| Project: src\roslyn\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. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Contracts.EditAndContinue; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; namespace Microsoft.CodeAnalysis.EditAndContinue; /// <summary> /// Implements core of Edit and Continue orchestration: management of edit sessions and connecting EnC related services. /// In VS, this service should only be instantiated in OOP and accessed remotely via <see cref="IRemoteEditAndContinueService"/>. /// The service is used directly only by dotnet-watch and for testing. /// </summary> internal sealed class EditAndContinueService : IEditAndContinueService { [ExportWorkspaceServiceFactory(typeof(IEditAndContinueWorkspaceService)), Shared] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal sealed class WorkspaceServiceFactory() : IWorkspaceServiceFactory { public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) => new WorkspaceService( // The host may optionally provide a log reporter (e.g. to forward EnC logs to the debugger's hot reload // logger). When not available the service logs to file only. new EditAndContinueService(workspaceServices.GetService<IEditAndContinueLogReporter>())); } internal sealed class WorkspaceService(IEditAndContinueService service) : IEditAndContinueWorkspaceService { public IEditAndContinueService Service { get; } = service; } private static readonly string? s_logDir = GetLogDirectory(); internal readonly TraceLog Log; internal readonly TraceLog AnalysisLog; private Func<Project, CompilationOutputs> _compilationOutputsProvider = GetCompilationOutputs; /// <summary> /// List of active debugging sessions (small number of simoultaneously active sessions is expected). /// </summary> private readonly List<DebuggingSession> _debuggingSessions = []; private static int s_debuggingSessionId; public EditAndContinueService(IEditAndContinueLogReporter? logReporter) { Log = new TraceLog("Session", logReporter); AnalysisLog = new TraceLog("Analysis", logReporter); if (s_logDir != null) { Log.SetLogDirectory(s_logDir); AnalysisLog.SetLogDirectory(s_logDir); } } private static string? GetLogDirectory() { try { var path = Environment.GetEnvironmentVariable("Microsoft_CodeAnalysis_EditAndContinue_LogDir"); if (string.IsNullOrWhiteSpace(path)) { return null; } Directory.CreateDirectory(path); return path; } catch { return null; } } public void SetFileLoggingDirectory(string? logDirectory) { logDirectory ??= GetLogDirectory(); Log.SetLogDirectory(logDirectory); AnalysisLog.SetLogDirectory(logDirectory); } private static CompilationOutputs GetCompilationOutputs(Project project) { // The Project System doesn't always indicate whether we emit PDB, what kind of PDB we emit nor the path of the PDB. // To work around we look for the PDB on the path specified in the PDB debug directory. // https://github.com/dotnet/roslyn/issues/35065 return new CompilationOutputFilesWithImplicitPdbPath(project.CompilationOutputInfo.AssemblyPath); } private DebuggingSession? TryGetDebuggingSession(DebuggingSessionId sessionId) { lock (_debuggingSessions) { return _debuggingSessions.SingleOrDefault(s => s.Id == sessionId); } } private ImmutableArray<DebuggingSession> GetActiveDebuggingSessions() { lock (_debuggingSessions) { return [.. _debuggingSessions]; } } private ImmutableArray<DebuggingSession> GetDiagnosticReportingDebuggingSessions() { lock (_debuggingSessions) { return [.. _debuggingSessions.Where(s => s.ReportDiagnostics)]; } } internal static async ValueTask HydrateDocumentsAsync(Solution solution, CancellationToken cancellationToken) { var documentTasks = from project in solution.Projects where !project.IgnoreForEditAndContinue() from documentState in GetDocumentStates(project.State) where !documentState.IgnoreForEditAndContinue() select documentState.GetTextAsync(cancellationToken).AsTask(); _ = await Task.WhenAll(documentTasks).ConfigureAwait(false); static IEnumerable<TextDocumentState> GetDocumentStates(ProjectState projectState) => ((IEnumerable<TextDocumentState>)projectState.DocumentStates.States.Values).Concat( projectState.AdditionalDocumentStates.States.Values).Concat( projectState.AnalyzerConfigDocumentStates.States.Values); } public DebuggingSessionId StartDebuggingSession( Solution solution, IManagedHotReloadService debuggerService, IPdbMatchingSourceTextProvider sourceTextProvider, bool reportDiagnostics) { try { // Make sure the solution snapshot has all source-generated documents up-to-date: solution = solution.WithUpToDateSourceGeneratorDocuments(solution.ProjectIds); var sessionId = new DebuggingSessionId(Interlocked.Increment(ref s_debuggingSessionId)); var session = new DebuggingSession(sessionId, solution, debuggerService, _compilationOutputsProvider, sourceTextProvider, Log, AnalysisLog, reportDiagnostics); lock (_debuggingSessions) { _debuggingSessions.Add(session); } Log.Write($"Session #{sessionId} started."); return sessionId; } catch (Exception ex) when (FatalError.ReportAndPropagate(ex)) { throw ExceptionUtilities.Unreachable(); } } public void EndDebuggingSession(DebuggingSessionId sessionId) { DebuggingSession? debuggingSession; lock (_debuggingSessions) { _debuggingSessions.TryRemoveFirst((s, sessionId) => s.Id == sessionId, sessionId, out debuggingSession); } Contract.ThrowIfNull(debuggingSession, "Debugging session has not started."); debuggingSession.EndSession(out var telemetryData); Log.Write($"Session #{debuggingSession.Id} ended."); } public void BreakStateOrCapabilitiesChanged(DebuggingSessionId sessionId, bool? inBreakState) { var debuggingSession = TryGetDebuggingSession(sessionId); Contract.ThrowIfNull(debuggingSession); debuggingSession.BreakStateOrCapabilitiesChanged(inBreakState); } public ValueTask<ImmutableArray<Diagnostic>> GetDocumentDiagnosticsAsync(Document document, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) { return GetDiagnosticReportingDebuggingSessions().SelectManyAsArrayAsync( (s, arg, cancellationToken) => s.GetDocumentDiagnosticsAsync(arg.document, arg.activeStatementSpanProvider, cancellationToken), (document, activeStatementSpanProvider), cancellationToken); } public ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync( DebuggingSessionId sessionId, Solution solution, ImmutableDictionary<ProjectId, RunningProjectOptions> runningProjects, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) { var debuggingSession = TryGetDebuggingSession(sessionId); if (debuggingSession == null) { return ValueTask.FromResult(EmitSolutionUpdateResults.Empty); } return debuggingSession.EmitSolutionUpdateAsync(solution, runningProjects, activeStatementSpanProvider, cancellationToken); } public void CommitSolutionUpdate(DebuggingSessionId sessionId) { var debuggingSession = TryGetDebuggingSession(sessionId); Contract.ThrowIfNull(debuggingSession); debuggingSession.CommitSolutionUpdate(); } public void DiscardSolutionUpdate(DebuggingSessionId sessionId) { var debuggingSession = TryGetDebuggingSession(sessionId); Contract.ThrowIfNull(debuggingSession); debuggingSession.DiscardSolutionUpdate(); } public ValueTask<ImmutableArray<ImmutableArray<ActiveStatementSpan>>> GetBaseActiveStatementSpansAsync(DebuggingSessionId sessionId, Solution solution, ImmutableArray<DocumentId> documentIds, CancellationToken cancellationToken) { var debuggingSession = TryGetDebuggingSession(sessionId); if (debuggingSession == null) { return default; } return debuggingSession.GetBaseActiveStatementSpansAsync(solution, documentIds, cancellationToken); } public ValueTask<ImmutableArray<ActiveStatementSpan>> GetAdjustedActiveStatementSpansAsync(DebuggingSessionId sessionId, TextDocument mappedDocument, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) { var debuggingSession = TryGetDebuggingSession(sessionId); if (debuggingSession == null) { return ValueTask.FromResult(ImmutableArray<ActiveStatementSpan>.Empty); } return debuggingSession.GetAdjustedActiveStatementSpansAsync(mappedDocument, activeStatementSpanProvider, cancellationToken); } internal TestAccessor GetTestAccessor() => new(this); internal readonly struct TestAccessor(EditAndContinueService service) { private readonly EditAndContinueService _service = service; public void SetOutputProvider(Func<Project, CompilationOutputs> value) => _service._compilationOutputsProvider = value; public DebuggingSession GetDebuggingSession(DebuggingSessionId id) => _service.TryGetDebuggingSession(id) ?? throw ExceptionUtilities.UnexpectedValue(id); public ImmutableArray<DebuggingSession> GetActiveDebuggingSessions() => _service.GetActiveDebuggingSessions(); } }