From f8ed477c01aac6e9b63b096568486ffe9b4152e4 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Mon, 29 Feb 2016 21:02:03 -0700 Subject: [PATCH 1/5] Fixes #173 - implementation for function breakpoints This commit also introduces support for the configurationDone request. In fact, due to the order in which set*Breakpoints requests and the launch request come in, we now defer the execution of the script until we receive the configurationDone request. When we executed from the launch request, the debug host would appear to hang. It did not like executing Set/Remove-PSBreakpoint commands when the host debugger was running (not in a stopped state) AFAICT. This commit has a known issue with VSCode mixing up which breakpoint has a condition associated with it when you add/remove a breakpoint (see https://github.com/Microsoft/vscode/issues/3558). The next commit will fix this issue. --- .../Client/DebugAdapterClientBase.cs | 9 +- .../DebugAdapter/Breakpoint.cs | 11 +- .../DebugAdapter/ConfigurationDoneRequest.cs | 16 ++ .../SetFunctionBreakpointsRequest.cs | 31 +++ .../PowerShellEditorServices.Protocol.csproj | 2 + .../Server/DebugAdapter.cs | 63 ++++- .../Server/DebugAdapterBase.cs | 2 + .../Debugging/BreakpointDetails.cs | 21 +- .../Debugging/BreakpointDetailsBase.cs | 31 +++ .../Debugging/DebugService.cs | 250 +++++++++++++----- .../Debugging/FunctionBreakpointDetails.cs | 74 ++++++ .../PowerShellEditorServices.csproj | 2 + .../DebugAdapterTests.cs | 5 +- .../Debugging/DebugTest.ps1 | 4 +- .../Debugging/DebugServiceTests.cs | 76 +++++- 15 files changed, 494 insertions(+), 103 deletions(-) create mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/ConfigurationDoneRequest.cs create mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/SetFunctionBreakpointsRequest.cs create mode 100644 src/PowerShellEditorServices/Debugging/BreakpointDetailsBase.cs create mode 100644 src/PowerShellEditorServices/Debugging/FunctionBreakpointDetails.cs diff --git a/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs b/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs index bd4ed5b0b..cc6ff4dcb 100644 --- a/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs +++ b/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs @@ -17,14 +17,15 @@ public DebugAdapterClient(ChannelBase clientChannel) { } - public Task LaunchScript(string scriptFilePath) + public async Task LaunchScript(string scriptFilePath) { - return this.SendRequest( + await this.SendRequest( LaunchRequest.Type, - new LaunchRequestArguments - { + new LaunchRequestArguments { Program = scriptFilePath }); + + await this.SendRequest(ConfigurationDoneRequest.Type, null); } protected override async Task OnStart() diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/Breakpoint.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/Breakpoint.cs index fb51a727e..b34899a56 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/Breakpoint.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/Breakpoint.cs @@ -21,7 +21,7 @@ public class Breakpoint public string Source { get; set; } - public int Line { get; set; } + public int? Line { get; set; } public int? Column { get; set; } @@ -41,5 +41,14 @@ public static Breakpoint Create( Column = breakpointDetails.ColumnNumber }; } + + public static Breakpoint Create( + FunctionBreakpointDetails breakpointDetails) + { + return new Breakpoint { + Verified = breakpointDetails.Verified, + Message = breakpointDetails.Message + }; + } } } diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/ConfigurationDoneRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/ConfigurationDoneRequest.cs new file mode 100644 index 000000000..ef5e0c8ef --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/ConfigurationDoneRequest.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; + +namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter +{ + public class ConfigurationDoneRequest + { + public static readonly + RequestType Type = + RequestType.Create("configurationDone"); + } +} diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetFunctionBreakpointsRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/SetFunctionBreakpointsRequest.cs new file mode 100644 index 000000000..434c6dd59 --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/SetFunctionBreakpointsRequest.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; + +namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter +{ + public class SetFunctionBreakpointsRequest + { + public static readonly + RequestType Type = + RequestType.Create("setFunctionBreakpoints"); + } + + public class SetFunctionBreakpointsRequestArguments + { + public FunctionBreakpoint[] Breakpoints { get; set; } + } + + public class FunctionBreakpoint + { + /// + /// Gets or sets the name of the function to break on when it is invoked. + /// + public string Name { get; set; } + + public string Condition { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj b/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj index 7913b1ecd..20b57d50d 100644 --- a/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj +++ b/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj @@ -50,7 +50,9 @@ + + diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs index d5c67774c..42dab4da0 100644 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs @@ -20,6 +20,8 @@ public class DebugAdapter : DebugAdapterBase { private EditorSession editorSession; private OutputDebouncer outputDebouncer; + private string scriptPathToLaunch; + private string arguments; public DebugAdapter() : this(new StdioServerChannel()) { @@ -42,10 +44,12 @@ protected override void Initialize() this.SetRequestHandler(LaunchRequest.Type, this.HandleLaunchRequest); this.SetRequestHandler(AttachRequest.Type, this.HandleAttachRequest); + this.SetRequestHandler(ConfigurationDoneRequest.Type, this.HandleConfigurationDoneRequest); this.SetRequestHandler(DisconnectRequest.Type, this.HandleDisconnectRequest); this.SetRequestHandler(SetBreakpointsRequest.Type, this.HandleSetBreakpointsRequest); this.SetRequestHandler(SetExceptionBreakpointsRequest.Type, this.HandleSetExceptionBreakpointsRequest); + this.SetRequestHandler(SetFunctionBreakpointsRequest.Type, this.HandleSetFunctionBreakpointsRequest); this.SetRequestHandler(ContinueRequest.Type, this.HandleContinueRequest); this.SetRequestHandler(NextRequest.Type, this.HandleNextRequest); @@ -110,12 +114,35 @@ protected async Task HandleLaunchRequest( Logger.Write(LogLevel.Verbose, "Script arguments are: " + arguments); } + // NOTE: We don't actually launch the script in response to this + // request. We wait until we receive the configurationDone request + // to actually launch the script under the debugger. This gives + // us and VSCode a chance to finish configuring all the types of + // breakpoints. + this.scriptPathToLaunch = launchParams.Program; + this.arguments = arguments; + + await requestContext.SendResult(null); + } + + protected Task HandleAttachRequest( + AttachRequestArguments attachParams, + RequestContext requestContext) + { + // TODO: Implement this once we support attaching to processes + throw new NotImplementedException(); + } + + protected async Task HandleConfigurationDoneRequest( + object args, + RequestContext requestContext) + { // Execute the given PowerShell script and send the response. // Note that we aren't waiting for execution to complete here // because the debugger could stop while the script executes. Task executeTask = editorSession.PowerShellContext - .ExecuteScriptAtPath(launchParams.Program, arguments) + .ExecuteScriptAtPath(this.scriptPathToLaunch, this.arguments) .ContinueWith( async (t) => { Logger.Write(LogLevel.Verbose, "Execution completed, terminating..."); @@ -131,14 +158,6 @@ await requestContext.SendEvent( await requestContext.SendResult(null); } - protected Task HandleAttachRequest( - AttachRequestArguments attachParams, - RequestContext requestContext) - { - // TODO: Implement this once we support attaching to processes - throw new NotImplementedException(); - } - protected Task HandleDisconnectRequest( object disconnectParams, RequestContext requestContext) @@ -198,6 +217,32 @@ await requestContext.SendResult( }); } + protected async Task HandleSetFunctionBreakpointsRequest( + SetFunctionBreakpointsRequestArguments setBreakpointsParams, + RequestContext requestContext) + { + var breakpointDetails = new FunctionBreakpointDetails[setBreakpointsParams.Breakpoints.Length]; + for (int i = 0; i < breakpointDetails.Length; i++) + { + FunctionBreakpoint funcBreakpoint = setBreakpointsParams.Breakpoints[i]; + breakpointDetails[i] = FunctionBreakpointDetails.Create( + funcBreakpoint.Name, + funcBreakpoint.Condition); + } + + FunctionBreakpointDetails[] breakpoints = + await editorSession.DebugService.SetFunctionBreakpoints( + breakpointDetails); + + await requestContext.SendResult( + new SetBreakpointsResponseBody { + Breakpoints = + breakpoints + .Select(Protocol.DebugAdapter.Breakpoint.Create) + .ToArray() + }); + } + protected async Task HandleSetExceptionBreakpointsRequest( SetExceptionBreakpointsRequestArguments setExceptionBreakpointsParams, RequestContext requestContext) diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs index 4ea424073..07a07c8bb 100644 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs @@ -63,7 +63,9 @@ await requestContext.SendEvent( await requestContext.SendResult( new InitializeResponseBody { + SupportsConfigurationDoneRequest = true, SupportsConditionalBreakpoints = true, + SupportsFunctionBreakpoints = true }); } } diff --git a/src/PowerShellEditorServices/Debugging/BreakpointDetails.cs b/src/PowerShellEditorServices/Debugging/BreakpointDetails.cs index cf096ca56..e37d1bfc5 100644 --- a/src/PowerShellEditorServices/Debugging/BreakpointDetails.cs +++ b/src/PowerShellEditorServices/Debugging/BreakpointDetails.cs @@ -13,20 +13,8 @@ namespace Microsoft.PowerShell.EditorServices /// Provides details about a breakpoint that is set in the /// PowerShell debugger. /// - public class BreakpointDetails + public class BreakpointDetails : BreakpointDetailsBase { - /// - /// Gets or sets a boolean indicator that if true, breakpoint could be set - /// (but not necessarily at the desired location). - /// - public bool Verified { get; set; } - - /// - /// Gets or set an optional message about the state of the breakpoint. This is shown to the user - /// and can be used to explain why a breakpoint could not be verified. - /// - public string Message { get; set; } - /// /// Gets the source where the breakpoint is located. Used only for debug purposes. /// @@ -42,11 +30,6 @@ public class BreakpointDetails /// public int? ColumnNumber { get; private set; } - /// - /// Gets the breakpoint condition string. - /// - public string Condition { get; private set; } - private BreakpointDetails() { } @@ -91,7 +74,7 @@ public static BreakpointDetails Create(Breakpoint breakpoint) if (lineBreakpoint == null) { throw new ArgumentException( - "Expected breakpoint type:" + breakpoint.GetType().Name); + "Unexpected breakpoint type: " + breakpoint.GetType().Name); } var breakpointDetails = new BreakpointDetails diff --git a/src/PowerShellEditorServices/Debugging/BreakpointDetailsBase.cs b/src/PowerShellEditorServices/Debugging/BreakpointDetailsBase.cs new file mode 100644 index 000000000..3fa718782 --- /dev/null +++ b/src/PowerShellEditorServices/Debugging/BreakpointDetailsBase.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides details about a breakpoint that is set in the + /// PowerShell debugger. + /// + public abstract class BreakpointDetailsBase + { + /// + /// Gets or sets a boolean indicator that if true, breakpoint could be set + /// (but not necessarily at the desired location). + /// + public bool Verified { get; set; } + + /// + /// Gets or set an optional message about the state of the breakpoint. This is shown to the user + /// and can be used to explain why a breakpoint could not be verified. + /// + public string Message { get; set; } + + /// + /// Gets the breakpoint condition string. + /// + public string Condition { get; protected set; } + } +} diff --git a/src/PowerShellEditorServices/Debugging/DebugService.cs b/src/PowerShellEditorServices/Debugging/DebugService.cs index 8e846a3af..bdfe03660 100644 --- a/src/PowerShellEditorServices/Debugging/DebugService.cs +++ b/src/PowerShellEditorServices/Debugging/DebugService.cs @@ -122,62 +122,111 @@ public async Task SetLineBreakpoints( { // 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 message I could rely back to the client. + // 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 (breakpoint.Condition != null) { - try + ScriptBlock actionScriptBlock = + GetBreakpointActionScriptBlock(breakpoint); + + // If there was a problem with the condition string, + // move onto the next breakpoint. + if (actionScriptBlock == null) { - ScriptBlock actionScriptBlock = ScriptBlock.Create(breakpoint.Condition); - - // Check for simple, common errors that ScriptBlock parsing will not catch - // e.g. $i == 3 and $i > 3 - string message; - if (!ValidateBreakpointConditionAst(actionScriptBlock.Ast, out message)) - { - breakpoint.Verified = false; - breakpoint.Message = message; - resultBreakpointDetails.Add(breakpoint); - continue; - } - - // 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 ({breakpoint.Condition}) {{ break }}"; - actionScriptBlock = ScriptBlock.Create(wrappedCondition); - } - - psCommand.AddParameter("Action", actionScriptBlock); + resultBreakpointDetails.Add(breakpoint); + continue; } - catch (ParseException ex) + + psCommand.AddParameter("Action", actionScriptBlock); + } + + IEnumerable configuredBreakpoints = + await this.powerShellContext.ExecuteCommand(psCommand); + + resultBreakpointDetails.AddRange( + configuredBreakpoints.Select(BreakpointDetails.Create)); + } + } + + return resultBreakpointDetails.ToArray(); + } + + /// + /// Sets the list of line breakpoints for the current debugging session. + /// + /// The ScriptFile in which breakpoints will be set. + /// BreakpointDetails for each breakpoint that will be set. + /// If true, causes all existing breakpoints to be cleared before setting new ones. + /// An awaitable Task that will provide details about the breakpoints that were set. + public async Task SetFunctionBreakpoints( + FunctionBreakpointDetails[] breakpoints, + bool clearExisting = true) + { + var resultBreakpointDetails = new List(); + + if (clearExisting) + { + await this.ClearCommandBreakpoints(); + } + + if (breakpoints.Length > 0) + { + // Line function breakpoints with no condition are the most common, + // so let's optimize for that case by making a single call to Set-PSBreakpoint + // with all the command names to set a breakpoint on. + string[] commandOnlyBreakpoints = + breakpoints.Where(b => (b.Condition == null)) + .Select(b => b.Name) + .ToArray(); + + if (commandOnlyBreakpoints.Length > 0) + { + PSCommand psCommand = new PSCommand(); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint"); + psCommand.AddParameter("Command", commandOnlyBreakpoints); + + var configuredBreakpoints = + await this.powerShellContext.ExecuteCommand(psCommand); + + resultBreakpointDetails.AddRange( + configuredBreakpoints.Select(FunctionBreakpointDetails.Create)); + } + + // Process the rest of the breakpoints + var advancedCommandBreakpoints = + breakpoints.Where(b => (b.Condition != null)) + .ToArray(); + + foreach (FunctionBreakpointDetails breakpoint in advancedCommandBreakpoints) + { + PSCommand psCommand = new PSCommand(); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint"); + psCommand.AddParameter("Command", breakpoint.Name); + + // Check if this is a "conditional" line breakpoint. + if (breakpoint.Condition != null) + { + ScriptBlock actionScriptBlock = GetBreakpointActionScriptBlock(breakpoint); + + // If there was a problem with the condition string, + // move onto the next breakpoint. + if (actionScriptBlock == null) { - // 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); resultBreakpointDetails.Add(breakpoint); continue; } + + psCommand.AddParameter("Action", actionScriptBlock); } IEnumerable configuredBreakpoints = await this.powerShellContext.ExecuteCommand(psCommand); resultBreakpointDetails.AddRange( - configuredBreakpoints.Select(BreakpointDetails.Create)); + configuredBreakpoints.Select(FunctionBreakpointDetails.Create)); } } @@ -402,7 +451,7 @@ private async Task ClearBreakpointsInFile(ScriptFile scriptFile) if (breakpoints.Count > 0) { PSCommand psCommand = new PSCommand(); - psCommand.AddCommand("Remove-PSBreakpoint"); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); psCommand.AddParameter("Breakpoint", breakpoints.ToArray()); await this.powerShellContext.ExecuteCommand(psCommand); @@ -413,6 +462,16 @@ private async Task ClearBreakpointsInFile(ScriptFile scriptFile) } } + private async Task ClearCommandBreakpoints() + { + 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.ExecuteCommand(psCommand); + } + private async Task FetchStackFramesAndVariables() { this.nextVariableId = VariableDetailsBase.FirstVariableId; @@ -547,6 +606,59 @@ private async Task FetchStackFrames() } } + /// + /// 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 = ScriptBlock.Create(breakpoint.Condition); + + // Check for simple, common errors that ScriptBlock parsing will not catch + // e.g. $i == 3 and $i > 3 + string message; + if (!ValidateBreakpointConditionAst(actionScriptBlock.Ast, out 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 ({breakpoint.Condition}) {{ break }}"; + actionScriptBlock = ScriptBlock.Create(wrappedCondition); + } + + 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; @@ -654,38 +766,44 @@ private async void OnDebuggerStop(object sender, DebuggerStopEventArgs e) private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) { - List breakpoints = null; + // This event callback also gets called when a CommandBreakpoint is modified. + // Only execute the following code for LineBreakpoint so we can keep track + // of which line breakpoints exist per script file. We use this later when + // we need to clear all breakpoints in a script file. We do not need to do + // this for CommandBreakpoint, as those span all script files. + LineBreakpoint lineBreakpoint = e.Breakpoint as LineBreakpoint; + if (lineBreakpoint != null) + { + List breakpoints; - // Normalize the script filename for proper indexing - string normalizedScriptName = e.Breakpoint.Script.ToLower(); + // Normalize the script filename for proper indexing + string normalizedScriptName = lineBreakpoint.Script.ToLower(); - // Get the list of breakpoints for this file - if (!this.breakpointsPerFile.TryGetValue(normalizedScriptName, out breakpoints)) - { - breakpoints = new List(); - this.breakpointsPerFile.Add( - normalizedScriptName, - breakpoints); - } + // Get the list of breakpoints for this file + if (!this.breakpointsPerFile.TryGetValue(normalizedScriptName, out breakpoints)) + { + breakpoints = new List(); + this.breakpointsPerFile.Add( + normalizedScriptName, + breakpoints); + } - // Add or remove the breakpoint based on the update type - if (e.UpdateType == BreakpointUpdateType.Set) - { - breakpoints.Add(e.Breakpoint); - } - else if(e.UpdateType == BreakpointUpdateType.Removed) - { - breakpoints.Remove(e.Breakpoint); - } - else - { - // TODO: Do I need to switch out instances for updated breakpoints? + // Add or remove the breakpoint based on the update type + if (e.UpdateType == BreakpointUpdateType.Set) + { + breakpoints.Add(e.Breakpoint); + } + else if (e.UpdateType == BreakpointUpdateType.Removed) + { + breakpoints.Remove(e.Breakpoint); + } + else + { + // TODO: Do I need to switch out instances for updated breakpoints? + } } - if (this.BreakpointUpdated != null) - { - this.BreakpointUpdated(sender, e); - } + this.BreakpointUpdated?.Invoke(sender, e); } #endregion diff --git a/src/PowerShellEditorServices/Debugging/FunctionBreakpointDetails.cs b/src/PowerShellEditorServices/Debugging/FunctionBreakpointDetails.cs new file mode 100644 index 000000000..bf1fda5f0 --- /dev/null +++ b/src/PowerShellEditorServices/Debugging/FunctionBreakpointDetails.cs @@ -0,0 +1,74 @@ +// +// 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.Management.Automation; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides details about a function breakpoint that is set in the + /// PowerShell debugger. + /// + public class FunctionBreakpointDetails : BreakpointDetailsBase + { + /// + /// Gets the name of the function or command name for a function breakpoint. + /// + public string Name { get; private set; } + + private FunctionBreakpointDetails() + { + } + + /// + /// Creates an instance of the BreakpointDetails class from the individual + /// pieces of breakpoint information provided by the client. + /// + /// + /// + /// + /// + /// + public static FunctionBreakpointDetails Create( + string name, + string condition = null) + { + Validate.IsNotNull(nameof(name), name); + + return new FunctionBreakpointDetails { + Name = name, + Condition = condition + }; + } + + /// + /// Creates an instance of the BreakpointDetails class from a + /// PowerShell Breakpoint object. + /// + /// The Breakpoint instance from which details will be taken. + /// A new instance of the BreakpointDetails class. + public static FunctionBreakpointDetails Create(Breakpoint breakpoint) + { + Validate.IsNotNull("breakpoint", breakpoint); + + CommandBreakpoint commandBreakpoint = breakpoint as CommandBreakpoint; + if (commandBreakpoint == null) + { + throw new ArgumentException( + "Unexpected breakpoint type: " + breakpoint.GetType().Name); + } + + var breakpointDetails = new FunctionBreakpointDetails { + Verified = true, + Name = commandBreakpoint.Command, + Condition = commandBreakpoint.Action?.ToString() + }; + + return breakpointDetails; + } + } +} diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index 9396fc104..9608cb57f 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -60,7 +60,9 @@ + + diff --git a/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs b/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs index d2a283aff..e5a2a4530 100644 --- a/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs +++ b/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs @@ -105,9 +105,10 @@ public async Task DebugAdapterReceivesOutputEvents() terminatedEvent); } - private Task LaunchScript(string scriptPath) + private async Task LaunchScript(string scriptPath) { - return this.debugAdapterClient.LaunchScript(scriptPath); + await this.debugAdapterClient.LaunchScript(scriptPath); + await this.SendRequest(ConfigurationDoneRequest.Type, null); } } } diff --git a/test/PowerShellEditorServices.Test.Shared/Debugging/DebugTest.ps1 b/test/PowerShellEditorServices.Test.Shared/Debugging/DebugTest.ps1 index 69a139cb3..fd510721f 100644 --- a/test/PowerShellEditorServices.Test.Shared/Debugging/DebugTest.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Debugging/DebugTest.ps1 @@ -7,4 +7,6 @@ while ($i -le 500000) $i = $i + 1 } -Write-Host "Done!" \ No newline at end of file +Write-Host "Done!" +Get-Date +Get-Host \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index 44c18ec0c..9e8b8c0b8 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -148,7 +148,81 @@ await this.debugService.SetLineBreakpoints( } [Fact] - public async Task DebuggerSetsAndClearsBreakpoints() + public async Task DebuggerSetsAndClearsFunctionBreakpoints() + { + FunctionBreakpointDetails[] breakpoints = + await this.debugService.SetFunctionBreakpoints( + new[] { + FunctionBreakpointDetails.Create("Write-Host"), + FunctionBreakpointDetails.Create("Get-Date") + }); + + Assert.Equal(2, breakpoints.Length); + Assert.Equal("Write-Host", breakpoints[0].Name); + Assert.Equal("Get-Date", breakpoints[1].Name); + + breakpoints = + await this.debugService.SetFunctionBreakpoints( + new[] { FunctionBreakpointDetails.Create("Get-Host") }); + + Assert.Equal(1, breakpoints.Length); + Assert.Equal("Get-Host", breakpoints[0].Name); + + breakpoints = + await this.debugService.SetFunctionBreakpoints( + new FunctionBreakpointDetails[] {}); + + Assert.Equal(0, breakpoints.Length); + } + + [Fact] + public async Task DebuggerStopsOnFunctionBreakpoints() + { + FunctionBreakpointDetails[] breakpoints = + await this.debugService.SetFunctionBreakpoints( + new[] { + FunctionBreakpointDetails.Create("Write-Host") + }); + + await this.AssertStateChange(PowerShellContextState.Ready); + + Task executeTask = + this.powerShellContext.ExecuteScriptAtPath( + this.debugScriptFile.FilePath); + + // Wait for function breakpoint to hit + await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 6); + + StackFrameDetails[] stackFrames = debugService.GetStackFrames(); + VariableDetailsBase[] variables = + debugService.GetVariables(stackFrames[0].LocalVariables.Id); + + // Verify the function breakpoint broke at Write-Host and $i is 1 + var i = variables.FirstOrDefault(v => v.Name == "$i"); + Assert.NotNull(i); + Assert.False(i.IsExpandable); + Assert.Equal("1", i.ValueString); + + // The function breakpoint should fire the next time through the loop. + this.debugService.Continue(); + await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 6); + + stackFrames = debugService.GetStackFrames(); + variables = debugService.GetVariables(stackFrames[0].LocalVariables.Id); + + // Verify the function breakpoint broke at Write-Host and $i is 1 + i = variables.FirstOrDefault(v => v.Name == "$i"); + Assert.NotNull(i); + Assert.False(i.IsExpandable); + Assert.Equal("2", i.ValueString); + + // Abort script execution early and wait for completion + this.debugService.Abort(); + await executeTask; + } + + [Fact] + public async Task DebuggerSetsAndClearsLineBreakpoints() { BreakpointDetails[] breakpoints = await this.debugService.SetLineBreakpoints( From f61b67f91f35bcc9efbde112366b3ac3c178ed42 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Mon, 29 Feb 2016 21:10:32 -0700 Subject: [PATCH 2/5] This fixes the conditional breakpoint moving around issue by removing some breakpoint setting "optimization" code that unfortunately, messes up the order the breakpoint info returned to VSCode. --- .../Debugging/DebugService.cs | 57 +------------------ 1 file changed, 2 insertions(+), 55 deletions(-) diff --git a/src/PowerShellEditorServices/Debugging/DebugService.cs b/src/PowerShellEditorServices/Debugging/DebugService.cs index bdfe03660..344892e81 100644 --- a/src/PowerShellEditorServices/Debugging/DebugService.cs +++ b/src/PowerShellEditorServices/Debugging/DebugService.cs @@ -83,34 +83,7 @@ public async Task SetLineBreakpoints( string escapedScriptPath = PowerShellContext.EscapePath(scriptFile.FilePath, escapeSpaces: false); - // Line breakpoints with no condition and no column number are the most common, - // so let's optimize for that case by making a single call to Set-PSBreakpoint - // with all the lines to set a breakpoint on. - int[] lineOnlyBreakpoints = - breakpoints.Where(b => (b.ColumnNumber == null) && (b.Condition == null)) - .Select(b => b.LineNumber) - .ToArray(); - - if (lineOnlyBreakpoints.Length > 0) - { - PSCommand psCommand = new PSCommand(); - psCommand.AddCommand("Set-PSBreakpoint"); - psCommand.AddParameter("Script", escapedScriptPath); - psCommand.AddParameter("Line", lineOnlyBreakpoints); - - var configuredBreakpoints = - await this.powerShellContext.ExecuteCommand(psCommand); - - resultBreakpointDetails.AddRange( - configuredBreakpoints.Select(BreakpointDetails.Create)); - } - - // Process the rest of the breakpoints - var advancedLineBreakpoints = - breakpoints.Where(b => (b.ColumnNumber != null) || (b.Condition != null)) - .ToArray(); - - foreach (BreakpointDetails breakpoint in advancedLineBreakpoints) + foreach (BreakpointDetails breakpoint in breakpoints) { PSCommand psCommand = new PSCommand(); psCommand.AddCommand("Set-PSBreakpoint"); @@ -174,33 +147,7 @@ public async Task SetFunctionBreakpoints( if (breakpoints.Length > 0) { - // Line function breakpoints with no condition are the most common, - // so let's optimize for that case by making a single call to Set-PSBreakpoint - // with all the command names to set a breakpoint on. - string[] commandOnlyBreakpoints = - breakpoints.Where(b => (b.Condition == null)) - .Select(b => b.Name) - .ToArray(); - - if (commandOnlyBreakpoints.Length > 0) - { - PSCommand psCommand = new PSCommand(); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint"); - psCommand.AddParameter("Command", commandOnlyBreakpoints); - - var configuredBreakpoints = - await this.powerShellContext.ExecuteCommand(psCommand); - - resultBreakpointDetails.AddRange( - configuredBreakpoints.Select(FunctionBreakpointDetails.Create)); - } - - // Process the rest of the breakpoints - var advancedCommandBreakpoints = - breakpoints.Where(b => (b.Condition != null)) - .ToArray(); - - foreach (FunctionBreakpointDetails breakpoint in advancedCommandBreakpoints) + foreach (FunctionBreakpointDetails breakpoint in breakpoints) { PSCommand psCommand = new PSCommand(); psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint"); From 97e2053a948f4c61042ac03909b1f1b28f315161 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Mon, 29 Feb 2016 21:19:57 -0700 Subject: [PATCH 3/5] Had a few bad XML doc comments. Forgot to build Release before my checkin. Doh! --- src/PowerShellEditorServices/Debugging/DebugService.cs | 1 - .../Debugging/FunctionBreakpointDetails.cs | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/PowerShellEditorServices/Debugging/DebugService.cs b/src/PowerShellEditorServices/Debugging/DebugService.cs index 344892e81..91eb54120 100644 --- a/src/PowerShellEditorServices/Debugging/DebugService.cs +++ b/src/PowerShellEditorServices/Debugging/DebugService.cs @@ -130,7 +130,6 @@ public async Task SetLineBreakpoints( /// /// Sets the list of line breakpoints for the current debugging session. /// - /// The ScriptFile in which breakpoints will be set. /// BreakpointDetails for each breakpoint that will be set. /// If true, causes all existing breakpoints to be cleared before setting new ones. /// An awaitable Task that will provide details about the breakpoints that were set. diff --git a/src/PowerShellEditorServices/Debugging/FunctionBreakpointDetails.cs b/src/PowerShellEditorServices/Debugging/FunctionBreakpointDetails.cs index bf1fda5f0..e08545c72 100644 --- a/src/PowerShellEditorServices/Debugging/FunctionBreakpointDetails.cs +++ b/src/PowerShellEditorServices/Debugging/FunctionBreakpointDetails.cs @@ -28,10 +28,8 @@ private FunctionBreakpointDetails() /// Creates an instance of the BreakpointDetails class from the individual /// pieces of breakpoint information provided by the client. /// - /// - /// - /// - /// + /// The name of the function or command to break on. + /// Condition string that would be applied to the breakpoint Action parameter. /// public static FunctionBreakpointDetails Create( string name, From f6ccf074702acb09a8fa707541762dddc0ffac47 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Mon, 29 Feb 2016 21:32:10 -0700 Subject: [PATCH 4/5] Fixed a misleading comment. --- src/PowerShellEditorServices/Debugging/DebugService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Debugging/DebugService.cs b/src/PowerShellEditorServices/Debugging/DebugService.cs index 91eb54120..116488b1a 100644 --- a/src/PowerShellEditorServices/Debugging/DebugService.cs +++ b/src/PowerShellEditorServices/Debugging/DebugService.cs @@ -152,7 +152,7 @@ public async Task SetFunctionBreakpoints( psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint"); psCommand.AddParameter("Command", breakpoint.Name); - // Check if this is a "conditional" line breakpoint. + // Check if this is a "conditional" command breakpoint. if (breakpoint.Condition != null) { ScriptBlock actionScriptBlock = GetBreakpointActionScriptBlock(breakpoint); From 06f57561a6052d0fa30f57a1ed3cdbcf6f09d157 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Mon, 29 Feb 2016 21:54:10 -0700 Subject: [PATCH 5/5] More comment cleanup. Sigh... Also renamed SetFunctionBreakpoints to SetCommandBreakpoints. This is a "PowerShell" service and as such its API should be "PowerShell" oriented. --- .../Server/DebugAdapter.cs | 2 +- .../Debugging/DebugService.cs | 12 ++++++++---- .../Debugging/DebugServiceTests.cs | 8 ++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs index 42dab4da0..ac445ef83 100644 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs @@ -231,7 +231,7 @@ protected async Task HandleSetFunctionBreakpointsRequest( } FunctionBreakpointDetails[] breakpoints = - await editorSession.DebugService.SetFunctionBreakpoints( + await editorSession.DebugService.SetCommandBreakpoints( breakpointDetails); await requestContext.SendResult( diff --git a/src/PowerShellEditorServices/Debugging/DebugService.cs b/src/PowerShellEditorServices/Debugging/DebugService.cs index 116488b1a..bb63894ea 100644 --- a/src/PowerShellEditorServices/Debugging/DebugService.cs +++ b/src/PowerShellEditorServices/Debugging/DebugService.cs @@ -119,6 +119,8 @@ public async Task SetLineBreakpoints( IEnumerable configuredBreakpoints = await this.powerShellContext.ExecuteCommand(psCommand); + // 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)); } @@ -128,12 +130,12 @@ public async Task SetLineBreakpoints( } /// - /// Sets the list of line breakpoints for the current debugging session. + /// Sets the list of command breakpoints for the current debugging session. /// - /// BreakpointDetails for each breakpoint that will be set. - /// If true, causes all existing breakpoints to be cleared before setting new ones. + /// BreakpointDetails for each command breakpoint that will be set. + /// If true, causes all existing function breakpoints to be cleared before setting new ones. /// An awaitable Task that will provide details about the breakpoints that were set. - public async Task SetFunctionBreakpoints( + public async Task SetCommandBreakpoints( FunctionBreakpointDetails[] breakpoints, bool clearExisting = true) { @@ -171,6 +173,8 @@ public async Task SetFunctionBreakpoints( IEnumerable configuredBreakpoints = await this.powerShellContext.ExecuteCommand(psCommand); + // 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(FunctionBreakpointDetails.Create)); } diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index 9e8b8c0b8..14df8404f 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -151,7 +151,7 @@ await this.debugService.SetLineBreakpoints( public async Task DebuggerSetsAndClearsFunctionBreakpoints() { FunctionBreakpointDetails[] breakpoints = - await this.debugService.SetFunctionBreakpoints( + await this.debugService.SetCommandBreakpoints( new[] { FunctionBreakpointDetails.Create("Write-Host"), FunctionBreakpointDetails.Create("Get-Date") @@ -162,14 +162,14 @@ await this.debugService.SetFunctionBreakpoints( Assert.Equal("Get-Date", breakpoints[1].Name); breakpoints = - await this.debugService.SetFunctionBreakpoints( + await this.debugService.SetCommandBreakpoints( new[] { FunctionBreakpointDetails.Create("Get-Host") }); Assert.Equal(1, breakpoints.Length); Assert.Equal("Get-Host", breakpoints[0].Name); breakpoints = - await this.debugService.SetFunctionBreakpoints( + await this.debugService.SetCommandBreakpoints( new FunctionBreakpointDetails[] {}); Assert.Equal(0, breakpoints.Length); @@ -179,7 +179,7 @@ await this.debugService.SetFunctionBreakpoints( public async Task DebuggerStopsOnFunctionBreakpoints() { FunctionBreakpointDetails[] breakpoints = - await this.debugService.SetFunctionBreakpoints( + await this.debugService.SetCommandBreakpoints( new[] { FunctionBreakpointDetails.Create("Write-Host") });