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..ac445ef83 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.SetCommandBreakpoints( + 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..bb63894ea 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"); @@ -122,62 +95,88 @@ 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); + + // 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)); + } + } + + return resultBreakpointDetails.ToArray(); + } + + /// + /// Sets the list of command breakpoints for the current debugging session. + /// + /// 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 SetCommandBreakpoints( + FunctionBreakpointDetails[] breakpoints, + bool clearExisting = true) + { + var resultBreakpointDetails = new List(); + + if (clearExisting) + { + await this.ClearCommandBreakpoints(); + } + + if (breakpoints.Length > 0) + { + foreach (FunctionBreakpointDetails 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 (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); + // 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)); + configuredBreakpoints.Select(FunctionBreakpointDetails.Create)); } } @@ -402,7 +401,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 +412,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 +556,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 +716,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..e08545c72 --- /dev/null +++ b/src/PowerShellEditorServices/Debugging/FunctionBreakpointDetails.cs @@ -0,0 +1,72 @@ +// +// 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. + /// + /// 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, + 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..14df8404f 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.SetCommandBreakpoints( + 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.SetCommandBreakpoints( + new[] { FunctionBreakpointDetails.Create("Get-Host") }); + + Assert.Equal(1, breakpoints.Length); + Assert.Equal("Get-Host", breakpoints[0].Name); + + breakpoints = + await this.debugService.SetCommandBreakpoints( + new FunctionBreakpointDetails[] {}); + + Assert.Equal(0, breakpoints.Length); + } + + [Fact] + public async Task DebuggerStopsOnFunctionBreakpoints() + { + FunctionBreakpointDetails[] breakpoints = + await this.debugService.SetCommandBreakpoints( + 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(