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,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.Reflection;
using System.Collections.Concurrent;
using Ardalis.GuardClauses;
using Microsoft.Diagnostics.NETCore.Client;
using Microsoft.VisualStudio.Shared.VSCodeDebugProtocol;
@@ -16,8 +15,10 @@ namespace SharpIDE.Application.Features.Debugging;
#pragma warning disable VSTHRD101
public class DebuggingService
{
private DebugProtocolHost _debugProtocolHost = null!;
public async Task Attach(int debuggeeProcessId, DebuggerExecutableInfo? debuggerExecutableInfo, Dictionary<SharpIdeFile, List<Breakpoint>> breakpointsByFile, SharpIdeProjectModel project, CancellationToken cancellationToken = default)
private ConcurrentDictionary<DebuggerSessionId, DebugProtocolHost> _debugProtocolHosts = [];
/// <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.");
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
@@ -26,7 +27,6 @@ public class DebuggingService
var debugProtocolHost = new DebugProtocolHost(inputStream, outputStream, false);
var initializedEventTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
_debugProtocolHost = debugProtocolHost;
debugProtocolHost.LogMessage += (sender, args) =>
{
//Console.WriteLine($"Log message: {args.Message}");
@@ -70,7 +70,7 @@ public class DebuggingService
{
Console.WriteLine("Stopped due to exception, continuing");
var continueRequest = new ContinueRequest { ThreadId = @event.ThreadId!.Value };
_debugProtocolHost.SendRequestSync(continueRequest);
debugProtocolHost.SendRequestSync(continueRequest);
return;
}
var additionalProperties = @event.AdditionalProperties;
@@ -157,51 +157,72 @@ public class DebuggingService
debugProtocolHost.SendRequestSync(configurationDoneRequest);
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
{
Source = new Source { Path = file.Path },
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);
var debugProtocolHost = _debugProtocolHosts[debuggerSessionId];
var nextRequest = new NextRequest(threadId);
_debugProtocolHost.SendRequestSync(nextRequest);
debugProtocolHost.SendRequestSync(nextRequest);
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);
var debugProtocolHost = _debugProtocolHosts[debuggerSessionId];
var stepInRequest = new StepInRequest(threadId);
_debugProtocolHost.SendRequestSync(stepInRequest);
debugProtocolHost.SendRequestSync(stepInRequest);
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);
var debugProtocolHost = _debugProtocolHosts[debuggerSessionId];
var stepOutRequest = new StepOutRequest(threadId);
_debugProtocolHost.SendRequestSync(stepOutRequest);
debugProtocolHost.SendRequestSync(stepOutRequest);
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);
var debugProtocolHost = _debugProtocolHosts[debuggerSessionId];
var continueRequest = new ContinueRequest(threadId);
_debugProtocolHost.SendRequestSync(continueRequest);
debugProtocolHost.SendRequestSync(continueRequest);
GlobalEvents.Instance.DebuggerExecutionContinued.InvokeParallelFireAndForget();
}
public async Task<List<ThreadModel>> GetThreadsAtStopPoint()
public async Task<List<ThreadModel>> GetThreadsAtStopPoint(DebuggerSessionId debuggerSessionId)
{
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
{
Id = s.Id,
@@ -210,10 +231,11 @@ public class DebuggingService
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 stackTraceResponse = _debugProtocolHost.SendRequestSync(stackTraceRequest);
var debugProtocolHost = _debugProtocolHosts[debuggerSessionId];
var stackTraceResponse = debugProtocolHost.SendRequestSync(stackTraceRequest);
var stackFrames = stackTraceResponse.StackFrames;
var mappedStackFrames = stackFrames!.Select(frame =>
@@ -234,24 +256,26 @@ public class DebuggingService
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 scopesResponse = _debugProtocolHost.SendRequestSync(scopesRequest);
var debugProtocolHost = _debugProtocolHosts[debuggerSessionId];
var scopesResponse = debugProtocolHost.SendRequestSync(scopesRequest);
var allVariables = new List<Variable>();
foreach (var scope in scopesResponse.Scopes)
{
var variablesRequest = new VariablesRequest { VariablesReference = scope.VariablesReference };
var variablesResponse = _debugProtocolHost.SendRequestSync(variablesRequest);
var variablesResponse = debugProtocolHost.SendRequestSync(variablesRequest);
allVariables.AddRange(variablesResponse.Variables);
}
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 variablesResponse = _debugProtocolHost.SendRequestSync(variablesRequest);
var variablesResponse = debugProtocolHost.SendRequestSync(variablesRequest);
return variablesResponse.Variables;
}

View File

@@ -15,14 +15,16 @@ using Breakpoint = SharpIDE.Application.Features.Debugging.Breakpoint;
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 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 RoslynAnalysis _roslynAnalysis = roslynAnalysis;
private readonly BuildService _buildService = buildService;
private readonly DebuggingService _debuggingService = debuggingService;
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)
{
// Attach debugger (which internally uses a DiagnosticClient to resume startup)
var debugger = new Debugger { Project = project, ProcessId = process.ProcessId };
_debugger = debugger;
await debugger.Attach(debuggerExecutableInfo, Breakpoints.ToDictionary(), project, project.RunningCancellationTokenSource.Token).ConfigureAwait(false);
var debuggerSessionId = await _debuggingService.Attach(process.ProcessId, debuggerExecutableInfo, Breakpoints.ToDictionary(), project, project.RunningCancellationTokenSource.Token);
//_projectDebuggerSessionIds[project] = debuggerSessionId;
_debuggerSessionId = debuggerSessionId;
}
project.Running = true;
@@ -148,7 +150,9 @@ public partial class RunService(ILogger<RunService> logger, RoslynAnalysis rosly
project.Running = false;
if (isDebug)
{
_debugger = null;
await _debuggingService.CloseDebuggerSession(_debuggerSessionId!.Value);
//_projectDebuggerSessionIds.TryRemove(project, out _);
_debuggerSessionId = null;
GlobalEvents.Instance.ProjectStoppedDebugging.InvokeParallelFireAndForget(project);
}
else
@@ -176,26 +180,26 @@ public partial class RunService(ILogger<RunService> logger, RoslynAnalysis rosly
await project.RunningCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
public async Task SendDebuggerStepOver(int threadId) => await _debugger!.StepOver(threadId);
public async Task SendDebuggerStepInto(int threadId) => await _debugger!.StepInto(threadId);
public async Task SendDebuggerStepOut(int threadId) => await _debugger!.StepOut(threadId);
public async Task SendDebuggerContinue(int threadId) => await _debugger!.Continue(threadId);
public async Task SendDebuggerStepOver(int threadId, CancellationToken cancellationToken = default) => await _debuggingService!.StepOver(_debuggerSessionId!.Value, threadId, cancellationToken);
public async Task SendDebuggerStepInto(int threadId, CancellationToken cancellationToken = default) => await _debuggingService!.StepInto(_debuggerSessionId!.Value, threadId, cancellationToken);
public async Task SendDebuggerStepOut(int threadId, CancellationToken cancellationToken = default) => await _debuggingService!.StepOut(_debuggerSessionId!.Value, threadId, cancellationToken);
public async Task SendDebuggerContinue(int threadId, CancellationToken cancellationToken = default) => await _debuggingService!.Continue(_debuggerSessionId!.Value, threadId, cancellationToken);
public async Task<List<ThreadModel>> GetThreadsAtStopPoint()
{
return await _debugger!.GetThreadsAtStopPoint();
return await _debuggingService!.GetThreadsAtStopPoint(_debuggerSessionId!.Value);
}
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)
{
return await _debugger!.GetVariablesForStackFrame(frameId);
return await _debuggingService!.GetVariablesForStackFrame(_debuggerSessionId!.Value, frameId);
}
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)

View File

@@ -15,9 +15,9 @@ public partial class RunService
var breakpoints = Breakpoints.GetOrAdd(file, []);
var breakpoint = new Breakpoint { Line = line };
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 breakpoint = breakpoints.Single(b => b.Line == line);
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);
}
}
}