diff --git a/src/SharpIDE.Application/DependencyInjection.cs b/src/SharpIDE.Application/DependencyInjection.cs index 5c6ee65..1eaa695 100644 --- a/src/SharpIDE.Application/DependencyInjection.cs +++ b/src/SharpIDE.Application/DependencyInjection.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using SharpIDE.Application.Features.Analysis; using SharpIDE.Application.Features.Build; +using SharpIDE.Application.Features.Debugging; using SharpIDE.Application.Features.Editor; using SharpIDE.Application.Features.Evaluation; using SharpIDE.Application.Features.FilePersistence; @@ -19,6 +20,7 @@ public static class DependencyInjection { services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/SharpIDE.Application/Features/Debugging/Debugger.cs b/src/SharpIDE.Application/Features/Debugging/Debugger.cs deleted file mode 100644 index cded38e..0000000 --- a/src/SharpIDE.Application/Features/Debugging/Debugger.cs +++ /dev/null @@ -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> breakpointsByFile, SharpIdeProjectModel project, CancellationToken cancellationToken) - { - await _debuggingService.Attach(ProcessId, debuggerExecutableInfo, breakpointsByFile, project, cancellationToken); - } - public async Task SetBreakpointsForFile(SharpIdeFile file, List 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> GetThreadsAtStopPoint() => await _debuggingService.GetThreadsAtStopPoint(); - public async Task> GetStackFramesForThread(int threadId) => await _debuggingService.GetStackFramesForThread(threadId); - public async Task> GetVariablesForStackFrame(int frameId) => await _debuggingService.GetVariablesForStackFrame(frameId); - public async Task> GetVariablesForVariablesReference(int variablesReferenceId) => await _debuggingService.GetVariablesForVariablesReference(variablesReferenceId); -} diff --git a/src/SharpIDE.Application/Features/Debugging/DebuggerSessionId.cs b/src/SharpIDE.Application/Features/Debugging/DebuggerSessionId.cs new file mode 100644 index 0000000..19ae7e5 --- /dev/null +++ b/src/SharpIDE.Application/Features/Debugging/DebuggerSessionId.cs @@ -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; + } +} diff --git a/src/SharpIDE.Application/Features/Debugging/DebuggingService.cs b/src/SharpIDE.Application/Features/Debugging/DebuggingService.cs index 96a6967..f2b8154 100644 --- a/src/SharpIDE.Application/Features/Debugging/DebuggingService.cs +++ b/src/SharpIDE.Application/Features/Debugging/DebuggingService.cs @@ -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> breakpointsByFile, SharpIdeProjectModel project, CancellationToken cancellationToken = default) + private ConcurrentDictionary _debugProtocolHosts = []; + + /// The debugging session ID + public async Task Attach(int debuggeeProcessId, DebuggerExecutableInfo? debuggerExecutableInfo, Dictionary> 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 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 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> GetThreadsAtStopPoint() + public async Task> 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> GetStackFramesForThread(int threadId) + public async Task> 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> GetVariablesForStackFrame(int frameId) + public async Task> 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(); 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> GetVariablesForVariablesReference(int variablesReference) + public async Task> 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; } diff --git a/src/SharpIDE.Application/Features/Run/RunService.cs b/src/SharpIDE.Application/Features/Run/RunService.cs index fb0eb1b..c1148d8 100644 --- a/src/SharpIDE.Application/Features/Run/RunService.cs +++ b/src/SharpIDE.Application/Features/Run/RunService.cs @@ -15,14 +15,16 @@ using Breakpoint = SharpIDE.Application.Features.Debugging.Breakpoint; namespace SharpIDE.Application.Features.Run; -public partial class RunService(ILogger logger, RoslynAnalysis roslynAnalysis, BuildService buildService) +public partial class RunService(ILogger logger, RoslynAnalysis roslynAnalysis, BuildService buildService, DebuggingService debuggingService) { private readonly ConcurrentDictionary _projectLocks = []; - private Debugger? _debugger; // TODO: Support multiple debuggers for multiple running projects + //private readonly ConcurrentDictionary _projectDebuggerSessionIds = []; + private DebuggerSessionId? _debuggerSessionId; // TODO: Support multiple debuggers for multiple running projects private readonly ILogger _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 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 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 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> GetThreadsAtStopPoint() { - return await _debugger!.GetThreadsAtStopPoint(); + return await _debuggingService!.GetThreadsAtStopPoint(_debuggerSessionId!.Value); } public async Task> GetStackFrames(int threadId) { - return await _debugger!.GetStackFramesForThread(threadId); + return await _debuggingService!.GetStackFramesForThread(_debuggerSessionId!.Value, threadId); } public async Task> GetVariablesForStackFrame(int frameId) { - return await _debugger!.GetVariablesForStackFrame(frameId); + return await _debuggingService!.GetVariablesForStackFrame(_debuggerSessionId!.Value, frameId); } public async Task> GetVariablesForVariablesReference(int variablesReferenceId) { - return await _debugger!.GetVariablesForVariablesReference(variablesReferenceId); + return await _debuggingService!.GetVariablesForVariablesReference(_debuggerSessionId!.Value, variablesReferenceId); } private async Task GetRunArguments(SharpIdeProjectModel project) diff --git a/src/SharpIDE.Application/Features/Run/RunService_Breakpoints.cs b/src/SharpIDE.Application/Features/Run/RunService_Breakpoints.cs index 9e4dc40..4758440 100644 --- a/src/SharpIDE.Application/Features/Run/RunService_Breakpoints.cs +++ b/src/SharpIDE.Application/Features/Run/RunService_Breakpoints.cs @@ -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); } } }