Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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() ?? Array.Empty<BreakpointRequest>();

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

var responseBreakpoints = breakpoints.Select(bp => new MSBreakpoint
{
Expand Down
32 changes: 30 additions & 2 deletions src/SharpDbg.Infrastructure/Debugger/BreakpointManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,23 @@ 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)
{
Expand All @@ -63,7 +74,10 @@ public BreakpointInfo CreateBreakpoint(string filePath, int line)
Id = id,
FilePath = filePath,
Line = line,
Verified = false
Verified = false,
Condition = condition,
HitCondition = hitCondition,
HitCount = 0
};

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

/// <summary>
/// Reset hit counts for all breakpoints (e.g., when restarting debugging)
/// </summary>
public void ResetHitCounts()
{
lock (_lock)
{
foreach (var bp in _breakpoints.Values)
{
bp.HitCount = 0;
}
}
}

/// <summary>
/// Update breakpoint with ClrDebug breakpoint
/// </summary>
Expand Down
161 changes: 160 additions & 1 deletion src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs
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,30 @@ private async void HandleBreakpoint(object? sender, BreakpointCorDebugManagedCal

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

managedBreakpoint.HitCount++;

if (!string.IsNullOrEmpty(managedBreakpoint.HitCondition))
{
if (!EvaluateHitCondition(managedBreakpoint.HitCount, managedBreakpoint.HitCondition))
{
_logger?.Invoke($"Hit count condition not met: count={managedBreakpoint.HitCount}, condition={managedBreakpoint.HitCondition}");
Continue();
return;
}
}

if (!string.IsNullOrEmpty(managedBreakpoint.Condition))
{
var conditionResult = await EvaluateBreakpointCondition(corThread, managedBreakpoint.Condition);
if (!conditionResult)
{
_logger?.Invoke($"Conditional breakpoint condition not met: {managedBreakpoint.Condition}");
Continue();
return;
}
}

IsRunning = false;
OnStopped2?.Invoke(corThread.Id, managedBreakpoint.FilePath, managedBreakpoint.Line, "breakpoint");
}
Expand All @@ -154,6 +180,139 @@ private async void HandleBreakpoint(object? sender, BreakpointCorDebugManagedCal
}
}

/// <summary>
/// Evaluate a breakpoint condition expression
/// </summary>
private async Task<bool> EvaluateBreakpointCondition(CorDebugThread corThread, string condition)
{
try
{
if (_expressionInterpreter is null)
{
_logger?.Invoke("Expression interpreter not initialized, condition evaluation skipped");
return true; // Stop anyway if we can't evaluate
}

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
}
}

/// <summary>
/// Evaluate a hit count condition
/// </summary>
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 bool IsTruthyValue(CorDebugValue? value)
{
if (value is null) return false;

var unwrapped = value.UnwrapDebugValue();

if (unwrapped is CorDebugGenericValue genericValue)
{
IntPtr 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;
}

private void HandleStepComplete(object? sender, StepCompleteCorDebugManagedCallbackEventArgs stepCompleteEventArgs)
{
var corThread = stepCompleteEventArgs.Thread;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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
{
/// <summary>
Expand Down Expand Up @@ -172,12 +177,21 @@ public async void StepOut(int threadId)
}

/// <summary>
/// Set breakpoints for a source file
/// Set breakpoints for a source file without conditions
/// </summary>
public List<BreakpointManager.BreakpointInfo> SetBreakpoints(string filePath, int[] lines)
{
var requests = lines.Select(line => new BreakpointRequest(line)).ToArray();
return SetBreakpoints(filePath, requests);
}

/// <summary>
/// Set breakpoints for a source file with optional conditions
/// </summary>
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 @@ -199,9 +213,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
Loading