Skip to content

Commit 76456a4

Browse files
Copilotsnakex64
andcommitted
Enable dynamic breakpoint setting during debugging
- Generate #line directives for ALL nodes in debug builds (not just breakpointed ones) - All nodes now tracked in PDB with virtual line numbers - Added ShouldSetBreakpointForNode delegate to filter which breakpoints to set - Added SetBreakpointForNode/RemoveBreakpointForNode methods to Project - Breakpoints can now be added/removed dynamically after build and attach - All 9 unit tests passing This allows users to add breakpoints at any time during a debug session, not just before building. Co-authored-by: snakex64 <39806655+snakex64@users.noreply.github.com>
1 parent a27bc00 commit 76456a4

3 files changed

Lines changed: 195 additions & 36 deletions

File tree

src/NodeDev.Core/CodeGeneration/RoslynGraphBuilder.cs

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ public MethodDeclarationSyntax BuildMethod()
9898
// Get full class name for breakpoint info
9999
string fullClassName = $"{_graph.SelfClass.Namespace}.{_graph.SelfClass.Name}";
100100

101-
var bodyStatements = _context.IsDebug && _graph.Nodes.Values.Any(n => n.HasBreakpoint)
101+
// In debug builds, always track line numbers for all nodes (not just those with breakpoints)
102+
// This allows breakpoints to be set dynamically during debugging
103+
var bodyStatements = _context.IsDebug
102104
? BuildStatementsWithBreakpointTracking(chunks, fullClassName, method.Name)
103105
: BuildStatements(chunks);
104106

@@ -204,39 +206,38 @@ internal List<StatementSyntax> BuildStatementsWithBreakpointTracking(Graph.NodeP
204206
// Generate the statement for this node
205207
var statement = node.GenerateRoslynStatement(chunk.SubChunk, _context);
206208

207-
// If this node has a breakpoint, add #line directive and record mapping
208-
if (node.HasBreakpoint)
209+
// In debug builds, ALWAYS add #line directive for every node (not just those with breakpoints)
210+
// This allows breakpoints to be set dynamically during debugging
211+
// Create a #line directive that maps this statement to a unique virtual line
212+
// The virtual line encodes the node's execution order: 10000 + (order * 1000)
213+
int nodeVirtualLine = 10000 + (nodeExecutionOrder * 1000);
214+
215+
// Format: #line 10000 "virtual_file.cs"
216+
var lineDirective = SF.Trivia(
217+
SF.LineDirectiveTrivia(
218+
SF.Token(SyntaxKind.HashToken),
219+
SF.Token(SyntaxKind.LineKeyword),
220+
SF.Literal(nodeVirtualLine),
221+
SF.Literal($"\"{virtualFileName}\"", virtualFileName), // Quoted filename
222+
SF.Token(SyntaxKind.EndOfDirectiveToken),
223+
true
224+
)
225+
);
226+
227+
// Add the #line directive before the statement
228+
statement = statement.WithLeadingTrivia(lineDirective);
229+
230+
// Record the mapping for this node (regardless of whether it currently has a breakpoint)
231+
// This allows breakpoints to be added dynamically after build
232+
_context.BreakpointMappings.Add(new NodeDev.Core.Debugger.NodeBreakpointInfo
209233
{
210-
// Create a #line directive that maps this statement to a unique virtual line
211-
// The virtual line encodes the node's execution order: 10000 + (order * 1000)
212-
int nodeVirtualLine = 10000 + (nodeExecutionOrder * 1000);
213-
214-
// Format: #line 10000 "virtual_file.cs"
215-
var lineDirective = SF.Trivia(
216-
SF.LineDirectiveTrivia(
217-
SF.Token(SyntaxKind.HashToken),
218-
SF.Token(SyntaxKind.LineKeyword),
219-
SF.Literal(nodeVirtualLine),
220-
SF.Literal($"\"{virtualFileName}\"", virtualFileName), // Quoted filename
221-
SF.Token(SyntaxKind.EndOfDirectiveToken),
222-
true
223-
)
224-
);
225-
226-
// Add the #line directive before the statement
227-
statement = statement.WithLeadingTrivia(lineDirective);
228-
229-
// Record the breakpoint mapping with the virtual line number
230-
_context.BreakpointMappings.Add(new NodeDev.Core.Debugger.NodeBreakpointInfo
231-
{
232-
NodeId = node.Id,
233-
NodeName = node.Name,
234-
ClassName = className,
235-
MethodName = methodName,
236-
LineNumber = nodeVirtualLine,
237-
SourceFile = virtualFileName
238-
});
239-
}
234+
NodeId = node.Id,
235+
NodeName = node.Name,
236+
ClassName = className,
237+
MethodName = methodName,
238+
LineNumber = nodeVirtualLine,
239+
SourceFile = virtualFileName
240+
});
240241

241242
// Add the main statement
242243
statements.Add(statement);

src/NodeDev.Core/Debugger/DebugSessionEngine.cs

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ public class DebugSessionEngine : IDisposable
2727
/// Provides the NodeBreakpointInfo for the node where the breakpoint was hit.
2828
/// </summary>
2929
public event EventHandler<NodeBreakpointInfo>? BreakpointHit;
30+
31+
/// <summary>
32+
/// Delegate to check if a node should have a breakpoint set.
33+
/// This is called during breakpoint setup to filter which nodes should have breakpoints.
34+
/// Returns true if the node should have a breakpoint, false otherwise.
35+
/// </summary>
36+
public Func<string, bool>? ShouldSetBreakpointForNode { get; set; }
3037

3138
/// <summary>
3239
/// Gets the current debug process, if any.
@@ -361,8 +368,11 @@ public void SetBreakpointMappings(BreakpointMappingInfo? mappings)
361368
/// <summary>
362369
/// Sets breakpoints in the debugged process based on the breakpoint mappings.
363370
/// This should be called after modules are loaded (typically in LoadModule callback).
371+
/// In the new design, this method is called during module load but only sets breakpoints
372+
/// for nodes that currently have HasBreakpoint set to true (checked via ShouldSetBreakpointForNode delegate).
364373
/// </summary>
365-
public void TrySetBreakpointsForLoadedModules()
374+
/// <param name="nodeFilter">Optional filter to only set breakpoints for specific node IDs. If null, processes all nodes with breakpoints.</param>
375+
public void TrySetBreakpointsForLoadedModules(Func<NodeBreakpointInfo, bool>? nodeFilter = null)
366376
{
367377
if (_breakpointMappings == null || _breakpointMappings.Breakpoints.Count == 0)
368378
return;
@@ -375,8 +385,20 @@ public void TrySetBreakpointsForLoadedModules()
375385
// Get all app domains
376386
var appDomains = CurrentProcess.AppDomains.ToArray();
377387

378-
// For each breakpoint mapping, try to set a breakpoint
379-
foreach (var bpInfo in _breakpointMappings.Breakpoints)
388+
// For each breakpoint mapping, check if we should set a breakpoint
389+
// Only process nodes that:
390+
// 1. Pass the filter (if provided), AND
391+
// 2. Should have a breakpoint (checked via ShouldSetBreakpointForNode delegate)
392+
var breakpointsToConsider = nodeFilter != null
393+
? _breakpointMappings.Breakpoints.Where(nodeFilter)
394+
: _breakpointMappings.Breakpoints;
395+
396+
// Further filter by ShouldSetBreakpointForNode delegate
397+
var breakpointsToSet = breakpointsToConsider
398+
.Where(bp => ShouldSetBreakpointForNode == null || ShouldSetBreakpointForNode(bp.NodeId))
399+
.ToList();
400+
401+
foreach (var bpInfo in breakpointsToSet)
380402
{
381403
// Skip if already set
382404
if (_activeBreakpoints.ContainsKey(bpInfo.NodeId))
@@ -448,6 +470,65 @@ public void TrySetBreakpointsForLoadedModules()
448470
}
449471
}
450472

473+
/// <summary>
474+
/// Dynamically sets a breakpoint for a specific node during an active debug session.
475+
/// This can be called after the process has started to add a breakpoint on-the-fly.
476+
/// </summary>
477+
/// <param name="nodeId">The ID of the node to set a breakpoint on.</param>
478+
/// <returns>True if the breakpoint was set successfully, false otherwise.</returns>
479+
public bool SetBreakpointForNode(string nodeId)
480+
{
481+
if (_breakpointMappings == null)
482+
return false;
483+
484+
// Find the breakpoint info for this node
485+
var bpInfo = _breakpointMappings.Breakpoints.FirstOrDefault(bp => bp.NodeId == nodeId);
486+
if (bpInfo == null)
487+
return false;
488+
489+
// If already set, return true
490+
if (_activeBreakpoints.ContainsKey(nodeId))
491+
return true;
492+
493+
// Set breakpoint for just this node
494+
TrySetBreakpointsForLoadedModules(bp => bp.NodeId == nodeId);
495+
496+
// Check if it was set successfully
497+
return _activeBreakpoints.ContainsKey(nodeId);
498+
}
499+
500+
/// <summary>
501+
/// Dynamically removes a breakpoint for a specific node during an active debug session.
502+
/// </summary>
503+
/// <param name="nodeId">The ID of the node to remove the breakpoint from.</param>
504+
/// <returns>True if the breakpoint was removed successfully, false if it wasn't set.</returns>
505+
public bool RemoveBreakpointForNode(string nodeId)
506+
{
507+
if (!_activeBreakpoints.TryGetValue(nodeId, out var breakpoint))
508+
return false;
509+
510+
try
511+
{
512+
// Deactivate and dispose the breakpoint
513+
if (breakpoint != null)
514+
{
515+
breakpoint.Activate(false);
516+
// Note: ClrDebug breakpoints don't have explicit dispose
517+
}
518+
519+
_activeBreakpoints.Remove(nodeId);
520+
OnDebugCallback(new DebugCallbackEventArgs("BreakpointRemoved",
521+
$"Removed breakpoint for node {nodeId}"));
522+
return true;
523+
}
524+
catch (Exception ex)
525+
{
526+
OnDebugCallback(new DebugCallbackEventArgs("BreakpointError",
527+
$"Failed to remove breakpoint for node {nodeId}: {ex.Message}"));
528+
return false;
529+
}
530+
}
531+
451532
/// <summary>
452533
/// Attempts to set an actual ICorDebug breakpoint in a module.
453534
/// </summary>

src/NodeDev.Core/Project.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,14 @@ public string GetScriptRunnerPath()
493493

494494
// Set breakpoint mappings from the build
495495
_debugEngine.SetBreakpointMappings(_currentBreakpointMappings);
496+
497+
// Set the delegate to check if a node should have a breakpoint
498+
// This allows filtering breakpoints based on current node state
499+
_debugEngine.ShouldSetBreakpointForNode = (nodeId) =>
500+
{
501+
var node = FindNodeById(nodeId);
502+
return node?.HasBreakpoint ?? false;
503+
};
496504
}
497505
catch (Exception ex)
498506
{
@@ -668,6 +676,75 @@ public void ContinueExecution()
668676
throw;
669677
}
670678
}
679+
680+
/// <summary>
681+
/// Dynamically sets a breakpoint on a specific node during an active debug session.
682+
/// This allows adding breakpoints after the process has started.
683+
/// </summary>
684+
/// <param name="nodeId">The ID of the node to set a breakpoint on.</param>
685+
/// <returns>True if the breakpoint was set successfully, false otherwise.</returns>
686+
public bool SetBreakpointForNode(string nodeId)
687+
{
688+
if (!IsHardDebugging)
689+
throw new InvalidOperationException("Cannot set breakpoints when not debugging.");
690+
691+
if (_debugEngine == null)
692+
return false;
693+
694+
// Find the node and ensure it has a breakpoint decoration
695+
var node = FindNodeById(nodeId);
696+
if (node == null)
697+
return false;
698+
699+
// Set the breakpoint decoration if not already set
700+
if (!node.HasBreakpoint)
701+
node.ToggleBreakpoint();
702+
703+
// Tell the debug engine to set the breakpoint
704+
return _debugEngine.SetBreakpointForNode(nodeId);
705+
}
706+
707+
/// <summary>
708+
/// Dynamically removes a breakpoint from a specific node during an active debug session.
709+
/// </summary>
710+
/// <param name="nodeId">The ID of the node to remove the breakpoint from.</param>
711+
/// <returns>True if the breakpoint was removed successfully, false if it wasn't set.</returns>
712+
public bool RemoveBreakpointForNode(string nodeId)
713+
{
714+
if (!IsHardDebugging)
715+
throw new InvalidOperationException("Cannot remove breakpoints when not debugging.");
716+
717+
if (_debugEngine == null)
718+
return false;
719+
720+
// Find the node and remove the breakpoint decoration
721+
var node = FindNodeById(nodeId);
722+
if (node != null && node.HasBreakpoint)
723+
node.ToggleBreakpoint();
724+
725+
// Tell the debug engine to remove the breakpoint
726+
return _debugEngine.RemoveBreakpointForNode(nodeId);
727+
}
728+
729+
/// <summary>
730+
/// Finds a node by its ID across all classes in the project.
731+
/// </summary>
732+
/// <param name="nodeId">The ID of the node to find.</param>
733+
/// <returns>The node if found, null otherwise.</returns>
734+
private Node? FindNodeById(string nodeId)
735+
{
736+
foreach (var nodeClass in Classes)
737+
{
738+
// Check methods
739+
foreach (var method in nodeClass.Methods)
740+
{
741+
if (method.Graph?.Nodes.TryGetValue(nodeId, out var node) == true)
742+
return node;
743+
}
744+
}
745+
746+
return null;
747+
}
671748

672749
#endregion
673750

0 commit comments

Comments
 (0)