Skip to content

Commit 2d4012e

Browse files
authored
Merge pull request #300 from rkeithhill/rkeithhill/hit-count-breakpoint
Add support for Hit Count breakpoints
2 parents 73c6110 + 4123b7e commit 2d4012e

File tree

10 files changed

+182
-29
lines changed

10 files changed

+182
-29
lines changed

src/PowerShellEditorServices.Protocol/DebugAdapter/InitializeRequest.cs

+6
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ public class InitializeResponseBody
4747
/// </summary>
4848
public bool SupportsConditionalBreakpoints { get; set; }
4949

50+
/// <summary>
51+
/// Gets or sets a boolean value that determines whether the debug adapter
52+
/// supports breakpoints that break execution after a specified number of hits.
53+
/// </summary>
54+
public bool SupportsHitConditionalBreakpoints { get; set; }
55+
5056
/// <summary>
5157
/// Gets or sets a boolean value that determines whether the debug adapter
5258
/// supports a (side effect free) evaluate request for data hovers.

src/PowerShellEditorServices.Protocol/DebugAdapter/SetBreakpointsRequest.cs

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public class SourceBreakpoint
3434
public int? Column { get; set; }
3535

3636
public string Condition { get; set; }
37+
38+
public string HitCondition { get; set; }
3739
}
3840

3941
public class SetBreakpointsResponseBody

src/PowerShellEditorServices.Protocol/DebugAdapter/SetFunctionBreakpointsRequest.cs

+2
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,7 @@ public class FunctionBreakpoint
2727
public string Name { get; set; }
2828

2929
public string Condition { get; set; }
30+
31+
public string HitCondition { get; set; }
3032
}
3133
}

src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,8 @@ await requestContext.SendResult(
262262
scriptFile.FilePath,
263263
srcBreakpoint.Line,
264264
srcBreakpoint.Column,
265-
srcBreakpoint.Condition);
265+
srcBreakpoint.Condition,
266+
srcBreakpoint.HitCondition);
266267
}
267268

268269
// If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints.
@@ -294,7 +295,8 @@ protected async Task HandleSetFunctionBreakpointsRequest(
294295
FunctionBreakpoint funcBreakpoint = setBreakpointsParams.Breakpoints[i];
295296
breakpointDetails[i] = CommandBreakpointDetails.Create(
296297
funcBreakpoint.Name,
297-
funcBreakpoint.Condition);
298+
funcBreakpoint.Condition,
299+
funcBreakpoint.HitCondition);
298300
}
299301

300302
// If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints.

src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@ private async Task HandleInitializeRequest(
5858
await requestContext.SendResult(
5959
new InitializeResponseBody {
6060
SupportsConfigurationDoneRequest = true,
61-
SupportsConditionalBreakpoints = true,
6261
SupportsFunctionBreakpoints = true,
62+
SupportsConditionalBreakpoints = true,
63+
SupportsHitConditionalBreakpoints = true,
6364
SupportsSetVariable = true
6465
});
6566

src/PowerShellEditorServices/Debugging/BreakpointDetails.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@ private BreakpointDetails()
4242
/// <param name="line"></param>
4343
/// <param name="column"></param>
4444
/// <param name="condition"></param>
45+
/// <param name="hitCondition"></param>
4546
/// <returns></returns>
4647
public static BreakpointDetails Create(
4748
string source,
4849
int line,
4950
int? column = null,
50-
string condition = null)
51+
string condition = null,
52+
string hitCondition = null)
5153
{
5254
Validate.IsNotNull("source", source);
5355

@@ -57,7 +59,8 @@ public static BreakpointDetails Create(
5759
Source = source,
5860
LineNumber = line,
5961
ColumnNumber = column,
60-
Condition = condition
62+
Condition = condition,
63+
HitCondition = hitCondition
6164
};
6265
}
6366

src/PowerShellEditorServices/Debugging/BreakpointDetailsBase.cs

+5
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,10 @@ public abstract class BreakpointDetailsBase
2727
/// Gets the breakpoint condition string.
2828
/// </summary>
2929
public string Condition { get; protected set; }
30+
31+
/// <summary>
32+
/// Gets the breakpoint hit condition string.
33+
/// </summary>
34+
public string HitCondition { get; protected set; }
3035
}
3136
}

src/PowerShellEditorServices/Debugging/CommandBreakpointDetails.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ private CommandBreakpointDetails()
2929
/// </summary>
3030
/// <param name="name">The name of the command to break on.</param>
3131
/// <param name="condition">Condition string that would be applied to the breakpoint Action parameter.</param>
32+
/// <param name="hitCondition">Hit condition string that would be applied to the breakpoint Action parameter.</param>
3233
/// <returns></returns>
3334
public static CommandBreakpointDetails Create(
3435
string name,
35-
string condition = null)
36+
string condition = null,
37+
string hitCondition = null)
3638
{
3739
Validate.IsNotNull(nameof(name), name);
3840

src/PowerShellEditorServices/Debugging/DebugService.cs

+80-23
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ public class DebugService
3535
private VariableContainerDetails scriptScopeVariables;
3636
private StackFrameDetails[] stackFrameDetails;
3737

38+
private static int breakpointHitCounter = 0;
39+
3840
#endregion
3941

4042
#region Constructors
@@ -102,7 +104,8 @@ public async Task<BreakpointDetails[]> SetLineBreakpoints(
102104
}
103105

104106
// Check if this is a "conditional" line breakpoint.
105-
if (breakpoint.Condition != null)
107+
if (!String.IsNullOrWhiteSpace(breakpoint.Condition) ||
108+
!String.IsNullOrWhiteSpace(breakpoint.HitCondition))
106109
{
107110
ScriptBlock actionScriptBlock =
108111
GetBreakpointActionScriptBlock(breakpoint);
@@ -157,7 +160,8 @@ public async Task<CommandBreakpointDetails[]> SetCommandBreakpoints(
157160
psCommand.AddParameter("Command", breakpoint.Name);
158161

159162
// Check if this is a "conditional" command breakpoint.
160-
if (breakpoint.Condition != null)
163+
if (!String.IsNullOrWhiteSpace(breakpoint.Condition) ||
164+
!String.IsNullOrWhiteSpace(breakpoint.HitCondition))
161165
{
162166
ScriptBlock actionScriptBlock = GetBreakpointActionScriptBlock(breakpoint);
163167

@@ -719,32 +723,85 @@ private ScriptBlock GetBreakpointActionScriptBlock(
719723
{
720724
try
721725
{
722-
ScriptBlock actionScriptBlock = ScriptBlock.Create(breakpoint.Condition);
726+
ScriptBlock actionScriptBlock;
727+
int? hitCount = null;
723728

724-
// Check for simple, common errors that ScriptBlock parsing will not catch
725-
// e.g. $i == 3 and $i > 3
726-
string message;
727-
if (!ValidateBreakpointConditionAst(actionScriptBlock.Ast, out message))
729+
// If HitCondition specified, parse and verify it.
730+
if (!(String.IsNullOrWhiteSpace(breakpoint.HitCondition)))
728731
{
729-
breakpoint.Verified = false;
730-
breakpoint.Message = message;
731-
return null;
732+
int parsedHitCount;
733+
734+
if (Int32.TryParse(breakpoint.HitCondition, out parsedHitCount))
735+
{
736+
hitCount = parsedHitCount;
737+
}
738+
else
739+
{
740+
breakpoint.Verified = false;
741+
breakpoint.Message = $"The specified HitCount '{breakpoint.HitCondition}' is not valid. " +
742+
"The HitCount must be an integer number.";
743+
return null;
744+
}
732745
}
733746

734-
// Check for "advanced" condition syntax i.e. if the user has specified
735-
// a "break" or "continue" statement anywhere in their scriptblock,
736-
// pass their scriptblock through to the Action parameter as-is.
737-
Ast breakOrContinueStatementAst =
738-
actionScriptBlock.Ast.Find(
739-
ast => (ast is BreakStatementAst || ast is ContinueStatementAst), true);
740-
741-
// If this isn't advanced syntax then the conditions string should be a simple
742-
// expression that needs to be wrapped in a "if" test that conditionally executes
743-
// a break statement.
744-
if (breakOrContinueStatementAst == null)
747+
// Create an Action scriptblock based on condition and/or hit count passed in.
748+
if (hitCount.HasValue && String.IsNullOrWhiteSpace(breakpoint.Condition))
749+
{
750+
// In the HitCount only case, this is simple as we can just use the HitCount
751+
// property on the breakpoint object which is represented by $_.
752+
string action = $"if ($_.HitCount -eq {hitCount}) {{ break }}";
753+
actionScriptBlock = ScriptBlock.Create(action);
754+
}
755+
else if (!String.IsNullOrWhiteSpace(breakpoint.Condition))
756+
{
757+
// Must be either condition only OR condition and hit count.
758+
actionScriptBlock = ScriptBlock.Create(breakpoint.Condition);
759+
760+
// Check for simple, common errors that ScriptBlock parsing will not catch
761+
// e.g. $i == 3 and $i > 3
762+
string message;
763+
if (!ValidateBreakpointConditionAst(actionScriptBlock.Ast, out message))
764+
{
765+
breakpoint.Verified = false;
766+
breakpoint.Message = message;
767+
return null;
768+
}
769+
770+
// Check for "advanced" condition syntax i.e. if the user has specified
771+
// a "break" or "continue" statement anywhere in their scriptblock,
772+
// pass their scriptblock through to the Action parameter as-is.
773+
Ast breakOrContinueStatementAst =
774+
actionScriptBlock.Ast.Find(
775+
ast => (ast is BreakStatementAst || ast is ContinueStatementAst), true);
776+
777+
// If this isn't advanced syntax then the conditions string should be a simple
778+
// expression that needs to be wrapped in a "if" test that conditionally executes
779+
// a break statement.
780+
if (breakOrContinueStatementAst == null)
781+
{
782+
string wrappedCondition;
783+
784+
if (hitCount.HasValue)
785+
{
786+
string globalHitCountVarName =
787+
$"$global:__psEditorServices_BreakHitCounter_{breakpointHitCounter++}";
788+
789+
wrappedCondition =
790+
$"if ({breakpoint.Condition}) {{ if (++{globalHitCountVarName} -eq {hitCount}) {{ break }} }}";
791+
}
792+
else
793+
{
794+
wrappedCondition = $"if ({breakpoint.Condition}) {{ break }}";
795+
}
796+
797+
actionScriptBlock = ScriptBlock.Create(wrappedCondition);
798+
}
799+
}
800+
else
745801
{
746-
string wrappedCondition = $"if ({breakpoint.Condition}) {{ break }}";
747-
actionScriptBlock = ScriptBlock.Create(wrappedCondition);
802+
// Shouldn't get here unless someone called this with no condition and no hit count.
803+
actionScriptBlock = ScriptBlock.Create("break");
804+
Logger.Write(LogLevel.Warning, "No condition and no hit count specified by caller.");
748805
}
749806

750807
return actionScriptBlock;

test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs

+73
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,79 @@ await this.debugService.SetLineBreakpoints(
336336
await executeTask;
337337
}
338338

339+
[Fact]
340+
public async Task DebuggerStopsOnHitConditionBreakpoint()
341+
{
342+
const int hitCount = 5;
343+
344+
BreakpointDetails[] breakpoints =
345+
await this.debugService.SetLineBreakpoints(
346+
this.debugScriptFile,
347+
new[] {
348+
BreakpointDetails.Create("", 6, null, null, $"{hitCount}"),
349+
});
350+
351+
await this.AssertStateChange(PowerShellContextState.Ready);
352+
353+
Task executeTask =
354+
this.powerShellContext.ExecuteScriptAtPath(
355+
this.debugScriptFile.FilePath);
356+
357+
// Wait for conditional breakpoint to hit
358+
await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 6);
359+
360+
StackFrameDetails[] stackFrames = debugService.GetStackFrames();
361+
VariableDetailsBase[] variables =
362+
debugService.GetVariables(stackFrames[0].LocalVariables.Id);
363+
364+
// Verify the breakpoint only broke at the condition ie. $i -eq breakpointValue1
365+
var i = variables.FirstOrDefault(v => v.Name == "$i");
366+
Assert.NotNull(i);
367+
Assert.False(i.IsExpandable);
368+
Assert.Equal($"{hitCount}", i.ValueString);
369+
370+
// Abort script execution early and wait for completion
371+
this.debugService.Abort();
372+
await executeTask;
373+
}
374+
375+
[Fact]
376+
public async Task DebuggerStopsOnConditionalAndHitConditionBreakpoint()
377+
{
378+
const int hitCount = 5;
379+
380+
BreakpointDetails[] breakpoints =
381+
await this.debugService.SetLineBreakpoints(
382+
this.debugScriptFile,
383+
new[] {
384+
BreakpointDetails.Create("", 6, null, $"$i % 2 -eq 0", $"{hitCount}"),
385+
});
386+
387+
await this.AssertStateChange(PowerShellContextState.Ready);
388+
389+
Task executeTask =
390+
this.powerShellContext.ExecuteScriptAtPath(
391+
this.debugScriptFile.FilePath);
392+
393+
// Wait for conditional breakpoint to hit
394+
await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 6);
395+
396+
StackFrameDetails[] stackFrames = debugService.GetStackFrames();
397+
VariableDetailsBase[] variables =
398+
debugService.GetVariables(stackFrames[0].LocalVariables.Id);
399+
400+
// Verify the breakpoint only broke at the condition ie. $i -eq breakpointValue1
401+
var i = variables.FirstOrDefault(v => v.Name == "$i");
402+
Assert.NotNull(i);
403+
Assert.False(i.IsExpandable);
404+
// Condition is even numbers ($i starting at 1) should end up on 10 with a hit count of 5.
405+
Assert.Equal("10", i.ValueString);
406+
407+
// Abort script execution early and wait for completion
408+
this.debugService.Abort();
409+
await executeTask;
410+
}
411+
339412
[Fact]
340413
public async Task DebuggerProvidesMessageForInvalidConditionalBreakpoint()
341414
{

0 commit comments

Comments
 (0)