Skip to content

Commit 93d5334

Browse files
authored
Add support for HitCount and HitCondition (#1548)
* Add support for HitCount and HitCondition This PR adds in showing the Hit count for a breakpoint and for setting a HitCondition. Enables m_supportsHitConditionalBreakpoints, OpenDebugAD7 now properly builds a AD7BreakpointRequest with a hitCondition, AD7BoundBreakpoint now properly tracks hits and will prevent a break event if theres a condition set. Added CppTests and updated DebuggerTesting for associated commands. Addresses: #472 * Use -break-after * Handle updating hitCondition when with hitCount > 0 * Address PR issues * Re-add ShouldBreak()
1 parent a411c5b commit 93d5334

12 files changed

Lines changed: 1511 additions & 34 deletions

File tree

src/MICore/CommandFactories/MICommandFactory.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,15 @@ public virtual async Task BreakCondition(string bkptno, string expr)
567567
await _debugger.CmdAsync(command, ResultClass.done);
568568
}
569569

570+
/// <summary>
571+
/// Sends -break-after to set an ignore count on a breakpoint.
572+
/// </summary>
573+
public virtual async Task<Results> BreakAfter(string bkptno, uint count)
574+
{
575+
string command = string.Format(CultureInfo.InvariantCulture, "-break-after {0} {1}", bkptno, count);
576+
return await _debugger.CmdAsync(command, ResultClass.done);
577+
}
578+
570579
public virtual IEnumerable<Guid> GetSupportedExceptionCategories()
571580
{
572581
return new Guid[0];

src/MIDebugEngine/AD7.Impl/AD7BoundBreakpoint.cs

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.VisualStudio.Debugger.Interop;
66
using System;
77
using System.Diagnostics;
8+
using System.Threading.Tasks;
89

910
namespace Microsoft.MIDebugEngine
1011
{
@@ -19,6 +20,8 @@ internal class AD7BoundBreakpoint : IDebugBoundBreakpoint2
1920
private BoundBreakpoint _bp;
2021

2122
private bool _deleted;
23+
private enum_BP_PASSCOUNT_STYLE _passCountStyle;
24+
private uint _passCountValue;
2225

2326
internal bool Enabled
2427
{
@@ -37,6 +40,7 @@ internal bool Enabled
3740
internal string Number { get { return _bp.Number; } }
3841
internal AD7PendingBreakpoint PendingBreakpoint { get { return _pendingBreakpoint; } }
3942
internal bool IsDataBreakpoint { get { return PendingBreakpoint.IsDataBreakpoint; } }
43+
internal bool HasPassCount { get { return _passCountStyle != enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE; } }
4044

4145
public AD7BoundBreakpoint(AD7Engine engine, AD7PendingBreakpoint pendingBreakpoint, AD7BreakpointResolution breakpointResolution, BoundBreakpoint bp)
4246
{
@@ -143,8 +147,7 @@ int IDebugBoundBreakpoint2.GetState(enum_BP_STATE[] pState)
143147
return Constants.S_OK;
144148
}
145149

146-
// The sample engine does not support hit counts on breakpoints. A real-world debugger will want to keep track
147-
// of how many times a particular bound breakpoint has been hit and return it here.
150+
// Returns the number of times this breakpoint has been hit.
148151
int IDebugBoundBreakpoint2.GetHitCount(out uint pdwHitCount)
149152
{
150153
pdwHitCount = _bp.HitCount;
@@ -156,29 +159,91 @@ int IDebugBoundBreakpoint2.SetCondition(BP_CONDITION bpCondition)
156159
return ((IDebugPendingBreakpoint2)_pendingBreakpoint).SetCondition(bpCondition); // setting on the pending break will set the condition
157160
}
158161

159-
// The sample engine does not support hit counts on breakpoints. A real-world debugger will want to keep track
160-
// of how many times a particular bound breakpoint has been hit. The debugger calls SetHitCount when the user
161-
// resets a breakpoint's hit count.
162+
// Called by the debugger when the user resets a breakpoint's hit count.
162163
int IDebugBoundBreakpoint2.SetHitCount(uint dwHitCount)
163164
{
164-
throw new NotImplementedException();
165+
_bp.SetHitCount(dwHitCount);
166+
_pendingBreakpoint?.RecomputeBreakAfter(dwHitCount);
167+
168+
return Constants.S_OK;
169+
}
170+
171+
/// <summary>
172+
/// Syncs the hit count from GDB's "times" field using a delta
173+
/// to preserve any user-initiated hit count reset.
174+
/// </summary>
175+
internal void SetHitCount(uint hitCount)
176+
{
177+
_bp.SetGdbHitCount(hitCount);
165178
}
166179

167-
// The sample engine does not support pass counts on breakpoints.
168180
// This is used to specify the breakpoint hit count condition.
169181
int IDebugBoundBreakpoint2.SetPassCount(BP_PASSCOUNT bpPassCount)
170182
{
171-
if (bpPassCount.stylePassCount != enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE)
172-
{
173-
Delete();
174-
_engine.Callback.OnBreakpointUnbound(this, enum_BP_UNBOUND_REASON.BPUR_BREAKPOINT_ERROR);
175-
return Constants.E_FAIL;
176-
}
183+
_passCountStyle = bpPassCount.stylePassCount;
184+
_passCountValue = bpPassCount.dwPassCount;
177185
return Constants.S_OK;
178186
}
179187

180188
#endregion
181189

190+
internal uint HitCount => _bp.HitCount;
191+
192+
internal void IncrementHitCount()
193+
{
194+
_bp.IncrementHitCount();
195+
}
196+
197+
/// <summary>
198+
/// Evaluates whether the debugger should break at this breakpoint based on the
199+
/// current hit count and the configured pass count condition.
200+
/// Must be called after IncrementHitCount.
201+
/// </summary>
202+
internal bool ShouldBreak()
203+
{
204+
uint hitCount = _bp.HitCount;
205+
switch (_passCountStyle)
206+
{
207+
case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE:
208+
return true;
209+
case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_EQUAL:
210+
return hitCount == _passCountValue;
211+
case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_EQUAL_OR_GREATER:
212+
return hitCount >= _passCountValue;
213+
case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_MOD:
214+
return _passCountValue != 0 && (hitCount % _passCountValue) == 0;
215+
default:
216+
return true;
217+
}
218+
}
219+
220+
/// <summary>
221+
/// Re-sends -break-after to GDB after a pass count breakpoint fires.
222+
/// MOD: skips passCount-1 hits. EQUAL: clears the ignore count.
223+
/// </summary>
224+
internal async Task RearmBreakAfterAsync()
225+
{
226+
uint ignoreCount;
227+
switch (_passCountStyle)
228+
{
229+
case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_MOD:
230+
if (_passCountValue == 0) return;
231+
ignoreCount = _passCountValue - 1;
232+
break;
233+
case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_EQUAL:
234+
ignoreCount = 0;
235+
break;
236+
default:
237+
return;
238+
}
239+
240+
PendingBreakpoint bp = _pendingBreakpoint?.PendingBreakpoint;
241+
if (bp != null && _engine?.DebuggedProcess != null)
242+
{
243+
await bp.SetBreakAfterAsync(ignoreCount, _engine.DebuggedProcess);
244+
}
245+
}
246+
182247
internal void UpdateAddr(ulong addr)
183248
{
184249
_bp.Addr = addr;

src/MIDebugEngine/AD7.Impl/AD7PendingBreakpoint.cs

Lines changed: 103 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,6 @@ private bool CanBind()
115115
return false;
116116
}
117117
}
118-
if ((_bpRequestInfo.dwFields & enum_BPREQI_FIELDS.BPREQI_PASSCOUNT) != 0)
119-
{
120-
this.SetError(new AD7ErrorBreakpoint(this, ResourceStrings.UnsupportedPassCountBreakpoint, enum_BP_ERROR_TYPE.BPET_GENERAL_ERROR));
121-
return false;
122-
}
123118

124119
return true;
125120
}
@@ -393,6 +388,40 @@ internal async Task BindAsync()
393388
}
394389
}
395390
}
391+
392+
// Set ignore count via -break-after if a pass count is configured
393+
if (_bp != null && (_bpRequestInfo.dwFields & enum_BPREQI_FIELDS.BPREQI_PASSCOUNT) != 0
394+
&& _bpRequestInfo.bpPassCount.stylePassCount != enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE)
395+
{
396+
uint ignoreCount = ComputeIgnoreCount(_bpRequestInfo.bpPassCount.stylePassCount, _bpRequestInfo.bpPassCount.dwPassCount, 0);
397+
await _bp.SetBreakAfterAsync(ignoreCount, _engine.DebuggedProcess);
398+
}
399+
}
400+
}
401+
402+
/// <summary>
403+
/// Computes the ignore count for -break-after, accounting for hits already
404+
/// counted from a prior breakpoint (<paramref name="currentHits"/>).
405+
/// </summary>
406+
private static uint ComputeIgnoreCount(enum_BP_PASSCOUNT_STYLE style, uint passCount, uint currentHits)
407+
{
408+
if (passCount == 0)
409+
{
410+
return 0;
411+
}
412+
413+
switch (style)
414+
{
415+
case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_EQUAL:
416+
case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_EQUAL_OR_GREATER:
417+
// Need to stop at hit N. Already counted currentHits, so skip (N - 1 - currentHits) more.
418+
return passCount - 1 > currentHits ? passCount - 1 - currentHits : 0;
419+
case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_MOD:
420+
// Next stop is at the next multiple of passCount after currentHits.
421+
uint remainder = currentHits % passCount;
422+
return remainder == 0 ? passCount - 1 : passCount - 1 - remainder;
423+
default:
424+
return 0;
396425
}
397426
}
398427

@@ -406,6 +435,11 @@ internal AD7BoundBreakpoint AddBoundBreakpoint(BoundBreakpoint bp)
406435
}
407436
AD7BreakpointResolution breakpointResolution = new AD7BreakpointResolution(_engine, IsDataBreakpoint, bp.Addr, bp.FunctionName, bp.DocumentContext(_engine));
408437
AD7BoundBreakpoint boundBreakpoint = new AD7BoundBreakpoint(_engine, this, breakpointResolution, bp);
438+
// Apply pass count (hit count condition) from the original request to the bound breakpoint
439+
if ((_bpRequestInfo.dwFields & enum_BPREQI_FIELDS.BPREQI_PASSCOUNT) != 0)
440+
{
441+
((IDebugBoundBreakpoint2)boundBreakpoint).SetPassCount(_bpRequestInfo.bpPassCount);
442+
}
409443
//check can bind one last time. If the pending breakpoint was deleted before now, we need to clean up gdb side
410444
if (CanBind())
411445
{
@@ -645,17 +679,77 @@ int IDebugPendingBreakpoint2.SetCondition(BP_CONDITION bpCondition)
645679
return Constants.S_OK;
646680
}
647681

648-
// The sample engine does not support pass counts on breakpoints.
649682
int IDebugPendingBreakpoint2.SetPassCount(BP_PASSCOUNT bpPassCount)
650683
{
651-
if (bpPassCount.stylePassCount != enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE)
684+
_bpRequestInfo.bpPassCount = bpPassCount;
685+
_bpRequestInfo.dwFields |= enum_BPREQI_FIELDS.BPREQI_PASSCOUNT;
686+
687+
PendingBreakpoint bp = null;
688+
lock (_boundBreakpoints)
652689
{
653-
this.SetError(new AD7ErrorBreakpoint(this, ResourceStrings.UnsupportedPassCountBreakpoint, enum_BP_ERROR_TYPE.BPET_GENERAL_ERROR), true);
654-
return Constants.E_FAIL;
690+
foreach (AD7BoundBreakpoint boundBp in _boundBreakpoints)
691+
{
692+
((IDebugBoundBreakpoint2)boundBp).SetPassCount(bpPassCount);
693+
}
694+
if (_bp != null)
695+
{
696+
bp = _bp;
697+
}
698+
}
699+
700+
// When the pass count is cleared (NONE), send ignore count 0 to clear
701+
// any stale GDB ignore count from the previous condition.
702+
if (bp != null)
703+
{
704+
uint ignoreCount = 0;
705+
if (bpPassCount.stylePassCount != enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE)
706+
{
707+
uint currentHits = 0;
708+
lock (_boundBreakpoints)
709+
{
710+
foreach (AD7BoundBreakpoint boundBp in _boundBreakpoints)
711+
{
712+
uint hc;
713+
if (((IDebugBoundBreakpoint2)boundBp).GetHitCount(out hc) == Constants.S_OK && hc > currentHits)
714+
{
715+
currentHits = hc;
716+
}
717+
}
718+
}
719+
ignoreCount = ComputeIgnoreCount(bpPassCount.stylePassCount, bpPassCount.dwPassCount, currentHits);
720+
}
721+
_engine.DebuggedProcess.WorkerThread.RunOperation(() =>
722+
{
723+
_engine.DebuggedProcess.AddInternalBreakAction(
724+
() => bp.SetBreakAfterAsync(ignoreCount, _engine.DebuggedProcess)
725+
);
726+
});
655727
}
656728
return Constants.S_OK;
657729
}
658730

731+
/// <summary>
732+
/// Re-sends -break-after to GDB after a hit count reset.
733+
/// </summary>
734+
internal void RecomputeBreakAfter(uint currentHits)
735+
{
736+
if (_bp == null
737+
|| (_bpRequestInfo.dwFields & enum_BPREQI_FIELDS.BPREQI_PASSCOUNT) == 0
738+
|| _bpRequestInfo.bpPassCount.stylePassCount == enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE)
739+
{
740+
return;
741+
}
742+
743+
PendingBreakpoint bp = _bp;
744+
uint ignoreCount = ComputeIgnoreCount(_bpRequestInfo.bpPassCount.stylePassCount, _bpRequestInfo.bpPassCount.dwPassCount, currentHits);
745+
_engine.DebuggedProcess.WorkerThread.RunOperation(() =>
746+
{
747+
_engine.DebuggedProcess.AddInternalBreakAction(
748+
() => bp.SetBreakAfterAsync(ignoreCount, _engine.DebuggedProcess)
749+
);
750+
});
751+
}
752+
659753
// Toggles the virtualized state of this pending breakpoint. When a pending breakpoint is virtualized,
660754
// the debug engine will attempt to bind it every time new code loads into the program.
661755
// The sample engine will does not support this.

src/MIDebugEngine/Engine.Impl/BreakpointManager.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,30 @@ public async Task BreakpointModified(object sender, EventArgs args)
7171
return;
7272
}
7373

74+
// Sync GDB's hit count ("times") for pass count breakpoints.
75+
// e.g. =breakpoint-modified,bkpt={number="1",...,times="5",ignore="2",...}
76+
string timesStr = bkpt.TryFindString("times");
77+
if (!string.IsNullOrEmpty(timesStr) && uint.TryParse(timesStr, out uint times))
78+
{
79+
foreach (AD7BoundBreakpoint boundBp in pending.EnumBoundBreakpoints())
80+
{
81+
if (boundBp.HasPassCount)
82+
{
83+
uint previousHitCount = boundBp.HitCount;
84+
boundBp.SetHitCount(times);
85+
86+
// Re-arm GDB's ignore count so it skips to the next target hit.
87+
// HitCount guard: -break-after itself triggers =breakpoint-modified.
88+
// ShouldBreak guard: GDB also emits =breakpoint-modified on ignored
89+
// hits, and re-arming then would shift the next stop.
90+
if (boundBp.HitCount != previousHitCount && boundBp.ShouldBreak())
91+
{
92+
await boundBp.RearmBreakAfterAsync();
93+
}
94+
}
95+
}
96+
}
97+
7498
string warning = bkpt.TryFindString("warning");
7599
if (!string.IsNullOrEmpty(warning))
76100
{
@@ -212,7 +236,15 @@ public AD7BoundBreakpoint[] FindHitBreakpoints(string bkptno, ulong addr, /*OPTI
212236
continue;
213237
}
214238

215-
hitBoundBreakpoints.Add(currBoundBp);
239+
// Pass count breakpoints get their hit count from =breakpoint-modified.
240+
if (!currBoundBp.HasPassCount)
241+
{
242+
currBoundBp.IncrementHitCount();
243+
}
244+
if (currBoundBp.ShouldBreak())
245+
{
246+
hitBoundBreakpoints.Add(currBoundBp);
247+
}
216248
}
217249

218250
fContinue = (hitBoundBreakpoints.Count == 0 && hitBps.Length != 0);

0 commit comments

Comments
 (0)