@@ -20,12 +20,9 @@ namespace Microsoft.PowerShell.EditorServices.Services
2020{
2121 internal class BreakpointService
2222 {
23- private const string s_psesGlobalVariableNamePrefix = "__psEditorServices_" ;
2423 private readonly ILogger < BreakpointService > _logger ;
2524 private readonly PowerShellContextService _powerShellContextService ;
2625
27- private static int breakpointHitCounter ;
28-
2926 public BreakpointService (
3027 ILoggerFactory factory ,
3128 PowerShellContextService powerShellContextService )
@@ -79,7 +76,7 @@ public async Task<BreakpointDetails[]> SetBreakpointsAsync(string escapedScriptP
7976 ! string . IsNullOrWhiteSpace ( breakpoint . HitCondition ) )
8077 {
8178 ScriptBlock actionScriptBlock =
82- GetBreakpointActionScriptBlock ( breakpoint ) ;
79+ BreakpointApiUtils . GetBreakpointActionScriptBlock ( breakpoint ) ;
8380
8481 // If there was a problem with the condition string,
8582 // move onto the next breakpoint.
@@ -140,7 +137,7 @@ public async Task<IEnumerable<CommandBreakpointDetails>> SetCommandBreakpoints(I
140137 ! string . IsNullOrWhiteSpace ( breakpoint . HitCondition ) )
141138 {
142139 ScriptBlock actionScriptBlock =
143- GetBreakpointActionScriptBlock ( breakpoint ) ;
140+ BreakpointApiUtils . GetBreakpointActionScriptBlock ( breakpoint ) ;
144141
145142 // If there was a problem with the condition string,
146143 // move onto the next breakpoint.
@@ -200,7 +197,7 @@ public async Task RemoveAllBreakpointsAsync()
200197 }
201198 }
202199
203- public async Task RemoveBreakpoints ( IEnumerable < Breakpoint > breakpoints )
200+ public async Task RemoveBreakpointsAsync ( IEnumerable < Breakpoint > breakpoints )
204201 {
205202 if ( VersionUtils . IsPS7OrGreater )
206203 {
@@ -226,187 +223,6 @@ public async Task RemoveBreakpoints(IEnumerable<Breakpoint> breakpoints)
226223 }
227224 }
228225
229- /// <summary>
230- /// Inspects the condition, putting in the appropriate scriptblock template
231- /// "if (expression) { break }". If errors are found in the condition, the
232- /// breakpoint passed in is updated to set Verified to false and an error
233- /// message is put into the breakpoint.Message property.
234- /// </summary>
235- /// <param name="breakpoint"></param>
236- /// <returns></returns>
237- private ScriptBlock GetBreakpointActionScriptBlock (
238- BreakpointDetailsBase breakpoint )
239- {
240- try
241- {
242- ScriptBlock actionScriptBlock ;
243- int ? hitCount = null ;
244-
245- // If HitCondition specified, parse and verify it.
246- if ( ! ( string . IsNullOrWhiteSpace ( breakpoint . HitCondition ) ) )
247- {
248- if ( int . TryParse ( breakpoint . HitCondition , out int parsedHitCount ) )
249- {
250- hitCount = parsedHitCount ;
251- }
252- else
253- {
254- breakpoint . Verified = false ;
255- breakpoint . Message = $ "The specified HitCount '{ breakpoint . HitCondition } ' is not valid. " +
256- "The HitCount must be an integer number." ;
257- return null ;
258- }
259- }
260-
261- // Create an Action scriptblock based on condition and/or hit count passed in.
262- if ( hitCount . HasValue && string . IsNullOrWhiteSpace ( breakpoint . Condition ) )
263- {
264- // In the HitCount only case, this is simple as we can just use the HitCount
265- // property on the breakpoint object which is represented by $_.
266- string action = $ "if ($_.HitCount -eq { hitCount } ) {{ break }}";
267- actionScriptBlock = ScriptBlock . Create ( action ) ;
268- }
269- else if ( ! string . IsNullOrWhiteSpace ( breakpoint . Condition ) )
270- {
271- // Must be either condition only OR condition and hit count.
272- actionScriptBlock = ScriptBlock . Create ( breakpoint . Condition ) ;
273-
274- // Check for simple, common errors that ScriptBlock parsing will not catch
275- // e.g. $i == 3 and $i > 3
276- if ( ! ValidateBreakpointConditionAst ( actionScriptBlock . Ast , out string message ) )
277- {
278- breakpoint . Verified = false ;
279- breakpoint . Message = message ;
280- return null ;
281- }
282-
283- // Check for "advanced" condition syntax i.e. if the user has specified
284- // a "break" or "continue" statement anywhere in their scriptblock,
285- // pass their scriptblock through to the Action parameter as-is.
286- Ast breakOrContinueStatementAst =
287- actionScriptBlock . Ast . Find (
288- ast => ( ast is BreakStatementAst || ast is ContinueStatementAst ) , true ) ;
289-
290- // If this isn't advanced syntax then the conditions string should be a simple
291- // expression that needs to be wrapped in a "if" test that conditionally executes
292- // a break statement.
293- if ( breakOrContinueStatementAst == null )
294- {
295- string wrappedCondition ;
296-
297- if ( hitCount . HasValue )
298- {
299- Interlocked . Increment ( ref breakpointHitCounter ) ;
300-
301- string globalHitCountVarName =
302- $ "$global:{ s_psesGlobalVariableNamePrefix } BreakHitCounter_{ breakpointHitCounter } ";
303-
304- wrappedCondition =
305- $ "if ({ breakpoint . Condition } ) {{ if (++{ globalHitCountVarName } -eq { hitCount } ) {{ break }} }}";
306- }
307- else
308- {
309- wrappedCondition = $ "if ({ breakpoint . Condition } ) {{ break }}";
310- }
311-
312- actionScriptBlock = ScriptBlock . Create ( wrappedCondition ) ;
313- }
314- }
315- else
316- {
317- // Shouldn't get here unless someone called this with no condition and no hit count.
318- actionScriptBlock = ScriptBlock . Create ( "break" ) ;
319- _logger . LogWarning ( "No condition and no hit count specified by caller." ) ;
320- }
321-
322- return actionScriptBlock ;
323- }
324- catch ( ParseException ex )
325- {
326- // Failed to create conditional breakpoint likely because the user provided an
327- // invalid PowerShell expression. Let the user know why.
328- breakpoint . Verified = false ;
329- breakpoint . Message = ExtractAndScrubParseExceptionMessage ( ex , breakpoint . Condition ) ;
330- return null ;
331- }
332- }
333226
334- private static bool ValidateBreakpointConditionAst ( Ast conditionAst , out string message )
335- {
336- message = string . Empty ;
337-
338- // We are only inspecting a few simple scenarios in the EndBlock only.
339- if ( conditionAst is ScriptBlockAst scriptBlockAst &&
340- scriptBlockAst . BeginBlock == null &&
341- scriptBlockAst . ProcessBlock == null &&
342- scriptBlockAst . EndBlock != null &&
343- scriptBlockAst . EndBlock . Statements . Count == 1 )
344- {
345- StatementAst statementAst = scriptBlockAst . EndBlock . Statements [ 0 ] ;
346- string condition = statementAst . Extent . Text ;
347-
348- if ( statementAst is AssignmentStatementAst )
349- {
350- message = FormatInvalidBreakpointConditionMessage ( condition , "Use '-eq' instead of '=='." ) ;
351- return false ;
352- }
353-
354- if ( statementAst is PipelineAst pipelineAst
355- && pipelineAst . PipelineElements . Count == 1
356- && pipelineAst . PipelineElements [ 0 ] . Redirections . Count > 0 )
357- {
358- message = FormatInvalidBreakpointConditionMessage ( condition , "Use '-gt' instead of '>'." ) ;
359- return false ;
360- }
361- }
362-
363- return true ;
364- }
365-
366- private static string ExtractAndScrubParseExceptionMessage ( ParseException parseException , string condition )
367- {
368- string [ ] messageLines = parseException . Message . Split ( '\n ' ) ;
369-
370- // Skip first line - it is a location indicator "At line:1 char: 4"
371- for ( int i = 1 ; i < messageLines . Length ; i ++ )
372- {
373- string line = messageLines [ i ] ;
374- if ( line . StartsWith ( "+" ) )
375- {
376- continue ;
377- }
378-
379- if ( ! string . IsNullOrWhiteSpace ( line ) )
380- {
381- // Note '==' and '>" do not generate parse errors
382- if ( line . Contains ( "'!='" ) )
383- {
384- line += " Use operator '-ne' instead of '!='." ;
385- }
386- else if ( line . Contains ( "'<'" ) && condition . Contains ( "<=" ) )
387- {
388- line += " Use operator '-le' instead of '<='." ;
389- }
390- else if ( line . Contains ( "'<'" ) )
391- {
392- line += " Use operator '-lt' instead of '<'." ;
393- }
394- else if ( condition . Contains ( ">=" ) )
395- {
396- line += " Use operator '-ge' instead of '>='." ;
397- }
398-
399- return FormatInvalidBreakpointConditionMessage ( condition , line ) ;
400- }
401- }
402-
403- // If the message format isn't in a form we expect, just return the whole message.
404- return FormatInvalidBreakpointConditionMessage ( condition , parseException . Message ) ;
405- }
406-
407- private static string FormatInvalidBreakpointConditionMessage ( string condition , string message )
408- {
409- return $ "'{ condition } ' is not a valid PowerShell expression. { message } ";
410- }
411227 }
412228}
0 commit comments