From c3a6b70610eb79d451bff318a976dc85c1b54910 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 10:48:22 -0700 Subject: [PATCH 01/21] Fix incorrect namespace --- .../Services/PowerShell/Console/ColorConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/ColorConfiguration.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/ColorConfiguration.cs index e7ebee377..da885c41d 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/ColorConfiguration.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/ColorConfiguration.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace PowerShellEditorServices.Services.PowerShell.Console +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console { internal class ColorConfiguration { From cb6a35ee410f098fa56f2445e6a6801df823be09 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 10:53:05 -0700 Subject: [PATCH 02/21] Remove defunct comment --- src/PowerShellEditorServices/Server/PsesLanguageServer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index 5b756f545..2014414c4 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -113,7 +113,6 @@ public async Task StartAsync() // _Initialize_ request: // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#initialize .OnInitialize( - // TODO: Either fix or ignore "method lacks 'await'" warning. (languageServer, request, cancellationToken) => { Log.Logger.Debug("Initializing OmniSharp Language Server"); From 45c6ba58e72bc8b501ee8c68124b1282748300df Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 10:54:21 -0700 Subject: [PATCH 03/21] Add comment about extension service --- .../Server/PsesServiceCollectionExtensions.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs index 8bd94c0a3..f6df2e83c 100644 --- a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs +++ b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs @@ -47,6 +47,11 @@ public static IServiceCollection AddPsesLanguageServices( provider.GetService(), provider.GetService()); + // This is where we create the $psEditor variable + // so that when the console is ready, it will be available + // TODO: Improve the sequencing here so that: + // - The variable is guaranteed to be initialized when the console first appears + // - Any errors that occur are handled rather than lost by the unawaited task extensionService.InitializeAsync(); return extensionService; From a1b9bd247d9e1d98a6e79f63e1cf167a93361ac0 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 10:54:27 -0700 Subject: [PATCH 04/21] Remove defunct code --- .../Services/Symbols/Vistors/AstOperations.cs | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/src/PowerShellEditorServices/Services/Symbols/Vistors/AstOperations.cs b/src/PowerShellEditorServices/Services/Symbols/Vistors/AstOperations.cs index d9c4e1ddd..e70eb4272 100644 --- a/src/PowerShellEditorServices/Services/Symbols/Vistors/AstOperations.cs +++ b/src/PowerShellEditorServices/Services/Symbols/Vistors/AstOperations.cs @@ -84,44 +84,8 @@ public static async Task GetCompletionsAsync( cursorPosition.LineNumber, cursorPosition.ColumnNumber)); - /* - if (!powerShellContext.IsAvailable) - { - return null; - } - */ - var stopwatch = new Stopwatch(); - // If the current runspace is out of process we can use - // CommandCompletion.CompleteInput because PSReadLine won't be taking up the - // main runspace. - /* - if (powerShellContext.IsCurrentRunspaceOutOfProcess()) - { - using (RunspaceHandle runspaceHandle = await powerShellContext.GetRunspaceHandleAsync(cancellationToken).ConfigureAwait(false)) - using (System.Management.Automation.PowerShell powerShell = System.Management.Automation.PowerShell.Create()) - { - powerShell.Runspace = runspaceHandle.Runspace; - stopwatch.Start(); - try - { - return CommandCompletion.CompleteInput( - scriptAst, - currentTokens, - cursorPosition, - options: null, - powershell: powerShell); - } - finally - { - stopwatch.Stop(); - logger.LogTrace($"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); - } - } - } - */ - CommandCompletion commandCompletion = null; await executionService.ExecuteDelegateAsync((pwsh, cancellationToken) => { From bc5eb6502df73357c9a0dce40aac7ba8c40644cf Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 10:59:15 -0700 Subject: [PATCH 05/21] Use correct wildcard escape method --- .../Services/DebugAdapter/DebugService.cs | 3 +- .../Utility/PathUtils.cs | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index 4649c12c4..d626c6f7b 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -17,7 +17,6 @@ using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; namespace Microsoft.PowerShell.EditorServices.Services { @@ -182,7 +181,7 @@ public async Task SetLineBreakpointsAsync( // Fix for issue #123 - file paths that contain wildcard chars [ and ] need to // quoted and have those wildcard chars escaped. - string escapedScriptPath = PathUtils.WildcardEscape(scriptPath); + string escapedScriptPath = PathUtils.WildcardEscapePath(scriptPath); if (dscBreakpoints == null || !dscBreakpoints.IsDscResourcePath(escapedScriptPath)) { diff --git a/src/PowerShellEditorServices/Utility/PathUtils.cs b/src/PowerShellEditorServices/Utility/PathUtils.cs index f3f9d45ae..cdfed6df1 100644 --- a/src/PowerShellEditorServices/Utility/PathUtils.cs +++ b/src/PowerShellEditorServices/Utility/PathUtils.cs @@ -4,6 +4,7 @@ using System.IO; using System.Management.Automation; using System.Runtime.InteropServices; +using System.Text; namespace Microsoft.PowerShell.EditorServices.Utility { @@ -41,5 +42,44 @@ public static string WildcardEscape(string path) { return WildcardPattern.Escape(path); } + + /// + /// Return the given path with all PowerShell globbing characters escaped, + /// plus optionally the whitespace. + /// + /// The path to process. + /// Specify True to escape spaces in the path, otherwise False. + /// The path with [ and ] escaped. + internal static string WildcardEscapePath(string path, bool escapeSpaces = false) + { + var sb = new StringBuilder(); + for (int i = 0; i < path.Length; i++) + { + char curr = path[i]; + switch (curr) + { + // Escape '[', ']', '?' and '*' with '`' + case '[': + case ']': + case '*': + case '?': + case '`': + sb.Append('`').Append(curr); + break; + + default: + // Escape whitespace if required + if (escapeSpaces && char.IsWhiteSpace(curr)) + { + sb.Append('`').Append(curr); + break; + } + sb.Append(curr); + break; + } + } + + return sb.ToString(); + } } } From e0ba98d451a33a4854a8c85a36b4d67857600927 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 11:01:34 -0700 Subject: [PATCH 06/21] Remove unused value --- .../Services/PowerShell/Context/PowerShellVersionDetails.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellVersionDetails.cs b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellVersionDetails.cs index 2b4d2afc1..64d848b2c 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellVersionDetails.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellVersionDetails.cs @@ -103,8 +103,6 @@ public static PowerShellVersionDetails GetVersionDetails(ILogger logger, PowerSh try { - var psVersionTableCommand = new PSCommand().AddScript("$PSVersionTable", useLocalScope: true); - Hashtable psVersionTable = pwsh .AddScript("$PSVersionTable", useLocalScope: true) .InvokeAndClear() From b3501ac7f59ac9d9ef69661c8e1d1a25566a2496 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 11:46:31 -0700 Subject: [PATCH 07/21] Add comment about debugger implementation --- .../Debugging/PowerShellDebugContext.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs b/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs index 655cc715b..d5ed45e89 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs @@ -10,6 +10,29 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging using OmniSharp.Extensions.LanguageServer.Protocol.Server; using System.Threading.Tasks; + /// + /// Handles the state of the PowerShell debugger. + /// + /// + /// + /// Debugging through a PowerShell Host is implemented by registering a handler + /// for the event. + /// Registering that handler causes debug actions in PowerShell like Set-PSBreakpoint + /// and Wait-Debugger to drop into the debugger and trigger the handler. + /// The handler is passed a mutable object + /// and the debugger stop lasts for the duration of the handler call. + /// The handler sets the property + /// when after it returns, the PowerShell debugger uses that as the direction on how to proceed. + /// + /// + /// When we handle the event, + /// we drop into a nested debug prompt and execute things in the debugger with , + /// which enables debugger commands like l, c, s, etc. + /// saves the event args object in its state, + /// and when one of the debugger commands is used, the result returned is used to set + /// on the saved event args object so that when the event handler returns, the PowerShell debugger takes the correct action. + /// + /// internal class PowerShellDebugContext : IPowerShellDebugContext { private readonly ILogger _logger; From 3c102c35c7ee0d0ccf47990c545064c562126b07 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 11:57:09 -0700 Subject: [PATCH 08/21] Add comment about Debugger.ProcessCommand() --- .../PowerShell/Execution/SynchronousPowerShellTask.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs index c45cfe421..f03a7ea4a 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs @@ -107,6 +107,9 @@ private IReadOnlyList ExecuteInDebugger(CancellationToken cancellationT var outputCollection = new PSDataCollection(); + // Out-Default doesn't work as needed in the debugger + // Instead we add Out-String to the command and collect results in a PSDataCollection + // and use the event handler to print output to the UI as its added to that collection if (_executionOptions.WriteOutputToHost) { _psCommand.AddDebugOutputCommand(); @@ -124,6 +127,10 @@ private IReadOnlyList ExecuteInDebugger(CancellationToken cancellationT DebuggerCommandResults debuggerResult = null; try { + // In the PowerShell debugger, extra debugger commands are made available, like "l", "s", "c", etc. + // Executing those commands produces a result that needs to be set on the debugger stop event args. + // So we use the Debugger.ProcessCommand() API to properly execute commands in the debugger + // and then call DebugContext.ProcessDebuggerResult() later to handle the command appropriately debuggerResult = _pwsh.Runspace.Debugger.ProcessCommand(_psCommand, outputCollection); cancellationToken.ThrowIfCancellationRequested(); } From dac3317672d4069bd166f14e24760bcd3619055a Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 12:12:04 -0700 Subject: [PATCH 09/21] Add comments around exception handling --- .../PowerShell/Execution/SynchronousPowerShellTask.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs index f03a7ea4a..ac00df574 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs @@ -71,10 +71,13 @@ private IReadOnlyList ExecuteNormally(CancellationToken cancellationTok result = _pwsh.InvokeCommand(_psCommand); cancellationToken.ThrowIfCancellationRequested(); } + // Test if we've been cancelled. If we're remoting, PSRemotingDataStructureException effectively means the pipeline was stopped. catch (Exception e) when (cancellationToken.IsCancellationRequested || e is PipelineStoppedException || e is PSRemotingDataStructureException) { throw new OperationCanceledException(); } + // We only catch RuntimeExceptions here in case writing errors to output was requested + // Other errors are bubbled up to the caller catch (RuntimeException e) { Logger.LogWarning($"Runtime exception occurred while executing command:{Environment.NewLine}{Environment.NewLine}{e}"); @@ -134,11 +137,14 @@ private IReadOnlyList ExecuteInDebugger(CancellationToken cancellationT debuggerResult = _pwsh.Runspace.Debugger.ProcessCommand(_psCommand, outputCollection); cancellationToken.ThrowIfCancellationRequested(); } + // Test if we've been cancelled. If we're remoting, PSRemotingDataStructureException effectively means the pipeline was stopped. catch (Exception e) when (cancellationToken.IsCancellationRequested || e is PipelineStoppedException || e is PSRemotingDataStructureException) { StopDebuggerIfRemoteDebugSessionFailed(); throw new OperationCanceledException(); } + // We only catch RuntimeExceptions here in case writing errors to output was requested + // Other errors are bubbled up to the caller catch (RuntimeException e) { Logger.LogWarning($"Runtime exception occurred while executing command:{Environment.NewLine}{Environment.NewLine}{e}"); From 4b7f9a1fa0c27ad601ef1a3e93fa80eb531445ad Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 12:12:45 -0700 Subject: [PATCH 10/21] Remove unused exception --- .../Execution/ExecutionCanceledException.cs | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionCanceledException.cs diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionCanceledException.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionCanceledException.cs deleted file mode 100644 index 16fb5cf65..000000000 --- a/src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionCanceledException.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution -{ - public class ExecutionCanceledException : Exception - { - public ExecutionCanceledException(string message, Exception innerException) - : base(message, innerException) - { - } - } -} From 57be93efe930bf786cfbdbaa84adae49d79b230a Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 12:16:33 -0700 Subject: [PATCH 11/21] Add comments to EvaluateHandler --- .../Services/PowerShell/Handlers/EvaluateHandler.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/EvaluateHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/EvaluateHandler.cs index 457d1e8ae..4fce47658 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/EvaluateHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/EvaluateHandler.cs @@ -10,6 +10,10 @@ namespace Microsoft.PowerShell.EditorServices.Handlers { + /// + /// Handler for a custom request type for evaluating PowerShell. + /// This is generally for F8 support, to allow execution of a highlighted code snippet in the console as if it were copy-pasted. + /// internal class EvaluateHandler : IEvaluateHandler { private readonly ILogger _logger; @@ -25,6 +29,9 @@ public EvaluateHandler( public Task Handle(EvaluateRequestArguments request, CancellationToken cancellationToken) { + // TODO: Understand why we currently handle this asynchronously and why we return a dummy result value + // instead of awaiting the execution and returing a real result of some kind + _executionService.ExecutePSCommandAsync( new PSCommand().AddScript(request.Expression), new PowerShellExecutionOptions { WriteInputToHost = true, WriteOutputToHost = true, AddToHistory = true, InterruptCommandPrompt = true }, From 15bdad0f7694c0fc040b427ec0338ad63dd71c46 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 12:29:36 -0700 Subject: [PATCH 12/21] Fix SessionDetails comment --- .../Services/PowerShell/Runspace/SessionDetails.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Runspace/SessionDetails.cs b/src/PowerShellEditorServices/Services/PowerShell/Runspace/SessionDetails.cs index 733f31d9a..f21f521bd 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Runspace/SessionDetails.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Runspace/SessionDetails.cs @@ -22,10 +22,9 @@ internal class SessionDetails private const string Property_InstanceId = "instanceId"; /// - /// Gets the PSCommand that gathers details from the - /// current session. + /// Runs a PowerShell command to gather details about the current session. /// - /// A PSCommand used to gather session details. + /// A data object containing details about the PowerShell session. public static SessionDetails GetFromPowerShell(PowerShell pwsh) { Hashtable detailsObject = pwsh From 14537f6f99b68e0e0bb9fc2935720d47b555bf80 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 12:47:08 -0700 Subject: [PATCH 13/21] Add CancellationContext comment --- .../PowerShell/Utility/CancellationContext.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/CancellationContext.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/CancellationContext.cs index b9d59c7ff..c7ef8c541 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Utility/CancellationContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/CancellationContext.cs @@ -4,6 +4,23 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility { + /// + /// Encapsulates the scoping logic for cancellation tokens. + /// As PowerShell commands nest, this class maintains a stack of cancellation scopes + /// that allow each scope of logic to be cancelled at its own level. + /// Implicitly handles the merging and cleanup of cancellation token sources. + /// + /// + /// The class + /// and the struct + /// are intended to be used with a using block so you can do this: + /// + /// using (CancellationScope cancellationScope = _cancellationContext.EnterScope(_globalCancellationSource.CancellationToken, localCancellationToken)) + /// { + /// ExecuteCommandAsync(command, cancellationScope.CancellationToken); + /// } + /// + /// internal class CancellationContext { private readonly ConcurrentStack _cancellationSourceStack; From ba52e84e728121b517501867de539555de7f301e Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 12:48:37 -0700 Subject: [PATCH 14/21] Use FirstOrDefault() --- .../Services/PowerShell/Utility/CommandHelpers.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/CommandHelpers.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/CommandHelpers.cs index c415420d2..da41cf885 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Utility/CommandHelpers.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/CommandHelpers.cs @@ -98,12 +98,7 @@ public static async Task GetCommandInfoAsync( .AddArgument(commandName) .AddParameter("ErrorAction", "Ignore"); - CommandInfo commandInfo = null; - foreach (CommandInfo result in await executionService.ExecutePSCommandAsync(command, new PowerShellExecutionOptions(), CancellationToken.None).ConfigureAwait(false)) - { - commandInfo = result; - break; - } + CommandInfo commandInfo = (await executionService.ExecutePSCommandAsync(command, new PowerShellExecutionOptions(), CancellationToken.None).ConfigureAwait(false)).FirstOrDefault(); // Only cache CmdletInfos since they're exposed in binaries they are likely to not change throughout the session. if (commandInfo?.CommandType == CommandTypes.Cmdlet) From 529c54df08a9f70b0c6b8552c4b01941ad48547b Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 14:05:57 -0700 Subject: [PATCH 15/21] Add explanation to ErrorRecordExtensions --- .../Services/PowerShell/Utility/ErrorRecordExtensions.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/ErrorRecordExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/ErrorRecordExtensions.cs index a7e820ae7..06853da3f 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Utility/ErrorRecordExtensions.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/ErrorRecordExtensions.cs @@ -24,11 +24,14 @@ static ErrorRecordExtensions() var errorObjectParameter = Expression.Parameter(typeof(PSObject)); + // Generates a call like: + // $errorPSObject.WriteStream = [System.Management.Automation.WriteStreamType]::Error + // So that error record PSObjects will be rendered in the console properly s_setWriteStreamProperty = Expression.Lambda>( Expression.Call( errorObjectParameter, writeStreamProperty.GetSetMethod(), - Expression.Constant(errorStreamType)), + Expression.Constant(errorStreamType)), errorObjectParameter) .Compile(); } From ac985fa8aee256747f4864fe0dcbde19376d92e8 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 14:16:34 -0700 Subject: [PATCH 16/21] Add comments to PSCommandExtensions --- .../PowerShell/Utility/PSCommandExtensions.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/PSCommandExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/PSCommandExtensions.cs index 2393c4fe4..33bb20280 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Utility/PSCommandExtensions.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/PSCommandExtensions.cs @@ -21,22 +21,26 @@ public static PSCommand AddDebugOutputCommand(this PSCommand psCommand) public static PSCommand MergePipelineResults(this PSCommand psCommand) { + // We need to do merge errors and output before rendering with an Out- cmdlet Command lastCommand = psCommand.Commands[psCommand.Commands.Count - 1]; lastCommand.MergeMyResults(PipelineResultTypes.Error, PipelineResultTypes.Output); lastCommand.MergeMyResults(PipelineResultTypes.Information, PipelineResultTypes.Output); return psCommand; } + /// + /// Get a representation of the PSCommand, for logging purposes. + /// public static string GetInvocationText(this PSCommand command) { - Command lastCommand = command.Commands[0]; + Command currentCommand = command.Commands[0]; var sb = new StringBuilder().AddCommandText(command.Commands[0]); for (int i = 1; i < command.Commands.Count; i++) { - sb.Append(lastCommand.IsEndOfStatement ? "; " : " | "); - lastCommand = command.Commands[i]; - sb.AddCommandText(lastCommand); + sb.Append(currentCommand.IsEndOfStatement ? "; " : " | "); + currentCommand = command.Commands[i]; + sb.AddCommandText(currentCommand); } return sb.ToString(); From f67e8c185312629c24f1b0ce76cd67dbd55175e5 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 14:27:20 -0700 Subject: [PATCH 17/21] Add code ref to ErrorRecordExtensions --- .../Services/PowerShell/Utility/ErrorRecordExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/ErrorRecordExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/ErrorRecordExtensions.cs index 06853da3f..da8d4cf2d 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Utility/ErrorRecordExtensions.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/ErrorRecordExtensions.cs @@ -27,6 +27,7 @@ static ErrorRecordExtensions() // Generates a call like: // $errorPSObject.WriteStream = [System.Management.Automation.WriteStreamType]::Error // So that error record PSObjects will be rendered in the console properly + // See https://github.com/PowerShell/PowerShell/blob/946341b2ebe6a61f081f4c9143668dc7be1f9119/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs#L2088-L2091 s_setWriteStreamProperty = Expression.Lambda>( Expression.Call( errorObjectParameter, From d83436b0edacfd63bbc27d4793f5df13f24da4a1 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 14:27:34 -0700 Subject: [PATCH 18/21] Add comment to PowerShellExtensions --- .../Services/PowerShell/Utility/PowerShellExtensions.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs index 89c54cbe7..2309e5d16 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs @@ -71,6 +71,9 @@ public static void InvokeCommand(this PowerShell pwsh, PSCommand psCommand) pwsh.InvokeAndClear(); } + /// + /// When running a remote session, waits for remote processing and output to complete. + /// public static void WaitForRemoteOutputIfNeeded(this PowerShell pwsh) { if (!pwsh.Runspace.RunspaceIsRemote) @@ -78,6 +81,11 @@ public static void WaitForRemoteOutputIfNeeded(this PowerShell pwsh) return; } + // These methods are required when running commands remotely. + // Remote rendering from command output is done asynchronously. + // So to ensure we wait for output to be rendered, + // we need these methods to wait for rendering. + // PowerShell does this in its own implementation: https://github.com/PowerShell/PowerShell/blob/883ca98dd74ea13b3d8c0dd62d301963a40483d6/src/System.Management.Automation/engine/debugger/debugger.cs#L4628-L4652 s_waitForServicingComplete(pwsh); s_suspendIncomingData(pwsh); } From 8695d532d3551a245b4e4d44eda1efe6100eea8e Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 14:29:38 -0700 Subject: [PATCH 19/21] Simplify $PROFILE code --- .../PowerShell/Utility/PowerShellExtensions.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs index 2309e5d16..fc4d30ad6 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs @@ -163,10 +163,10 @@ public static void LoadProfiles(this PowerShell pwsh, ProfilePathInfo profilePat { var profileVariable = new PSObject(); - pwsh.AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.AllUsersAllHosts), profilePaths.AllUsersAllHosts); - pwsh.AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.AllUsersCurrentHost), profilePaths.AllUsersCurrentHost); - pwsh.AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.CurrentUserAllHosts), profilePaths.CurrentUserAllHosts); - pwsh.AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.CurrentUserCurrentHost), profilePaths.CurrentUserCurrentHost); + pwsh.AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.AllUsersAllHosts), profilePaths.AllUsersAllHosts) + .AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.AllUsersCurrentHost), profilePaths.AllUsersCurrentHost) + .AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.CurrentUserAllHosts), profilePaths.CurrentUserAllHosts) + .AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.CurrentUserCurrentHost), profilePaths.CurrentUserCurrentHost); pwsh.Runspace.SessionStateProxy.SetVariable("PROFILE", profileVariable); } @@ -197,7 +197,7 @@ public static string GetErrorString(this PowerShell pwsh) return sb.ToString(); } - private static void AddProfileMemberAndLoadIfExists(this PowerShell pwsh, PSObject profileVariable, string profileName, string profilePath) + private static PowerShell AddProfileMemberAndLoadIfExists(this PowerShell pwsh, PSObject profileVariable, string profileName, string profilePath) { profileVariable.Members.Add(new PSNoteProperty(profileName, profilePath)); @@ -209,6 +209,8 @@ private static void AddProfileMemberAndLoadIfExists(this PowerShell pwsh, PSObje pwsh.InvokeCommand(psCommand); } + + return pwsh; } private static StringBuilder AddErrorString(this StringBuilder sb, ErrorRecord error, int errorIndex) From 94c1a30df4fa50112bf7c9a0a3f9f833eb7d0bdb Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 14:37:41 -0700 Subject: [PATCH 20/21] Document runspace extension method --- .../PowerShell/Utility/RunspaceExtensions.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/RunspaceExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/RunspaceExtensions.cs index c65176b98..f91ac11a8 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Utility/RunspaceExtensions.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/RunspaceExtensions.cs @@ -18,12 +18,9 @@ internal static class RunspaceExtensions static RunspaceExtensions() { // PowerShell ApartmentState APIs aren't available in PSStandard, so we need to use reflection. - if (!VersionUtils.IsNetCore || VersionUtils.IsPS7OrGreater) - { - MethodInfo setterInfo = typeof(Runspace).GetProperty("ApartmentState").GetSetMethod(); - Delegate setter = Delegate.CreateDelegate(typeof(Action), firstArgument: null, method: setterInfo); - s_runspaceApartmentStateSetter = (Action)setter; - } + MethodInfo setterInfo = typeof(Runspace).GetProperty("ApartmentState").GetSetMethod(); + Delegate setter = Delegate.CreateDelegate(typeof(Action), firstArgument: null, method: setterInfo); + s_runspaceApartmentStateSetter = (Action)setter; MethodInfo getRemotePromptMethod = typeof(HostUtilities).GetMethod("GetRemotePrompt", BindingFlags.NonPublic | BindingFlags.Static); ParameterExpression runspaceParam = Expression.Parameter(typeof(Runspace)); @@ -45,6 +42,13 @@ public static void SetApartmentStateToSta(this Runspace runspace) s_runspaceApartmentStateSetter?.Invoke(runspace, ApartmentState.STA); } + /// + /// Augment a given prompt string with a remote decoration. + /// This is an internal method on Runspace in PowerShell that we reuse via reflection. + /// + /// The runspace the prompt is for. + /// The base prompt to decorate. + /// A prompt string decorated with remote connection details. public static string GetRemotePrompt(this Runspace runspace, string basePrompt) { return s_getRemotePromptFunc(runspace, basePrompt); From 34836e88024ed93e2efad8d2da0ec93c811ee628 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 Sep 2021 14:58:21 -0700 Subject: [PATCH 21/21] Fix profiling loading logic in config handler --- .../Handlers/ConfigurationHandler.cs | 56 ++++++++----------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs b/src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs index 7efc33798..4aacea4b9 100644 --- a/src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs +++ b/src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs @@ -17,8 +17,6 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Server; using OmniSharp.Extensions.LanguageServer.Protocol.Window; using OmniSharp.Extensions.LanguageServer.Protocol.Workspace; -using System.IO; -using Microsoft.PowerShell.EditorServices.Services.PowerShell; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; using Microsoft.PowerShell.EditorServices.Services.Extension; @@ -81,14 +79,35 @@ public override async Task Handle(DidChangeConfigurationParams request, Ca _workspaceService.WorkspacePath, _logger); + // We need to load the profiles if: + // - Profile loading is configured, AND + // - Profiles haven't been loaded before, OR + // - The profile loading configuration just changed + bool loadProfiles = _configurationService.CurrentSettings.EnableProfileLoading + && (!_profilesLoaded || !profileLoadingPreviouslyEnabled); + if (!_psesHost.IsRunning) { - await _psesHost.StartAsync(new HostStartOptions + _logger.LogTrace("Starting command loop"); + + if (loadProfiles) { - LoadProfiles = _configurationService.CurrentSettings.EnableProfileLoading, - }, CancellationToken.None).ConfigureAwait(false); + _logger.LogTrace("Loading profiles..."); + } + + await _psesHost.StartAsync(new HostStartOptions(), CancellationToken.None).ConfigureAwait(false); + + _consoleReplStarted = true; + + if (loadProfiles) + { + _profilesLoaded = true; + _logger.LogTrace("Loaded!"); + } } + // TODO: Load profiles when the host is already running + if (!this._cwdSet) { if (!string.IsNullOrEmpty(_configurationService.CurrentSettings.Cwd) @@ -116,33 +135,6 @@ await _psesHost.SetInitialWorkingDirectoryAsync( this._cwdSet = true; } - // We need to load the profiles if: - // - Profile loading is configured, AND - // - Profiles haven't been loaded before, OR - // - The profile loading configuration just changed - bool loadProfiles = _configurationService.CurrentSettings.EnableProfileLoading - && (!_profilesLoaded || !profileLoadingPreviouslyEnabled); - - if (!_psesHost.IsRunning) - { - _logger.LogTrace("Starting command loop"); - - if (loadProfiles) - { - _logger.LogTrace("Loading profiles..."); - } - - await _psesHost.StartAsync(new HostStartOptions - { - LoadProfiles = loadProfiles, - }, CancellationToken.None).ConfigureAwait(false); - - _consoleReplStarted = true; - _profilesLoaded = loadProfiles; - - _logger.LogTrace("Loaded!"); - } - if (!_extensionServiceInitialized) { await _extensionService.InitializeAsync();