Skip to content
Open
13 changes: 10 additions & 3 deletions src/SharpDbg.Application/DebugAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ protected override InitializeResponse HandleInitializeRequest(InitializeArgument
{
SupportsConfigurationDoneRequest = true,
SupportsFunctionBreakpoints = true,
SupportsConditionalBreakpoints = false,
SupportsConditionalBreakpoints = true,
SupportsHitConditionalBreakpoints = true,
SupportsEvaluateForHovers = true,
SupportsStepBack = false,
SupportsSetVariable = false,
Expand Down Expand Up @@ -260,8 +261,14 @@ protected override SetBreakpointsResponse HandleSetBreakpointsRequest(SetBreakpo
throw new ProtocolException("Missing source path");
}

var lines = arguments.Breakpoints?.Select(bp => ConvertClientLineToDebugger(bp.Line)).ToArray() ?? Array.Empty<int>();
var breakpoints = _debugger.SetBreakpoints(arguments.Source.Path, lines);
var breakpointRequests = arguments.Breakpoints?
.Select(bp => new BreakpointRequest(
ConvertClientLineToDebugger(bp.Line),
bp.Condition,
bp.HitCondition))
.ToArray() ?? [];

var breakpoints = _debugger.SetBreakpoints(arguments.Source.Path, breakpointRequests);

var responseBreakpoints = breakpoints.Select(bp => new MSBreakpoint
{
Expand Down
34 changes: 32 additions & 2 deletions src/SharpDbg.Infrastructure/Debugger/BreakpointManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,38 @@ public class BreakpointInfo

/// <summary>Module base address where breakpoint is bound</summary>
public long? ModuleBaseAddress { get; set; }

// Conditional breakpoint support

/// <summary>Conditional expression to evaluate when breakpoint is hit</summary>
public string? Condition { get; set; }

/// <summary>Hit count condition (e.g., ">=10", "==5", "%3")</summary>
public string? HitCondition { get; set; }

/// <summary>Current hit count for this breakpoint</summary>
public int HitCount { get; set; }
}

/// <summary>
/// Create a new breakpoint
/// </summary>
public BreakpointInfo CreateBreakpoint(string filePath, int line)
public BreakpointInfo CreateBreakpoint(string filePath, int line, string? condition = null, string? hitCondition = null)
{
lock (_lock)
{
var id = _nextBreakpointId++;
if (string.IsNullOrWhiteSpace(condition)) condition = null;
if (string.IsNullOrWhiteSpace(hitCondition)) hitCondition = null;
var bp = new BreakpointInfo
{
Id = id,
FilePath = filePath,
Line = line,
Verified = false
Verified = false,
Condition = condition,
HitCondition = hitCondition,
HitCount = 0
};

_breakpoints[id] = bp;
Expand All @@ -78,6 +94,20 @@ public BreakpointInfo CreateBreakpoint(string filePath, int line)
}
}

/// <summary>
/// Reset hit counts for all breakpoints (e.g., when restarting debugging)
/// </summary>
public void ResetHitCounts()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unused - is it necessary? It does not seem so to me - restarting debugging will have brand new BreakpointInfo objects.

{
lock (_lock)
{
foreach (var bp in _breakpoints.Values)
{
bp.HitCount = 0;
}
}
}

/// <summary>
/// Update breakpoint with ClrDebug breakpoint
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/SharpDbg.Infrastructure/Debugger/ManagedDebugger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public partial class ManagedDebugger : IDisposable
private bool _isAttached;
private int? _pendingAttachProcessId;
private AsyncStepper? _asyncStepper;
private CompiledExpressionInterpreter? _expressionInterpreter;
private CompiledExpressionInterpreter _expressionInterpreter = null!;

public event Action<int, string>? OnStopped;
// ThreadId, FilePath, Line, Reason
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using System.Runtime.InteropServices;
using ClrDebug;
using SharpDbg.Infrastructure.Debugger.ExpressionEvaluator;
using SharpDbg.Infrastructure.Debugger.ExpressionEvaluator.Compiler;

namespace SharpDbg.Infrastructure.Debugger;

public partial class ManagedDebugger
{
private async Task<bool> EvaluateBreakpointCondition(CorDebugThread corThread, string condition)
{
try
{
var threadId = new ThreadId(corThread.Id);
var frameStackDepth = new FrameStackDepth(0); // Top frame

var compiledExpression = ExpressionCompiler.Compile(condition, false);
var evalContext = new CompiledExpressionEvaluationContext(corThread, threadId, frameStackDepth);
var result = await _expressionInterpreter.Interpret(compiledExpression, evalContext);

if (result.Error is not null)
{
_logger?.Invoke($"Condition evaluation error for '{condition}': {result.Error}");
return false; // Don't stop on error - condition couldn't be evaluated, so skip the breakpoint
}

return IsTruthyValue(result.Value);
}
catch (Exception ex)
{
_logger?.Invoke($"Exception evaluating condition '{condition}': {ex.Message}");
return false; // Don't stop on exception - condition couldn't be evaluated, so skip the breakpoint
}
}

private static bool EvaluateHitCondition(int hitCount, string hitCondition)
{
// Support common hit count formats:
// "10" or "==10" - break when hit count equals 10
// ">=10" - break when hit count is >= 10
// ">10" - break when hit count is > 10
// "%10" - break every 10th hit (modulo)

hitCondition = hitCondition.Trim();

if (hitCondition.StartsWith(">="))
{
if (int.TryParse(hitCondition[2..], out var threshold))
return hitCount >= threshold;
}
else if (hitCondition.StartsWith('>'))
{
if (int.TryParse(hitCondition[1..], out var threshold))
return hitCount > threshold;
}
else if (hitCondition.StartsWith("<="))
{
if (int.TryParse(hitCondition[2..], out var threshold))
return hitCount <= threshold;
}
else if (hitCondition.StartsWith('<'))
{
if (int.TryParse(hitCondition[1..], out var threshold))
return hitCount < threshold;
}
else if (hitCondition.StartsWith('%'))
{
if (int.TryParse(hitCondition[1..], out var modulo) && modulo > 0)
return hitCount % modulo == 0;
}
else if (hitCondition.StartsWith("=="))
{
if (int.TryParse(hitCondition[2..], out var target))
return hitCount == target;
}
else
{
// Plain number means "break when hit count equals this"
if (int.TryParse(hitCondition, out var target))
return hitCount == target;
}

return false;
}

/// <summary>
/// Check if a debug value is truthy (true, non-zero, non-null)
/// </summary>
private static bool IsTruthyValue(CorDebugValue? value)
{
if (value is null) return false;

var unwrapped = value.UnwrapDebugValue();

if (unwrapped is CorDebugGenericValue genericValue)
{
var buffer = Marshal.AllocHGlobal(genericValue.Size);
try
{
genericValue.GetValue(buffer);
return genericValue.Type switch
{
CorElementType.Boolean => Marshal.ReadByte(buffer) != 0,
CorElementType.I1 or CorElementType.U1 => Marshal.ReadByte(buffer) != 0,
CorElementType.I2 or CorElementType.U2 => Marshal.ReadInt16(buffer) != 0,
CorElementType.I4 or CorElementType.U4 => Marshal.ReadInt32(buffer) != 0,
CorElementType.I8 or CorElementType.U8 => Marshal.ReadInt64(buffer) != 0,
CorElementType.R4 => BitConverter.ToSingle(BitConverter.GetBytes(Marshal.ReadInt32(buffer)), 0) != 0,
CorElementType.R8 => BitConverter.ToDouble(BitConverter.GetBytes(Marshal.ReadInt64(buffer)), 0) != 0,
_ => true // Unknown types - default to true
};
}
catch
{
return false;
}
finally
{
Marshal.FreeHGlobal(buffer);
}
}

if (unwrapped is CorDebugReferenceValue refValue)
{
return !refValue.IsNull;
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using ClrDebug;
using System.Runtime.InteropServices;
using ClrDebug;
using SharpDbg.Infrastructure.Debugger.ExpressionEvaluator;
using SharpDbg.Infrastructure.Debugger.ExpressionEvaluator.Compiler;
using SharpDbg.Infrastructure.Debugger.ExpressionEvaluator.Interpreter;

namespace SharpDbg.Infrastructure.Debugger;
Expand Down Expand Up @@ -145,6 +147,23 @@ private async void HandleBreakpoint(object? sender, BreakpointCorDebugManagedCal

var managedBreakpoint = _breakpointManager.FindByCorBreakpoint(functionBreakpoint.Raw);
ArgumentNullException.ThrowIfNull(managedBreakpoint);

managedBreakpoint.HitCount++;

if (managedBreakpoint.HitCondition is not null && EvaluateHitCondition(managedBreakpoint.HitCount, managedBreakpoint.HitCondition) is false)
{
_logger?.Invoke($"Hit count condition not met: count={managedBreakpoint.HitCount}, condition={managedBreakpoint.HitCondition}");
Continue();
return;
}

if (managedBreakpoint.Condition is not null && await EvaluateBreakpointCondition(corThread, managedBreakpoint.Condition) is false)
{
_logger?.Invoke($"Conditional breakpoint condition not met: {managedBreakpoint.Condition}");
Continue();
return;
}

IsRunning = false;
OnStopped2?.Invoke(corThread.Id, managedBreakpoint.FilePath, managedBreakpoint.Line, "breakpoint");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@

namespace SharpDbg.Infrastructure.Debugger;

/// <summary>
/// Request to set a breakpoint with optional condition and hit condition
/// </summary>
public record BreakpointRequest(int Line, string? Condition = null, string? HitCondition = null);

public partial class ManagedDebugger
{
// Store launch info for deferred attach in ConfigurationDone
Expand Down Expand Up @@ -314,12 +319,12 @@ public async void StepOut(int threadId)
}

/// <summary>
/// Set breakpoints for a source file
/// Set breakpoints for a source file with optional conditions
/// </summary>
public List<BreakpointManager.BreakpointInfo> SetBreakpoints(string filePath, int[] lines)
public List<BreakpointManager.BreakpointInfo> SetBreakpoints(string filePath, BreakpointRequest[] breakpoints)
{
//System.Diagnostics.Debugger.Launch();
_logger?.Invoke($"SetBreakpoints: {filePath}, lines: {string.Join(",", lines)}");
_logger?.Invoke($"SetBreakpoints: {filePath}, breakpoints: {string.Join(",", breakpoints.Select(b => $"L{b.Line}" + (b.Condition != null ? $"[{b.Condition}]" : "")))}");

// Deactivate and clear existing breakpoints for this file
var existingBreakpoints = _breakpointManager.GetBreakpointsForFile(filePath);
Expand All @@ -341,9 +346,9 @@ public async void StepOut(int threadId)

// Create new breakpoints
var result = new List<BreakpointManager.BreakpointInfo>();
foreach (var line in lines)
foreach (var request in breakpoints)
{
var bp = _breakpointManager.CreateBreakpoint(filePath, line);
var bp = _breakpointManager.CreateBreakpoint(filePath, request.Line, request.Condition, request.HitCondition);

// Try to bind the breakpoint if we have a process
if (_process != null)
Expand Down
12 changes: 12 additions & 0 deletions tests/DebuggableConsoleApp/HitConditionClass.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace DebuggableConsoleApp;

public class HitConditionClass
{
private static int _count = 0;

public void Test()
{
_count++;
; // breakpoint here
}
}
2 changes: 2 additions & 0 deletions tests/DebuggableConsoleApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ public static void Main(string[] args)
var myClass = new MyClass();
var myAsyncClass = new MyAsyncClass();
var myClassNoMembers = new MyClassNoMembers();
var hitConditionClass = new HitConditionClass();
while (true)
{
// Keep the application running to allow debugging
myLambdaClass.Test();
myClass.MyMethod(13, 6);
myClassNoMembers.MyMethod(42);
hitConditionClass.Test();
var asyncResult = myAsyncClass.MyMethodAsync(4).GetAwaiter().GetResult();
Thread.Sleep(100);
//await Task.Delay(500);
Expand Down
Loading