add v1 debug service

This commit is contained in:
Matt Parker
2025-08-24 13:37:18 +10:00
parent 356a535085
commit 39292d27fd
4 changed files with 108 additions and 90 deletions

View File

@@ -0,0 +1,15 @@
using SharpIDE.Application.Features.Debugging.Experimental;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
namespace SharpIDE.Application.Features.Debugging;
public class Debugger
{
public required SharpIdeProjectModel Project { get; init; }
public required int ProcessId { get; init; }
public async Task Attach(CancellationToken cancellationToken)
{
var debuggingService = new DebuggingService();
await debuggingService.Attach(ProcessId, cancellationToken);
}
}

View File

@@ -1,119 +1,106 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO.Pipelines; using Ardalis.GuardClauses;
using OmniSharp.Extensions.DebugAdapter.Client; using Microsoft.Diagnostics.NETCore.Client;
using OmniSharp.Extensions.DebugAdapter.Protocol.Events; using Microsoft.VisualStudio.Shared.VSCodeDebugProtocol;
using OmniSharp.Extensions.DebugAdapter.Protocol.Models; using Microsoft.VisualStudio.Shared.VSCodeDebugProtocol.Messages;
using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; using Newtonsoft.Json.Linq;
using SharpIDE.Application.Features.Debugging.Experimental;
namespace SharpIDE.Application.Features.Debugging; namespace SharpIDE.Application.Features.Debugging;
public class DebuggingService public class DebuggingService
{ {
public async Task Attach(int debuggeeProcessId, CancellationToken cancellationToken = default)
public async Task Test(CancellationToken cancellationToken = default)
{ {
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);
var process = new Process var process = new Process
{ {
StartInfo = new ProcessStartInfo StartInfo = new ProcessStartInfo
{ {
FileName = @"C:\Users\Matthew\Downloads\netcoredbg-win64\netcoredbg\netcoredbg.exe", FileName = @"C:\Users\Matthew\Downloads\netcoredbg-win64\netcoredbg\netcoredbg.exe",
Arguments = """--interpreter=vscode""", //FileName = @"C:\Users\Matthew\.vscode-insiders\extensions\ms-dotnettools.csharp-2.83.5-win32-x64\.debugger\x86_64\vsdbg.exe",
Arguments = "--interpreter=vscode",
RedirectStandardInput = true, RedirectStandardInput = true,
RedirectStandardOutput = true, RedirectStandardOutput = true,
UseShellExecute = false UseShellExecute = false,
CreateNoWindow = true
} }
}; };
process.Start(); process.Start();
var stoppedTcs = new TaskCompletionSource<StoppedEvent>(); var debugProtocolHost = new DebugProtocolHost(process.StandardInput.BaseStream, process.StandardOutput.BaseStream, false);
debugProtocolHost.LogMessage += (sender, args) =>
var client = DebugAdapterClient.Create(options =>
{ {
options.AdapterId = "coreclr"; Console.WriteLine($"Log message: {args.Message}");
options.ClientId = "vscode"; };
options.ClientName = "Visual Studio Code"; debugProtocolHost.EventReceived += (sender, args) =>
options.LinesStartAt1 = true; {
options.ColumnsStartAt1 = true; Console.WriteLine($"Event received: {args.EventType}");
options.SupportsVariableType = true; };
options.SupportsVariablePaging = true; debugProtocolHost.DispatcherError += (sender, args) =>
options.SupportsRunInTerminalRequest = true; {
options.Locale = "en-us"; Console.WriteLine($"Dispatcher error: {args.Exception}");
options.PathFormat = PathFormat.Path; };
options debugProtocolHost.RequestReceived += (sender, args) =>
.WithInput(process.StandardOutput.BaseStream) {
.WithOutput(process.StandardInput.BaseStream) Console.WriteLine($"Request received: {args.Command}");
.OnStarted(async (adapterClient, token) => };
{ debugProtocolHost.RegisterEventType<OutputEvent>(@event =>
Console.WriteLine("Started"); {
}) ;
.OnCapabilities(async (capabilitiesEvent, token) =>
{
Console.WriteLine("Capabilities");
})
.OnBreakpoint(async (breakpointEvent, token) =>
{
Console.WriteLine($"Breakpoint hit: {breakpointEvent}");
})
.OnRunInTerminal(async (arguments, token) =>
{
Console.WriteLine("Run In Terminal");
return new RunInTerminalResponse();
})
.OnStopped(async (stoppedEvent, token) =>
{
Console.WriteLine($"Notification received: {stoppedEvent}");
stoppedTcs.SetResult(stoppedEvent);
})
.OnTerminated(async (terminatedEvent, token) =>
{
Console.WriteLine($"Terminated: {terminatedEvent}");
});
//.OnNotification(EventNames., );
}); });
debugProtocolHost.RegisterClientRequestType<HandshakeRequest, HandshakeArguments, HandshakeResponse>(responder =>
{
var args = responder.Arguments;
var signatureResponse = VsSigner.Sign(responder.Arguments.Value);
responder.SetResponse(new HandshakeResponse(signatureResponse));
});
debugProtocolHost.RegisterEventType<StoppedEvent>(@event =>
{
;
var threadId = @event.ThreadId;
});
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);
await client.Initialize(cancellationToken); var attachRequest = new AttachRequest
var breakpointsResponse = await client.SetBreakpoints(new SetBreakpointsArguments
{ {
Source = new Source { Path = @"C:\Users\Matthew\Documents\Git\BlazorCodeBreaker\src\WebApi\Program.cs" }, ConfigurationProperties = new Dictionary<string, JToken>
Breakpoints = new Container<SourceBreakpoint>(new SourceBreakpoint { Line = 7 })
}, cancellationToken);
var launchResponse = await client.Launch(new LaunchRequestArguments()
{
NoDebug = false,
ExtensionData = new Dictionary<string, object>
{ {
["name"] = "LaunchRequestName", ["name"] = "AttachRequestName",
["type"] = "coreclr", ["type"] = "coreclr",
["program"] = @"C:\Users\Matthew\Documents\Git\BlazorCodeBreaker\artifacts\bin\WebApi\debug\WebApi.dll", ["processId"] = debuggeeProcessId,
["cwd"] = "" ["console"] = "internalConsole", // integratedTerminal, externalTerminal, internalConsole
//["cwd"] = @"C:\Users\Matthew\Documents\Git\BlazorCodeBreaker\artifacts\bin\WebApi\debug", // working directory
//["stopAtEntry"] = true,
//["env"] = new Dictionary<string, string> { { "ASPNETCORE_ENVIRONMENT", "Development" } }
} }
}, cancellationToken); };
debugProtocolHost.SendRequestSync(attachRequest);
var configurationDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments(), cancellationToken); // var breakpointRequest = new SetBreakpointsRequest
// {
var stoppedEvent = await stoppedTcs.Task; // Source = new Source { Path = @"C:\Users\Matthew\Documents\Git\BlazorCodeBreaker\src\WebApi\Program.cs" },
var threads = await client.RequestThreads(new ThreadsArguments(), cancellationToken); // Breakpoints = [new SourceBreakpoint { Line = 7 }]
// };
var currentThread = threads.Threads!.Single(s => s.Id == stoppedEvent.ThreadId); // var breakpointsResponse = debugProtocolHost.SendRequestSync(breakpointRequest);
var stackTrace = await client.RequestStackTrace(new StackTraceArguments { ThreadId = currentThread.Id }, cancellationToken); new DiagnosticsClient(debuggeeProcessId).ResumeRuntime();
var frame = stackTrace.StackFrames!.First(); var configurationDoneRequest = new ConfigurationDoneRequest();
var scopes = await client.RequestScopes(new ScopesArguments { FrameId = frame.Id }, cancellationToken); debugProtocolHost.SendRequestSync(configurationDoneRequest);
var scope = scopes.Scopes.First();
var variablesResponse = await client.RequestVariables(new VariablesArguments() {VariablesReference = scope.VariablesReference}, cancellationToken);
var variable = variablesResponse.Variables!.Skip(1).First();
var variables2Response = await client.RequestVariables(new VariablesArguments() {VariablesReference = variable.VariablesReference}, cancellationToken);
var variable2 = variables2Response.Variables!.First();
var variables3Response = await client.RequestVariables(new VariablesArguments() {VariablesReference = variable2.VariablesReference}, cancellationToken);
//var continueResponse = await client.RequestContinue(new ContinueArguments(){ThreadId = 1}, cancellationToken);
//await Task.Delay(1000);
//var test = await client.RequestNext(new NextArguments(), cancellationToken: cancellationToken);
//var result = await client.RequestStepIn(new StepInArguments(), cancellationToken: cancellationToken);
await process.WaitForExitAsync();
} }
// Typically you would do attachRequest, configurationDoneRequest, setBreakpointsRequest, then ResumeRuntime. But netcoredbg blows up on configurationDoneRequuest if ResumeRuntime hasn't been called yet.
} }

View File

@@ -2,6 +2,7 @@
using System.Threading.Channels; using System.Threading.Channels;
using Ardalis.GuardClauses; using Ardalis.GuardClauses;
using AsyncReadProcess; using AsyncReadProcess;
using SharpIDE.Application.Features.Debugging;
using SharpIDE.Application.Features.Evaluation; using SharpIDE.Application.Features.Evaluation;
using SharpIDE.Application.Features.Events; using SharpIDE.Application.Features.Events;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
@@ -13,6 +14,7 @@ public class RunService
private readonly ConcurrentDictionary<SharpIdeProjectModel, SemaphoreSlim> _projectLocks = []; private readonly ConcurrentDictionary<SharpIdeProjectModel, SemaphoreSlim> _projectLocks = [];
public async Task RunProject(SharpIdeProjectModel project) public async Task RunProject(SharpIdeProjectModel project)
{ {
var isDebug = true;
Guard.Against.Null(project, nameof(project)); Guard.Against.Null(project, nameof(project));
Guard.Against.NullOrWhiteSpace(project.FilePath, nameof(project.FilePath), "Project file path cannot be null or empty."); Guard.Against.NullOrWhiteSpace(project.FilePath, nameof(project.FilePath), "Project file path cannot be null or empty.");
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
@@ -28,6 +30,7 @@ public class RunService
var launchProfile = launchProfiles.FirstOrDefault(); var launchProfile = launchProfiles.FirstOrDefault();
try try
{ {
// TODO: handle running Blazor projects
var processStartInfo = new ProcessStartInfo2 var processStartInfo = new ProcessStartInfo2
{ {
FileName = "dotnet", FileName = "dotnet",
@@ -46,6 +49,10 @@ public class RunService
} }
if (launchProfile.ApplicationUrl != null) processStartInfo.EnvironmentVariables["ASPNETCORE_URLS"] = launchProfile.ApplicationUrl; if (launchProfile.ApplicationUrl != null) processStartInfo.EnvironmentVariables["ASPNETCORE_URLS"] = launchProfile.ApplicationUrl;
} }
if (isDebug)
{
processStartInfo.EnvironmentVariables["DOTNET_DefaultDiagnosticPortSuspend"] = "1";
}
var process = new Process2 var process = new Process2
{ {
@@ -72,6 +79,13 @@ public class RunService
logsDrained.TrySetResult(); logsDrained.TrySetResult();
}); });
if (isDebug)
{
// Attach debugger (which internally uses a DiagnosticClient to resume startup)
var debuggingService = new Debugger { Project = project, ProcessId = process.ProcessId };
await debuggingService.Attach(project.RunningCancellationTokenSource.Token).ConfigureAwait(false);
}
project.Running = true; project.Running = true;
project.OpenInRunPanel = true; project.OpenInRunPanel = true;
GlobalEvents.InvokeProjectsRunningChanged(); GlobalEvents.InvokeProjectsRunningChanged();

View File

@@ -13,6 +13,8 @@
<HintPath>.\Microsoft.CodeAnalysis.Workspaces.MSBuild.dll</HintPath> <HintPath>.\Microsoft.CodeAnalysis.Workspaces.MSBuild.dll</HintPath>
<Private>true</Private> <Private>true</Private>
</Reference> </Reference>
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.641201" />
<PackageReference Include="Microsoft.VisualStudio.Shared.VSCodeDebugProtocol" Version="18.0.10427.1" />
<!-- If any Microsoft.Build.*.dll (Excluding Locator) ends up in the output, it will be prioritised for loading by MSBuild Nodes --> <!-- If any Microsoft.Build.*.dll (Excluding Locator) ends up in the output, it will be prioritised for loading by MSBuild Nodes -->
<PackageReference Include="OmniSharp.Extensions.DebugAdapter.Client" Version="0.19.9" /> <PackageReference Include="OmniSharp.Extensions.DebugAdapter.Client" Version="0.19.9" />
<PackageReference Include="Ardalis.GuardClauses" Version="5.0.0" /> <PackageReference Include="Ardalis.GuardClauses" Version="5.0.0" />