From 9669da3a124bb0ab73dd561590506e98e388087e Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 9 Nov 2015 17:09:11 -0800 Subject: [PATCH 1/2] Introduce OutputEvent and command eval in debugger This change introduces the new OutputEvent which allows the the language and debugging services to send script output text to the client to be displayed to the user. Currently this is used in VS Code's Debug Console but could be used in other contexts as well. This change also enables the user to evaluate PowerShell commands while stopped in the debugger to aid in debugging. In the future this will be expanded to allow commands to be executed at any time. --- .../MessageLoop.cs | 25 +++++------ .../Event/OutputEvent.cs | 27 ++++++++++++ ...ShellEditorServices.Transport.Stdio.csproj | 1 + .../Request/EvaluateRequest.cs | 2 +- .../Debugging/DebugService.cs | 42 ++++++++++++++----- .../Session/PowerShellSession.cs | 6 +-- .../ScenarioTests.cs | 13 +++--- 7 files changed, 81 insertions(+), 35 deletions(-) create mode 100644 src/PowerShellEditorServices.Transport.Stdio/Event/OutputEvent.cs diff --git a/src/PowerShellEditorServices.Host/MessageLoop.cs b/src/PowerShellEditorServices.Host/MessageLoop.cs index f08fc17a4..9600c1a9c 100644 --- a/src/PowerShellEditorServices.Host/MessageLoop.cs +++ b/src/PowerShellEditorServices.Host/MessageLoop.cs @@ -183,22 +183,17 @@ await this.messageWriter.WriteMessage( }, null); } - void PowerShellSession_OutputWritten(object sender, OutputWrittenEventArgs e) + async void PowerShellSession_OutputWritten(object sender, OutputWrittenEventArgs e) { - // TODO: change this to use the OutputEvent! - - //await this.messageWriter.WriteMessage( - // new ReplWriteOutputEvent - // { - // Body = new ReplWriteOutputEventBody - // { - // LineContents = e.OutputText, - // LineType = e.OutputType, - // IncludeNewLine = e.IncludeNewLine, - // ForegroundColor = e.ForegroundColor, - // BackgroundColor = e.BackgroundColor - // } - // }); + await this.messageWriter.WriteMessage( + new OutputEvent + { + Body = new OutputEventBody + { + Output = e.OutputText + (e.IncludeNewLine ? "\r\n" : string.Empty), + Category = (e.OutputType == OutputType.Error) ? "stderr" : "stdout" + } + }); } #endregion diff --git a/src/PowerShellEditorServices.Transport.Stdio/Event/OutputEvent.cs b/src/PowerShellEditorServices.Transport.Stdio/Event/OutputEvent.cs new file mode 100644 index 000000000..b5f43501e --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Event/OutputEvent.cs @@ -0,0 +1,27 @@ +// +// 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.Transport.Stdio.Message; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Event +{ + [MessageTypeName("output")] + public class OutputEvent : EventBase + { + } + + public class OutputEventBody + { + public string Category { get; set; } + + public string Output { get; set; } + } +} + diff --git a/src/PowerShellEditorServices.Transport.Stdio/PowerShellEditorServices.Transport.Stdio.csproj b/src/PowerShellEditorServices.Transport.Stdio/PowerShellEditorServices.Transport.Stdio.csproj index 5f8c81cd1..28fbd6eee 100644 --- a/src/PowerShellEditorServices.Transport.Stdio/PowerShellEditorServices.Transport.Stdio.csproj +++ b/src/PowerShellEditorServices.Transport.Stdio/PowerShellEditorServices.Transport.Stdio.csproj @@ -64,6 +64,7 @@ + Code diff --git a/src/PowerShellEditorServices.Transport.Stdio/Request/EvaluateRequest.cs b/src/PowerShellEditorServices.Transport.Stdio/Request/EvaluateRequest.cs index 4db1d4b22..5ebba6659 100644 --- a/src/PowerShellEditorServices.Transport.Stdio/Request/EvaluateRequest.cs +++ b/src/PowerShellEditorServices.Transport.Stdio/Request/EvaluateRequest.cs @@ -18,7 +18,7 @@ public override async Task ProcessMessage( MessageWriter messageWriter) { VariableDetails result = - editorSession.DebugService.EvaluateExpression( + await editorSession.DebugService.EvaluateExpression( this.Arguments.Expression, this.Arguments.FrameId); diff --git a/src/PowerShellEditorServices/Debugging/DebugService.cs b/src/PowerShellEditorServices/Debugging/DebugService.cs index 16747fc5e..8da15dc95 100644 --- a/src/PowerShellEditorServices/Debugging/DebugService.cs +++ b/src/PowerShellEditorServices/Debugging/DebugService.cs @@ -193,18 +193,17 @@ public VariableDetails[] GetVariables(int variableReferenceId) } /// - /// Evaluates an expression in the context of the stopped - /// debugger. For now, this just does simple evaluation of - /// a variable in the session. In the future it will execute - /// commands in the PowerShellSession. + /// Evaluates a variable expression in the context of the stopped + /// debugger. This method decomposes the variable expression to + /// walk the cached variable data for the specified stack frame. /// - /// The expression string to execute. - /// The ID of the stack frame in which the expression should be executed. + /// The variable expression string to evaluate. + /// The ID of the stack frame in which the expression should be evaluated. /// A VariableDetails object containing the result. - public VariableDetails EvaluateExpression(string expressionString, int stackFrameId) + public VariableDetails GetVariableFromExpression(string variableExpression, int stackFrameId) { // Break up the variable path - string[] variablePathParts = expressionString.Split('.'); + string[] variablePathParts = variableExpression.Split('.'); VariableDetails resolvedVariable = null; IEnumerable variableList = this.currentVariables; @@ -222,10 +221,10 @@ public VariableDetails EvaluateExpression(string expressionString, int stackFram v => string.Equals( v.Name, - expressionString, + variableExpression, StringComparison.InvariantCultureIgnoreCase)); - if (resolvedVariable != null && + if (resolvedVariable != null && resolvedVariable.IsExpandable) { // Continue by searching in this variable's children @@ -236,6 +235,29 @@ public VariableDetails EvaluateExpression(string expressionString, int stackFram return resolvedVariable; } + /// + /// Evaluates an expression in the context of the stopped + /// debugger. This method will execute the specified expression + /// PowerShellSession. + /// + /// The expression string to execute. + /// The ID of the stack frame in which the expression should be executed. + /// A VariableDetails object containing the result. + public async Task EvaluateExpression(string expressionString, int stackFrameId) + { + var results = + await this.powerShellSession.ExecuteScriptString( + expressionString); + + // Since this method should only be getting invoked in the debugger, + // we can assume that Out-String will be getting used to format results + // of command executions into string output. + + return new VariableDetails( + expressionString, + string.Join(Environment.NewLine, results)); + } + /// /// Gets the list of stack frames at the point where the /// debugger sf stopped. diff --git a/src/PowerShellEditorServices/Session/PowerShellSession.cs b/src/PowerShellEditorServices/Session/PowerShellSession.cs index 1f51da97b..81f768518 100644 --- a/src/PowerShellEditorServices/Session/PowerShellSession.cs +++ b/src/PowerShellEditorServices/Session/PowerShellSession.cs @@ -310,12 +310,12 @@ public Task ExecuteCommand(PSCommand psCommand) /// /// The script string to execute. /// A Task that can be awaited for the script completion. - public async Task ExecuteScriptString(string scriptString) + public async Task> ExecuteScriptString(string scriptString) { PSCommand psCommand = new PSCommand(); psCommand.AddScript(scriptString); - await this.ExecuteCommand(psCommand, true); + return await this.ExecuteCommand(psCommand, true); } /// @@ -328,7 +328,7 @@ public async Task ExecuteScriptAtPath(string scriptPath) PSCommand command = new PSCommand(); command.AddCommand(scriptPath); - await this.ExecuteCommand(command); + await this.ExecuteCommand(command, true); } /// diff --git a/test/PowerShellEditorServices.Test.Host/ScenarioTests.cs b/test/PowerShellEditorServices.Test.Host/ScenarioTests.cs index ae3681511..17bd3c3bb 100644 --- a/test/PowerShellEditorServices.Test.Host/ScenarioTests.cs +++ b/test/PowerShellEditorServices.Test.Host/ScenarioTests.cs @@ -371,20 +371,21 @@ await this.MessageWriter.WriteMessage( Assert.Equal(sigHelp.Body.ArgumentCount, 1); } - [Fact(Skip = "Console output events are disabled until we migrate to the updated debug protocol.")] + [Fact] public async Task ServiceExecutesReplCommandAndReceivesOutput() { await this.MessageWriter.WriteMessage( - new ReplExecuteRequest + new EvaluateRequest { - Arguments = new ReplExecuteArgs + Arguments = new EvaluateRequestArguments { - CommandString = "1 + 2" + Expression = "1 + 2" } }); - ReplWriteOutputEvent replWriteLineEvent = this.WaitForMessage(); - Assert.Equal("3", replWriteLineEvent.Body.LineContents); + OutputEvent outputEvent = this.WaitForMessage(); + Assert.Equal("3\r\n", outputEvent.Body.Output); + Assert.Equal("stdout", outputEvent.Body.Category); } [Fact(Skip = "Choice prompt functionality is currently in transition to a new model.")] From d392fce6bce92f419df4aa3f8eed220268c05298 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 9 Nov 2015 17:13:19 -0800 Subject: [PATCH 2/2] Fix #29: Variables should use proper '$' notation This change adds the dollar sign '$' character to top-level variables that are retrieved using the Get-Variables command via the DebugService. This character is used to refer to variables in PowerShell scripts so we need to add it to variable names that are returned from the debugging service for the sake of consistency. --- .../Debugging/VariableDetails.cs | 9 +++++++-- .../Debugging/DebugServiceTests.cs | 8 ++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/PowerShellEditorServices/Debugging/VariableDetails.cs b/src/PowerShellEditorServices/Debugging/VariableDetails.cs index 9f7c317aa..b8d5eb661 100644 --- a/src/PowerShellEditorServices/Debugging/VariableDetails.cs +++ b/src/PowerShellEditorServices/Debugging/VariableDetails.cs @@ -20,6 +20,11 @@ public class VariableDetails { #region Fields + /// + /// Provides a constant for the dollar sign variable prefix string. + /// + public const string DollarPrefix = "$"; + /// /// Provides a constant for the variable ID of the local variable scope. /// @@ -79,7 +84,7 @@ public class VariableDetails /// The PSVariable instance from which variable details will be obtained. /// public VariableDetails(PSVariable psVariable) - : this(psVariable.Name, psVariable.Value) + : this(DollarPrefix + psVariable.Name, psVariable.Value) { } @@ -105,8 +110,8 @@ public VariableDetails(string name, object value) { this.valueObject = value; - this.IsExpandable = GetIsExpandable(value); this.Name = name; + this.IsExpandable = GetIsExpandable(value); this.ValueString = this.IsExpandable == false ? GetValueString(value) : diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index a0ddaae79..0f2b40690 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -177,25 +177,25 @@ await this.debugService.SetBreakpoints( // TODO: Add checks for correct value strings as well - var strVar = variables.FirstOrDefault(v => v.Name == "strVar"); + var strVar = variables.FirstOrDefault(v => v.Name == "$strVar"); Assert.NotNull(strVar); Assert.False(strVar.IsExpandable); - var objVar = variables.FirstOrDefault(v => v.Name == "objVar"); + var objVar = variables.FirstOrDefault(v => v.Name == "$objVar"); Assert.NotNull(objVar); Assert.True(objVar.IsExpandable); var objChildren = debugService.GetVariables(objVar.Id); Assert.Equal(2, objChildren.Length); - var arrVar = variables.FirstOrDefault(v => v.Name == "arrVar"); + var arrVar = variables.FirstOrDefault(v => v.Name == "$arrVar"); Assert.NotNull(arrVar); Assert.True(arrVar.IsExpandable); var arrChildren = debugService.GetVariables(arrVar.Id); Assert.Equal(4, arrChildren.Length); - var classVar = variables.FirstOrDefault(v => v.Name == "classVar"); + var classVar = variables.FirstOrDefault(v => v.Name == "$classVar"); Assert.NotNull(classVar); Assert.True(classVar.IsExpandable);