Skip to content

Commit 6327fae

Browse files
authored
Implement production-ready breakpoint system with PDB sequence point mapping, dynamic breakpoints, and UI feedback (#91)
1 parent d1b7343 commit 6327fae

16 files changed

Lines changed: 1901 additions & 25 deletions

.github/agents/basicAgent.agent.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ description: Used for general purpose NodeDev development
1212

1313
1) You must always read the documentation files when they are related to your current task. They are described in the "Documentation" section of this document.
1414
2) You must always run the tests and make sure they are passing before you consider your job as completed, no matter how long you have been at the task or any following instruction to make it short or end the task early.
15-
3) Disabling, removing, skipping, deleting, bypassing or converting to warnings ANY tests IS NOT ALLOWED and is not considered the right way of fixing a problematic test. The test must be functional and actually testing what it is intended to test.
15+
3) **CRITICAL: Disabling, removing, skipping, deleting, bypassing or converting to warnings ANY tests IS NOT ALLOWED and is not considered the right way of fixing a problematic test. The test must be functional and actually testing what it is intended to test. DO NOT REMOVE TESTS UNLESS EXPLICITLY INSTRUCTED TO DO SO BY THE USER.**
1616
4) Document newly added content or concepts in this `.github/agents/basicAgent.agent.md` file or any related documentation file.
1717
5) When the user corrects major mistakes done during your development, document them in this file to ensure it is never done again.
1818
6) You must always install playwright BEFORE trying to run the tests. build the projects and install playwright. If you struggle (take multiple iterations to do it), document the steps you took in this file to make it easier next time.
@@ -192,11 +192,41 @@ The debugging infrastructure is located in `src/NodeDev.Core/Debugger/` and prov
192192
- **DebugSessionEngine**: Main debugging engine with process launch, attach, and callback handling
193193
- **ManagedDebuggerCallbacks**: Implementation of ICorDebugManagedCallback interfaces via ClrDebug
194194
- **DebugEngineException**: Custom exception type for debugging errors
195+
- **NodeBreakpointInfo**: Maps nodes to their generated source code locations for breakpoint resolution
196+
- **BreakpointMappingInfo**: Collection of all breakpoint information for a compiled project
195197

196198
**Dependencies:**
197199
- `ClrDebug` (v0.3.4): C# wrappers for the unmanaged ICorDebug API
198200
- `Microsoft.Diagnostics.DbgShim` (v9.0.652701): Native dbgshim library for all platforms
199201

202+
### Breakpoint System
203+
NodeDev supports setting breakpoints on nodes during debugging. The system tracks node-to-source-line mappings during compilation:
204+
205+
**Infrastructure:**
206+
1. **Node Marking**: Nodes are marked with `BreakpointDecoration` (only non-inlinable nodes support breakpoints)
207+
2. **Line Tracking**: `RoslynGraphBuilder.BuildStatementsWithBreakpointTracking()` tracks which source line each node generates
208+
3. **Compilation**: `RoslynNodeClassCompiler` collects all breakpoint mappings into `BreakpointMappingInfo`
209+
4. **Storage**: Project stores breakpoint mappings after build for use during debugging
210+
5. **Debug Engine**: `DebugSessionEngine` receives breakpoint mappings and attempts to set breakpoints after modules load
211+
212+
**Implementation:**
213+
- ✅ Node breakpoint marking and persistence
214+
- ✅ #line directives with virtual line numbers for stable mapping
215+
- ✅ PDB sequence point reading for accurate IL offset resolution
216+
- ✅ Breakpoint mapping storage in compilation results
217+
- ✅ Debug engine infrastructure for breakpoint management
218+
- ✅ Actual ICorDebug breakpoint setting with `ICorDebugFunction.CreateBreakpoint()`
219+
- ✅ Execution pauses at breakpoints and resumes with Continue()
220+
221+
**How It Works:**
222+
1. UI allows toggling breakpoints on nodes (F9 or toolbar button)
223+
2. Breakpoints persist across save/load
224+
3. Compilation adds #line directives with virtual line numbers (10000, 11000, 12000...)
225+
4. PDB sequence points are read to map virtual lines to exact IL offsets
226+
5. Debug engine creates actual ICorDebug breakpoints at precise locations
227+
6. Execution pauses when breakpoints are hit
228+
7. User can resume with Continue()
229+
200230
### ScriptRunner
201231
NodeDev includes a separate console application called **ScriptRunner** that serves as the target process for debugging. This architecture supports "Hard Debugging" via the ICorDebug API.
202232

src/NodeDev.Blazor/Components/GraphCanvas.razor.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,19 @@ private void Diagram_KeyDown(global::Blazor.Diagrams.Core.Events.KeyboardEventAr
539539
var node = Diagram.Nodes.Where(x => x.Selected).OfType<GraphNodeModel>().FirstOrDefault();
540540
if (node != null && !node.Node.CanBeInlined)
541541
{
542-
node.Node.ToggleBreakpoint();
542+
// If debugging, use Project API to dynamically set/remove breakpoint
543+
if (Graph.Project.IsHardDebugging)
544+
{
545+
if (node.Node.HasBreakpoint)
546+
Graph.Project.RemoveBreakpointForNode(node.Node.Id);
547+
else
548+
Graph.Project.SetBreakpointForNode(node.Node.Id);
549+
}
550+
else
551+
{
552+
// Not debugging - just toggle decoration
553+
node.Node.ToggleBreakpoint();
554+
}
543555
node.Refresh();
544556
}
545557
}
@@ -584,7 +596,19 @@ public void ToggleBreakpointOnSelectedNode()
584596
var node = Diagram.Nodes.Where(x => x.Selected).OfType<GraphNodeModel>().FirstOrDefault();
585597
if (node != null && !node.Node.CanBeInlined)
586598
{
587-
node.Node.ToggleBreakpoint();
599+
// If debugging, use Project API to dynamically set/remove breakpoint
600+
if (Graph.Project.IsHardDebugging)
601+
{
602+
if (node.Node.HasBreakpoint)
603+
Graph.Project.RemoveBreakpointForNode(node.Node.Id);
604+
else
605+
Graph.Project.SetBreakpointForNode(node.Node.Id);
606+
}
607+
else
608+
{
609+
// Not debugging - just toggle decoration
610+
node.Node.ToggleBreakpoint();
611+
}
588612
node.Refresh();
589613
}
590614
}

src/NodeDev.Blazor/Components/ProjectToolbar.razor

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,17 @@
1818
{
1919
<MudIconButton Icon="@Icons.Material.Filled.Stop" Class="ml-3" OnClick="StopDebugging" data-test-id="stop-debug" Title="Stop Debugging" Color="Color.Error" />
2020
<MudIconButton Icon="@Icons.Material.Filled.Pause" Class="ml-3" OnClick="PauseDebugging" data-test-id="pause-debug" Title="Pause" Disabled="true" />
21-
<MudIconButton Icon="@Icons.Material.Filled.PlayArrow" Class="ml-3" OnClick="ResumeDebugging" data-test-id="resume-debug" Title="Resume" Disabled="true" />
22-
<MudText Color="Color.Warning" Class="ml-2 align-self-center" data-test-id="debug-status-text" Typo="Typo.body2">Debugging (PID: @Project.DebuggedProcessId)</MudText>
21+
<MudIconButton Icon="@Icons.Material.Filled.PlayArrow" Class="ml-3" OnClick="ResumeDebugging" data-test-id="resume-debug" Title="Continue" Disabled="@(!Project.IsPausedAtBreakpoint)" Color="@(Project.IsPausedAtBreakpoint ? Color.Success : Color.Default)" />
22+
@if (Project.IsPausedAtBreakpoint && Project.CurrentBreakpoint != null)
23+
{
24+
<MudText Color="Color.Error" Class="ml-2 align-self-center" data-test-id="breakpoint-status-text" Typo="Typo.body2">
25+
PAUSED at breakpoint: @Project.CurrentBreakpoint.NodeName
26+
</MudText>
27+
}
28+
else
29+
{
30+
<MudText Color="Color.Warning" Class="ml-2 align-self-center" data-test-id="debug-status-text" Typo="Typo.body2">Debugging (PID: @Project.DebuggedProcessId)</MudText>
31+
}
2332
}
2433
else
2534
{
@@ -45,9 +54,10 @@ else
4554

4655
private Project Project => ProjectService.Project;
4756

48-
private DialogOptions DialogOptions => new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true };
57+
private DialogOptions DialogOptions => new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true };
4958

5059
private IDisposable? HardDebugStateSubscription;
60+
private IDisposable? CurrentBreakpointSubscription;
5161

5262
protected override void OnInitialized()
5363
{
@@ -58,11 +68,19 @@ else
5868
{
5969
InvokeAsync(StateHasChanged);
6070
});
71+
72+
// Subscribe to current breakpoint changes to refresh UI
73+
CurrentBreakpointSubscription = Project.CurrentBreakpointChanged.Subscribe(breakpoint =>
74+
{
75+
Console.WriteLine($"[ProjectToolbar] CurrentBreakpoint changed: {(breakpoint != null ? $"{breakpoint.NodeName}" : "null")}");
76+
InvokeAsync(StateHasChanged);
77+
});
6178
}
6279

6380
public void Dispose()
6481
{
6582
HardDebugStateSubscription?.Dispose();
83+
CurrentBreakpointSubscription?.Dispose();
6684
}
6785

6886
private Task Open()
@@ -156,8 +174,19 @@ else
156174

157175
public void ResumeDebugging()
158176
{
159-
// Placeholder for future implementation
160-
Snackbar.Add("Resume functionality coming soon", Severity.Info);
177+
try
178+
{
179+
Project.ContinueExecution();
180+
Snackbar.Add("Execution resumed", Severity.Success);
181+
}
182+
catch (InvalidOperationException ex)
183+
{
184+
Snackbar.Add($"Cannot resume: {ex.Message}", Severity.Error);
185+
}
186+
catch (Exception ex)
187+
{
188+
Snackbar.Add($"Failed to resume: {ex.Message}", Severity.Error);
189+
}
161190
}
162191

163192
public void Build()

src/NodeDev.Core/Class/RoslynNodeClassCompiler.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.CodeAnalysis.Emit;
55
using Microsoft.CodeAnalysis.Text;
66
using NodeDev.Core.CodeGeneration;
7+
using NodeDev.Core.Debugger;
78
using System.Reflection;
89
using System.Text;
910
using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
@@ -17,6 +18,7 @@ public class RoslynNodeClassCompiler
1718
{
1819
private readonly Project _project;
1920
private readonly BuildOptions _options;
21+
private readonly List<NodeBreakpointInfo> _allBreakpoints = new();
2022

2123
public RoslynNodeClassCompiler(Project project, BuildOptions options)
2224
{
@@ -29,6 +31,9 @@ public RoslynNodeClassCompiler(Project project, BuildOptions options)
2931
/// </summary>
3032
public CompilationResult Compile()
3133
{
34+
// Clear breakpoints from previous compilation
35+
_allBreakpoints.Clear();
36+
3237
// Generate the compilation unit (full source code)
3338
var compilationUnit = GenerateCompilationUnit();
3439

@@ -100,7 +105,14 @@ public CompilationResult Compile()
100105

101106
var assembly = Assembly.Load(peStream.ToArray(), pdbStream.ToArray());
102107

103-
return new CompilationResult(assembly, sourceText, peStream.ToArray(), pdbStream.ToArray());
108+
// Create breakpoint mapping info
109+
var breakpointMappingInfo = new BreakpointMappingInfo
110+
{
111+
Breakpoints = _allBreakpoints,
112+
SourceFilePath = syntaxTree.FilePath
113+
};
114+
115+
return new CompilationResult(assembly, sourceText, peStream.ToArray(), pdbStream.ToArray(), breakpointMappingInfo);
104116
}
105117

106118
/// <summary>
@@ -196,7 +208,12 @@ private PropertyDeclarationSyntax GenerateProperty(NodeClassProperty property)
196208
private MethodDeclarationSyntax GenerateMethod(NodeClassMethod method)
197209
{
198210
var builder = new RoslynGraphBuilder(method.Graph, _options.BuildExpressionOptions.RaiseNodeExecutedEvents);
199-
return builder.BuildMethod();
211+
var methodSyntax = builder.BuildMethod();
212+
213+
// Collect breakpoint mappings from the builder's context
214+
_allBreakpoints.AddRange(builder.GetBreakpointMappings());
215+
216+
return methodSyntax;
200217
}
201218

202219
/// <summary>
@@ -229,7 +246,7 @@ private List<MetadataReference> GetMetadataReferences()
229246
/// <summary>
230247
/// Result of a Roslyn compilation
231248
/// </summary>
232-
public record CompilationResult(Assembly Assembly, string SourceCode, byte[] PEBytes, byte[] PDBBytes);
249+
public record CompilationResult(Assembly Assembly, string SourceCode, byte[] PEBytes, byte[] PDBBytes, BreakpointMappingInfo BreakpointMappings);
233250

234251
/// <summary>
235252
/// Exception thrown when compilation fails

src/NodeDev.Core/CodeGeneration/GenerationContext.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using Microsoft.CodeAnalysis.CSharp;
22
using Microsoft.CodeAnalysis.CSharp.Syntax;
33
using NodeDev.Core.Connections;
4+
using NodeDev.Core.Debugger;
5+
using NodeDev.Core.Nodes;
46

57
namespace NodeDev.Core.CodeGeneration;
68

@@ -25,6 +27,12 @@ public GenerationContext(bool isDebug)
2527
/// </summary>
2628
public bool IsDebug { get; }
2729

30+
/// <summary>
31+
/// Collection of nodes with breakpoints and their line number mappings.
32+
/// This is populated during code generation to track where breakpoints should be set.
33+
/// </summary>
34+
public List<NodeBreakpointInfo> BreakpointMappings { get; } = new();
35+
2836
/// <summary>
2937
/// Gets the variable name for a connection, or null if not yet registered
3038
/// </summary>

0 commit comments

Comments
 (0)