refactor debugging to sessions
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user