From 04b7993a6e15c9b2e187f274eb6ddeb11f3b1583 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Mon, 2 Dec 2019 07:45:56 -0500 Subject: [PATCH 01/15] Initial switch to breakpoint APIs --- .../Server/PsesServiceCollectionExtensions.cs | 1 + .../DebugAdapter/BreakpointService.cs | 412 ++++++++++++++++++ .../Services/DebugAdapter/DebugService.cs | 338 +------------- .../Debugging/BreakpointApiUtils.cs | 145 ++++++ .../Handlers/BreakpointHandlers.cs | 10 +- .../Handlers/ConfigurationDoneHandler.cs | 21 +- .../Handlers/InitializeHandler.cs | 7 +- .../Handlers/LaunchAndAttachHandler.cs | 6 +- .../Capabilities/DscBreakpointCapability.cs | 4 +- .../Utility/VersionUtils.cs | 5 + 10 files changed, 617 insertions(+), 332 deletions(-) create mode 100644 src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs create mode 100644 src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs diff --git a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs index 7ad24f36b..acf5a299a 100644 --- a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs +++ b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs @@ -64,6 +64,7 @@ public static IServiceCollection AddPsesDebugServices( .AddSingleton(languageServiceProvider.GetService()) .AddSingleton(psesDebugServer) .AddSingleton() + .AddSingleton() .AddSingleton(new DebugStateService { OwnsEditorSession = useTempSession diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs new file mode 100644 index 000000000..6c59e8b81 --- /dev/null +++ b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs @@ -0,0 +1,412 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Language; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Logging; +using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Services +{ + internal class BreakpointService + { + private const string s_psesGlobalVariableNamePrefix = "__psEditorServices_"; + private readonly ILogger _logger; + private readonly PowerShellContextService _powerShellContextService; + + private readonly ConcurrentDictionary> _breakpointsPerFile = + new ConcurrentDictionary>(); + + private static int breakpointHitCounter; + + public BreakpointService( + ILoggerFactory factory, + PowerShellContextService powerShellContextService) + { + _logger = factory.CreateLogger(); + _powerShellContextService = powerShellContextService; + } + + public async Task SetBreakpointsAsync(string escapedScriptPath, IEnumerable breakpoints) + { + if (VersionUtils.IsPS7OrGreater) + { + return BreakpointApiUtils.SetBreakpoints( + _powerShellContextService.CurrentRunspace.Runspace.Debugger, + breakpoints) + .Select(BreakpointDetails.Create).ToArray(); + } + + // Legacy behavior + PSCommand psCommand = null; + List configuredBreakpoints = new List(); + foreach (BreakpointDetails breakpoint in breakpoints) + { + // On first iteration psCommand will be null, every subsequent + // iteration will need to start a new statement. + if (psCommand == null) + { + psCommand = new PSCommand(); + } + else + { + psCommand.AddStatement(); + } + + psCommand + .AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint") + .AddParameter("Script", escapedScriptPath) + .AddParameter("Line", breakpoint.LineNumber); + + // Check if the user has specified the column number for the breakpoint. + if (breakpoint.ColumnNumber.HasValue && breakpoint.ColumnNumber.Value > 0) + { + // It bums me out that PowerShell will silently ignore a breakpoint + // where either the line or the column is invalid. I'd rather have an + // error or warning message I could relay back to the client. + psCommand.AddParameter("Column", breakpoint.ColumnNumber.Value); + } + + // Check if this is a "conditional" line breakpoint. + if (!string.IsNullOrWhiteSpace(breakpoint.Condition) || + !string.IsNullOrWhiteSpace(breakpoint.HitCondition)) + { + ScriptBlock actionScriptBlock = + GetBreakpointActionScriptBlock(breakpoint); + + // If there was a problem with the condition string, + // move onto the next breakpoint. + if (actionScriptBlock == null) + { + configuredBreakpoints.Add(breakpoint); + continue; + } + + psCommand.AddParameter("Action", actionScriptBlock); + } + } + + // If no PSCommand was created then there are no breakpoints to set. + if (psCommand != null) + { + IEnumerable setBreakpoints = + await _powerShellContextService.ExecuteCommandAsync(psCommand); + configuredBreakpoints.AddRange( + setBreakpoints.Select(BreakpointDetails.Create)); + } + + return configuredBreakpoints.ToArray(); + } + + public async Task> SetCommandBreakpoints(IEnumerable breakpoints) + { + if (VersionUtils.IsPS7OrGreater) + { + return BreakpointApiUtils.SetBreakpoints( + _powerShellContextService.CurrentRunspace.Runspace.Debugger, + breakpoints) + .Select(CommandBreakpointDetails.Create); + } + + // Legacy behavior + PSCommand psCommand = null; + List configuredBreakpoints = new List(); + foreach (CommandBreakpointDetails breakpoint in breakpoints) + { + // On first iteration psCommand will be null, every subsequent + // iteration will need to start a new statement. + if (psCommand == null) + { + psCommand = new PSCommand(); + } + else + { + psCommand.AddStatement(); + } + + psCommand + .AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint") + .AddParameter("Command", breakpoint.Name); + + // Check if this is a "conditional" line breakpoint. + if (!string.IsNullOrWhiteSpace(breakpoint.Condition) || + !string.IsNullOrWhiteSpace(breakpoint.HitCondition)) + { + ScriptBlock actionScriptBlock = + GetBreakpointActionScriptBlock(breakpoint); + + // If there was a problem with the condition string, + // move onto the next breakpoint. + if (actionScriptBlock == null) + { + configuredBreakpoints.Add(breakpoint); + continue; + } + + psCommand.AddParameter("Action", actionScriptBlock); + } + } + + // If no PSCommand was created then there are no breakpoints to set. + if (psCommand != null) + { + IEnumerable setBreakpoints = + await _powerShellContextService.ExecuteCommandAsync(psCommand); + configuredBreakpoints.AddRange( + setBreakpoints.Select(CommandBreakpointDetails.Create)); + } + + return configuredBreakpoints; + } + + /// + /// Clears all breakpoints in the current session. + /// + public async Task RemoveAllBreakpointsAsync() + { + try + { + if (VersionUtils.IsPS7OrGreater) + { + foreach (Breakpoint breakpoint in BreakpointApiUtils.GetBreakpoints( + _powerShellContextService.CurrentRunspace.Runspace.Debugger)) + { + BreakpointApiUtils.RemoveBreakpoint( + _powerShellContextService.CurrentRunspace.Runspace.Debugger, + breakpoint); + } + + return; + } + + // Legacy behavior + + PSCommand psCommand = new PSCommand(); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint"); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); + + await _powerShellContextService.ExecuteCommandAsync(psCommand); + } + catch (Exception e) + { + _logger.LogException("Caught exception while clearing breakpoints from session", e); + } + } + + public async Task RemoveBreakpoints(IEnumerable breakpoints) + { + if (VersionUtils.IsPS7OrGreater) + { + foreach (Breakpoint breakpoint in breakpoints) + { + BreakpointApiUtils.RemoveBreakpoint( + _powerShellContextService.CurrentRunspace.Runspace.Debugger, + breakpoint); + } + + return; + } + + // Legacy behavior + var breakpointIds = breakpoints.Select(b => b.Id).ToArray(); + if(breakpointIds.Length > 0) + { + PSCommand psCommand = new PSCommand(); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); + psCommand.AddParameter("Id", breakpoints.Select(b => b.Id).ToArray()); + + await _powerShellContextService.ExecuteCommandAsync(psCommand); + } + } + + /// + /// Inspects the condition, putting in the appropriate scriptblock template + /// "if (expression) { break }". If errors are found in the condition, the + /// breakpoint passed in is updated to set Verified to false and an error + /// message is put into the breakpoint.Message property. + /// + /// + /// + private ScriptBlock GetBreakpointActionScriptBlock( + BreakpointDetailsBase breakpoint) + { + try + { + ScriptBlock actionScriptBlock; + int? hitCount = null; + + // If HitCondition specified, parse and verify it. + if (!(string.IsNullOrWhiteSpace(breakpoint.HitCondition))) + { + if (int.TryParse(breakpoint.HitCondition, out int parsedHitCount)) + { + hitCount = parsedHitCount; + } + else + { + breakpoint.Verified = false; + breakpoint.Message = $"The specified HitCount '{breakpoint.HitCondition}' is not valid. " + + "The HitCount must be an integer number."; + return null; + } + } + + // Create an Action scriptblock based on condition and/or hit count passed in. + if (hitCount.HasValue && string.IsNullOrWhiteSpace(breakpoint.Condition)) + { + // In the HitCount only case, this is simple as we can just use the HitCount + // property on the breakpoint object which is represented by $_. + string action = $"if ($_.HitCount -eq {hitCount}) {{ break }}"; + actionScriptBlock = ScriptBlock.Create(action); + } + else if (!string.IsNullOrWhiteSpace(breakpoint.Condition)) + { + // Must be either condition only OR condition and hit count. + actionScriptBlock = ScriptBlock.Create(breakpoint.Condition); + + // Check for simple, common errors that ScriptBlock parsing will not catch + // e.g. $i == 3 and $i > 3 + if (!ValidateBreakpointConditionAst(actionScriptBlock.Ast, out string message)) + { + breakpoint.Verified = false; + breakpoint.Message = message; + return null; + } + + // Check for "advanced" condition syntax i.e. if the user has specified + // a "break" or "continue" statement anywhere in their scriptblock, + // pass their scriptblock through to the Action parameter as-is. + Ast breakOrContinueStatementAst = + actionScriptBlock.Ast.Find( + ast => (ast is BreakStatementAst || ast is ContinueStatementAst), true); + + // If this isn't advanced syntax then the conditions string should be a simple + // expression that needs to be wrapped in a "if" test that conditionally executes + // a break statement. + if (breakOrContinueStatementAst == null) + { + string wrappedCondition; + + if (hitCount.HasValue) + { + string globalHitCountVarName = + $"$global:{s_psesGlobalVariableNamePrefix}BreakHitCounter_{breakpointHitCounter++}"; + + wrappedCondition = + $"if ({breakpoint.Condition}) {{ if (++{globalHitCountVarName} -eq {hitCount}) {{ break }} }}"; + } + else + { + wrappedCondition = $"if ({breakpoint.Condition}) {{ break }}"; + } + + actionScriptBlock = ScriptBlock.Create(wrappedCondition); + } + } + else + { + // Shouldn't get here unless someone called this with no condition and no hit count. + actionScriptBlock = ScriptBlock.Create("break"); + _logger.LogWarning("No condition and no hit count specified by caller."); + } + + return actionScriptBlock; + } + catch (ParseException ex) + { + // Failed to create conditional breakpoint likely because the user provided an + // invalid PowerShell expression. Let the user know why. + breakpoint.Verified = false; + breakpoint.Message = ExtractAndScrubParseExceptionMessage(ex, breakpoint.Condition); + return null; + } + } + + private bool ValidateBreakpointConditionAst(Ast conditionAst, out string message) + { + message = string.Empty; + + // We are only inspecting a few simple scenarios in the EndBlock only. + if (conditionAst is ScriptBlockAst scriptBlockAst && + scriptBlockAst.BeginBlock == null && + scriptBlockAst.ProcessBlock == null && + scriptBlockAst.EndBlock != null && + scriptBlockAst.EndBlock.Statements.Count == 1) + { + StatementAst statementAst = scriptBlockAst.EndBlock.Statements[0]; + string condition = statementAst.Extent.Text; + + if (statementAst is AssignmentStatementAst) + { + message = FormatInvalidBreakpointConditionMessage(condition, "Use '-eq' instead of '=='."); + return false; + } + + if (statementAst is PipelineAst pipelineAst + && pipelineAst.PipelineElements.Count == 1 + && pipelineAst.PipelineElements[0].Redirections.Count > 0) + { + message = FormatInvalidBreakpointConditionMessage(condition, "Use '-gt' instead of '>'."); + return false; + } + } + + return true; + } + + private string ExtractAndScrubParseExceptionMessage(ParseException parseException, string condition) + { + string[] messageLines = parseException.Message.Split('\n'); + + // Skip first line - it is a location indicator "At line:1 char: 4" + for (int i = 1; i < messageLines.Length; i++) + { + string line = messageLines[i]; + if (line.StartsWith("+")) + { + continue; + } + + if (!string.IsNullOrWhiteSpace(line)) + { + // Note '==' and '>" do not generate parse errors + if (line.Contains("'!='")) + { + line += " Use operator '-ne' instead of '!='."; + } + else if (line.Contains("'<'") && condition.Contains("<=")) + { + line += " Use operator '-le' instead of '<='."; + } + else if (line.Contains("'<'")) + { + line += " Use operator '-lt' instead of '<'."; + } + else if (condition.Contains(">=")) + { + line += " Use operator '-ge' instead of '>='."; + } + + return FormatInvalidBreakpointConditionMessage(condition, line); + } + } + + // If the message format isn't in a form we expect, just return the whole message. + return FormatInvalidBreakpointConditionMessage(condition, parseException.Message); + } + + private string FormatInvalidBreakpointConditionMessage(string condition, string message) + { + return $"'{condition}' is not a valid PowerShell expression. {message}"; + } + } +} diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index d2f5a7a7a..93749d6b9 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -31,12 +31,15 @@ internal class DebugService private const string PsesGlobalVariableNamePrefix = "__psEditorServices_"; private const string TemporaryScriptFileName = "Script Listing.ps1"; + private readonly BreakpointDetails[] s_emptyBreakpointDetailsArray = new BreakpointDetails[0]; private readonly ILogger logger; private readonly PowerShellContextService powerShellContext; + private readonly BreakpointService _breakpointService; private RemoteFileManagerService remoteFileManager; // TODO: This needs to be managed per nested session + // TODO: Move to BreakpointService private readonly Dictionary> breakpointsPerFile = new Dictionary>(); @@ -104,12 +107,14 @@ internal class DebugService public DebugService( PowerShellContextService powerShellContext, RemoteFileManagerService remoteFileManager, + BreakpointService breakpointService, ILoggerFactory factory) { Validate.IsNotNull(nameof(powerShellContext), powerShellContext); this.logger = factory.CreateLogger(); this.powerShellContext = powerShellContext; + _breakpointService = breakpointService; this.powerShellContext.DebuggerStop += this.OnDebuggerStopAsync; this.powerShellContext.DebuggerResumed += this.OnDebuggerResumed; @@ -140,8 +145,6 @@ public async Task SetLineBreakpointsAsync( BreakpointDetails[] breakpoints, bool clearExisting = true) { - var resultBreakpointDetails = new List(); - var dscBreakpoints = this.powerShellContext .CurrentRunspace @@ -157,7 +160,7 @@ public async Task SetLineBreakpointsAsync( this.logger.LogTrace( $"Could not set breakpoints for local path '{scriptPath}' in a remote session."); - return resultBreakpointDetails.ToArray(); + return s_emptyBreakpointDetailsArray; } string mappedPath = @@ -174,7 +177,7 @@ public async Task SetLineBreakpointsAsync( this.logger.LogTrace( $"Could not set breakpoint on temporary script listing path '{scriptPath}'."); - return resultBreakpointDetails.ToArray(); + return s_emptyBreakpointDetailsArray; } // Fix for issue #123 - file paths that contain wildcard chars [ and ] need to @@ -189,75 +192,13 @@ public async Task SetLineBreakpointsAsync( await this.ClearBreakpointsInFileAsync(scriptFile).ConfigureAwait(false); } - PSCommand psCommand = null; - foreach (BreakpointDetails breakpoint in breakpoints) - { - // On first iteration psCommand will be null, every subsequent - // iteration will need to start a new statement. - if (psCommand == null) - { - psCommand = new PSCommand(); - } - else - { - psCommand.AddStatement(); - } - - psCommand - .AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint") - .AddParameter("Script", escapedScriptPath) - .AddParameter("Line", breakpoint.LineNumber); - - // Check if the user has specified the column number for the breakpoint. - if (breakpoint.ColumnNumber.HasValue && breakpoint.ColumnNumber.Value > 0) - { - // It bums me out that PowerShell will silently ignore a breakpoint - // where either the line or the column is invalid. I'd rather have an - // error or warning message I could relay back to the client. - psCommand.AddParameter("Column", breakpoint.ColumnNumber.Value); - } - - // Check if this is a "conditional" line breakpoint. - if (!String.IsNullOrWhiteSpace(breakpoint.Condition) || - !String.IsNullOrWhiteSpace(breakpoint.HitCondition)) - { - ScriptBlock actionScriptBlock = - GetBreakpointActionScriptBlock(breakpoint); - - // If there was a problem with the condition string, - // move onto the next breakpoint. - if (actionScriptBlock == null) - { - resultBreakpointDetails.Add(breakpoint); - continue; - } - - psCommand.AddParameter("Action", actionScriptBlock); - } - } - - // If no PSCommand was created then there are no breakpoints to set. - if (psCommand != null) - { - IEnumerable configuredBreakpoints = - await this.powerShellContext.ExecuteCommandAsync(psCommand).ConfigureAwait(false); - - // The order in which the breakpoints are returned is significant to the - // VSCode client and should match the order in which they are passed in. - resultBreakpointDetails.AddRange( - configuredBreakpoints.Select(BreakpointDetails.Create)); - } - } - else - { - resultBreakpointDetails = - await dscBreakpoints.SetLineBreakpointsAsync( - powerShellContext, - escapedScriptPath, - breakpoints).ConfigureAwait(false); + return await _breakpointService.SetBreakpointsAsync(escapedScriptPath, breakpoints).ConfigureAwait(false); } - return resultBreakpointDetails.ToArray(); + return await dscBreakpoints.SetLineBreakpointsAsync( + this.powerShellContext, + escapedScriptPath, + breakpoints); } /// @@ -270,49 +211,20 @@ public async Task SetCommandBreakpointsAsync( CommandBreakpointDetails[] breakpoints, bool clearExisting = true) { - var resultBreakpointDetails = new List(); + CommandBreakpointDetails[] resultBreakpointDetails = null; if (clearExisting) { - await this.ClearCommandBreakpointsAsync().ConfigureAwait(false); + // Flatten dictionary values into one list and remove them all. + await _breakpointService.RemoveBreakpoints(this.breakpointsPerFile.Values.SelectMany( i => i ).Where( i => i is CommandBreakpoint)).ConfigureAwait(false); } if (breakpoints.Length > 0) { - foreach (CommandBreakpointDetails breakpoint in breakpoints) - { - PSCommand psCommand = new PSCommand(); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint"); - psCommand.AddParameter("Command", breakpoint.Name); - - // Check if this is a "conditional" command breakpoint. - if (!String.IsNullOrWhiteSpace(breakpoint.Condition) || - !String.IsNullOrWhiteSpace(breakpoint.HitCondition)) - { - ScriptBlock actionScriptBlock = GetBreakpointActionScriptBlock(breakpoint); - - // If there was a problem with the condition string, - // move onto the next breakpoint. - if (actionScriptBlock == null) - { - resultBreakpointDetails.Add(breakpoint); - continue; - } - - psCommand.AddParameter("Action", actionScriptBlock); - } - - IEnumerable configuredBreakpoints = - await this.powerShellContext.ExecuteCommandAsync(psCommand).ConfigureAwait(false); - - // The order in which the breakpoints are returned is significant to the - // VSCode client and should match the order in which they are passed in. - resultBreakpointDetails.AddRange( - configuredBreakpoints.Select(CommandBreakpointDetails.Create)); - } + resultBreakpointDetails = (await _breakpointService.SetCommandBreakpoints(breakpoints).ConfigureAwait(false)).ToArray(); } - return resultBreakpointDetails.ToArray(); + return resultBreakpointDetails ?? new CommandBreakpointDetails[0]; } /// @@ -753,25 +665,6 @@ public VariableScope[] GetVariableScopes(int stackFrameId) }; } - /// - /// Clears all breakpoints in the current session. - /// - public async Task ClearAllBreakpointsAsync() - { - try - { - PSCommand psCommand = new PSCommand(); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint"); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); - - await this.powerShellContext.ExecuteCommandAsync(psCommand).ConfigureAwait(false); - } - catch (Exception e) - { - logger.LogException("Caught exception while clearing breakpoints from session", e); - } - } - #endregion #region Private Methods @@ -783,11 +676,7 @@ private async Task ClearBreakpointsInFileAsync(ScriptFile scriptFile) { if (breakpoints.Count > 0) { - PSCommand psCommand = new PSCommand(); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); - psCommand.AddParameter("Id", breakpoints.Select(b => b.Id).ToArray()); - - await this.powerShellContext.ExecuteCommandAsync(psCommand).ConfigureAwait(false); + await _breakpointService.RemoveBreakpoints(breakpoints).ConfigureAwait(false); // Clear the existing breakpoints list for the file breakpoints.Clear(); @@ -795,16 +684,6 @@ private async Task ClearBreakpointsInFileAsync(ScriptFile scriptFile) } } - private async Task ClearCommandBreakpointsAsync() - { - PSCommand psCommand = new PSCommand(); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint"); - psCommand.AddParameter("Type", "Command"); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); - - await this.powerShellContext.ExecuteCommandAsync(psCommand).ConfigureAwait(false); - } - private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride) { await this.debugInfoHandle.WaitAsync().ConfigureAwait(false); @@ -998,187 +877,6 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) } } - /// - /// Inspects the condition, putting in the appropriate scriptblock template - /// "if (expression) { break }". If errors are found in the condition, the - /// breakpoint passed in is updated to set Verified to false and an error - /// message is put into the breakpoint.Message property. - /// - /// - /// - private ScriptBlock GetBreakpointActionScriptBlock( - BreakpointDetailsBase breakpoint) - { - try - { - ScriptBlock actionScriptBlock; - int? hitCount = null; - - // If HitCondition specified, parse and verify it. - if (!(String.IsNullOrWhiteSpace(breakpoint.HitCondition))) - { - if (Int32.TryParse(breakpoint.HitCondition, out int parsedHitCount)) - { - hitCount = parsedHitCount; - } - else - { - breakpoint.Verified = false; - breakpoint.Message = $"The specified HitCount '{breakpoint.HitCondition}' is not valid. " + - "The HitCount must be an integer number."; - return null; - } - } - - // Create an Action scriptblock based on condition and/or hit count passed in. - if (hitCount.HasValue && string.IsNullOrWhiteSpace(breakpoint.Condition)) - { - // In the HitCount only case, this is simple as we can just use the HitCount - // property on the breakpoint object which is represented by $_. - string action = $"if ($_.HitCount -eq {hitCount}) {{ break }}"; - actionScriptBlock = ScriptBlock.Create(action); - } - else if (!string.IsNullOrWhiteSpace(breakpoint.Condition)) - { - // Must be either condition only OR condition and hit count. - actionScriptBlock = ScriptBlock.Create(breakpoint.Condition); - - // Check for simple, common errors that ScriptBlock parsing will not catch - // e.g. $i == 3 and $i > 3 - if (!ValidateBreakpointConditionAst(actionScriptBlock.Ast, out string message)) - { - breakpoint.Verified = false; - breakpoint.Message = message; - return null; - } - - // Check for "advanced" condition syntax i.e. if the user has specified - // a "break" or "continue" statement anywhere in their scriptblock, - // pass their scriptblock through to the Action parameter as-is. - Ast breakOrContinueStatementAst = - actionScriptBlock.Ast.Find( - ast => (ast is BreakStatementAst || ast is ContinueStatementAst), true); - - // If this isn't advanced syntax then the conditions string should be a simple - // expression that needs to be wrapped in a "if" test that conditionally executes - // a break statement. - if (breakOrContinueStatementAst == null) - { - string wrappedCondition; - - if (hitCount.HasValue) - { - string globalHitCountVarName = - $"$global:{PsesGlobalVariableNamePrefix}BreakHitCounter_{breakpointHitCounter++}"; - - wrappedCondition = - $"if ({breakpoint.Condition}) {{ if (++{globalHitCountVarName} -eq {hitCount}) {{ break }} }}"; - } - else - { - wrappedCondition = $"if ({breakpoint.Condition}) {{ break }}"; - } - - actionScriptBlock = ScriptBlock.Create(wrappedCondition); - } - } - else - { - // Shouldn't get here unless someone called this with no condition and no hit count. - actionScriptBlock = ScriptBlock.Create("break"); - this.logger.LogWarning("No condition and no hit count specified by caller."); - } - - return actionScriptBlock; - } - catch (ParseException ex) - { - // Failed to create conditional breakpoint likely because the user provided an - // invalid PowerShell expression. Let the user know why. - breakpoint.Verified = false; - breakpoint.Message = ExtractAndScrubParseExceptionMessage(ex, breakpoint.Condition); - return null; - } - } - - private bool ValidateBreakpointConditionAst(Ast conditionAst, out string message) - { - message = string.Empty; - - // We are only inspecting a few simple scenarios in the EndBlock only. - if (conditionAst is ScriptBlockAst scriptBlockAst && - scriptBlockAst.BeginBlock == null && - scriptBlockAst.ProcessBlock == null && - scriptBlockAst.EndBlock != null && - scriptBlockAst.EndBlock.Statements.Count == 1) - { - StatementAst statementAst = scriptBlockAst.EndBlock.Statements[0]; - string condition = statementAst.Extent.Text; - - if (statementAst is AssignmentStatementAst) - { - message = FormatInvalidBreakpointConditionMessage(condition, "Use '-eq' instead of '=='."); - return false; - } - - if (statementAst is PipelineAst pipelineAst - && pipelineAst.PipelineElements.Count == 1 - && pipelineAst.PipelineElements[0].Redirections.Count > 0) - { - message = FormatInvalidBreakpointConditionMessage(condition, "Use '-gt' instead of '>'."); - return false; - } - } - - return true; - } - - private string ExtractAndScrubParseExceptionMessage(ParseException parseException, string condition) - { - string[] messageLines = parseException.Message.Split('\n'); - - // Skip first line - it is a location indicator "At line:1 char: 4" - for (int i = 1; i < messageLines.Length; i++) - { - string line = messageLines[i]; - if (line.StartsWith("+")) - { - continue; - } - - if (!string.IsNullOrWhiteSpace(line)) - { - // Note '==' and '>" do not generate parse errors - if (line.Contains("'!='")) - { - line += " Use operator '-ne' instead of '!='."; - } - else if (line.Contains("'<'") && condition.Contains("<=")) - { - line += " Use operator '-le' instead of '<='."; - } - else if (line.Contains("'<'")) - { - line += " Use operator '-lt' instead of '<'."; - } - else if (condition.Contains(">=")) - { - line += " Use operator '-ge' instead of '>='."; - } - - return FormatInvalidBreakpointConditionMessage(condition, line); - } - } - - // If the message format isn't in a form we expect, just return the whole message. - return FormatInvalidBreakpointConditionMessage(condition, parseException.Message); - } - - private string FormatInvalidBreakpointConditionMessage(string condition, string message) - { - return $"'{condition}' is not a valid PowerShell expression. {message}"; - } - private string TrimScriptListingLine(PSObject scriptLineObj, ref int prefixLength) { string scriptLine = scriptLineObj.ToString(); diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs new file mode 100644 index 000000000..a85609423 --- /dev/null +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs @@ -0,0 +1,145 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Management.Automation; +using System.Reflection; + +namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter + +{ + internal static class BreakpointApiUtils + { + #region Private Static Fields + + private static readonly Lazy> s_setLineBreakpointLazy; + + private static readonly Lazy> s_setCommandBreakpointLazy; + + private static readonly Lazy>> s_getBreakpointsLazy; + + private static readonly Lazy> s_removeBreakpointLazy; + + private static readonly Lazy> s_newLineBreakpointLazy; + + private static readonly Lazy> s_newCommandBreakpointLazy; + + #endregion + + #region Static Constructor + + static BreakpointApiUtils() + { + // If this version of PowerShell does not support the new Breakpoint APIs introduced in PowerShell 7.0.0-preview.4, + // do nothing as this class will not get used. + if (typeof(Debugger).GetMethod("SetLineBreakpoint", BindingFlags.Public | BindingFlags.Instance) == null) + { + return; + } + + s_setLineBreakpointLazy = new Lazy>(() => + { + MethodInfo setLineBreakpointMethod = typeof(Debugger).GetMethod("SetLineBreakpoint", BindingFlags.Public | BindingFlags.Instance); + + return (Func)Delegate.CreateDelegate( + typeof(Func), + firstArgument: null, + setLineBreakpointMethod); + }); + + s_setCommandBreakpointLazy = new Lazy>(() => + { + MethodInfo setCommandBreakpointMethod = typeof(Debugger).GetMethod("SetCommandBreakpoint", BindingFlags.Public | BindingFlags.Instance); + + return (Func)Delegate.CreateDelegate( + typeof(Func), + firstArgument: null, + setCommandBreakpointMethod); + }); + + s_getBreakpointsLazy = new Lazy>>(() => + { + MethodInfo removeBreakpointMethod = typeof(Debugger).GetMethod("GetBreakpoints", BindingFlags.Public | BindingFlags.Instance); + + return (Func>)Delegate.CreateDelegate( + typeof(Func>), + firstArgument: null, + removeBreakpointMethod); + }); + + s_removeBreakpointLazy = new Lazy>(() => + { + MethodInfo removeBreakpointMethod = typeof(Debugger).GetMethod("RemoveBreakpoint", BindingFlags.Public | BindingFlags.Instance); + + return (Func)Delegate.CreateDelegate( + typeof(Func), + firstArgument: null, + removeBreakpointMethod); + }); + } + + #endregion + + #region Public Static Properties + + private static Func SetLineBreakpointDelegate => s_setLineBreakpointLazy.Value; + + private static Func SetCommandBreakpointDelegate => s_setCommandBreakpointLazy.Value; + + private static Func> GetBreakpointsDelegate => s_getBreakpointsLazy.Value; + + private static Func RemoveBreakpointDelegate => s_removeBreakpointLazy.Value; + + private static Func CreateLineBreakpointDelegate => s_newLineBreakpointLazy.Value; + + private static Func CreateCommandBreakpointDelegate => s_newCommandBreakpointLazy.Value; + + #endregion + + #region Public Static Methods + + public static IEnumerable SetBreakpoints(Debugger debugger, IEnumerable breakpoints) + { + var psBreakpoints = new List(breakpoints.Count()); + + foreach (BreakpointDetailsBase breakpoint in breakpoints) + { + Breakpoint psBreakpoint; + switch (breakpoint) + { + case BreakpointDetails lineBreakpoint: + psBreakpoint = SetLineBreakpointDelegate(debugger, lineBreakpoint.Source, lineBreakpoint.LineNumber, lineBreakpoint.ColumnNumber ?? 0, null); + break; + + case CommandBreakpointDetails commandBreakpoint: + psBreakpoint = SetCommandBreakpointDelegate(debugger, commandBreakpoint.Name, null, null); + break; + + default: + throw new NotImplementedException("Other breakpoints not supported yet"); + } + + psBreakpoints.Add(psBreakpoint); + } + + return psBreakpoints; + } + + public static List GetBreakpoints(Debugger debugger) + { + return GetBreakpointsDelegate(debugger); + } + + public static bool RemoveBreakpoint(Debugger debugger, Breakpoint breakpoint) + { + return RemoveBreakpointDelegate(debugger, breakpoint); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs index 45db430fe..056b159e4 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs @@ -145,10 +145,8 @@ public async Task Handle(SetBreakpointsArguments request // When you set a breakpoint in the right pane of a Git diff window on a PS1 file, // the Source.Path comes through as Untitled-X. That's why we check for IsUntitledPath. - if (!ScriptFile.IsUntitledPath(request.Source.Path) && - !_workspaceService.TryGetFile( - request.Source.Path, - out scriptFile)) + if (!_workspaceService.TryGetFile(request.Source.Path, out scriptFile) && + !ScriptFile.IsUntitledPath(request.Source.Path)) { string message = _debugStateService.NoDebug ? string.Empty : "Source file could not be accessed, breakpoint not set."; var srcBreakpoints = request.Breakpoints @@ -164,7 +162,9 @@ public async Task Handle(SetBreakpointsArguments request // Verify source file is a PowerShell script file. string fileExtension = Path.GetExtension(scriptFile?.FilePath ?? "")?.ToLower(); - if (string.IsNullOrEmpty(fileExtension) || ((fileExtension != ".ps1") && (fileExtension != ".psm1"))) + bool isUntitledPath = ScriptFile.IsUntitledPath(request.Source.Path); + if ((!isUntitledPath && fileExtension != ".ps1" && fileExtension != ".psm1") || + (!VersionUtils.IsPS7OrGreater && isUntitledPath)) { _logger.LogWarning( $"Attempted to set breakpoints on a non-PowerShell file: {request.Source.Path}"); diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs index 8a1f00c7e..51952c706 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs @@ -3,12 +3,15 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System.Management.Automation; +using System.Management.Automation.Language; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.DebugAdapter.Protocol.Events; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; using OmniSharp.Extensions.JsonRpc; @@ -97,8 +100,22 @@ private async Task LaunchScriptAsync(string scriptToLaunch) { ScriptFile untitledScript = _workspaceService.GetFile(scriptToLaunch); - await _powerShellContextService - .ExecuteScriptStringAsync(untitledScript.Contents, true, true).ConfigureAwait(false); + if (VersionUtils.IsPS7OrGreater) + { + ScriptBlockAst ast = Parser.ParseInput(untitledScript.Contents, untitledScript.DocumentUri, out Token[] tokens, out ParseError[] errors); + + // This seems to be the simplest way to invoke a script block (which contains breakpoint information) via the PowerShell API. + PSCommand cmd = new PSCommand().AddScript("& $args[0]").AddArgument(ast.GetScriptBlock()); + await _powerShellContextService + .ExecuteCommandAsync(cmd, sendOutputToHost: true, sendErrorToHost:true) + .ConfigureAwait(false); + } + else + { + await _powerShellContextService + .ExecuteScriptStringAsync(untitledScript.Contents, writeInputToHost: true, writeOutputToHost: true) + .ConfigureAwait(false); + } } else { diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/InitializeHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/InitializeHandler.cs index fda53620f..9e9baffad 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/InitializeHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/InitializeHandler.cs @@ -15,19 +15,22 @@ internal class InitializeHandler : IInitializeHandler { private readonly ILogger _logger; private readonly DebugService _debugService; + private readonly BreakpointService _breakpointService; public InitializeHandler( ILoggerFactory factory, - DebugService debugService) + DebugService debugService, + BreakpointService breakpointService) { _logger = factory.CreateLogger(); _debugService = debugService; + _breakpointService = breakpointService; } public async Task Handle(InitializeRequestArguments request, CancellationToken cancellationToken) { // Clear any existing breakpoints before proceeding - await _debugService.ClearAllBreakpointsAsync().ConfigureAwait(false); + await _breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(false); // Now send the Initialize response to continue setup return new InitializeResponse diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs index fa99f0e2f..014300091 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs @@ -212,6 +212,7 @@ internal class AttachHandler : IPsesAttachHandler private readonly ILogger _logger; private readonly DebugService _debugService; + private readonly BreakpointService _breakpointService; private readonly PowerShellContextService _powerShellContextService; private readonly DebugStateService _debugStateService; private readonly DebugEventHandlerService _debugEventHandlerService; @@ -223,11 +224,13 @@ public AttachHandler( DebugService debugService, PowerShellContextService powerShellContextService, DebugStateService debugStateService, + BreakpointService breakpointService, DebugEventHandlerService debugEventHandlerService) { _logger = factory.CreateLogger(); _jsonRpcServer = jsonRpcServer; _debugService = debugService; + _breakpointService = breakpointService; _powerShellContextService = powerShellContextService; _debugStateService = debugStateService; _debugEventHandlerService = debugEventHandlerService; @@ -323,7 +326,7 @@ await _powerShellContextService.ExecuteScriptStringAsync( } // Clear any existing breakpoints before proceeding - await _debugService.ClearAllBreakpointsAsync().ConfigureAwait(continueOnCapturedContext: false); + await _breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(continueOnCapturedContext: false); // Execute the Debug-Runspace command but don't await it because it // will block the debug adapter initialization process. The @@ -357,6 +360,7 @@ await _powerShellContextService.ExecuteScriptStringAsync( .ExecuteScriptStringAsync(debugRunspaceCmd) .ContinueWith(OnExecutionCompletedAsync); + _jsonRpcServer.SendNotification(EventNames.Initialized); return Unit.Value; } diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs index 220801dfa..247c1d13e 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs @@ -24,7 +24,7 @@ internal class DscBreakpointCapability : IRunspaceCapability private Dictionary breakpointsPerFile = new Dictionary(); - public async Task> SetLineBreakpointsAsync( + public async Task SetLineBreakpointsAsync( PowerShellContextService powerShellContext, string scriptPath, BreakpointDetails[] breakpoints) @@ -68,7 +68,7 @@ await powerShellContext.ExecuteScriptStringAsync( breakpoint.Verified = true; } - return breakpoints.ToList(); + return breakpoints.ToArray(); } public bool IsDscResourcePath(string scriptPath) diff --git a/src/PowerShellEditorServices/Utility/VersionUtils.cs b/src/PowerShellEditorServices/Utility/VersionUtils.cs index 159ceb97a..b7ec12b5a 100644 --- a/src/PowerShellEditorServices/Utility/VersionUtils.cs +++ b/src/PowerShellEditorServices/Utility/VersionUtils.cs @@ -49,6 +49,11 @@ internal static class VersionUtils /// public static bool IsPS7 { get; } = PSVersion.Major == 7; + /// + /// True if we are running in PowerShell 7, false otherwise. + /// + public static bool IsPS7OrGreater { get; } = PSVersion.Major >= 7; + /// /// True if we are running in on Windows, false otherwise. /// From 7f10333919b3c486e196a7ee7916d47745f48f99 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Mon, 2 Dec 2019 20:29:25 -0500 Subject: [PATCH 02/15] codacy --- .../Services/DebugAdapter/BreakpointService.cs | 10 +++++----- .../DebugAdapter/Debugging/BreakpointApiUtils.cs | 8 -------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs index 6c59e8b81..7b11579cd 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Logging; @@ -23,9 +24,6 @@ internal class BreakpointService private readonly ILogger _logger; private readonly PowerShellContextService _powerShellContextService; - private readonly ConcurrentDictionary> _breakpointsPerFile = - new ConcurrentDictionary>(); - private static int breakpointHitCounter; public BreakpointService( @@ -298,8 +296,10 @@ private ScriptBlock GetBreakpointActionScriptBlock( if (hitCount.HasValue) { + Interlocked.Increment(ref breakpointHitCounter); + string globalHitCountVarName = - $"$global:{s_psesGlobalVariableNamePrefix}BreakHitCounter_{breakpointHitCounter++}"; + $"$global:{s_psesGlobalVariableNamePrefix}BreakHitCounter_{breakpointHitCounter}"; wrappedCondition = $"if ({breakpoint.Condition}) {{ if (++{globalHitCountVarName} -eq {hitCount}) {{ break }} }}"; @@ -404,7 +404,7 @@ private string ExtractAndScrubParseExceptionMessage(ParseException parseExceptio return FormatInvalidBreakpointConditionMessage(condition, parseException.Message); } - private string FormatInvalidBreakpointConditionMessage(string condition, string message) + private static string FormatInvalidBreakpointConditionMessage(string condition, string message) { return $"'{condition}' is not a valid PowerShell expression. {message}"; } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs index a85609423..ffd9b787f 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs @@ -25,10 +25,6 @@ internal static class BreakpointApiUtils private static readonly Lazy> s_removeBreakpointLazy; - private static readonly Lazy> s_newLineBreakpointLazy; - - private static readonly Lazy> s_newCommandBreakpointLazy; - #endregion #region Static Constructor @@ -95,10 +91,6 @@ static BreakpointApiUtils() private static Func RemoveBreakpointDelegate => s_removeBreakpointLazy.Value; - private static Func CreateLineBreakpointDelegate => s_newLineBreakpointLazy.Value; - - private static Func CreateCommandBreakpointDelegate => s_newCommandBreakpointLazy.Value; - #endregion #region Public Static Methods From 4bfa92ba55023afe73fbe3d29b2eea4612763a18 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Mon, 2 Dec 2019 20:43:16 -0500 Subject: [PATCH 03/15] codacy2 --- .../Services/DebugAdapter/BreakpointService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs index 7b11579cd..a54dba056 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs @@ -331,7 +331,7 @@ private ScriptBlock GetBreakpointActionScriptBlock( } } - private bool ValidateBreakpointConditionAst(Ast conditionAst, out string message) + private static bool ValidateBreakpointConditionAst(Ast conditionAst, out string message) { message = string.Empty; @@ -363,7 +363,7 @@ private bool ValidateBreakpointConditionAst(Ast conditionAst, out string message return true; } - private string ExtractAndScrubParseExceptionMessage(ParseException parseException, string condition) + private static string ExtractAndScrubParseExceptionMessage(ParseException parseException, string condition) { string[] messageLines = parseException.Message.Split('\n'); From eaf2f52856314a4cecdfdcc46c27cbc171513da4 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 4 Dec 2019 08:30:52 -0500 Subject: [PATCH 04/15] address keith feedback --- .../DebugAdapter/BreakpointService.cs | 190 +---------------- .../Services/DebugAdapter/DebugService.cs | 4 +- .../Debugging/BreakpointApiUtils.cs | 199 +++++++++++++++++- 3 files changed, 203 insertions(+), 190 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs index a54dba056..2d6749144 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs @@ -20,12 +20,9 @@ namespace Microsoft.PowerShell.EditorServices.Services { internal class BreakpointService { - private const string s_psesGlobalVariableNamePrefix = "__psEditorServices_"; private readonly ILogger _logger; private readonly PowerShellContextService _powerShellContextService; - private static int breakpointHitCounter; - public BreakpointService( ILoggerFactory factory, PowerShellContextService powerShellContextService) @@ -79,7 +76,7 @@ public async Task SetBreakpointsAsync(string escapedScriptP !string.IsNullOrWhiteSpace(breakpoint.HitCondition)) { ScriptBlock actionScriptBlock = - GetBreakpointActionScriptBlock(breakpoint); + BreakpointApiUtils.GetBreakpointActionScriptBlock(breakpoint); // If there was a problem with the condition string, // move onto the next breakpoint. @@ -140,7 +137,7 @@ public async Task> SetCommandBreakpoints(I !string.IsNullOrWhiteSpace(breakpoint.HitCondition)) { ScriptBlock actionScriptBlock = - GetBreakpointActionScriptBlock(breakpoint); + BreakpointApiUtils.GetBreakpointActionScriptBlock(breakpoint); // If there was a problem with the condition string, // move onto the next breakpoint. @@ -200,7 +197,7 @@ public async Task RemoveAllBreakpointsAsync() } } - public async Task RemoveBreakpoints(IEnumerable breakpoints) + public async Task RemoveBreakpointsAsync(IEnumerable breakpoints) { if (VersionUtils.IsPS7OrGreater) { @@ -226,187 +223,6 @@ public async Task RemoveBreakpoints(IEnumerable breakpoints) } } - /// - /// Inspects the condition, putting in the appropriate scriptblock template - /// "if (expression) { break }". If errors are found in the condition, the - /// breakpoint passed in is updated to set Verified to false and an error - /// message is put into the breakpoint.Message property. - /// - /// - /// - private ScriptBlock GetBreakpointActionScriptBlock( - BreakpointDetailsBase breakpoint) - { - try - { - ScriptBlock actionScriptBlock; - int? hitCount = null; - - // If HitCondition specified, parse and verify it. - if (!(string.IsNullOrWhiteSpace(breakpoint.HitCondition))) - { - if (int.TryParse(breakpoint.HitCondition, out int parsedHitCount)) - { - hitCount = parsedHitCount; - } - else - { - breakpoint.Verified = false; - breakpoint.Message = $"The specified HitCount '{breakpoint.HitCondition}' is not valid. " + - "The HitCount must be an integer number."; - return null; - } - } - - // Create an Action scriptblock based on condition and/or hit count passed in. - if (hitCount.HasValue && string.IsNullOrWhiteSpace(breakpoint.Condition)) - { - // In the HitCount only case, this is simple as we can just use the HitCount - // property on the breakpoint object which is represented by $_. - string action = $"if ($_.HitCount -eq {hitCount}) {{ break }}"; - actionScriptBlock = ScriptBlock.Create(action); - } - else if (!string.IsNullOrWhiteSpace(breakpoint.Condition)) - { - // Must be either condition only OR condition and hit count. - actionScriptBlock = ScriptBlock.Create(breakpoint.Condition); - - // Check for simple, common errors that ScriptBlock parsing will not catch - // e.g. $i == 3 and $i > 3 - if (!ValidateBreakpointConditionAst(actionScriptBlock.Ast, out string message)) - { - breakpoint.Verified = false; - breakpoint.Message = message; - return null; - } - - // Check for "advanced" condition syntax i.e. if the user has specified - // a "break" or "continue" statement anywhere in their scriptblock, - // pass their scriptblock through to the Action parameter as-is. - Ast breakOrContinueStatementAst = - actionScriptBlock.Ast.Find( - ast => (ast is BreakStatementAst || ast is ContinueStatementAst), true); - - // If this isn't advanced syntax then the conditions string should be a simple - // expression that needs to be wrapped in a "if" test that conditionally executes - // a break statement. - if (breakOrContinueStatementAst == null) - { - string wrappedCondition; - - if (hitCount.HasValue) - { - Interlocked.Increment(ref breakpointHitCounter); - - string globalHitCountVarName = - $"$global:{s_psesGlobalVariableNamePrefix}BreakHitCounter_{breakpointHitCounter}"; - - wrappedCondition = - $"if ({breakpoint.Condition}) {{ if (++{globalHitCountVarName} -eq {hitCount}) {{ break }} }}"; - } - else - { - wrappedCondition = $"if ({breakpoint.Condition}) {{ break }}"; - } - - actionScriptBlock = ScriptBlock.Create(wrappedCondition); - } - } - else - { - // Shouldn't get here unless someone called this with no condition and no hit count. - actionScriptBlock = ScriptBlock.Create("break"); - _logger.LogWarning("No condition and no hit count specified by caller."); - } - - return actionScriptBlock; - } - catch (ParseException ex) - { - // Failed to create conditional breakpoint likely because the user provided an - // invalid PowerShell expression. Let the user know why. - breakpoint.Verified = false; - breakpoint.Message = ExtractAndScrubParseExceptionMessage(ex, breakpoint.Condition); - return null; - } - } - private static bool ValidateBreakpointConditionAst(Ast conditionAst, out string message) - { - message = string.Empty; - - // We are only inspecting a few simple scenarios in the EndBlock only. - if (conditionAst is ScriptBlockAst scriptBlockAst && - scriptBlockAst.BeginBlock == null && - scriptBlockAst.ProcessBlock == null && - scriptBlockAst.EndBlock != null && - scriptBlockAst.EndBlock.Statements.Count == 1) - { - StatementAst statementAst = scriptBlockAst.EndBlock.Statements[0]; - string condition = statementAst.Extent.Text; - - if (statementAst is AssignmentStatementAst) - { - message = FormatInvalidBreakpointConditionMessage(condition, "Use '-eq' instead of '=='."); - return false; - } - - if (statementAst is PipelineAst pipelineAst - && pipelineAst.PipelineElements.Count == 1 - && pipelineAst.PipelineElements[0].Redirections.Count > 0) - { - message = FormatInvalidBreakpointConditionMessage(condition, "Use '-gt' instead of '>'."); - return false; - } - } - - return true; - } - - private static string ExtractAndScrubParseExceptionMessage(ParseException parseException, string condition) - { - string[] messageLines = parseException.Message.Split('\n'); - - // Skip first line - it is a location indicator "At line:1 char: 4" - for (int i = 1; i < messageLines.Length; i++) - { - string line = messageLines[i]; - if (line.StartsWith("+")) - { - continue; - } - - if (!string.IsNullOrWhiteSpace(line)) - { - // Note '==' and '>" do not generate parse errors - if (line.Contains("'!='")) - { - line += " Use operator '-ne' instead of '!='."; - } - else if (line.Contains("'<'") && condition.Contains("<=")) - { - line += " Use operator '-le' instead of '<='."; - } - else if (line.Contains("'<'")) - { - line += " Use operator '-lt' instead of '<'."; - } - else if (condition.Contains(">=")) - { - line += " Use operator '-ge' instead of '>='."; - } - - return FormatInvalidBreakpointConditionMessage(condition, line); - } - } - - // If the message format isn't in a form we expect, just return the whole message. - return FormatInvalidBreakpointConditionMessage(condition, parseException.Message); - } - - private static string FormatInvalidBreakpointConditionMessage(string condition, string message) - { - return $"'{condition}' is not a valid PowerShell expression. {message}"; - } } } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index 93749d6b9..e80b321f5 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -216,7 +216,7 @@ public async Task SetCommandBreakpointsAsync( if (clearExisting) { // Flatten dictionary values into one list and remove them all. - await _breakpointService.RemoveBreakpoints(this.breakpointsPerFile.Values.SelectMany( i => i ).Where( i => i is CommandBreakpoint)).ConfigureAwait(false); + await _breakpointService.RemoveBreakpointsAsync(this.breakpointsPerFile.Values.SelectMany( i => i ).Where( i => i is CommandBreakpoint)).ConfigureAwait(false); } if (breakpoints.Length > 0) @@ -676,7 +676,7 @@ private async Task ClearBreakpointsInFileAsync(ScriptFile scriptFile) { if (breakpoints.Count > 0) { - await _breakpointService.RemoveBreakpoints(breakpoints).ConfigureAwait(false); + await _breakpointService.RemoveBreakpointsAsync(breakpoints).ConfigureAwait(false); // Clear the existing breakpoints list for the file breakpoints.Clear(); diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs index ffd9b787f..b37d0482d 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs @@ -8,7 +8,10 @@ using System.Linq; using System.Linq.Expressions; using System.Management.Automation; +using System.Management.Automation.Language; using System.Reflection; +using System.Threading; +using Microsoft.Extensions.Logging; namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter @@ -17,6 +20,8 @@ internal static class BreakpointApiUtils { #region Private Static Fields + private const string s_psesGlobalVariableNamePrefix = "__psEditorServices_"; + private static readonly Lazy> s_setLineBreakpointLazy; private static readonly Lazy> s_setCommandBreakpointLazy; @@ -25,6 +30,8 @@ internal static class BreakpointApiUtils private static readonly Lazy> s_removeBreakpointLazy; + private static int breakpointHitCounter; + #endregion #region Static Constructor @@ -101,11 +108,19 @@ public static IEnumerable SetBreakpoints(Debugger debugger, IEnumera foreach (BreakpointDetailsBase breakpoint in breakpoints) { + ScriptBlock actionScriptBlock = null; + // Check if this is a "conditional" line breakpoint. + if (!string.IsNullOrWhiteSpace(breakpoint.Condition) || + !string.IsNullOrWhiteSpace(breakpoint.HitCondition)) + { + actionScriptBlock = GetBreakpointActionScriptBlock(breakpoint); + } + Breakpoint psBreakpoint; switch (breakpoint) { case BreakpointDetails lineBreakpoint: - psBreakpoint = SetLineBreakpointDelegate(debugger, lineBreakpoint.Source, lineBreakpoint.LineNumber, lineBreakpoint.ColumnNumber ?? 0, null); + psBreakpoint = SetLineBreakpointDelegate(debugger, lineBreakpoint.Source, lineBreakpoint.LineNumber, lineBreakpoint.ColumnNumber ?? 0, actionScriptBlock); break; case CommandBreakpointDetails commandBreakpoint: @@ -132,6 +147,188 @@ public static bool RemoveBreakpoint(Debugger debugger, Breakpoint breakpoint) return RemoveBreakpointDelegate(debugger, breakpoint); } + /// + /// Inspects the condition, putting in the appropriate scriptblock template + /// "if (expression) { break }". If errors are found in the condition, the + /// breakpoint passed in is updated to set Verified to false and an error + /// message is put into the breakpoint.Message property. + /// + /// + /// ScriptBlock + public static ScriptBlock GetBreakpointActionScriptBlock( + BreakpointDetailsBase breakpoint) + { + try + { + ScriptBlock actionScriptBlock; + int? hitCount = null; + + // If HitCondition specified, parse and verify it. + if (!(string.IsNullOrWhiteSpace(breakpoint.HitCondition))) + { + if (int.TryParse(breakpoint.HitCondition, out int parsedHitCount)) + { + hitCount = parsedHitCount; + } + else + { + breakpoint.Verified = false; + breakpoint.Message = $"The specified HitCount '{breakpoint.HitCondition}' is not valid. " + + "The HitCount must be an integer number."; + return null; + } + } + + // Create an Action scriptblock based on condition and/or hit count passed in. + if (hitCount.HasValue && string.IsNullOrWhiteSpace(breakpoint.Condition)) + { + // In the HitCount only case, this is simple as we can just use the HitCount + // property on the breakpoint object which is represented by $_. + string action = $"if ($_.HitCount -eq {hitCount}) {{ break }}"; + actionScriptBlock = ScriptBlock.Create(action); + } + else if (!string.IsNullOrWhiteSpace(breakpoint.Condition)) + { + // Must be either condition only OR condition and hit count. + actionScriptBlock = ScriptBlock.Create(breakpoint.Condition); + + // Check for simple, common errors that ScriptBlock parsing will not catch + // e.g. $i == 3 and $i > 3 + if (!ValidateBreakpointConditionAst(actionScriptBlock.Ast, out string message)) + { + breakpoint.Verified = false; + breakpoint.Message = message; + return null; + } + + // Check for "advanced" condition syntax i.e. if the user has specified + // a "break" or "continue" statement anywhere in their scriptblock, + // pass their scriptblock through to the Action parameter as-is. + Ast breakOrContinueStatementAst = + actionScriptBlock.Ast.Find( + ast => (ast is BreakStatementAst || ast is ContinueStatementAst), true); + + // If this isn't advanced syntax then the conditions string should be a simple + // expression that needs to be wrapped in a "if" test that conditionally executes + // a break statement. + if (breakOrContinueStatementAst == null) + { + string wrappedCondition; + + if (hitCount.HasValue) + { + Interlocked.Increment(ref breakpointHitCounter); + + string globalHitCountVarName = + $"$global:{s_psesGlobalVariableNamePrefix}BreakHitCounter_{breakpointHitCounter}"; + + wrappedCondition = + $"if ({breakpoint.Condition}) {{ if (++{globalHitCountVarName} -eq {hitCount}) {{ break }} }}"; + } + else + { + wrappedCondition = $"if ({breakpoint.Condition}) {{ break }}"; + } + + actionScriptBlock = ScriptBlock.Create(wrappedCondition); + } + } + else + { + // Shouldn't get here unless someone called this with no condition and no hit count. + actionScriptBlock = ScriptBlock.Create("break"); + } + + return actionScriptBlock; + } + catch (ParseException ex) + { + // Failed to create conditional breakpoint likely because the user provided an + // invalid PowerShell expression. Let the user know why. + breakpoint.Verified = false; + breakpoint.Message = ExtractAndScrubParseExceptionMessage(ex, breakpoint.Condition); + return null; + } + } + + private static bool ValidateBreakpointConditionAst(Ast conditionAst, out string message) + { + message = string.Empty; + + // We are only inspecting a few simple scenarios in the EndBlock only. + if (conditionAst is ScriptBlockAst scriptBlockAst && + scriptBlockAst.BeginBlock == null && + scriptBlockAst.ProcessBlock == null && + scriptBlockAst.EndBlock != null && + scriptBlockAst.EndBlock.Statements.Count == 1) + { + StatementAst statementAst = scriptBlockAst.EndBlock.Statements[0]; + string condition = statementAst.Extent.Text; + + if (statementAst is AssignmentStatementAst) + { + message = FormatInvalidBreakpointConditionMessage(condition, "Use '-eq' instead of '=='."); + return false; + } + + if (statementAst is PipelineAst pipelineAst + && pipelineAst.PipelineElements.Count == 1 + && pipelineAst.PipelineElements[0].Redirections.Count > 0) + { + message = FormatInvalidBreakpointConditionMessage(condition, "Use '-gt' instead of '>'."); + return false; + } + } + + return true; + } + + private static string ExtractAndScrubParseExceptionMessage(ParseException parseException, string condition) + { + string[] messageLines = parseException.Message.Split('\n'); + + // Skip first line - it is a location indicator "At line:1 char: 4" + for (int i = 1; i < messageLines.Length; i++) + { + string line = messageLines[i]; + if (line.StartsWith("+")) + { + continue; + } + + if (!string.IsNullOrWhiteSpace(line)) + { + // Note '==' and '>" do not generate parse errors + if (line.Contains("'!='")) + { + line += " Use operator '-ne' instead of '!='."; + } + else if (line.Contains("'<'") && condition.Contains("<=")) + { + line += " Use operator '-le' instead of '<='."; + } + else if (line.Contains("'<'")) + { + line += " Use operator '-lt' instead of '<'."; + } + else if (condition.Contains(">=")) + { + line += " Use operator '-ge' instead of '>='."; + } + + return FormatInvalidBreakpointConditionMessage(condition, line); + } + } + + // If the message format isn't in a form we expect, just return the whole message. + return FormatInvalidBreakpointConditionMessage(condition, parseException.Message); + } + + private static string FormatInvalidBreakpointConditionMessage(string condition, string message) + { + return $"'{condition}' is not a valid PowerShell expression. {message}"; + } + #endregion } } From 131712a292f24f206a0d916fe37eb61653436e94 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Thu, 5 Dec 2019 09:36:20 -0800 Subject: [PATCH 05/15] misc other code --- .../DebugAdapter/BreakpointService.cs | 77 +++++++++---- .../Debugging/BreakpointApiUtils.cs | 104 +++++++++++++----- .../Debugging/BreakpointDetails.cs | 8 +- 3 files changed, 136 insertions(+), 53 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs index 2d6749144..646a9902a 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs @@ -31,14 +31,25 @@ public BreakpointService( _powerShellContextService = powerShellContextService; } - public async Task SetBreakpointsAsync(string escapedScriptPath, IEnumerable breakpoints) + public async Task> SetBreakpointsAsync(string escapedScriptPath, IEnumerable breakpoints) { if (VersionUtils.IsPS7OrGreater) { - return BreakpointApiUtils.SetBreakpoints( - _powerShellContextService.CurrentRunspace.Runspace.Debugger, - breakpoints) - .Select(BreakpointDetails.Create).ToArray(); + foreach (BreakpointDetails breakpointDetails in breakpoints) + { + try + { + BreakpointApiUtils.SetBreakpoint(_powerShellContextService.CurrentRunspace.Runspace.Debugger, breakpointDetails); + + } + catch(InvalidOperationException e) + { + breakpointDetails.Message = e.Message; + breakpointDetails.Verified = false; + } + } + + return breakpoints; } // Legacy behavior @@ -46,6 +57,27 @@ public async Task SetBreakpointsAsync(string escapedScriptP List configuredBreakpoints = new List(); foreach (BreakpointDetails breakpoint in breakpoints) { + ScriptBlock actionScriptBlock = null; + + // Check if this is a "conditional" line breakpoint. + if (!string.IsNullOrWhiteSpace(breakpoint.Condition) || + !string.IsNullOrWhiteSpace(breakpoint.HitCondition) || + !string.IsNullOrWhiteSpace(breakpoint.LogMessage)) + { + try + { + actionScriptBlock = BreakpointApiUtils.GetBreakpointActionScriptBlock( + breakpoint.Condition, + breakpoint.HitCondition, + breakpoint.LogMessage); + } + catch (InvalidOperationException e) + { + breakpoint.Verified = false; + breakpoint.Message = e.Message; + } + } + // On first iteration psCommand will be null, every subsequent // iteration will need to start a new statement. if (psCommand == null) @@ -71,21 +103,8 @@ public async Task SetBreakpointsAsync(string escapedScriptP psCommand.AddParameter("Column", breakpoint.ColumnNumber.Value); } - // Check if this is a "conditional" line breakpoint. - if (!string.IsNullOrWhiteSpace(breakpoint.Condition) || - !string.IsNullOrWhiteSpace(breakpoint.HitCondition)) + if (actionScriptBlock != null) { - ScriptBlock actionScriptBlock = - BreakpointApiUtils.GetBreakpointActionScriptBlock(breakpoint); - - // If there was a problem with the condition string, - // move onto the next breakpoint. - if (actionScriptBlock == null) - { - configuredBreakpoints.Add(breakpoint); - continue; - } - psCommand.AddParameter("Action", actionScriptBlock); } } @@ -99,17 +118,27 @@ public async Task SetBreakpointsAsync(string escapedScriptP setBreakpoints.Select(BreakpointDetails.Create)); } - return configuredBreakpoints.ToArray(); + return configuredBreakpoints; } public async Task> SetCommandBreakpoints(IEnumerable breakpoints) { if (VersionUtils.IsPS7OrGreater) { - return BreakpointApiUtils.SetBreakpoints( - _powerShellContextService.CurrentRunspace.Runspace.Debugger, - breakpoints) - .Select(CommandBreakpointDetails.Create); + foreach (CommandBreakpointDetails commandBreakpointDetails in breakpoints) + { + try + { + BreakpointApiUtils.SetBreakpoint(_powerShellContextService.CurrentRunspace.Runspace.Debugger, commandBreakpointDetails); + } + catch(InvalidOperationException e) + { + commandBreakpointDetails.Message = e.Message; + commandBreakpointDetails.Verified = false; + } + } + + return breakpoints; } // Legacy behavior diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs index b37d0482d..9ab106f0a 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs @@ -10,6 +10,7 @@ using System.Management.Automation; using System.Management.Automation.Language; using System.Reflection; +using System.Text; using System.Threading; using Microsoft.Extensions.Logging; @@ -102,39 +103,29 @@ static BreakpointApiUtils() #region Public Static Methods - public static IEnumerable SetBreakpoints(Debugger debugger, IEnumerable breakpoints) + public static Breakpoint SetBreakpoint(Debugger debugger, BreakpointDetailsBase breakpoint) { - var psBreakpoints = new List(breakpoints.Count()); - - foreach (BreakpointDetailsBase breakpoint in breakpoints) + ScriptBlock actionScriptBlock = null; + string logMessage = breakpoint is BreakpointDetails bd ? bd.LogMessage : null; + // Check if this is a "conditional" line breakpoint. + if (!string.IsNullOrWhiteSpace(breakpoint.Condition) || + !string.IsNullOrWhiteSpace(breakpoint.HitCondition) || + !string.IsNullOrWhiteSpace(logMessage)) { - ScriptBlock actionScriptBlock = null; - // Check if this is a "conditional" line breakpoint. - if (!string.IsNullOrWhiteSpace(breakpoint.Condition) || - !string.IsNullOrWhiteSpace(breakpoint.HitCondition)) - { - actionScriptBlock = GetBreakpointActionScriptBlock(breakpoint); - } - - Breakpoint psBreakpoint; - switch (breakpoint) - { - case BreakpointDetails lineBreakpoint: - psBreakpoint = SetLineBreakpointDelegate(debugger, lineBreakpoint.Source, lineBreakpoint.LineNumber, lineBreakpoint.ColumnNumber ?? 0, actionScriptBlock); - break; + actionScriptBlock = GetBreakpointActionScriptBlock(breakpoint.Condition, breakpoint.HitCondition, logMessage); + } - case CommandBreakpointDetails commandBreakpoint: - psBreakpoint = SetCommandBreakpointDelegate(debugger, commandBreakpoint.Name, null, null); - break; + switch (breakpoint) + { + case BreakpointDetails lineBreakpoint: + return SetLineBreakpointDelegate(debugger, lineBreakpoint.Source, lineBreakpoint.LineNumber, lineBreakpoint.ColumnNumber ?? 0, actionScriptBlock); - default: - throw new NotImplementedException("Other breakpoints not supported yet"); - } + case CommandBreakpointDetails commandBreakpoint: + return SetCommandBreakpointDelegate(debugger, commandBreakpoint.Name, null, null); - psBreakpoints.Add(psBreakpoint); + default: + throw new NotImplementedException("Other breakpoints not supported yet"); } - - return psBreakpoints; } public static List GetBreakpoints(Debugger debugger) @@ -147,6 +138,65 @@ public static bool RemoveBreakpoint(Debugger debugger, Breakpoint breakpoint) return RemoveBreakpointDelegate(debugger, breakpoint); } + public static ScriptBlock GetBreakpointActionScriptBlock(string condition, string hitCondition, string logMessage) + { + StringBuilder builder = new StringBuilder( + string.IsNullOrEmpty(logMessage) + ? "break" + : $"Microsoft.PowerShell.Utility\\Write-Host '{logMessage}'"); + + // If HitCondition specified, parse and verify it. + if (!(string.IsNullOrWhiteSpace(hitCondition))) + { + if (!int.TryParse(hitCondition, out int parsedHitCount)) + { + throw new InvalidOperationException("Hit Count was not a valid integer."); + } + + if(string.IsNullOrWhiteSpace(condition)) + { + // In the HitCount only case, this is simple as we can just use the HitCount + // property on the breakpoint object which is represented by $_. + builder.Insert(0, $"if ($_.HitCount -eq {parsedHitCount}) {{ ") + .Append(" }}"); + } + + Interlocked.Increment(ref breakpointHitCounter); + + string globalHitCountVarName = + $"$global:{s_psesGlobalVariableNamePrefix}BreakHitCounter_{breakpointHitCounter}"; + + builder.Insert(0, $"if (++{globalHitCountVarName} -eq {parsedHitCount}) {{ ") + .Append(" }}"); + } + + if (!string.IsNullOrWhiteSpace(condition)) + { + ScriptBlock parsed = ScriptBlock.Create(condition); + + // Check for simple, common errors that ScriptBlock parsing will not catch + // e.g. $i == 3 and $i > 3 + if (!ValidateBreakpointConditionAst(parsed.Ast, out string message)) + { + throw new InvalidOperationException(message); + } + + // Check for "advanced" condition syntax i.e. if the user has specified + // a "break" or "continue" statement anywhere in their scriptblock, + // pass their scriptblock through to the Action parameter as-is. + if (parsed.Ast.Find(ast => + (ast is BreakStatementAst || ast is ContinueStatementAst), true) != null) + { + return parsed; + } + + builder.Insert(0, $"if ({condition}) {{ ") + .Append(" }}"); + } + + return ScriptBlock.Create(builder.ToString()); + } + /// /// Inspects the condition, putting in the appropriate scriptblock template /// "if (expression) { break }". If errors are found in the condition, the diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointDetails.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointDetails.cs index 4e4ee6340..63b781d54 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointDetails.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointDetails.cs @@ -36,6 +36,8 @@ public class BreakpointDetails : BreakpointDetailsBase /// public int? ColumnNumber { get; private set; } + public string LogMessage { get; private set; } + private BreakpointDetails() { } @@ -55,7 +57,8 @@ public static BreakpointDetails Create( int line, int? column = null, string condition = null, - string hitCondition = null) + string hitCondition = null, + string logMessage = null) { Validate.IsNotNull("source", source); @@ -66,7 +69,8 @@ public static BreakpointDetails Create( LineNumber = line, ColumnNumber = column, Condition = condition, - HitCondition = hitCondition + HitCondition = hitCondition, + LogMessage = logMessage }; } From 5467ab60e8f2b5ebe6bd39b54b2925fb0afbbd09 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Thu, 12 Dec 2019 12:46:12 -0800 Subject: [PATCH 06/15] move to runspaceId --- .../DebugAdapter/BreakpointService.cs | 54 ++++++++++++--- .../Services/DebugAdapter/DebugService.cs | 33 ++++----- .../DebugAdapter/DebugStateService.cs | 2 + .../Debugging/BreakpointApiUtils.cs | 68 +++++++++++-------- .../Handlers/LaunchAndAttachHandler.cs | 16 ++++- 5 files changed, 116 insertions(+), 57 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs index 646a9902a..f3ba27e10 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs @@ -22,13 +22,30 @@ internal class BreakpointService { private readonly ILogger _logger; private readonly PowerShellContextService _powerShellContextService; + private readonly DebugStateService _debugStateService; + + // TODO: This needs to be managed per nested session + internal readonly Dictionary> BreakpointsPerFile = + new Dictionary>(); + + internal readonly HashSet CommandBreakpoints = + new HashSet(); public BreakpointService( ILoggerFactory factory, - PowerShellContextService powerShellContextService) + PowerShellContextService powerShellContextService, + DebugStateService debugStateService) { _logger = factory.CreateLogger(); _powerShellContextService = powerShellContextService; + _debugStateService = debugStateService; + } + + public List GetBreakpoints() + { + return BreakpointApiUtils.GetBreakpoints( + _powerShellContextService.CurrentRunspace.Runspace.Debugger, + _debugStateService.RunspaceId); } public async Task> SetBreakpointsAsync(string escapedScriptPath, IEnumerable breakpoints) @@ -39,7 +56,7 @@ public async Task> SetBreakpointsAsync(string esc { try { - BreakpointApiUtils.SetBreakpoint(_powerShellContextService.CurrentRunspace.Runspace.Debugger, breakpointDetails); + BreakpointApiUtils.SetBreakpoint(_powerShellContextService.CurrentRunspace.Runspace.Debugger, breakpointDetails, _debugStateService.RunspaceId); } catch(InvalidOperationException e) @@ -129,7 +146,7 @@ public async Task> SetCommandBreakpoints(I { try { - BreakpointApiUtils.SetBreakpoint(_powerShellContextService.CurrentRunspace.Runspace.Debugger, commandBreakpointDetails); + BreakpointApiUtils.SetBreakpoint(_powerShellContextService.CurrentRunspace.Runspace.Debugger, commandBreakpointDetails, _debugStateService.RunspaceId); } catch(InvalidOperationException e) { @@ -195,18 +212,23 @@ public async Task> SetCommandBreakpoints(I /// /// Clears all breakpoints in the current session. /// - public async Task RemoveAllBreakpointsAsync() + public async Task RemoveAllBreakpointsAsync(string scriptPath = null) { try { if (VersionUtils.IsPS7OrGreater) { foreach (Breakpoint breakpoint in BreakpointApiUtils.GetBreakpoints( - _powerShellContextService.CurrentRunspace.Runspace.Debugger)) - { - BreakpointApiUtils.RemoveBreakpoint( _powerShellContextService.CurrentRunspace.Runspace.Debugger, - breakpoint); + _debugStateService.RunspaceId)) + { + if (scriptPath == null || scriptPath == breakpoint.Script) + { + BreakpointApiUtils.RemoveBreakpoint( + _powerShellContextService.CurrentRunspace.Runspace.Debugger, + breakpoint, + _debugStateService.RunspaceId); + } } return; @@ -234,7 +256,21 @@ public async Task RemoveBreakpointsAsync(IEnumerable breakpoints) { BreakpointApiUtils.RemoveBreakpoint( _powerShellContextService.CurrentRunspace.Runspace.Debugger, - breakpoint); + breakpoint, + _debugStateService.RunspaceId); + + switch (breakpoint) + { + case CommandBreakpoint commandBreakpoint: + CommandBreakpoints.Remove(commandBreakpoint); + break; + case LineBreakpoint lineBreakpoint: + if (BreakpointsPerFile.TryGetValue(lineBreakpoint.Script, out HashSet bps)) + { + bps.Remove(lineBreakpoint); + } + break; + } } return; diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index e80b321f5..4435af2af 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -18,6 +18,7 @@ using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; +using System.Collections.Concurrent; namespace Microsoft.PowerShell.EditorServices.Services { @@ -38,11 +39,6 @@ internal class DebugService private readonly BreakpointService _breakpointService; private RemoteFileManagerService remoteFileManager; - // TODO: This needs to be managed per nested session - // TODO: Move to BreakpointService - private readonly Dictionary> breakpointsPerFile = - new Dictionary>(); - private int nextVariableId; private string temporaryScriptListingPath; private List variables; @@ -192,7 +188,7 @@ public async Task SetLineBreakpointsAsync( await this.ClearBreakpointsInFileAsync(scriptFile).ConfigureAwait(false); } - return await _breakpointService.SetBreakpointsAsync(escapedScriptPath, breakpoints).ConfigureAwait(false); + return (await _breakpointService.SetBreakpointsAsync(escapedScriptPath, breakpoints).ConfigureAwait(false)).ToArray(); } return await dscBreakpoints.SetLineBreakpointsAsync( @@ -216,7 +212,7 @@ public async Task SetCommandBreakpointsAsync( if (clearExisting) { // Flatten dictionary values into one list and remove them all. - await _breakpointService.RemoveBreakpointsAsync(this.breakpointsPerFile.Values.SelectMany( i => i ).Where( i => i is CommandBreakpoint)).ConfigureAwait(false); + await _breakpointService.RemoveBreakpointsAsync(_breakpointService.GetBreakpoints().Where( i => i is CommandBreakpoint)).ConfigureAwait(false); } if (breakpoints.Length > 0) @@ -672,16 +668,17 @@ public VariableScope[] GetVariableScopes(int stackFrameId) private async Task ClearBreakpointsInFileAsync(ScriptFile scriptFile) { // Get the list of breakpoints for this file - if (this.breakpointsPerFile.TryGetValue(scriptFile.Id, out List breakpoints)) - { - if (breakpoints.Count > 0) - { - await _breakpointService.RemoveBreakpointsAsync(breakpoints).ConfigureAwait(false); + // if (_breakpointService.BreakpointsPerFile.TryGetValue(scriptFile.Id, out HashSet breakpoints)) + // { + // if (breakpoints.Count > 0) + // { + await _breakpointService.RemoveBreakpointsAsync(_breakpointService.GetBreakpoints() + .Where(bp => bp is LineBreakpoint lbp && string.Equals(lbp.Script, scriptFile.FilePath))).ConfigureAwait(false); // Clear the existing breakpoints list for the file - breakpoints.Clear(); - } - } + // breakpoints.Clear(); + // } + // } } private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride) @@ -1035,10 +1032,10 @@ private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) string normalizedScriptName = scriptPath.ToLower(); // Get the list of breakpoints for this file - if (!this.breakpointsPerFile.TryGetValue(normalizedScriptName, out List breakpoints)) + if (!_breakpointService.BreakpointsPerFile.TryGetValue(normalizedScriptName, out HashSet breakpoints)) { - breakpoints = new List(); - this.breakpointsPerFile.Add( + breakpoints = new HashSet(); + _breakpointService.BreakpointsPerFile.Add( normalizedScriptName, breakpoints); } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs index 0dd5be42e..f60d945a6 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs @@ -19,6 +19,8 @@ internal class DebugStateService internal bool IsRemoteAttach { get; set; } + internal int? RunspaceId { get; set; } + internal bool IsAttachSession { get; set; } internal bool WaitingForAttach { get; set; } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs index 9ab106f0a..a93c4219c 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs @@ -23,13 +23,15 @@ internal static class BreakpointApiUtils private const string s_psesGlobalVariableNamePrefix = "__psEditorServices_"; - private static readonly Lazy> s_setLineBreakpointLazy; + private static readonly Lazy> s_setLineBreakpointLazy; - private static readonly Lazy> s_setCommandBreakpointLazy; + private static readonly Lazy> s_setCommandBreakpointLazy; - private static readonly Lazy>> s_getBreakpointsLazy; + private static readonly Lazy>> s_getBreakpointsLazy; - private static readonly Lazy> s_removeBreakpointLazy; + private static readonly Lazy, int?>> s_setBreakpointsLazy; + + private static readonly Lazy> s_removeBreakpointLazy; private static int breakpointHitCounter; @@ -46,42 +48,52 @@ static BreakpointApiUtils() return; } - s_setLineBreakpointLazy = new Lazy>(() => + s_setLineBreakpointLazy = new Lazy>(() => { MethodInfo setLineBreakpointMethod = typeof(Debugger).GetMethod("SetLineBreakpoint", BindingFlags.Public | BindingFlags.Instance); - return (Func)Delegate.CreateDelegate( - typeof(Func), + return (Func)Delegate.CreateDelegate( + typeof(Func), firstArgument: null, setLineBreakpointMethod); }); - s_setCommandBreakpointLazy = new Lazy>(() => + s_setCommandBreakpointLazy = new Lazy>(() => { MethodInfo setCommandBreakpointMethod = typeof(Debugger).GetMethod("SetCommandBreakpoint", BindingFlags.Public | BindingFlags.Instance); - return (Func)Delegate.CreateDelegate( - typeof(Func), + return (Func)Delegate.CreateDelegate( + typeof(Func), firstArgument: null, setCommandBreakpointMethod); }); - s_getBreakpointsLazy = new Lazy>>(() => + s_getBreakpointsLazy = new Lazy>>(() => { MethodInfo removeBreakpointMethod = typeof(Debugger).GetMethod("GetBreakpoints", BindingFlags.Public | BindingFlags.Instance); - return (Func>)Delegate.CreateDelegate( - typeof(Func>), + return (Func>)Delegate.CreateDelegate( + typeof(Func>), + firstArgument: null, + removeBreakpointMethod); + }); + + s_setBreakpointsLazy = new Lazy, int?>>(() => + { + MethodInfo removeBreakpointMethod = typeof(Debugger).GetMethod("SetBreakpoints", BindingFlags.Public | BindingFlags.Instance); + + return (Action, int?>)Action.CreateDelegate( + typeof(Action, int?>), firstArgument: null, removeBreakpointMethod); }); - s_removeBreakpointLazy = new Lazy>(() => + s_removeBreakpointLazy = new Lazy>(() => { MethodInfo removeBreakpointMethod = typeof(Debugger).GetMethod("RemoveBreakpoint", BindingFlags.Public | BindingFlags.Instance); - return (Func)Delegate.CreateDelegate( - typeof(Func), + return (Func)Delegate.CreateDelegate( + typeof(Func), firstArgument: null, removeBreakpointMethod); }); @@ -91,19 +103,21 @@ static BreakpointApiUtils() #region Public Static Properties - private static Func SetLineBreakpointDelegate => s_setLineBreakpointLazy.Value; + private static Func SetLineBreakpointDelegate => s_setLineBreakpointLazy.Value; + + private static Func SetCommandBreakpointDelegate => s_setCommandBreakpointLazy.Value; - private static Func SetCommandBreakpointDelegate => s_setCommandBreakpointLazy.Value; + private static Func> GetBreakpointsDelegate => s_getBreakpointsLazy.Value; - private static Func> GetBreakpointsDelegate => s_getBreakpointsLazy.Value; + private static Action, int?> SetBreakpointsDelegate => s_setBreakpointsLazy.Value; - private static Func RemoveBreakpointDelegate => s_removeBreakpointLazy.Value; + private static Func RemoveBreakpointDelegate => s_removeBreakpointLazy.Value; #endregion #region Public Static Methods - public static Breakpoint SetBreakpoint(Debugger debugger, BreakpointDetailsBase breakpoint) + public static Breakpoint SetBreakpoint(Debugger debugger, BreakpointDetailsBase breakpoint, int? runspaceId = null) { ScriptBlock actionScriptBlock = null; string logMessage = breakpoint is BreakpointDetails bd ? bd.LogMessage : null; @@ -118,24 +132,24 @@ public static Breakpoint SetBreakpoint(Debugger debugger, BreakpointDetailsBase switch (breakpoint) { case BreakpointDetails lineBreakpoint: - return SetLineBreakpointDelegate(debugger, lineBreakpoint.Source, lineBreakpoint.LineNumber, lineBreakpoint.ColumnNumber ?? 0, actionScriptBlock); + return SetLineBreakpointDelegate(debugger, lineBreakpoint.Source, lineBreakpoint.LineNumber, lineBreakpoint.ColumnNumber ?? 0, actionScriptBlock, runspaceId); case CommandBreakpointDetails commandBreakpoint: - return SetCommandBreakpointDelegate(debugger, commandBreakpoint.Name, null, null); + return SetCommandBreakpointDelegate(debugger, commandBreakpoint.Name, null, null, runspaceId); default: throw new NotImplementedException("Other breakpoints not supported yet"); } } - public static List GetBreakpoints(Debugger debugger) + public static List GetBreakpoints(Debugger debugger, int? runspaceId = null) { - return GetBreakpointsDelegate(debugger); + return GetBreakpointsDelegate(debugger, runspaceId); } - public static bool RemoveBreakpoint(Debugger debugger, Breakpoint breakpoint) + public static bool RemoveBreakpoint(Debugger debugger, Breakpoint breakpoint, int? runspaceId = null) { - return RemoveBreakpointDelegate(debugger, breakpoint); + return RemoveBreakpointDelegate(debugger, breakpoint, runspaceId); } public static ScriptBlock GetBreakpointActionScriptBlock(string condition, string hitCondition, string logMessage) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs index 014300091..d3d91d589 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs @@ -325,9 +325,6 @@ await _powerShellContextService.ExecuteScriptStringAsync( throw new RpcErrorException(0, "A positive integer must be specified for the processId field."); } - // Clear any existing breakpoints before proceeding - await _breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(continueOnCapturedContext: false); - // Execute the Debug-Runspace command but don't await it because it // will block the debug adapter initialization process. The // InitializedEvent will be sent as soon as the RunspaceChanged @@ -336,6 +333,12 @@ await _powerShellContextService.ExecuteScriptStringAsync( string debugRunspaceCmd; if (request.RunspaceName != null) { + var ids = await _powerShellContextService.ExecuteScriptStringAsync($"Get-Runspace -Name {request.RunspaceName} | % Id"); + foreach (var id in ids) + { + _debugStateService.RunspaceId = (int?) id; + break; + } debugRunspaceCmd = $"\nDebug-Runspace -Name '{request.RunspaceName}'"; } else if (request.RunspaceId != null) @@ -348,13 +351,20 @@ await _powerShellContextService.ExecuteScriptStringAsync( throw new RpcErrorException(0, "A positive integer must be specified for the RunspaceId field."); } + _debugStateService.RunspaceId = runspaceId; + debugRunspaceCmd = $"\nDebug-Runspace -Id {runspaceId}"; } else { + _debugStateService.RunspaceId = 1; + debugRunspaceCmd = "\nDebug-Runspace -Id 1"; } + // Clear any existing breakpoints before proceeding + await _breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(continueOnCapturedContext: false); + _debugStateService.WaitingForAttach = true; Task nonAwaitedTask = _powerShellContextService .ExecuteScriptStringAsync(debugRunspaceCmd) From a803fd83fd3ea68c4a5d2a9deed96ce1d9334c8a Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 29 Jan 2020 10:44:56 -0800 Subject: [PATCH 07/15] fallback get-psbreakpoint --- .../DebugAdapter/BreakpointService.cs | 26 +++++++++++++++---- .../Services/DebugAdapter/DebugService.cs | 7 ++--- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs index f3ba27e10..286468074 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; @@ -41,11 +42,20 @@ public BreakpointService( _debugStateService = debugStateService; } - public List GetBreakpoints() + public async Task> GetBreakpointsAsync() { - return BreakpointApiUtils.GetBreakpoints( - _powerShellContextService.CurrentRunspace.Runspace.Debugger, - _debugStateService.RunspaceId); + if (VersionUtils.IsPS7OrGreater) + { + return BreakpointApiUtils.GetBreakpoints( + _powerShellContextService.CurrentRunspace.Runspace.Debugger, + _debugStateService.RunspaceId); + } + + // Legacy behavior + PSCommand psCommand = new PSCommand(); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint"); + IEnumerable breakpoints = await _powerShellContextService.ExecuteCommandAsync(psCommand); + return breakpoints.ToList(); } public async Task> SetBreakpointsAsync(string escapedScriptPath, IEnumerable breakpoints) @@ -238,9 +248,15 @@ public async Task RemoveAllBreakpointsAsync(string scriptPath = null) PSCommand psCommand = new PSCommand(); psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint"); + + if (!string.IsNullOrEmpty(scriptPath)) + { + psCommand.AddParameter("Script", scriptPath); + } + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); - await _powerShellContextService.ExecuteCommandAsync(psCommand); + await _powerShellContextService.ExecuteCommandAsync(psCommand).ConfigureAwait(false); } catch (Exception e) { diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index 4435af2af..cb2496504 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -212,7 +212,7 @@ public async Task SetCommandBreakpointsAsync( if (clearExisting) { // Flatten dictionary values into one list and remove them all. - await _breakpointService.RemoveBreakpointsAsync(_breakpointService.GetBreakpoints().Where( i => i is CommandBreakpoint)).ConfigureAwait(false); + await _breakpointService.RemoveBreakpointsAsync((await _breakpointService.GetBreakpointsAsync()).Where( i => i is CommandBreakpoint)).ConfigureAwait(false); } if (breakpoints.Length > 0) @@ -672,8 +672,9 @@ private async Task ClearBreakpointsInFileAsync(ScriptFile scriptFile) // { // if (breakpoints.Count > 0) // { - await _breakpointService.RemoveBreakpointsAsync(_breakpointService.GetBreakpoints() - .Where(bp => bp is LineBreakpoint lbp && string.Equals(lbp.Script, scriptFile.FilePath))).ConfigureAwait(false); + await _breakpointService.RemoveAllBreakpointsAsync(scriptFile.FilePath).ConfigureAwait(false); + // await _breakpointService.RemoveBreakpointsAsync((await _breakpointService.GetBreakpointsAsync()) + // .Where(bp => bp is LineBreakpoint lbp && string.Equals(lbp.Script, scriptFile.FilePath))).ConfigureAwait(false); // Clear the existing breakpoints list for the file // breakpoints.Clear(); From 546534b61c0f5289498aff5885fb5d032a13460e Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Thu, 30 Jan 2020 11:55:13 -0800 Subject: [PATCH 08/15] remove notification that caused issues --- .../Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs index d3d91d589..c925f20d3 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs @@ -370,7 +370,6 @@ await _powerShellContextService.ExecuteScriptStringAsync( .ExecuteScriptStringAsync(debugRunspaceCmd) .ContinueWith(OnExecutionCompletedAsync); - _jsonRpcServer.SendNotification(EventNames.Initialized); return Unit.Value; } From ce6e88d907e84aa664c7b32288b831298a3ae8f5 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Thu, 30 Jan 2020 16:02:56 -0800 Subject: [PATCH 09/15] misc clean up and fixed action script blocks --- .../DebugAdapter/BreakpointService.cs | 15 ++--- .../Debugging/BreakpointApiUtils.cs | 16 +++-- .../Handlers/BreakpointHandlers.cs | 5 +- .../Handlers/ConfigurationDoneHandler.cs | 10 +++- .../Handlers/InitializeHandler.cs | 3 +- .../Handlers/LaunchAndAttachHandler.cs | 59 ++++++++++--------- 6 files changed, 60 insertions(+), 48 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs index 286468074..75d6b41c5 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs @@ -4,18 +4,13 @@ // using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using System.Management.Automation; -using System.Management.Automation.Language; -using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Logging; using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; -using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices.Services { @@ -44,7 +39,7 @@ public BreakpointService( public async Task> GetBreakpointsAsync() { - if (VersionUtils.IsPS7OrGreater) + if (BreakpointApiUtils.SupportsBreakpointApis) { return BreakpointApiUtils.GetBreakpoints( _powerShellContextService.CurrentRunspace.Runspace.Debugger, @@ -60,7 +55,7 @@ public async Task> GetBreakpointsAsync() public async Task> SetBreakpointsAsync(string escapedScriptPath, IEnumerable breakpoints) { - if (VersionUtils.IsPS7OrGreater) + if (BreakpointApiUtils.SupportsBreakpointApis) { foreach (BreakpointDetails breakpointDetails in breakpoints) { @@ -150,7 +145,7 @@ public async Task> SetBreakpointsAsync(string esc public async Task> SetCommandBreakpoints(IEnumerable breakpoints) { - if (VersionUtils.IsPS7OrGreater) + if (BreakpointApiUtils.SupportsBreakpointApis) { foreach (CommandBreakpointDetails commandBreakpointDetails in breakpoints) { @@ -226,7 +221,7 @@ public async Task RemoveAllBreakpointsAsync(string scriptPath = null) { try { - if (VersionUtils.IsPS7OrGreater) + if (BreakpointApiUtils.SupportsBreakpointApis) { foreach (Breakpoint breakpoint in BreakpointApiUtils.GetBreakpoints( _powerShellContextService.CurrentRunspace.Runspace.Debugger, @@ -266,7 +261,7 @@ public async Task RemoveAllBreakpointsAsync(string scriptPath = null) public async Task RemoveBreakpointsAsync(IEnumerable breakpoints) { - if (VersionUtils.IsPS7OrGreater) + if (BreakpointApiUtils.SupportsBreakpointApis) { foreach (Breakpoint breakpoint in breakpoints) { diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs index a93c4219c..6b052e869 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs @@ -5,14 +5,12 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; using System.Management.Automation; using System.Management.Automation.Language; using System.Reflection; using System.Text; using System.Threading; -using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter @@ -115,6 +113,14 @@ static BreakpointApiUtils() #endregion + #region Public Static Properties + + // TODO: Try to compute this more dynamically. If we're launching a script in the PSIC, there are APIs are available in PS 5.1 and up. + // For now, only PS7 or greater gets this feature. + public static bool SupportsBreakpointApis => VersionUtils.IsPS7OrGreater; + + #endregion + #region Public Static Methods public static Breakpoint SetBreakpoint(Debugger debugger, BreakpointDetailsBase breakpoint, int? runspaceId = null) @@ -172,7 +178,7 @@ public static ScriptBlock GetBreakpointActionScriptBlock(string condition, strin // In the HitCount only case, this is simple as we can just use the HitCount // property on the breakpoint object which is represented by $_. builder.Insert(0, $"if ($_.HitCount -eq {parsedHitCount}) {{ ") - .Append(" }}"); + .Append(" }"); } Interlocked.Increment(ref breakpointHitCounter); @@ -181,7 +187,7 @@ public static ScriptBlock GetBreakpointActionScriptBlock(string condition, strin $"$global:{s_psesGlobalVariableNamePrefix}BreakHitCounter_{breakpointHitCounter}"; builder.Insert(0, $"if (++{globalHitCountVarName} -eq {parsedHitCount}) {{ ") - .Append(" }}"); + .Append(" }"); } if (!string.IsNullOrWhiteSpace(condition)) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs index 056b159e4..16fd07155 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs @@ -164,7 +164,7 @@ public async Task Handle(SetBreakpointsArguments request string fileExtension = Path.GetExtension(scriptFile?.FilePath ?? "")?.ToLower(); bool isUntitledPath = ScriptFile.IsUntitledPath(request.Source.Path); if ((!isUntitledPath && fileExtension != ".ps1" && fileExtension != ".psm1") || - (!VersionUtils.IsPS7OrGreater && isUntitledPath)) + (!BreakpointApiUtils.SupportsBreakpointApis && isUntitledPath)) { _logger.LogWarning( $"Attempted to set breakpoints on a non-PowerShell file: {request.Source.Path}"); @@ -189,7 +189,8 @@ public async Task Handle(SetBreakpointsArguments request (int)srcBreakpoint.Line, (int?)srcBreakpoint.Column, srcBreakpoint.Condition, - srcBreakpoint.HitCondition)) + srcBreakpoint.HitCondition, + srcBreakpoint.LogMessage)) .ToArray(); // If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints. diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs index 51952c706..80355a109 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs @@ -9,9 +9,9 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using Microsoft.PowerShell.EditorServices.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.DebugAdapter.Protocol.Events; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; using OmniSharp.Extensions.JsonRpc; @@ -100,12 +100,16 @@ private async Task LaunchScriptAsync(string scriptToLaunch) { ScriptFile untitledScript = _workspaceService.GetFile(scriptToLaunch); - if (VersionUtils.IsPS7OrGreater) + if (BreakpointApiUtils.SupportsBreakpointApis) { + // Parse untitled files with their `Untitled:` URI as the file name which will cache the URI & contents within the PowerShell parser. + // By doing this, we light up the ability to debug Untitled files with breakpoints. + // This is only possible via the direct usage of the breakpoint APIs in PowerShell because + // Set-PSBreakpoint validates that paths are actually on the filesystem. ScriptBlockAst ast = Parser.ParseInput(untitledScript.Contents, untitledScript.DocumentUri, out Token[] tokens, out ParseError[] errors); // This seems to be the simplest way to invoke a script block (which contains breakpoint information) via the PowerShell API. - PSCommand cmd = new PSCommand().AddScript("& $args[0]").AddArgument(ast.GetScriptBlock()); + var cmd = new PSCommand().AddScript("& $args[0]").AddArgument(ast.GetScriptBlock()); await _powerShellContextService .ExecuteCommandAsync(cmd, sendOutputToHost: true, sendErrorToHost:true) .ConfigureAwait(false); diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/InitializeHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/InitializeHandler.cs index 9e9baffad..4fb305090 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/InitializeHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/InitializeHandler.cs @@ -35,10 +35,11 @@ public async Task Handle(InitializeRequestArguments request, // Now send the Initialize response to continue setup return new InitializeResponse { + SupportsConditionalBreakpoints = true, SupportsConfigurationDoneRequest = true, SupportsFunctionBreakpoints = true, - SupportsConditionalBreakpoints = true, SupportsHitConditionalBreakpoints = true, + SupportsLogPoints = true, SupportsSetVariable = true }; } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs index c925f20d3..b4be01335 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Logging; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using OmniSharp.Extensions.DebugAdapter.Protocol.Events; @@ -370,6 +371,10 @@ await _powerShellContextService.ExecuteScriptStringAsync( .ExecuteScriptStringAsync(debugRunspaceCmd) .ContinueWith(OnExecutionCompletedAsync); + if (runspaceVersion.Version.Major >= 7) + { + _jsonRpcServer.SendNotification(EventNames.Initialized); + } return Unit.Value; } @@ -387,33 +392,33 @@ private async Task OnExecutionCompletedAsync(Task executeTask) _logger.LogTrace("Execution completed, terminating..."); - //_debugStateService.ExecutionCompleted = true; - - //_debugEventHandlerService.UnregisterEventHandlers(); - - //if (_debugStateService.IsAttachSession) - //{ - // // Pop the sessions - // if (_powerShellContextService.CurrentRunspace.Context == RunspaceContext.EnteredProcess) - // { - // try - // { - // await _powerShellContextService.ExecuteScriptStringAsync("Exit-PSHostProcess"); - - // if (_debugStateService.IsRemoteAttach && - // _powerShellContextService.CurrentRunspace.Location == RunspaceLocation.Remote) - // { - // await _powerShellContextService.ExecuteScriptStringAsync("Exit-PSSession"); - // } - // } - // catch (Exception e) - // { - // _logger.LogException("Caught exception while popping attached process after debugging", e); - // } - // } - //} - - //_debugService.IsClientAttached = false; + _debugStateService.ExecutionCompleted = true; + + _debugEventHandlerService.UnregisterEventHandlers(); + + if (_debugStateService.IsAttachSession) + { + // Pop the sessions + if (_powerShellContextService.CurrentRunspace.Context == RunspaceContext.EnteredProcess) + { + try + { + await _powerShellContextService.ExecuteScriptStringAsync("Exit-PSHostProcess"); + + if (_debugStateService.IsRemoteAttach && + _powerShellContextService.CurrentRunspace.Location == RunspaceLocation.Remote) + { + await _powerShellContextService.ExecuteScriptStringAsync("Exit-PSSession"); + } + } + catch (Exception e) + { + _logger.LogException("Caught exception while popping attached process after debugging", e); + } + } + } + + _debugService.IsClientAttached = false; _jsonRpcServer.SendNotification(EventNames.Terminated); } } From 8ce4d5ce90166e2b052e819ec065c15a4c3a84fb Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Thu, 30 Jan 2020 16:19:20 -0800 Subject: [PATCH 10/15] codacy --- .../DebugAdapter/BreakpointService.cs | 2 ++ .../Services/DebugAdapter/DebugService.cs | 19 +------------------ .../Handlers/BreakpointHandlers.cs | 4 ++-- 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs index 75d6b41c5..48b7e63df 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs @@ -281,6 +281,8 @@ public async Task RemoveBreakpointsAsync(IEnumerable breakpoints) bps.Remove(lineBreakpoint); } break; + default: + throw new ArgumentException("Unsupported breakpoint type."); } } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index cb2496504..d0cb54e57 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -185,7 +185,7 @@ public async Task SetLineBreakpointsAsync( { if (clearExisting) { - await this.ClearBreakpointsInFileAsync(scriptFile).ConfigureAwait(false); + await _breakpointService.RemoveAllBreakpointsAsync(scriptFile.FilePath).ConfigureAwait(false); } return (await _breakpointService.SetBreakpointsAsync(escapedScriptPath, breakpoints).ConfigureAwait(false)).ToArray(); @@ -665,23 +665,6 @@ public VariableScope[] GetVariableScopes(int stackFrameId) #region Private Methods - private async Task ClearBreakpointsInFileAsync(ScriptFile scriptFile) - { - // Get the list of breakpoints for this file - // if (_breakpointService.BreakpointsPerFile.TryGetValue(scriptFile.Id, out HashSet breakpoints)) - // { - // if (breakpoints.Count > 0) - // { - await _breakpointService.RemoveAllBreakpointsAsync(scriptFile.FilePath).ConfigureAwait(false); - // await _breakpointService.RemoveBreakpointsAsync((await _breakpointService.GetBreakpointsAsync()) - // .Where(bp => bp is LineBreakpoint lbp && string.Equals(lbp.Script, scriptFile.FilePath))).ConfigureAwait(false); - - // Clear the existing breakpoints list for the file - // breakpoints.Clear(); - // } - // } - } - private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride) { await this.debugInfoHandle.WaitAsync().ConfigureAwait(false); diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs index 16fd07155..07641da1a 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs @@ -142,11 +142,12 @@ public SetBreakpointsHandler( public async Task Handle(SetBreakpointsArguments request, CancellationToken cancellationToken) { ScriptFile scriptFile = null; + bool isUntitledPath = ScriptFile.IsUntitledPath(request.Source.Path); // When you set a breakpoint in the right pane of a Git diff window on a PS1 file, // the Source.Path comes through as Untitled-X. That's why we check for IsUntitledPath. if (!_workspaceService.TryGetFile(request.Source.Path, out scriptFile) && - !ScriptFile.IsUntitledPath(request.Source.Path)) + !isUntitledPath) { string message = _debugStateService.NoDebug ? string.Empty : "Source file could not be accessed, breakpoint not set."; var srcBreakpoints = request.Breakpoints @@ -162,7 +163,6 @@ public async Task Handle(SetBreakpointsArguments request // Verify source file is a PowerShell script file. string fileExtension = Path.GetExtension(scriptFile?.FilePath ?? "")?.ToLower(); - bool isUntitledPath = ScriptFile.IsUntitledPath(request.Source.Path); if ((!isUntitledPath && fileExtension != ".ps1" && fileExtension != ".psm1") || (!BreakpointApiUtils.SupportsBreakpointApis && isUntitledPath)) { From 1b4a357129a01ccfdd2cf555446bf064820f6a8a Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Fri, 31 Jan 2020 10:39:14 -0800 Subject: [PATCH 11/15] get rid of extra GetBreakpointActionScriptBlock --- .../DebugAdapter/BreakpointService.cs | 29 +-- .../Debugging/BreakpointApiUtils.cs | 181 ++++++------------ 2 files changed, 74 insertions(+), 136 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs index 48b7e63df..3b22ca9de 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs @@ -86,17 +86,18 @@ public async Task> SetBreakpointsAsync(string esc !string.IsNullOrWhiteSpace(breakpoint.HitCondition) || !string.IsNullOrWhiteSpace(breakpoint.LogMessage)) { - try - { - actionScriptBlock = BreakpointApiUtils.GetBreakpointActionScriptBlock( - breakpoint.Condition, - breakpoint.HitCondition, - breakpoint.LogMessage); - } - catch (InvalidOperationException e) + actionScriptBlock = BreakpointApiUtils.GetBreakpointActionScriptBlock( + breakpoint.Condition, + breakpoint.HitCondition, + breakpoint.LogMessage, + out string errorMessage); + + if (!string.IsNullOrEmpty(errorMessage)) { breakpoint.Verified = false; - breakpoint.Message = e.Message; + breakpoint.Message = errorMessage; + configuredBreakpoints.Add(breakpoint); + continue; } } @@ -188,12 +189,18 @@ public async Task> SetCommandBreakpoints(I !string.IsNullOrWhiteSpace(breakpoint.HitCondition)) { ScriptBlock actionScriptBlock = - BreakpointApiUtils.GetBreakpointActionScriptBlock(breakpoint); + BreakpointApiUtils.GetBreakpointActionScriptBlock( + breakpoint.Condition, + breakpoint.HitCondition, + logMessage: null, + out string errorMessage); // If there was a problem with the condition string, // move onto the next breakpoint. - if (actionScriptBlock == null) + if (!string.IsNullOrEmpty(errorMessage)) { + breakpoint.Verified = false; + breakpoint.Message = errorMessage; configuredBreakpoints.Add(breakpoint); continue; } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs index 6b052e869..0a19c46d7 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs @@ -132,7 +132,17 @@ public static Breakpoint SetBreakpoint(Debugger debugger, BreakpointDetailsBase !string.IsNullOrWhiteSpace(breakpoint.HitCondition) || !string.IsNullOrWhiteSpace(logMessage)) { - actionScriptBlock = GetBreakpointActionScriptBlock(breakpoint.Condition, breakpoint.HitCondition, logMessage); + actionScriptBlock = GetBreakpointActionScriptBlock( + breakpoint.Condition, + breakpoint.HitCondition, + logMessage, + out string errorMessage); + + if (!string.IsNullOrEmpty(errorMessage)) + { + // This is handled by the caller where it will set the 'Message' and 'Verified' on the BreakpointDetails + throw new InvalidOperationException(errorMessage); + } } switch (breakpoint) @@ -158,165 +168,86 @@ public static bool RemoveBreakpoint(Debugger debugger, Breakpoint breakpoint, in return RemoveBreakpointDelegate(debugger, breakpoint, runspaceId); } - public static ScriptBlock GetBreakpointActionScriptBlock(string condition, string hitCondition, string logMessage) - { - StringBuilder builder = new StringBuilder( - string.IsNullOrEmpty(logMessage) - ? "break" - : $"Microsoft.PowerShell.Utility\\Write-Host '{logMessage}'"); - - // If HitCondition specified, parse and verify it. - if (!(string.IsNullOrWhiteSpace(hitCondition))) - { - if (!int.TryParse(hitCondition, out int parsedHitCount)) - { - throw new InvalidOperationException("Hit Count was not a valid integer."); - } - - if(string.IsNullOrWhiteSpace(condition)) - { - // In the HitCount only case, this is simple as we can just use the HitCount - // property on the breakpoint object which is represented by $_. - builder.Insert(0, $"if ($_.HitCount -eq {parsedHitCount}) {{ ") - .Append(" }"); - } - - Interlocked.Increment(ref breakpointHitCounter); - - string globalHitCountVarName = - $"$global:{s_psesGlobalVariableNamePrefix}BreakHitCounter_{breakpointHitCounter}"; - - builder.Insert(0, $"if (++{globalHitCountVarName} -eq {parsedHitCount}) {{ ") - .Append(" }"); - } - - if (!string.IsNullOrWhiteSpace(condition)) - { - ScriptBlock parsed = ScriptBlock.Create(condition); - - // Check for simple, common errors that ScriptBlock parsing will not catch - // e.g. $i == 3 and $i > 3 - if (!ValidateBreakpointConditionAst(parsed.Ast, out string message)) - { - throw new InvalidOperationException(message); - } - - // Check for "advanced" condition syntax i.e. if the user has specified - // a "break" or "continue" statement anywhere in their scriptblock, - // pass their scriptblock through to the Action parameter as-is. - if (parsed.Ast.Find(ast => - (ast is BreakStatementAst || ast is ContinueStatementAst), true) != null) - { - return parsed; - } - - builder.Insert(0, $"if ({condition}) {{ ") - .Append(" }}"); - } - - return ScriptBlock.Create(builder.ToString()); - } - /// /// Inspects the condition, putting in the appropriate scriptblock template /// "if (expression) { break }". If errors are found in the condition, the /// breakpoint passed in is updated to set Verified to false and an error /// message is put into the breakpoint.Message property. /// - /// + /// The expression that needs to be true for the breakpoint to be triggered. + /// The amount of times this line should be hit til the breakpoint is triggered. + /// The log message to write instead of calling 'break'. In VS Code, this is called a 'logPoint'. /// ScriptBlock - public static ScriptBlock GetBreakpointActionScriptBlock( - BreakpointDetailsBase breakpoint) + public static ScriptBlock GetBreakpointActionScriptBlock(string condition, string hitCondition, string logMessage, out string errorMessage) { + errorMessage = null; + try { - ScriptBlock actionScriptBlock; - int? hitCount = null; + StringBuilder builder = new StringBuilder( + string.IsNullOrEmpty(logMessage) + ? "break" + : $"Microsoft.PowerShell.Utility\\Write-Host '{logMessage}'"); // If HitCondition specified, parse and verify it. - if (!(string.IsNullOrWhiteSpace(breakpoint.HitCondition))) + if (!(string.IsNullOrWhiteSpace(hitCondition))) { - if (int.TryParse(breakpoint.HitCondition, out int parsedHitCount)) + if (!int.TryParse(hitCondition, out int parsedHitCount)) { - hitCount = parsedHitCount; + throw new InvalidOperationException("Hit Count was not a valid integer."); } - else + + if(string.IsNullOrWhiteSpace(condition)) { - breakpoint.Verified = false; - breakpoint.Message = $"The specified HitCount '{breakpoint.HitCondition}' is not valid. " + - "The HitCount must be an integer number."; - return null; + // In the HitCount only case, this is simple as we can just use the HitCount + // property on the breakpoint object which is represented by $_. + builder.Insert(0, $"if ($_.HitCount -eq {parsedHitCount}) {{ ") + .Append(" }"); } - } - // Create an Action scriptblock based on condition and/or hit count passed in. - if (hitCount.HasValue && string.IsNullOrWhiteSpace(breakpoint.Condition)) - { - // In the HitCount only case, this is simple as we can just use the HitCount - // property on the breakpoint object which is represented by $_. - string action = $"if ($_.HitCount -eq {hitCount}) {{ break }}"; - actionScriptBlock = ScriptBlock.Create(action); + int incrementResult = Interlocked.Increment(ref breakpointHitCounter); + + string globalHitCountVarName = + $"$global:{s_psesGlobalVariableNamePrefix}BreakHitCounter_{incrementResult}"; + + builder.Insert(0, $"if (++{globalHitCountVarName} -eq {parsedHitCount}) {{ ") + .Append(" }"); } - else if (!string.IsNullOrWhiteSpace(breakpoint.Condition)) + + if (!string.IsNullOrWhiteSpace(condition)) { - // Must be either condition only OR condition and hit count. - actionScriptBlock = ScriptBlock.Create(breakpoint.Condition); + ScriptBlock parsed = ScriptBlock.Create(condition); // Check for simple, common errors that ScriptBlock parsing will not catch // e.g. $i == 3 and $i > 3 - if (!ValidateBreakpointConditionAst(actionScriptBlock.Ast, out string message)) + if (!ValidateBreakpointConditionAst(parsed.Ast, out string message)) { - breakpoint.Verified = false; - breakpoint.Message = message; - return null; + throw new InvalidOperationException(message); } // Check for "advanced" condition syntax i.e. if the user has specified // a "break" or "continue" statement anywhere in their scriptblock, // pass their scriptblock through to the Action parameter as-is. - Ast breakOrContinueStatementAst = - actionScriptBlock.Ast.Find( - ast => (ast is BreakStatementAst || ast is ContinueStatementAst), true); - - // If this isn't advanced syntax then the conditions string should be a simple - // expression that needs to be wrapped in a "if" test that conditionally executes - // a break statement. - if (breakOrContinueStatementAst == null) + if (parsed.Ast.Find(ast => + (ast is BreakStatementAst || ast is ContinueStatementAst), true) != null) { - string wrappedCondition; - - if (hitCount.HasValue) - { - Interlocked.Increment(ref breakpointHitCounter); - - string globalHitCountVarName = - $"$global:{s_psesGlobalVariableNamePrefix}BreakHitCounter_{breakpointHitCounter}"; - - wrappedCondition = - $"if ({breakpoint.Condition}) {{ if (++{globalHitCountVarName} -eq {hitCount}) {{ break }} }}"; - } - else - { - wrappedCondition = $"if ({breakpoint.Condition}) {{ break }}"; - } - - actionScriptBlock = ScriptBlock.Create(wrappedCondition); + return parsed; } - } - else - { - // Shouldn't get here unless someone called this with no condition and no hit count. - actionScriptBlock = ScriptBlock.Create("break"); + + builder.Insert(0, $"if ({condition}) {{ ") + .Append(" }"); } - return actionScriptBlock; + return ScriptBlock.Create(builder.ToString()); + } + catch (ParseException e) + { + errorMessage = ExtractAndScrubParseExceptionMessage(e, condition); + return null; } - catch (ParseException ex) + catch (InvalidOperationException e) { - // Failed to create conditional breakpoint likely because the user provided an - // invalid PowerShell expression. Let the user know why. - breakpoint.Verified = false; - breakpoint.Message = ExtractAndScrubParseExceptionMessage(ex, breakpoint.Condition); + errorMessage = e.Message; return null; } } From 9f3cf01efc3725a335a065974cf0677a9e77944b Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Mon, 3 Feb 2020 12:23:36 -0800 Subject: [PATCH 12/15] new new breakpoint apis --- .../Debugging/BreakpointApiUtils.cs | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs index 0a19c46d7..353ebf561 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointApiUtils.cs @@ -27,8 +27,6 @@ internal static class BreakpointApiUtils private static readonly Lazy>> s_getBreakpointsLazy; - private static readonly Lazy, int?>> s_setBreakpointsLazy; - private static readonly Lazy> s_removeBreakpointLazy; private static int breakpointHitCounter; @@ -39,16 +37,17 @@ internal static class BreakpointApiUtils static BreakpointApiUtils() { - // If this version of PowerShell does not support the new Breakpoint APIs introduced in PowerShell 7.0.0-preview.4, + // If this version of PowerShell does not support the new Breakpoint APIs introduced in PowerShell 7.0.0, // do nothing as this class will not get used. - if (typeof(Debugger).GetMethod("SetLineBreakpoint", BindingFlags.Public | BindingFlags.Instance) == null) + if (!SupportsBreakpointApis) { return; } s_setLineBreakpointLazy = new Lazy>(() => { - MethodInfo setLineBreakpointMethod = typeof(Debugger).GetMethod("SetLineBreakpoint", BindingFlags.Public | BindingFlags.Instance); + Type[] setLineBreakpointParameters = new[] { typeof(string), typeof(int), typeof(int), typeof(ScriptBlock), typeof(int?) }; + MethodInfo setLineBreakpointMethod = typeof(Debugger).GetMethod("SetLineBreakpoint", setLineBreakpointParameters); return (Func)Delegate.CreateDelegate( typeof(Func), @@ -58,7 +57,8 @@ static BreakpointApiUtils() s_setCommandBreakpointLazy = new Lazy>(() => { - MethodInfo setCommandBreakpointMethod = typeof(Debugger).GetMethod("SetCommandBreakpoint", BindingFlags.Public | BindingFlags.Instance); + Type[] setCommandBreakpointParameters = new[] { typeof(string), typeof(ScriptBlock), typeof(string), typeof(int?) }; + MethodInfo setCommandBreakpointMethod = typeof(Debugger).GetMethod("SetCommandBreakpoint", setCommandBreakpointParameters); return (Func)Delegate.CreateDelegate( typeof(Func), @@ -68,27 +68,19 @@ static BreakpointApiUtils() s_getBreakpointsLazy = new Lazy>>(() => { - MethodInfo removeBreakpointMethod = typeof(Debugger).GetMethod("GetBreakpoints", BindingFlags.Public | BindingFlags.Instance); + Type[] getBreakpointsParameters = new[] { typeof(int?) }; + MethodInfo getBreakpointsMethod = typeof(Debugger).GetMethod("GetBreakpoints", getBreakpointsParameters); return (Func>)Delegate.CreateDelegate( typeof(Func>), firstArgument: null, - removeBreakpointMethod); - }); - - s_setBreakpointsLazy = new Lazy, int?>>(() => - { - MethodInfo removeBreakpointMethod = typeof(Debugger).GetMethod("SetBreakpoints", BindingFlags.Public | BindingFlags.Instance); - - return (Action, int?>)Action.CreateDelegate( - typeof(Action, int?>), - firstArgument: null, - removeBreakpointMethod); + getBreakpointsMethod); }); s_removeBreakpointLazy = new Lazy>(() => { - MethodInfo removeBreakpointMethod = typeof(Debugger).GetMethod("RemoveBreakpoint", BindingFlags.Public | BindingFlags.Instance); + Type[] removeBreakpointParameters = new[] { typeof(Breakpoint), typeof(int?) }; + MethodInfo removeBreakpointMethod = typeof(Debugger).GetMethod("RemoveBreakpoint", removeBreakpointParameters); return (Func)Delegate.CreateDelegate( typeof(Func), @@ -107,8 +99,6 @@ static BreakpointApiUtils() private static Func> GetBreakpointsDelegate => s_getBreakpointsLazy.Value; - private static Action, int?> SetBreakpointsDelegate => s_setBreakpointsLazy.Value; - private static Func RemoveBreakpointDelegate => s_removeBreakpointLazy.Value; #endregion From a29dd3c0f80a5f3fcc939953c77733c46e4d5ceb Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Mon, 3 Feb 2020 17:57:33 -0800 Subject: [PATCH 13/15] misc feedback --- .../Services/DebugAdapter/DebugService.cs | 2 -- .../Services/DebugAdapter/Handlers/BreakpointHandlers.cs | 9 ++------- .../DebugAdapter/Handlers/LaunchAndAttachHandler.cs | 9 ++++++++- .../PowerShellContext/PowerShellContextService.cs | 2 +- src/PowerShellEditorServices/Utility/VersionUtils.cs | 5 ----- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index d0cb54e57..b6460a99a 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -14,11 +14,9 @@ using Microsoft.PowerShell.EditorServices.Utility; using System.Threading; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Logging; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; -using System.Collections.Concurrent; namespace Microsoft.PowerShell.EditorServices.Services { diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs index 07641da1a..02c6e9080 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs @@ -141,13 +141,7 @@ public SetBreakpointsHandler( public async Task Handle(SetBreakpointsArguments request, CancellationToken cancellationToken) { - ScriptFile scriptFile = null; - bool isUntitledPath = ScriptFile.IsUntitledPath(request.Source.Path); - - // When you set a breakpoint in the right pane of a Git diff window on a PS1 file, - // the Source.Path comes through as Untitled-X. That's why we check for IsUntitledPath. - if (!_workspaceService.TryGetFile(request.Source.Path, out scriptFile) && - !isUntitledPath) + if (!_workspaceService.TryGetFile(request.Source.Path, out ScriptFile scriptFile)) { string message = _debugStateService.NoDebug ? string.Empty : "Source file could not be accessed, breakpoint not set."; var srcBreakpoints = request.Breakpoints @@ -163,6 +157,7 @@ public async Task Handle(SetBreakpointsArguments request // Verify source file is a PowerShell script file. string fileExtension = Path.GetExtension(scriptFile?.FilePath ?? "")?.ToLower(); + bool isUntitledPath = ScriptFile.IsUntitledPath(request.Source.Path); if ((!isUntitledPath && fileExtension != ".ps1" && fileExtension != ".psm1") || (!BreakpointApiUtils.SupportsBreakpointApis && isUntitledPath)) { diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs index b4be01335..8a72721cc 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs @@ -16,6 +16,7 @@ using OmniSharp.Extensions.DebugAdapter.Protocol.Events; using MediatR; using OmniSharp.Extensions.JsonRpc; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; namespace Microsoft.PowerShell.EditorServices.Handlers { @@ -183,7 +184,13 @@ public async Task Handle(PsesLaunchRequestArguments request, CancellationT _debugStateService.Arguments = arguments; _debugStateService.IsUsingTempIntegratedConsole = request.CreateTemporaryIntegratedConsole; - // TODO: Bring this back + if (request.CreateTemporaryIntegratedConsole + && !string.IsNullOrEmpty(request.Script) + && ScriptFile.IsUntitledPath(request.Script)) + { + throw new RpcErrorException(0, "Running an Untitled file in a temporary integrated console is currently not supported."); + } + // If the current session is remote, map the script path to the remote // machine if necessary if (_debugStateService.ScriptToLaunch != null && diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs index 599453546..3c6413325 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs @@ -46,7 +46,7 @@ public class PowerShellContextService : IDisposable, IHostSupportsInteractiveSes static PowerShellContextService() { // PowerShell ApartmentState APIs aren't available in PSStandard, so we need to use reflection - if (!VersionUtils.IsNetCore || VersionUtils.IsPS7) + if (!VersionUtils.IsNetCore || VersionUtils.IsPS7OrGreater) { MethodInfo setterInfo = typeof(Runspace).GetProperty("ApartmentState").GetSetMethod(); Delegate setter = Delegate.CreateDelegate(typeof(Action), firstArgument: null, method: setterInfo); diff --git a/src/PowerShellEditorServices/Utility/VersionUtils.cs b/src/PowerShellEditorServices/Utility/VersionUtils.cs index b7ec12b5a..3dddd7914 100644 --- a/src/PowerShellEditorServices/Utility/VersionUtils.cs +++ b/src/PowerShellEditorServices/Utility/VersionUtils.cs @@ -44,11 +44,6 @@ internal static class VersionUtils /// public static bool IsPS6 { get; } = PSVersion.Major == 6; - /// - /// True if we are running in PowerShell 7, false otherwise. - /// - public static bool IsPS7 { get; } = PSVersion.Major == 7; - /// /// True if we are running in PowerShell 7, false otherwise. /// From 20d68bcfba123e6acc4459e11e44eb9804ec819d Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Tue, 4 Feb 2020 09:42:48 -0800 Subject: [PATCH 14/15] make things internal for codacy --- .../Services/DebugAdapter/Debugging/BreakpointDetails.cs | 6 +++--- .../DebugAdapter/Debugging/CommandBreakpointDetails.cs | 6 +++--- src/PowerShellEditorServices/Utility/LspDebugUtils.cs | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointDetails.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointDetails.cs index 63b781d54..74bcd9ce4 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointDetails.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointDetails.cs @@ -13,7 +13,7 @@ namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter /// Provides details about a breakpoint that is set in the /// PowerShell debugger. /// - public class BreakpointDetails : BreakpointDetailsBase + internal class BreakpointDetails : BreakpointDetailsBase { /// /// Gets the unique ID of the breakpoint. @@ -52,7 +52,7 @@ private BreakpointDetails() /// /// /// - public static BreakpointDetails Create( + internal static BreakpointDetails Create( string source, int line, int? column = null, @@ -80,7 +80,7 @@ public static BreakpointDetails Create( /// /// The Breakpoint instance from which details will be taken. /// A new instance of the BreakpointDetails class. - public static BreakpointDetails Create(Breakpoint breakpoint) + internal static BreakpointDetails Create(Breakpoint breakpoint) { Validate.IsNotNull("breakpoint", breakpoint); diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/CommandBreakpointDetails.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/CommandBreakpointDetails.cs index 2b7c65def..c16928186 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/CommandBreakpointDetails.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/CommandBreakpointDetails.cs @@ -12,7 +12,7 @@ namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter /// /// Provides details about a command breakpoint that is set in the PowerShell debugger. /// - public class CommandBreakpointDetails : BreakpointDetailsBase + internal class CommandBreakpointDetails : BreakpointDetailsBase { /// /// Gets the name of the command on which the command breakpoint has been set. @@ -31,7 +31,7 @@ private CommandBreakpointDetails() /// Condition string that would be applied to the breakpoint Action parameter. /// Hit condition string that would be applied to the breakpoint Action parameter. /// - public static CommandBreakpointDetails Create( + internal static CommandBreakpointDetails Create( string name, string condition = null, string hitCondition = null) @@ -50,7 +50,7 @@ public static CommandBreakpointDetails Create( /// /// The Breakpoint instance from which details will be taken. /// A new instance of the BreakpointDetails class. - public static CommandBreakpointDetails Create(Breakpoint breakpoint) + internal static CommandBreakpointDetails Create(Breakpoint breakpoint) { Validate.IsNotNull("breakpoint", breakpoint); diff --git a/src/PowerShellEditorServices/Utility/LspDebugUtils.cs b/src/PowerShellEditorServices/Utility/LspDebugUtils.cs index 4423252ac..abaeb4546 100644 --- a/src/PowerShellEditorServices/Utility/LspDebugUtils.cs +++ b/src/PowerShellEditorServices/Utility/LspDebugUtils.cs @@ -4,9 +4,9 @@ namespace Microsoft.PowerShell.EditorServices.Utility { - public static class LspDebugUtils + internal static class LspDebugUtils { - public static Breakpoint CreateBreakpoint( + internal static Breakpoint CreateBreakpoint( BreakpointDetails breakpointDetails) { Validate.IsNotNull(nameof(breakpointDetails), breakpointDetails); @@ -22,7 +22,7 @@ public static Breakpoint CreateBreakpoint( }; } - public static Breakpoint CreateBreakpoint( + internal static Breakpoint CreateBreakpoint( CommandBreakpointDetails breakpointDetails) { Validate.IsNotNull(nameof(breakpointDetails), breakpointDetails); From 748a2911cb1208bf9580c65b601d75fb4cbbb113 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Fri, 7 Feb 2020 08:59:23 -0800 Subject: [PATCH 15/15] patrick comments --- .../Services/DebugAdapter/BreakpointService.cs | 1 - .../DebugAdapter/Handlers/ConfigurationDoneHandler.cs | 2 +- .../DebugAdapter/Handlers/LaunchAndAttachHandler.cs | 9 +++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs index 3b22ca9de..6ce6f6ef0 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs @@ -62,7 +62,6 @@ public async Task> SetBreakpointsAsync(string esc try { BreakpointApiUtils.SetBreakpoint(_powerShellContextService.CurrentRunspace.Runspace.Debugger, breakpointDetails, _debugStateService.RunspaceId); - } catch(InvalidOperationException e) { diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs index 80355a109..f566b2c53 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs @@ -109,7 +109,7 @@ private async Task LaunchScriptAsync(string scriptToLaunch) ScriptBlockAst ast = Parser.ParseInput(untitledScript.Contents, untitledScript.DocumentUri, out Token[] tokens, out ParseError[] errors); // This seems to be the simplest way to invoke a script block (which contains breakpoint information) via the PowerShell API. - var cmd = new PSCommand().AddScript("& $args[0]").AddArgument(ast.GetScriptBlock()); + var cmd = new PSCommand().AddScript(". $args[0]").AddArgument(ast.GetScriptBlock()); await _powerShellContextService .ExecuteCommandAsync(cmd, sendOutputToHost: true, sendErrorToHost:true) .ConfigureAwait(false); diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs index 8a72721cc..223aa995e 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Management.Automation; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -341,10 +342,14 @@ await _powerShellContextService.ExecuteScriptStringAsync( string debugRunspaceCmd; if (request.RunspaceName != null) { - var ids = await _powerShellContextService.ExecuteScriptStringAsync($"Get-Runspace -Name {request.RunspaceName} | % Id"); + IEnumerable ids = await _powerShellContextService.ExecuteCommandAsync(new PSCommand() + .AddCommand("Microsoft.PowerShell.Utility\\Get-Runspace") + .AddParameter("Name", request.RunspaceName) + .AddCommand("Microsoft.PowerShell.Utility\\Select-Object") + .AddParameter("ExpandProperty", "Id")); foreach (var id in ids) { - _debugStateService.RunspaceId = (int?) id; + _debugStateService.RunspaceId = id; break; } debugRunspaceCmd = $"\nDebug-Runspace -Name '{request.RunspaceName}'";