refactor debugging to sessions

This commit is contained in:
Matt Parker
2026-01-21 00:38:11 +10:00
parent 29e59018fe
commit debd4db89f
6 changed files with 87 additions and 71 deletions

View File

@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using SharpIDE.Application.Features.Analysis; using SharpIDE.Application.Features.Analysis;
using SharpIDE.Application.Features.Build; using SharpIDE.Application.Features.Build;
using SharpIDE.Application.Features.Debugging;
using SharpIDE.Application.Features.Editor; using SharpIDE.Application.Features.Editor;
using SharpIDE.Application.Features.Evaluation; using SharpIDE.Application.Features.Evaluation;
using SharpIDE.Application.Features.FilePersistence; using SharpIDE.Application.Features.FilePersistence;
@@ -19,6 +20,7 @@ public static class DependencyInjection
{ {
services.AddScoped<BuildService>(); services.AddScoped<BuildService>();
services.AddScoped<RunService>(); services.AddScoped<RunService>();
services.AddScoped<DebuggingService>();
services.AddScoped<SearchService>(); services.AddScoped<SearchService>();
services.AddScoped<IdeFileExternalChangeHandler>(); services.AddScoped<IdeFileExternalChangeHandler>();
services.AddScoped<IdeCodeActionService>(); services.AddScoped<IdeCodeActionService>();

View File

@@ -1,28 +0,0 @@
using Microsoft.VisualStudio.Shared.VSCodeDebugProtocol.Messages;
using SharpIDE.Application.Features.Run;
using SharpIDE.Application.Features.SolutionDiscovery;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
namespace SharpIDE.Application.Features.Debugging;
// TODO: Why does this exist separate from DebuggingService?
public class Debugger
{
public required SharpIdeProjectModel Project { get; init; }
public required int ProcessId { get; init; }
private DebuggingService _debuggingService = new DebuggingService();
public async Task Attach(DebuggerExecutableInfo? debuggerExecutableInfo, Dictionary<SharpIdeFile, List<Breakpoint>> breakpointsByFile, SharpIdeProjectModel project, CancellationToken cancellationToken)
{
await _debuggingService.Attach(ProcessId, debuggerExecutableInfo, breakpointsByFile, project, cancellationToken);
}
public async Task SetBreakpointsForFile(SharpIdeFile file, List<Breakpoint> breakpoints, CancellationToken cancellationToken = default) => await _debuggingService.SetBreakpointsForFile(file, breakpoints, cancellationToken);
public async Task StepOver(int threadId, CancellationToken cancellationToken = default) => await _debuggingService.StepOver(threadId, cancellationToken);
public async Task StepInto(int threadId, CancellationToken cancellationToken = default) => await _debuggingService.StepInto(threadId, cancellationToken);
public async Task StepOut(int threadId, CancellationToken cancellationToken = default) => await _debuggingService.StepOut(threadId, cancellationToken);
public async Task Continue(int threadId, CancellationToken cancellationToken = default) => await _debuggingService.Continue(threadId, cancellationToken);
public async Task<List<ThreadModel>> GetThreadsAtStopPoint() => await _debuggingService.GetThreadsAtStopPoint();
public async Task<List<StackFrameModel>> GetStackFramesForThread(int threadId) => await _debuggingService.GetStackFramesForThread(threadId);
public async Task<List<Variable>> GetVariablesForStackFrame(int frameId) => await _debuggingService.GetVariablesForStackFrame(frameId);
public async Task<List<Variable>> GetVariablesForVariablesReference(int variablesReferenceId) => await _debuggingService.GetVariablesForVariablesReference(variablesReferenceId);
}

View File

@@ -0,0 +1,14 @@
using Ardalis.GuardClauses;
namespace SharpIDE.Application.Features.Debugging;
public readonly record struct DebuggerSessionId
{
public readonly Guid Value;
public DebuggerSessionId(Guid value)
{
if (value == Guid.Empty) throw new ArgumentException("DebuggerSessionId cannot be an empty Guid", nameof(value));
Value = value;
}
}

View File

@@ -1,5 +1,4 @@
using System.Diagnostics; using System.Collections.Concurrent;
using System.Reflection;
using Ardalis.GuardClauses; using Ardalis.GuardClauses;
using Microsoft.Diagnostics.NETCore.Client; using Microsoft.Diagnostics.NETCore.Client;
using Microsoft.VisualStudio.Shared.VSCodeDebugProtocol; using Microsoft.VisualStudio.Shared.VSCodeDebugProtocol;
@@ -16,8 +15,10 @@ namespace SharpIDE.Application.Features.Debugging;
#pragma warning disable VSTHRD101 #pragma warning disable VSTHRD101
public class DebuggingService public class DebuggingService
{ {
private DebugProtocolHost _debugProtocolHost = null!; private ConcurrentDictionary<DebuggerSessionId, DebugProtocolHost> _debugProtocolHosts = [];
public async Task Attach(int debuggeeProcessId, DebuggerExecutableInfo? debuggerExecutableInfo, Dictionary<SharpIdeFile, List<Breakpoint>> breakpointsByFile, SharpIdeProjectModel project, CancellationToken cancellationToken = default)
/// <returns>The debugging session ID</returns>
public async Task<DebuggerSessionId> Attach(int debuggeeProcessId, DebuggerExecutableInfo? debuggerExecutableInfo, Dictionary<SharpIdeFile, List<Breakpoint>> breakpointsByFile, SharpIdeProjectModel project, CancellationToken cancellationToken = default)
{ {
Guard.Against.NegativeOrZero(debuggeeProcessId, nameof(debuggeeProcessId), "Process ID must be a positive integer."); Guard.Against.NegativeOrZero(debuggeeProcessId, nameof(debuggeeProcessId), "Process ID must be a positive integer.");
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
@@ -26,7 +27,6 @@ public class DebuggingService
var debugProtocolHost = new DebugProtocolHost(inputStream, outputStream, false); var debugProtocolHost = new DebugProtocolHost(inputStream, outputStream, false);
var initializedEventTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var initializedEventTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
_debugProtocolHost = debugProtocolHost;
debugProtocolHost.LogMessage += (sender, args) => debugProtocolHost.LogMessage += (sender, args) =>
{ {
//Console.WriteLine($"Log message: {args.Message}"); //Console.WriteLine($"Log message: {args.Message}");
@@ -70,7 +70,7 @@ public class DebuggingService
{ {
Console.WriteLine("Stopped due to exception, continuing"); Console.WriteLine("Stopped due to exception, continuing");
var continueRequest = new ContinueRequest { ThreadId = @event.ThreadId!.Value }; var continueRequest = new ContinueRequest { ThreadId = @event.ThreadId!.Value };
_debugProtocolHost.SendRequestSync(continueRequest); debugProtocolHost.SendRequestSync(continueRequest);
return; return;
} }
var additionalProperties = @event.AdditionalProperties; var additionalProperties = @event.AdditionalProperties;
@@ -157,51 +157,72 @@ public class DebuggingService
debugProtocolHost.SendRequestSync(configurationDoneRequest); debugProtocolHost.SendRequestSync(configurationDoneRequest);
new DiagnosticsClient(debuggeeProcessId).ResumeRuntime(); new DiagnosticsClient(debuggeeProcessId).ResumeRuntime();
} }
var sessionId = new DebuggerSessionId(Guid.NewGuid());
_debugProtocolHosts[sessionId] = debugProtocolHost;
return sessionId;
} }
public async Task SetBreakpointsForFile(SharpIdeFile file, List<Breakpoint> breakpoints, CancellationToken cancellationToken = default) public async Task CloseDebuggerSession(DebuggerSessionId debuggerSessionId)
{ {
if (_debugProtocolHosts.TryRemove(debuggerSessionId, out var debugProtocolHost))
{
debugProtocolHost.Stop();
}
else
{
throw new InvalidOperationException($"Attempted to close non-existent Debugger session with ID '{debuggerSessionId.Value}'");
}
}
public async Task SetBreakpointsForFile(DebuggerSessionId debuggerSessionId, SharpIdeFile file, List<Breakpoint> breakpoints, CancellationToken cancellationToken = default)
{
var debugProtocolHost = _debugProtocolHosts[debuggerSessionId];
var setBreakpointsRequest = new SetBreakpointsRequest var setBreakpointsRequest = new SetBreakpointsRequest
{ {
Source = new Source { Path = file.Path }, Source = new Source { Path = file.Path },
Breakpoints = breakpoints.Select(b => new SourceBreakpoint { Line = b.Line }).ToList() Breakpoints = breakpoints.Select(b => new SourceBreakpoint { Line = b.Line }).ToList()
}; };
var breakpointsResponse = _debugProtocolHost.SendRequestSync(setBreakpointsRequest); var breakpointsResponse = debugProtocolHost.SendRequestSync(setBreakpointsRequest);
} }
public async Task StepOver(int threadId, CancellationToken cancellationToken) public async Task StepOver(DebuggerSessionId debuggerSessionId, int threadId, CancellationToken cancellationToken)
{ {
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
var debugProtocolHost = _debugProtocolHosts[debuggerSessionId];
var nextRequest = new NextRequest(threadId); var nextRequest = new NextRequest(threadId);
_debugProtocolHost.SendRequestSync(nextRequest); debugProtocolHost.SendRequestSync(nextRequest);
GlobalEvents.Instance.DebuggerExecutionContinued.InvokeParallelFireAndForget(); GlobalEvents.Instance.DebuggerExecutionContinued.InvokeParallelFireAndForget();
} }
public async Task StepInto(int threadId, CancellationToken cancellationToken) public async Task StepInto(DebuggerSessionId debuggerSessionId, int threadId, CancellationToken cancellationToken)
{ {
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
var debugProtocolHost = _debugProtocolHosts[debuggerSessionId];
var stepInRequest = new StepInRequest(threadId); var stepInRequest = new StepInRequest(threadId);
_debugProtocolHost.SendRequestSync(stepInRequest); debugProtocolHost.SendRequestSync(stepInRequest);
GlobalEvents.Instance.DebuggerExecutionContinued.InvokeParallelFireAndForget(); GlobalEvents.Instance.DebuggerExecutionContinued.InvokeParallelFireAndForget();
} }
public async Task StepOut(int threadId, CancellationToken cancellationToken) public async Task StepOut(DebuggerSessionId debuggerSessionId, int threadId, CancellationToken cancellationToken)
{ {
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
var debugProtocolHost = _debugProtocolHosts[debuggerSessionId];
var stepOutRequest = new StepOutRequest(threadId); var stepOutRequest = new StepOutRequest(threadId);
_debugProtocolHost.SendRequestSync(stepOutRequest); debugProtocolHost.SendRequestSync(stepOutRequest);
GlobalEvents.Instance.DebuggerExecutionContinued.InvokeParallelFireAndForget(); GlobalEvents.Instance.DebuggerExecutionContinued.InvokeParallelFireAndForget();
} }
public async Task Continue(int threadId, CancellationToken cancellationToken) public async Task Continue(DebuggerSessionId debuggerSessionId, int threadId, CancellationToken cancellationToken)
{ {
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
var debugProtocolHost = _debugProtocolHosts[debuggerSessionId];
var continueRequest = new ContinueRequest(threadId); var continueRequest = new ContinueRequest(threadId);
_debugProtocolHost.SendRequestSync(continueRequest); debugProtocolHost.SendRequestSync(continueRequest);
GlobalEvents.Instance.DebuggerExecutionContinued.InvokeParallelFireAndForget(); GlobalEvents.Instance.DebuggerExecutionContinued.InvokeParallelFireAndForget();
} }
public async Task<List<ThreadModel>> GetThreadsAtStopPoint() public async Task<List<ThreadModel>> GetThreadsAtStopPoint(DebuggerSessionId debuggerSessionId)
{ {
var threadsRequest = new ThreadsRequest(); var threadsRequest = new ThreadsRequest();
var threadsResponse = _debugProtocolHost.SendRequestSync(threadsRequest); var debugProtocolHost = _debugProtocolHosts[debuggerSessionId];
var threadsResponse = debugProtocolHost.SendRequestSync(threadsRequest);
var mappedThreads = threadsResponse.Threads.Select(s => new ThreadModel var mappedThreads = threadsResponse.Threads.Select(s => new ThreadModel
{ {
Id = s.Id, Id = s.Id,
@@ -210,10 +231,11 @@ public class DebuggingService
return mappedThreads; return mappedThreads;
} }
public async Task<List<StackFrameModel>> GetStackFramesForThread(int threadId) public async Task<List<StackFrameModel>> GetStackFramesForThread(DebuggerSessionId debuggerSessionId, int threadId)
{ {
var stackTraceRequest = new StackTraceRequest { ThreadId = threadId }; var stackTraceRequest = new StackTraceRequest { ThreadId = threadId };
var stackTraceResponse = _debugProtocolHost.SendRequestSync(stackTraceRequest); var debugProtocolHost = _debugProtocolHosts[debuggerSessionId];
var stackTraceResponse = debugProtocolHost.SendRequestSync(stackTraceRequest);
var stackFrames = stackTraceResponse.StackFrames; var stackFrames = stackTraceResponse.StackFrames;
var mappedStackFrames = stackFrames!.Select(frame => var mappedStackFrames = stackFrames!.Select(frame =>
@@ -234,24 +256,26 @@ public class DebuggingService
return mappedStackFrames; return mappedStackFrames;
} }
public async Task<List<Variable>> GetVariablesForStackFrame(int frameId) public async Task<List<Variable>> GetVariablesForStackFrame(DebuggerSessionId debuggerSessionId, int frameId)
{ {
var scopesRequest = new ScopesRequest { FrameId = frameId }; var scopesRequest = new ScopesRequest { FrameId = frameId };
var scopesResponse = _debugProtocolHost.SendRequestSync(scopesRequest); var debugProtocolHost = _debugProtocolHosts[debuggerSessionId];
var scopesResponse = debugProtocolHost.SendRequestSync(scopesRequest);
var allVariables = new List<Variable>(); var allVariables = new List<Variable>();
foreach (var scope in scopesResponse.Scopes) foreach (var scope in scopesResponse.Scopes)
{ {
var variablesRequest = new VariablesRequest { VariablesReference = scope.VariablesReference }; var variablesRequest = new VariablesRequest { VariablesReference = scope.VariablesReference };
var variablesResponse = _debugProtocolHost.SendRequestSync(variablesRequest); var variablesResponse = debugProtocolHost.SendRequestSync(variablesRequest);
allVariables.AddRange(variablesResponse.Variables); allVariables.AddRange(variablesResponse.Variables);
} }
return allVariables; return allVariables;
} }
public async Task<List<Variable>> GetVariablesForVariablesReference(int variablesReference) public async Task<List<Variable>> GetVariablesForVariablesReference(DebuggerSessionId debuggerSessionId, int variablesReference)
{ {
var debugProtocolHost = _debugProtocolHosts[debuggerSessionId];
var variablesRequest = new VariablesRequest { VariablesReference = variablesReference }; var variablesRequest = new VariablesRequest { VariablesReference = variablesReference };
var variablesResponse = _debugProtocolHost.SendRequestSync(variablesRequest); var variablesResponse = debugProtocolHost.SendRequestSync(variablesRequest);
return variablesResponse.Variables; return variablesResponse.Variables;
} }

View File

@@ -15,14 +15,16 @@ using Breakpoint = SharpIDE.Application.Features.Debugging.Breakpoint;
namespace SharpIDE.Application.Features.Run; namespace SharpIDE.Application.Features.Run;
public partial class RunService(ILogger<RunService> logger, RoslynAnalysis roslynAnalysis, BuildService buildService) public partial class RunService(ILogger<RunService> logger, RoslynAnalysis roslynAnalysis, BuildService buildService, DebuggingService debuggingService)
{ {
private readonly ConcurrentDictionary<SharpIdeProjectModel, SemaphoreSlim> _projectLocks = []; private readonly ConcurrentDictionary<SharpIdeProjectModel, SemaphoreSlim> _projectLocks = [];
private Debugger? _debugger; // TODO: Support multiple debuggers for multiple running projects //private readonly ConcurrentDictionary<SharpIdeProjectModel, DebuggerSessionId> _projectDebuggerSessionIds = [];
private DebuggerSessionId? _debuggerSessionId; // TODO: Support multiple debuggers for multiple running projects
private readonly ILogger<RunService> _logger = logger; private readonly ILogger<RunService> _logger = logger;
private readonly RoslynAnalysis _roslynAnalysis = roslynAnalysis; private readonly RoslynAnalysis _roslynAnalysis = roslynAnalysis;
private readonly BuildService _buildService = buildService; private readonly BuildService _buildService = buildService;
private readonly DebuggingService _debuggingService = debuggingService;
public async Task RunProject(SharpIdeProjectModel project, bool isDebug = false, DebuggerExecutableInfo? debuggerExecutableInfo = null) public async Task RunProject(SharpIdeProjectModel project, bool isDebug = false, DebuggerExecutableInfo? debuggerExecutableInfo = null)
{ {
@@ -117,9 +119,9 @@ public partial class RunService(ILogger<RunService> logger, RoslynAnalysis rosly
if (isDebug) if (isDebug)
{ {
// Attach debugger (which internally uses a DiagnosticClient to resume startup) // Attach debugger (which internally uses a DiagnosticClient to resume startup)
var debugger = new Debugger { Project = project, ProcessId = process.ProcessId }; var debuggerSessionId = await _debuggingService.Attach(process.ProcessId, debuggerExecutableInfo, Breakpoints.ToDictionary(), project, project.RunningCancellationTokenSource.Token);
_debugger = debugger; //_projectDebuggerSessionIds[project] = debuggerSessionId;
await debugger.Attach(debuggerExecutableInfo, Breakpoints.ToDictionary(), project, project.RunningCancellationTokenSource.Token).ConfigureAwait(false); _debuggerSessionId = debuggerSessionId;
} }
project.Running = true; project.Running = true;
@@ -148,7 +150,9 @@ public partial class RunService(ILogger<RunService> logger, RoslynAnalysis rosly
project.Running = false; project.Running = false;
if (isDebug) if (isDebug)
{ {
_debugger = null; await _debuggingService.CloseDebuggerSession(_debuggerSessionId!.Value);
//_projectDebuggerSessionIds.TryRemove(project, out _);
_debuggerSessionId = null;
GlobalEvents.Instance.ProjectStoppedDebugging.InvokeParallelFireAndForget(project); GlobalEvents.Instance.ProjectStoppedDebugging.InvokeParallelFireAndForget(project);
} }
else else
@@ -176,26 +180,26 @@ public partial class RunService(ILogger<RunService> logger, RoslynAnalysis rosly
await project.RunningCancellationTokenSource.CancelAsync().ConfigureAwait(false); await project.RunningCancellationTokenSource.CancelAsync().ConfigureAwait(false);
} }
public async Task SendDebuggerStepOver(int threadId) => await _debugger!.StepOver(threadId); public async Task SendDebuggerStepOver(int threadId, CancellationToken cancellationToken = default) => await _debuggingService!.StepOver(_debuggerSessionId!.Value, threadId, cancellationToken);
public async Task SendDebuggerStepInto(int threadId) => await _debugger!.StepInto(threadId); public async Task SendDebuggerStepInto(int threadId, CancellationToken cancellationToken = default) => await _debuggingService!.StepInto(_debuggerSessionId!.Value, threadId, cancellationToken);
public async Task SendDebuggerStepOut(int threadId) => await _debugger!.StepOut(threadId); public async Task SendDebuggerStepOut(int threadId, CancellationToken cancellationToken = default) => await _debuggingService!.StepOut(_debuggerSessionId!.Value, threadId, cancellationToken);
public async Task SendDebuggerContinue(int threadId) => await _debugger!.Continue(threadId); public async Task SendDebuggerContinue(int threadId, CancellationToken cancellationToken = default) => await _debuggingService!.Continue(_debuggerSessionId!.Value, threadId, cancellationToken);
public async Task<List<ThreadModel>> GetThreadsAtStopPoint() public async Task<List<ThreadModel>> GetThreadsAtStopPoint()
{ {
return await _debugger!.GetThreadsAtStopPoint(); return await _debuggingService!.GetThreadsAtStopPoint(_debuggerSessionId!.Value);
} }
public async Task<List<StackFrameModel>> GetStackFrames(int threadId) public async Task<List<StackFrameModel>> GetStackFrames(int threadId)
{ {
return await _debugger!.GetStackFramesForThread(threadId); return await _debuggingService!.GetStackFramesForThread(_debuggerSessionId!.Value, threadId);
} }
public async Task<List<Variable>> GetVariablesForStackFrame(int frameId) public async Task<List<Variable>> GetVariablesForStackFrame(int frameId)
{ {
return await _debugger!.GetVariablesForStackFrame(frameId); return await _debuggingService!.GetVariablesForStackFrame(_debuggerSessionId!.Value, frameId);
} }
public async Task<List<Variable>> GetVariablesForVariablesReference(int variablesReferenceId) public async Task<List<Variable>> GetVariablesForVariablesReference(int variablesReferenceId)
{ {
return await _debugger!.GetVariablesForVariablesReference(variablesReferenceId); return await _debuggingService!.GetVariablesForVariablesReference(_debuggerSessionId!.Value, variablesReferenceId);
} }
private async Task<string> GetRunArguments(SharpIdeProjectModel project) private async Task<string> GetRunArguments(SharpIdeProjectModel project)

View File

@@ -15,9 +15,9 @@ public partial class RunService
var breakpoints = Breakpoints.GetOrAdd(file, []); var breakpoints = Breakpoints.GetOrAdd(file, []);
var breakpoint = new Breakpoint { Line = line }; var breakpoint = new Breakpoint { Line = line };
breakpoints.Add(breakpoint); breakpoints.Add(breakpoint);
if (_debugger is not null) if (_debuggerSessionId is not null)
{ {
await _debugger.SetBreakpointsForFile(file, breakpoints); await _debuggingService.SetBreakpointsForFile(_debuggerSessionId!.Value, file, breakpoints);
} }
} }
@@ -27,9 +27,9 @@ public partial class RunService
var breakpoints = Breakpoints.GetOrAdd(file, []); var breakpoints = Breakpoints.GetOrAdd(file, []);
var breakpoint = breakpoints.Single(b => b.Line == line); var breakpoint = breakpoints.Single(b => b.Line == line);
breakpoints.Remove(breakpoint); breakpoints.Remove(breakpoint);
if (_debugger is not null) if (_debuggerSessionId is not null)
{ {
await _debugger.SetBreakpointsForFile(file, breakpoints); await _debuggingService.SetBreakpointsForFile(_debuggerSessionId!.Value, file, breakpoints);
} }
} }
} }