using System.Diagnostics; using System.Reflection; using Ardalis.GuardClauses; using Microsoft.Diagnostics.NETCore.Client; using Microsoft.VisualStudio.Shared.VSCodeDebugProtocol; using Microsoft.VisualStudio.Shared.VSCodeDebugProtocol.Messages; using Newtonsoft.Json.Linq; using SharpIDE.Application.Features.Debugging.Experimental.VsDbg; using SharpIDE.Application.Features.Events; using SharpIDE.Application.Features.SolutionDiscovery; namespace SharpIDE.Application.Features.Debugging; #pragma warning disable VSTHRD101 public class DebuggingService { private DebugProtocolHost _debugProtocolHost = null!; public async Task Attach(int debuggeeProcessId, string? debuggerExecutablePath, Dictionary> breakpointsByFile, CancellationToken cancellationToken = default) { Guard.Against.NegativeOrZero(debuggeeProcessId, nameof(debuggeeProcessId), "Process ID must be a positive integer."); await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); if (string.IsNullOrWhiteSpace(debuggerExecutablePath)) { throw new ArgumentNullException(nameof(debuggerExecutablePath), "Debugger executable path cannot be null or empty."); } var process = new Process { StartInfo = new ProcessStartInfo { //FileName = @"C:\Users\Matthew\Downloads\netcoredbg-win64\netcoredbg\netcoredbg.exe", FileName = debuggerExecutablePath, Arguments = "--interpreter=vscode", RedirectStandardInput = true, RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true } }; process.Start(); var debugProtocolHost = new DebugProtocolHost(process.StandardInput.BaseStream, process.StandardOutput.BaseStream, false); _debugProtocolHost = debugProtocolHost; debugProtocolHost.LogMessage += (sender, args) => { //Console.WriteLine($"Log message: {args.Message}"); }; debugProtocolHost.EventReceived += (sender, args) => { Console.WriteLine($"Event received: {args.EventType}"); }; debugProtocolHost.DispatcherError += (sender, args) => { Console.WriteLine($"Dispatcher error: {args.Exception}"); }; debugProtocolHost.RequestReceived += (sender, args) => { Console.WriteLine($"Request received: {args.Command}"); }; debugProtocolHost.RegisterEventType(@event => { ; }); debugProtocolHost.RegisterEventType(async void (@event) => { await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); // The VS Code Debug Protocol throws if you try to send a request from the dispatcher thread debugProtocolHost.SendRequestSync(new DisconnectRequest()); }); debugProtocolHost.RegisterClientRequestType(async void (responder) => { var signatureResponse = await DebuggerHandshakeSigner.Sign(responder.Arguments.Value); responder.SetResponse(new HandshakeResponse(signatureResponse)); }); debugProtocolHost.RegisterEventType(async void (@event) => { await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); // The VS Code Debug Protocol throws if you try to send a request from the dispatcher thread var additionalProperties = @event.AdditionalProperties; // source, line, column var filePath = additionalProperties?["source"]?["path"]!.Value()!; var line = (additionalProperties?["line"]?.Value()!).Value; var executionStopInfo = new ExecutionStopInfo { FilePath = filePath, Line = line, ThreadId = @event.ThreadId!.Value }; GlobalEvents.Instance.DebuggerExecutionStopped.InvokeParallelFireAndForget(executionStopInfo); if (@event.Reason is StoppedEvent.ReasonValue.Exception) { Console.WriteLine("Stopped due to exception, continuing"); var continueRequest = new ContinueRequest { ThreadId = @event.ThreadId!.Value }; _debugProtocolHost.SendRequestSync(continueRequest); } }); debugProtocolHost.VerifySynchronousOperationAllowed(); var initializeRequest = new InitializeRequest { ClientID = "vscode", ClientName = "Visual Studio Code", AdapterID = "coreclr", Locale = "en-us", LinesStartAt1 = true, ColumnsStartAt1 = true, PathFormat = InitializeArguments.PathFormatValue.Path, SupportsVariableType = true, SupportsVariablePaging = true, SupportsRunInTerminalRequest = true, SupportsHandshakeRequest = true }; debugProtocolHost.Run(); var response = debugProtocolHost.SendRequestSync(initializeRequest); var attachRequest = new AttachRequest { ConfigurationProperties = new Dictionary { ["name"] = "AttachRequestName", ["type"] = "coreclr", ["processId"] = debuggeeProcessId, ["console"] = "internalConsole", // integratedTerminal, externalTerminal, internalConsole } }; debugProtocolHost.SendRequestSync(attachRequest); foreach (var breakpoint in breakpointsByFile) { var setBreakpointsRequest = new SetBreakpointsRequest { Source = new Source { Path = breakpoint.Key.Path }, Breakpoints = breakpoint.Value.Select(b => new SourceBreakpoint { Line = b.Line }).ToList() }; var breakpointsResponse = debugProtocolHost.SendRequestSync(setBreakpointsRequest); } new DiagnosticsClient(debuggeeProcessId).ResumeRuntime(); var configurationDoneRequest = new ConfigurationDoneRequest(); debugProtocolHost.SendRequestSync(configurationDoneRequest); } // Typically you would do attachRequest, configurationDoneRequest, setBreakpointsRequest, then ResumeRuntime. But netcoredbg blows up on configurationDoneRequuest if ResumeRuntime hasn't been called yet. public async Task StepOver(int threadId, CancellationToken cancellationToken) { await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); var nextRequest = new NextRequest(threadId); _debugProtocolHost.SendRequestSync(nextRequest); } public async Task GetInfoAtStopPoint() { var model = new ThreadsStackTraceModel(); try { var threads = _debugProtocolHost.SendRequestSync(new ThreadsRequest()); foreach (var thread in threads.Threads) { var threadModel = new ThreadModel { Id = thread.Id, Name = thread.Name }; model.Threads.Add(threadModel); var stackTrace = _debugProtocolHost.SendRequestSync(new StackTraceRequest { ThreadId = thread.Id }); var frame = stackTrace.StackFrames!.FirstOrDefault(); if (frame == null) continue; var name = frame.Name; if (name == "[External Code]") continue; // TODO: handle this case // Infrastructure.dll!Infrastructure.DependencyInjection.AddInfrastructure(Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.Extensions.Configuration.IConfiguration configuration) Line 23 // need to parse out the class name, method name, namespace, assembly name var methodName = name.Split('!')[1].Split('(')[0]; var className = methodName.Split('.').Reverse().Skip(1).First(); var namespaceName = string.Join('.', methodName.Split('.').Reverse().Skip(2).Reverse()); var assemblyName = name.Split('!')[0]; methodName = methodName.Split('.').Reverse().First(); var frameModel = new StackFrameModel { Id = frame.Id, Name = frame.Name, Line = frame.Line, Column = frame.Column, Source = frame.Source?.Path, ClassName = className, MethodName = methodName, Namespace = namespaceName, AssemblyName = assemblyName }; threadModel.StackFrames.Add(frameModel); var scopes = _debugProtocolHost.SendRequestSync(new ScopesRequest { FrameId = frame.Id }); foreach (var scope in scopes.Scopes) { var scopeModel = new ScopeModel { Name = scope.Name }; frameModel.Scopes.Add(scopeModel); var variablesResponse = _debugProtocolHost.SendRequestSync(new VariablesRequest { VariablesReference = scope.VariablesReference }); scopeModel.Variables = variablesResponse.Variables; } } } catch (Exception) { throw; } return model; } }