From 73dddcab4b2f8f33d4947f1730c0bb47bccad9df Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Mon, 11 Apr 2022 16:48:40 -0400 Subject: [PATCH 01/12] Fix an occassional dead lock on restarting debug --- src/PowerShellEditorServices/Server/PsesDebugServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Server/PsesDebugServer.cs b/src/PowerShellEditorServices/Server/PsesDebugServer.cs index 89bdaeb3c..3e07f14b4 100644 --- a/src/PowerShellEditorServices/Server/PsesDebugServer.cs +++ b/src/PowerShellEditorServices/Server/PsesDebugServer.cs @@ -119,7 +119,7 @@ public void Dispose() // It represents the debugger on the PowerShell process we're in, // while a new debug server is spun up for every debugging session _psesHost.DebugContext.IsDebugServerActive = false; - _debugAdapterServer.Dispose(); + _debugAdapterServer?.Dispose(); _inputStream.Dispose(); _outputStream.Dispose(); _serverStopped.SetResult(true); From 8980a36169082d520df4e4cbc585ee4630280166 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Mon, 11 Apr 2022 16:50:43 -0400 Subject: [PATCH 02/12] Fix attach to process and other debugging issues Fixes #1736 - Add a new context frame for runspace entrance and REPL loops - Ensure runspace access is not attempted on a broken runspace - When a remote runspace hits a debugger stop, ensure that the origin pipeline thread does not run a REPL concurrently - Fix many race conditions --- .../DebugAdapter/BreakpointService.cs | 2 + .../Services/DebugAdapter/DebugService.cs | 172 +++++++------ .../Handlers/LaunchAndAttachHandler.cs | 74 +++++- .../Context/PowerShellContextFrame.cs | 56 +++- .../PowerShell/Context/PowerShellFrameType.cs | 11 +- .../Debugging/IPowerShellDebugContext.cs | 2 + .../Debugging/PowerShellDebugContext.cs | 47 +++- .../Execution/SynchronousPowerShellTask.cs | 126 +++++++-- .../PowerShell/Host/PsesInternalHost.cs | 239 +++++++++++++++--- .../Utility/PowerShellDebugDisplay.cs | 35 +++ .../Utility/PowerShellExtensions.cs | 32 +++ .../PowerShell/Utility/RunspaceExtensions.cs | 11 + 12 files changed, 663 insertions(+), 144 deletions(-) create mode 100644 src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellDebugDisplay.cs diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs index d978b67e5..c876273ee 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs @@ -12,6 +12,7 @@ using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; using Microsoft.PowerShell.EditorServices.Services.PowerShell; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; namespace Microsoft.PowerShell.EditorServices.Services { @@ -43,6 +44,7 @@ public async Task> GetBreakpointsAsync() { if (BreakpointApiUtils.SupportsBreakpointApis(_editorServicesHost.CurrentRunspace)) { + _editorServicesHost.Runspace.ThrowCancelledIfUnusable(); return BreakpointApiUtils.GetBreakpoints( _editorServicesHost.Runspace.Debugger, _debugStateService.RunspaceId); diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index 680f81be6..4898d80bd 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -16,6 +16,7 @@ using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Utility; @@ -74,6 +75,15 @@ internal class DebugService /// public DebuggerStoppedEventArgs CurrentDebuggerStoppedEventArgs { get; private set; } + /// + /// Tracks whether we are running Debug-Runspace in an out-of-process runspace. + /// + public bool IsDebuggingRemoteRunspace + { + get => _debugContext.IsDebuggingRemoteRunspace; + set => _debugContext.IsDebuggingRemoteRunspace = value; + } + #endregion #region Constructors @@ -128,6 +138,8 @@ public async Task SetLineBreakpointsAsync( DscBreakpointCapability dscBreakpoints = await _debugContext.GetDscBreakpointCapabilityAsync(CancellationToken.None).ConfigureAwait(false); string scriptPath = scriptFile.FilePath; + + _psesHost.Runspace.ThrowCancelledIfUnusable(); // Make sure we're using the remote script path if (_psesHost.CurrentRunspace.IsOnRemoteMachine && _remoteFileManager is not null) { @@ -795,23 +807,24 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) const string callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack"; const string getPSCallStack = $"Get-PSCallStack | ForEach-Object {{ [void]{callStackVarName}.Add(@($PSItem, $PSItem.GetFrameVariables())) }}"; + _psesHost.Runspace.ThrowCancelledIfUnusable(); // If we're attached to a remote runspace, we need to serialize the list prior to // transport because the default depth is too shallow. From testing, we determined the - // correct depth is 3. The script always calls `Get-PSCallStack`. On a local machine, we - // just return its results. On a remote machine we serialize it first and then later + // correct depth is 3. The script always calls `Get-PSCallStack`. In a local runspace, we + // just return its results. In a remote runspace we serialize it first and then later // deserialize it. - bool isOnRemoteMachine = _psesHost.CurrentRunspace.IsOnRemoteMachine; - string returnSerializedIfOnRemoteMachine = isOnRemoteMachine + bool isRemoteRunspace = _psesHost.CurrentRunspace.Runspace.RunspaceIsRemote; + string returnSerializedIfInRemoteRunspace = isRemoteRunspace ? $"[Management.Automation.PSSerializer]::Serialize({callStackVarName}, 3)" : callStackVarName; // PSObject is used here instead of the specific type because we get deserialized // objects from remote sessions and want a common interface. - var psCommand = new PSCommand().AddScript($"[Collections.ArrayList]{callStackVarName} = @(); {getPSCallStack}; {returnSerializedIfOnRemoteMachine}"); + var psCommand = new PSCommand().AddScript($"[Collections.ArrayList]{callStackVarName} = @(); {getPSCallStack}; {returnSerializedIfInRemoteRunspace}"); IReadOnlyList results = await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); - IEnumerable callStack = isOnRemoteMachine - ? (PSSerializer.Deserialize(results[0].BaseObject as string) as PSObject).BaseObject as IList + IEnumerable callStack = isRemoteRunspace + ? (PSSerializer.Deserialize(results[0].BaseObject as string) as PSObject)?.BaseObject as IList : results; var stackFrameDetailList = new List(); @@ -819,10 +832,10 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) foreach (var callStackFrameItem in callStack) { // We have to use reflection to get the variable dictionary. - var callStackFrameComponents = (callStackFrameItem as PSObject).BaseObject as IList; + var callStackFrameComponents = (callStackFrameItem as PSObject)?.BaseObject as IList; var callStackFrame = callStackFrameComponents[0] as PSObject; - IDictionary callStackVariables = isOnRemoteMachine - ? (callStackFrameComponents[1] as PSObject).BaseObject as IDictionary + IDictionary callStackVariables = isRemoteRunspace + ? (callStackFrameComponents[1] as PSObject)?.BaseObject as IDictionary : callStackFrameComponents[1] as IDictionary; var autoVariables = new VariableContainerDetails( @@ -885,7 +898,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) { stackFrameDetailsEntry.ScriptPath = scriptNameOverride; } - else if (isOnRemoteMachine + else if (_psesHost.CurrentRunspace.IsOnRemoteMachine && _remoteFileManager is not null && !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) { @@ -929,83 +942,98 @@ private static string TrimScriptListingLine(PSObject scriptLineObj, ref int pref internal async void OnDebuggerStopAsync(object sender, DebuggerStopEventArgs e) { - bool noScriptName = false; - string localScriptPath = e.InvocationInfo.ScriptName; - - // If there's no ScriptName, get the "list" of the current source - if (_remoteFileManager is not null && string.IsNullOrEmpty(localScriptPath)) + try { - // Get the current script listing and create the buffer - PSCommand command = new PSCommand().AddScript($"list 1 {int.MaxValue}"); + bool noScriptName = false; + string localScriptPath = e.InvocationInfo.ScriptName; - IReadOnlyList scriptListingLines = - await _executionService.ExecutePSCommandAsync( - command, CancellationToken.None).ConfigureAwait(false); - - if (scriptListingLines is not null) + // If there's no ScriptName, get the "list" of the current source + if (_remoteFileManager is not null && string.IsNullOrEmpty(localScriptPath)) { - int linePrefixLength = 0; + // Get the current script listing and create the buffer + PSCommand command = new PSCommand().AddScript($"list 1 {int.MaxValue}"); - string scriptListing = - string.Join( - Environment.NewLine, - scriptListingLines - .Select(o => TrimScriptListingLine(o, ref linePrefixLength)) - .Where(s => s is not null)); + IReadOnlyList scriptListingLines = + await _executionService.ExecutePSCommandAsync( + command, CancellationToken.None).ConfigureAwait(false); - temporaryScriptListingPath = - _remoteFileManager.CreateTemporaryFile( - $"[{_psesHost.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}", - scriptListing, - _psesHost.CurrentRunspace); + if (scriptListingLines is not null) + { + int linePrefixLength = 0; + + string scriptListing = + string.Join( + Environment.NewLine, + scriptListingLines + .Select(o => TrimScriptListingLine(o, ref linePrefixLength)) + .Where(s => s is not null)); + + temporaryScriptListingPath = + _remoteFileManager.CreateTemporaryFile( + $"[{_psesHost.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}", + scriptListing, + _psesHost.CurrentRunspace); + + localScriptPath = + temporaryScriptListingPath + ?? StackFrameDetails.NoFileScriptPath; + + noScriptName = localScriptPath is not null; + } + else + { + _logger.LogWarning("Could not load script context"); + } + } - localScriptPath = - temporaryScriptListingPath - ?? StackFrameDetails.NoFileScriptPath; + // Get call stack and variables. + await FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null).ConfigureAwait(false); - noScriptName = localScriptPath is not null; + // If this is a remote connection and the debugger stopped at a line + // in a script file, get the file contents + if (_psesHost.CurrentRunspace.IsOnRemoteMachine + && _remoteFileManager is not null + && !noScriptName) + { + localScriptPath = + await _remoteFileManager.FetchRemoteFileAsync( + e.InvocationInfo.ScriptName, + _psesHost.CurrentRunspace).ConfigureAwait(false); } - else + + if (stackFrameDetails.Length > 0) { - _logger.LogWarning("Could not load script context"); + // Augment the top stack frame with details from the stop event + if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent) + { + stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber; + stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber; + stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber; + stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber; + } } - } - // Get call stack and variables. - await FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null).ConfigureAwait(false); + CurrentDebuggerStoppedEventArgs = + new DebuggerStoppedEventArgs( + e, + _psesHost.CurrentRunspace, + localScriptPath); - // If this is a remote connection and the debugger stopped at a line - // in a script file, get the file contents - if (_psesHost.CurrentRunspace.IsOnRemoteMachine - && _remoteFileManager is not null - && !noScriptName) + // Notify the host that the debugger is stopped. + DebuggerStopped?.Invoke(sender, CurrentDebuggerStoppedEventArgs); + } + catch (OperationCanceledException) { - localScriptPath = - await _remoteFileManager.FetchRemoteFileAsync( - e.InvocationInfo.ScriptName, - _psesHost.CurrentRunspace).ConfigureAwait(false); + // Ignore, likely means that a remote runspace has closed. } - - if (stackFrameDetails.Length > 0) + catch (Exception exception) { - // Augment the top stack frame with details from the stop event - if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent) - { - stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber; - stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber; - stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber; - stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber; - } + // Log in a catch all so we don't crash the process. + _logger.LogError( + exception, + "Error occurred while obtaining debug info. Message: {message}", + exception.Message); } - - CurrentDebuggerStoppedEventArgs = - new DebuggerStoppedEventArgs( - e, - _psesHost.CurrentRunspace, - localScriptPath); - - // Notify the host that the debugger is stopped. - DebuggerStopped?.Invoke(sender, CurrentDebuggerStoppedEventArgs); } private void OnDebuggerResuming(object sender, DebuggerResumingEventArgs debuggerResumingEventArgs) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs index c2d0e34f8..951938109 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Management.Automation; +using System.Management.Automation.Remoting; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -18,6 +19,7 @@ using Microsoft.PowerShell.EditorServices.Services.PowerShell; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Context; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; namespace Microsoft.PowerShell.EditorServices.Handlers { @@ -261,6 +263,19 @@ public async Task Handle(PsesAttachRequestArguments request, Can _debugStateService.IsRemoteAttach = true; } + // Set up a temporary runspace changed event handler so we can ensure + // that the context switch is complete before attempting to debug + // a runspace in the target. + TaskCompletionSource runspaceChanged = new(); + + void RunspaceChangedHandler(object s, RunspaceChangedEventArgs _) + { + ((IInternalPowerShellExecutionService)s).RunspaceChanged -= RunspaceChangedHandler; + runspaceChanged.TrySetResult(true); + } + + _executionService.RunspaceChanged += RunspaceChangedHandler; + if (processIdIsSet && int.TryParse(request.ProcessId, out int processId) && (processId > 0)) { if (runspaceVersion.Version.Major < 5) @@ -274,11 +289,20 @@ public async Task Handle(PsesAttachRequestArguments request, Can try { - await _executionService.ExecutePSCommandAsync(enterPSHostProcessCommand, cancellationToken).ConfigureAwait(false); + await _executionService.ExecutePSCommandAsync( + enterPSHostProcessCommand, + cancellationToken, + new PowerShellExecutionOptions() + { + MustRunInForeground = true, + InterruptCurrentForeground = true, + AddToHistory = false, + Priority = ExecutionPriority.Next, + }).ConfigureAwait(false); } catch (Exception e) { - string msg = $"Could not attach to process '{processId}'"; + string msg = $"Could not attach to process with Id: '{request.ProcessId}'"; _logger.LogError(e, msg); throw new RpcErrorException(0, msg); } @@ -313,6 +337,8 @@ public async Task Handle(PsesAttachRequestArguments request, Can throw new RpcErrorException(0, "A positive integer must be specified for the processId field."); } + await runspaceChanged.Task.ConfigureAwait(false); + // Execute the Debug-Runspace command but don't await it because it // will block the debug adapter initialization process. The // InitializedEvent will be sent as soon as the RunspaceChanged @@ -327,13 +353,27 @@ public async Task Handle(PsesAttachRequestArguments request, Can .AddCommand("Microsoft.PowerShell.Utility\\Select-Object") .AddParameter("ExpandProperty", "Id"); - IEnumerable ids = await _executionService.ExecutePSCommandAsync(getRunspaceIdCommand, cancellationToken).ConfigureAwait(false); - foreach (var id in ids) + try { - _debugStateService.RunspaceId = id; - break; + IEnumerable ids = await _executionService.ExecutePSCommandAsync( + getRunspaceIdCommand, + cancellationToken) + .ConfigureAwait(false); - // TODO: If we don't end up setting this, we should throw + foreach (var id in ids) + { + _debugStateService.RunspaceId = id; + break; + + // TODO: If we don't end up setting this, we should throw + } + } + catch (Exception getRunspaceException) + { + _logger.LogError( + getRunspaceException, + "Unable to determine runspace to attach to. Message: {message}", + getRunspaceException.Message); } // TODO: We have the ID, why not just use that? @@ -363,6 +403,7 @@ public async Task Handle(PsesAttachRequestArguments request, Can // Clear any existing breakpoints before proceeding await _breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(continueOnCapturedContext: false); + _debugService.IsDebuggingRemoteRunspace = true; _debugStateService.WaitingForAttach = true; Task nonAwaitedTask = _executionService .ExecutePSCommandAsync(debugRunspaceCmd, CancellationToken.None) @@ -397,10 +438,15 @@ public async Task OnStarted(IDebugAdapterServer server, CancellationToken cancel private async Task OnExecutionCompletedAsync(Task executeTask) { + bool isRunspaceClosed = false; try { await executeTask.ConfigureAwait(false); } + catch (PSRemotingTransportException) + { + isRunspaceClosed = true; + } catch (Exception e) { _logger.LogError( @@ -413,14 +459,24 @@ private async Task OnExecutionCompletedAsync(Task executeTask) _debugEventHandlerService.UnregisterEventHandlers(); - if (_debugStateService.IsAttachSession) + _debugService.IsDebuggingRemoteRunspace = false; + + if (!isRunspaceClosed && _debugStateService.IsAttachSession) { // Pop the sessions if (_runspaceContext.CurrentRunspace.RunspaceOrigin == RunspaceOrigin.EnteredProcess) { try { - await _executionService.ExecutePSCommandAsync(new PSCommand().AddCommand("Exit-PSHostProcess"), CancellationToken.None).ConfigureAwait(false); + await _executionService.ExecutePSCommandAsync( + new PSCommand().AddCommand("Exit-PSHostProcess"), + CancellationToken.None, + new PowerShellExecutionOptions() + { + MustRunInForeground = true, + InterruptCurrentForeground = true, + Priority = ExecutionPriority.Next, + }).ConfigureAwait(false); if (_debugStateService.IsRemoteAttach && _runspaceContext.CurrentRunspace.RunspaceOrigin != RunspaceOrigin.Local) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellContextFrame.cs b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellContextFrame.cs index 9a31bf629..c172e40fc 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellContextFrame.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellContextFrame.cs @@ -1,13 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; +using System.Diagnostics; +using System.Text; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace; -using System; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; using SMA = System.Management.Automation; namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Context { + [DebuggerDisplay("{ToDebuggerDisplayString()}")] internal class PowerShellContextFrame : IDisposable { public static PowerShellContextFrame CreateForPowerShellInstance( @@ -35,13 +39,26 @@ public PowerShellContextFrame(SMA.PowerShell powerShell, RunspaceInfo runspaceIn public PowerShellFrameType FrameType { get; } + public bool IsAwaitingPop { get; set; } + + public bool SessionExiting { get; set; } + protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { - PowerShell.Dispose(); + // When runspace is popping from `Exit-PSHostProcess` or similar, attempting + // to dispose directly in the same frame would dead lock. + if (SessionExiting) + { + PowerShell.DisposeWhenCompleted(); + } + else + { + PowerShell.Dispose(); + } } disposedValue = true; @@ -54,5 +71,40 @@ public void Dispose() Dispose(disposing: true); GC.SuppressFinalize(this); } + +#if DEBUG + private string ToDebuggerDisplayString() + { + StringBuilder text = new(); + + if ((FrameType & PowerShellFrameType.Nested) is not 0) + { + text.Append("Ne-"); + } + + if ((FrameType & PowerShellFrameType.Debug) is not 0) + { + text.Append("De-"); + } + + if ((FrameType & PowerShellFrameType.Remote) is not 0) + { + text.Append("Rem-"); + } + + if ((FrameType & PowerShellFrameType.NonInteractive) is not 0) + { + text.Append("NI-"); + } + + if ((FrameType & PowerShellFrameType.Repl) is not 0) + { + text.Append("Repl-"); + } + + text.Append(PowerShellDebugDisplay.ToDebuggerString(PowerShell)); + return text.ToString(); + } +#endif } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellFrameType.cs b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellFrameType.cs index cb20ff8ff..9bf2fca02 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellFrameType.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellFrameType.cs @@ -8,10 +8,11 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Context [Flags] internal enum PowerShellFrameType { - Normal = 0x0, - Nested = 0x1, - Debug = 0x2, - Remote = 0x4, - NonInteractive = 0x8, + Normal = 0 << 0, + Nested = 1 << 0, + Debug = 1 << 1, + Remote = 1 << 2, + NonInteractive = 1 << 3, + Repl = 1 << 4, } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Debugging/IPowerShellDebugContext.cs b/src/PowerShellEditorServices/Services/PowerShell/Debugging/IPowerShellDebugContext.cs index bf78d80b1..173e5992b 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Debugging/IPowerShellDebugContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Debugging/IPowerShellDebugContext.cs @@ -14,6 +14,8 @@ internal interface IPowerShellDebugContext DebuggerStopEventArgs LastStopEventArgs { get; } + public bool IsDebuggingRemoteRunspace { get; set; } + public event Action DebuggerStopped; public event Action DebuggerResuming; diff --git a/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs b/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs index a6c1d27e1..c334d96b6 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs @@ -6,7 +6,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Context; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging { @@ -68,6 +70,11 @@ public PowerShellDebugContext( /// public bool IsDebugServerActive { get; set; } + /// + /// Tracks whether we are running Debug-Runspace in an out-of-process runspace. + /// + public bool IsDebuggingRemoteRunspace { get; set; } + public DebuggerStopEventArgs LastStopEventArgs { get; private set; } public event Action DebuggerStopped; @@ -76,6 +83,7 @@ public PowerShellDebugContext( public Task GetDscBreakpointCapabilityAsync(CancellationToken cancellationToken) { + _psesHost.Runspace.ThrowCancelledIfUnusable(); return _psesHost.CurrentRunspace.GetDscBreakpointCapabilityAsync(_logger, _psesHost, cancellationToken); } @@ -83,10 +91,11 @@ public Task GetDscBreakpointCapabilityAsync(Cancellatio // runspace may not have these options set and attempting to set breakpoints remotely fails. public void EnableDebugMode() { + _psesHost.Runspace.ThrowCancelledIfUnusable(); _psesHost.Runspace.Debugger.SetDebugMode(DebugModes.LocalScript | DebugModes.RemoteScript); } - public void Abort() => SetDebugResuming(DebuggerResumeAction.Stop); + public void Abort() => SetDebugResuming(DebuggerResumeAction.Stop, isDisconnect: true); public void BreakExecution() => _psesHost.Runspace.Debugger.SetDebuggerStepMode(enabled: true); @@ -98,7 +107,7 @@ public void EnableDebugMode() public void StepOver() => SetDebugResuming(DebuggerResumeAction.StepOver); - public void SetDebugResuming(DebuggerResumeAction debuggerResumeAction) + public void SetDebugResuming(DebuggerResumeAction debuggerResumeAction, bool isDisconnect = false) { // NOTE: We exit because the paused/stopped debugger is currently in a prompt REPL, and // to resume the debugger we must exit that REPL. @@ -114,7 +123,24 @@ public void SetDebugResuming(DebuggerResumeAction debuggerResumeAction) // then we'd accidentally cancel the debugged task since no prompt is running. We can // test this by checking if the UI's type is NullPSHostUI which is used specifically in // this scenario. This mostly applies to unit tests. - if (_psesHost.UI is not NullPSHostUI) + if (_psesHost.UI is NullPSHostUI) + { + return; + } + + if (debuggerResumeAction is DebuggerResumeAction.Stop) + { + if (isDisconnect) + { + _psesHost.UnwindCallStack(); + return; + } + + _psesHost.CancelIdleParentTask(); + return; + } + + if ((_psesHost.CurrentFrame.FrameType & PowerShellFrameType.Repl) is not 0) { _psesHost.CancelCurrentTask(); } @@ -140,7 +166,22 @@ public void ProcessDebuggerResult(DebuggerCommandResults debuggerResult) if (debuggerResult?.ResumeAction is not null) { SetDebugResuming(debuggerResult.ResumeAction.Value); + + // If a debugging command like `c` is specified in a nested remote + // debugging prompt we need to unwind the nested execution loop. + if ((_psesHost.CurrentFrame.FrameType & PowerShellFrameType.Remote) is not 0) + { + _psesHost.ForceSetExit(); + } + RaiseDebuggerResumingEvent(new DebuggerResumingEventArgs(debuggerResult.ResumeAction.Value)); + + // The Terminate exception is used by the engine for flow control + // when it needs to unwind the callstack out of the debugger. + if (debuggerResult.ResumeAction is DebuggerResumeAction.Stop) + { + throw new TerminateException(); + } } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs index c5fdcc698..e1e3f703e 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs @@ -8,6 +8,7 @@ using System.Management.Automation.Remoting; using System.Threading; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Context; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; using Microsoft.PowerShell.EditorServices.Utility; @@ -17,6 +18,8 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution { internal class SynchronousPowerShellTask : SynchronousTask> { + private static readonly PowerShellExecutionOptions s_defaultPowerShellExecutionOptions = new(); + private readonly ILogger _logger; private readonly PsesInternalHost _psesHost; @@ -25,7 +28,7 @@ internal class SynchronousPowerShellTask : SynchronousTask Run(CancellationToken cancellationToken) { - _pwsh = _psesHost.CurrentPowerShell; + _psesHost.Runspace.ThrowCancelledIfUnusable(); + PowerShellContextFrame frame = _psesHost.PushPowerShellForExecution(); + try + { + _pwsh = _psesHost.CurrentPowerShell; - if (PowerShellExecutionOptions.WriteInputToHost) + if (PowerShellExecutionOptions.WriteInputToHost) + { + _psesHost.WriteWithPrompt(_psCommand, cancellationToken); + } + + return _pwsh.Runspace.Debugger.InBreakpoint + && (IsDebuggerCommand(_psCommand) || _pwsh.Runspace.RunspaceIsRemote) + ? ExecuteInDebugger(cancellationToken) + : ExecuteNormally(cancellationToken); + } + finally { - _psesHost.WriteWithPrompt(_psCommand, cancellationToken); + _psesHost.PopPowerShellForExecution(frame); } - - return _pwsh.Runspace.Debugger.InBreakpoint - && Array.Exists( - DebuggerCommands, - c => c.Equals(_psCommand.GetInvocationText(), StringComparison.CurrentCultureIgnoreCase)) - ? ExecuteInDebugger(cancellationToken) - : ExecuteNormally(cancellationToken); } public override string ToString() @@ -71,8 +81,29 @@ public override string ToString() return _psCommand.GetInvocationText(); } + private static bool IsDebuggerCommand(PSCommand command) + { + if (command.Commands.Count is not 1 + || command.Commands[0] is { IsScript: false } or { Parameters.Count: > 0 }) + { + return false; + } + + string commandText = command.Commands[0].CommandText; + foreach (string knownCommand in DebuggerCommands) + { + if (commandText.Equals(knownCommand, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + private IReadOnlyList ExecuteNormally(CancellationToken cancellationToken) { + _frame = _psesHost.CurrentFrame; if (PowerShellExecutionOptions.WriteOutputToHost) { _psCommand.AddOutputCommand(); @@ -96,6 +127,11 @@ private IReadOnlyList ExecuteNormally(CancellationToken cancellationTok result = _pwsh.InvokeCommand(_psCommand, invocationSettings); cancellationToken.ThrowIfCancellationRequested(); } + // Allow terminate exceptions to propogate for flow control. + catch (TerminateException) + { + throw; + } // 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) @@ -111,6 +147,17 @@ private IReadOnlyList ExecuteNormally(CancellationToken cancellationTok // Other errors are bubbled up to the caller catch (RuntimeException e) { + if (e is PSRemotingTransportException) + { + _ = System.Threading.Tasks.Task.Run( + () => _psesHost.UnwindCallStack(), + CancellationToken.None) + .HandleErrorsAsync(_logger); + + _psesHost.WaitForExternalDebuggerStops(); + throw new OperationCanceledException("The operation was canceled.", e); + } + Logger.LogWarning($"Runtime exception occurred while executing command:{Environment.NewLine}{Environment.NewLine}{e}"); if (PowerShellExecutionOptions.ThrowOnError) @@ -122,7 +169,14 @@ private IReadOnlyList ExecuteNormally(CancellationToken cancellationTok .AddOutputCommand() .AddParameter("InputObject", e.ErrorRecord.AsPSObject()); - _pwsh.InvokeCommand(command); + if (_pwsh.Runspace.RunspaceStateInfo.IsUsable()) + { + _pwsh.InvokeCommand(command); + } + else + { + _psesHost.UI.WriteErrorLine(e.ToString()); + } } finally { @@ -176,7 +230,11 @@ private IReadOnlyList ExecuteInDebugger(CancellationToken cancellationT debuggerResult = _pwsh.Runspace.Debugger.ProcessCommand(_psCommand, outputCollection); cancellationToken.ThrowIfCancellationRequested(); } - + // Allow terminate exceptions to propogate for flow control. + catch (TerminateException) + { + throw; + } // 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) @@ -188,6 +246,17 @@ private IReadOnlyList ExecuteInDebugger(CancellationToken cancellationT // Other errors are bubbled up to the caller catch (RuntimeException e) { + if (e is PSRemotingTransportException) + { + _ = System.Threading.Tasks.Task.Run( + () => _psesHost.UnwindCallStack(), + CancellationToken.None) + .HandleErrorsAsync(_logger); + + _psesHost.WaitForExternalDebuggerStops(); + throw new OperationCanceledException("The operation was canceled.", e); + } + Logger.LogWarning($"Runtime exception occurred while executing command:{Environment.NewLine}{Environment.NewLine}{e}"); if (PowerShellExecutionOptions.ThrowOnError) @@ -195,14 +264,9 @@ private IReadOnlyList ExecuteInDebugger(CancellationToken cancellationT throw; } - var errorOutputCollection = new PSDataCollection(); - errorOutputCollection.DataAdded += (object sender, DataAddedEventArgs args) => - { - for (int i = args.Index; i < outputCollection.Count; i++) - { - _psesHost.UI.WriteLine(outputCollection[i].ToString()); - } - }; + using var errorOutputCollection = new PSDataCollection(); + errorOutputCollection.DataAdding += (object sender, DataAddingEventArgs args) + => _psesHost.UI.WriteLine(args.ItemAdded?.ToString()); var command = new PSCommand() .AddDebugOutputCommand() @@ -252,6 +316,7 @@ private void StopDebuggerIfRemoteDebugSessionFailed() // Instead we have to query the remote directly if (_pwsh.Runspace.RunspaceIsRemote) { + _pwsh.Runspace.ThrowCancelledIfUnusable(); var assessDebuggerCommand = new PSCommand().AddScript("$Host.Runspace.Debugger.InBreakpoint"); var outputCollection = new PSDataCollection(); @@ -271,11 +336,30 @@ private void StopDebuggerIfRemoteDebugSessionFailed() private void CancelNormalExecution() { + if (!_pwsh.Runspace.RunspaceStateInfo.IsUsable()) + { + return; + } + + // If we're signaled to exit a runspace then that'll trigger a stop, + // if we block on that stop we'll never exit the runspace ( + // and essentially deadlock). + if (_frame.SessionExiting) + { + _pwsh.BeginStop(null, null); + return; + } + _pwsh.Stop(); } private void CancelDebugExecution() { + if (!_pwsh.Runspace.RunspaceStateInfo.IsUsable()) + { + return; + } + _pwsh.Runspace.Debugger.StopProcessCommand(); } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index bf0316a65..ea246aba9 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -152,13 +152,15 @@ public PsesInternalHost( IRunspaceInfo IRunspaceContext.CurrentRunspace => CurrentRunspace; - private PowerShellContextFrame CurrentFrame => _psFrameStack.Peek(); + internal PowerShellContextFrame CurrentFrame => _psFrameStack.Peek(); public event Action RunspaceChanged; private bool ShouldExitExecutionLoop => _shouldExit || _shuttingDown != 0; - public override void EnterNestedPrompt() => PushPowerShellAndRunLoop(CreateNestedPowerShell(CurrentRunspace), PowerShellFrameType.Nested); + public override void EnterNestedPrompt() => PushPowerShellAndRunLoop( + CreateNestedPowerShell(CurrentRunspace), + PowerShellFrameType.Nested | PowerShellFrameType.Repl); public override void ExitNestedPrompt() => SetExit(); @@ -168,18 +170,38 @@ public PsesInternalHost( public void PopRunspace() { + if (!Runspace.RunspaceIsRemote) + { + return; + } + IsRunspacePushed = false; + CurrentFrame.SessionExiting = true; + PopPowerShell(); SetExit(); } public void PushRunspace(Runspace runspace) { IsRunspacePushed = true; - PushPowerShellAndRunLoop(CreatePowerShellForRunspace(runspace), PowerShellFrameType.Remote); + PushPowerShellAndMaybeRunLoop( + CreatePowerShellForRunspace(runspace), + PowerShellFrameType.Remote | PowerShellFrameType.Repl, + skipRunLoop: true); } // TODO: Handle exit code if needed - public override void SetShouldExit(int exitCode) => SetExit(); + public override void SetShouldExit(int exitCode) + { + if ((CurrentFrame.FrameType & PowerShellFrameType.Remote) is not 0) + { + // PopRunspace also calls SetExit. + PopRunspace(); + return; + } + + SetExit(); + } /// /// Try to start the PowerShell loop in the host. @@ -239,7 +261,8 @@ public void SetExit() { // Can't exit from the top level of PSES // since if you do, you lose all LSP services - if (_psFrameStack.Count <= 1) + PowerShellContextFrame frame = CurrentFrame; + if ((frame.FrameType & PowerShellFrameType.Repl) is 0 || _psFrameStack.Count <= 1) { return; } @@ -247,6 +270,8 @@ public void SetExit() _shouldExit = true; } + internal void ForceSetExit() => _shouldExit = true; + public Task InvokeTaskOnPipelineThreadAsync( SynchronousTask task) { @@ -288,6 +313,16 @@ public void CancelCurrentTask() _cancellationContext.CancelCurrentTask(); } + public void CancelIdleParentTask() + { + _cancellationContext.CancelIdleParentTask(); + } + + public void UnwindCallStack() + { + _cancellationContext.CancelCurrentTaskStack(); + } + public Task ExecuteDelegateAsync( string representation, ExecutionOptions executionOptions, @@ -406,7 +441,7 @@ private void Run() _mainRunspaceEngineIntrinsics = engineIntrinsics; _localComputerName = localRunspaceInfo.SessionDetails.ComputerName; _runspaceStack.Push(new RunspaceFrame(pwsh.Runspace, localRunspaceInfo)); - PushPowerShellAndRunLoop(pwsh, PowerShellFrameType.Normal, localRunspaceInfo); + PushPowerShellAndRunLoop(pwsh, PowerShellFrameType.Normal | PowerShellFrameType.Repl, localRunspaceInfo); } catch (Exception e) { @@ -422,7 +457,30 @@ private void Run() return (pwsh, localRunspaceInfo, engineIntrinsics); } + internal PowerShellContextFrame PushPowerShellForExecution() + { + PowerShellContextFrame frame = CurrentFrame; + PowerShellFrameType currentFrameType = frame.FrameType; + currentFrameType &= ~PowerShellFrameType.Repl; + PowerShellContextFrame newFrame = new( + frame.PowerShell.CloneForNewFrame(), + frame.RunspaceInfo, + currentFrameType); + + PushPowerShell(newFrame); + return newFrame; + } + private void PushPowerShellAndRunLoop(PowerShell pwsh, PowerShellFrameType frameType, RunspaceInfo newRunspaceInfo = null) + { + PushPowerShellAndMaybeRunLoop(pwsh, frameType, newRunspaceInfo, skipRunLoop: false); + } + + private void PushPowerShellAndMaybeRunLoop( + PowerShell pwsh, + PowerShellFrameType frameType, + RunspaceInfo newRunspaceInfo = null, + bool skipRunLoop = false) { // TODO: Improve runspace origin detection here if (newRunspaceInfo is null) @@ -437,7 +495,7 @@ private void PushPowerShellAndRunLoop(PowerShell pwsh, PowerShellFrameType frame } } - PushPowerShellAndRunLoop(new PowerShellContextFrame(pwsh, newRunspaceInfo, frameType)); + PushPowerShellAndMaybeRunLoop(new PowerShellContextFrame(pwsh, newRunspaceInfo, frameType), skipRunLoop); } private RunspaceInfo GetRunspaceInfoForPowerShell(PowerShell pwsh, out bool isNewRunspace, out RunspaceFrame oldRunspaceFrame) @@ -462,9 +520,13 @@ private RunspaceInfo GetRunspaceInfoForPowerShell(PowerShell pwsh, out bool isNe return RunspaceInfo.CreateFromPowerShell(_logger, pwsh, _localComputerName); } - private void PushPowerShellAndRunLoop(PowerShellContextFrame frame) + private void PushPowerShellAndMaybeRunLoop(PowerShellContextFrame frame, bool skipRunLoop = false) { PushPowerShell(frame); + if (skipRunLoop) + { + return; + } try { @@ -483,7 +545,14 @@ private void PushPowerShellAndRunLoop(PowerShellContextFrame frame) } finally { - PopPowerShell(); + if (CurrentFrame != frame) + { + frame.IsAwaitingPop = true; + } + else + { + PopPowerShell(); + } } } @@ -491,6 +560,12 @@ private void PushPowerShell(PowerShellContextFrame frame) { if (_psFrameStack.Count > 0) { + if (frame.PowerShell.Runspace == CurrentFrame.PowerShell.Runspace) + { + _psFrameStack.Push(frame); + return; + } + RemoveRunspaceEventHandlers(CurrentFrame.PowerShell.Runspace); } @@ -499,15 +574,25 @@ private void PushPowerShell(PowerShellContextFrame frame) _psFrameStack.Push(frame); } + internal void PopPowerShellForExecution(PowerShellContextFrame expectedFrame) + { + if (CurrentFrame != expectedFrame) + { + expectedFrame.IsAwaitingPop = true; + return; + } + + PopPowerShellImpl(); + } + private void PopPowerShell(RunspaceChangeAction runspaceChangeAction = RunspaceChangeAction.Exit) { _shouldExit = false; - PowerShellContextFrame frame = _psFrameStack.Pop(); - try + PopPowerShellImpl(_ => { // If we're changing runspace, make sure we move the handlers over. If we just // popped the last frame, then we're exiting and should pop the runspace too. - if (_psFrameStack.Count == 0 || CurrentRunspace.Runspace != CurrentPowerShell.Runspace) + if (_psFrameStack.Count == 0 || Runspace != CurrentPowerShell.Runspace) { RunspaceFrame previousRunspaceFrame = _runspaceStack.Pop(); RemoveRunspaceEventHandlers(previousRunspaceFrame.Runspace); @@ -526,11 +611,24 @@ private void PopPowerShell(RunspaceChangeAction runspaceChangeAction = RunspaceC newRunspaceFrame.RunspaceInfo)); } } - } - finally + }); + } + + private void PopPowerShellImpl(Action action = null) + { + do { - frame.Dispose(); + PowerShellContextFrame frame = _psFrameStack.Pop(); + try + { + action?.Invoke(frame); + } + finally + { + frame.Dispose(); + } } + while (CurrentFrame.IsAwaitingPop); } private void RunTopLevelExecutionLoop() @@ -546,7 +644,21 @@ private void RunTopLevelExecutionLoop() // Signal that we are ready for outside services to use _started.TrySetResult(true); - RunExecutionLoop(); + // While loop is purely so we can recover gracefully from a + // terminate exception. + while (true) + { + try + { + RunExecutionLoop(); + break; + } + catch (TerminateException) + { + // Do nothing, since we are at the top level of the loop + // the call stack has been unwound successfully. + } + } } catch (Exception e) { @@ -564,7 +676,7 @@ private void RunDebugExecutionLoop() try { DebugContext.EnterDebugLoop(); - RunExecutionLoop(); + RunExecutionLoop(isForDebug: true); } finally { @@ -572,11 +684,17 @@ private void RunDebugExecutionLoop() } } - private void RunExecutionLoop() + private void RunExecutionLoop(bool isForDebug = false) { + Runspace initialRunspace = Runspace; while (!ShouldExitExecutionLoop) { - using CancellationScope cancellationScope = _cancellationContext.EnterScope(isIdleScope: false); + if (isForDebug && !initialRunspace.RunspaceStateInfo.IsUsable()) + { + return; + } + + using CancellationScope cancellationScope = _cancellationContext.EnterScope(false); DoOneRepl(cancellationScope.CancellationToken); while (!ShouldExitExecutionLoop @@ -584,6 +702,20 @@ private void RunExecutionLoop() && _taskQueue.TryTake(out ISynchronousTask task)) { task.ExecuteSynchronously(cancellationScope.CancellationToken); + while (Runspace is { RunspaceIsRemote: true } remoteRunspace + && !remoteRunspace.RunspaceStateInfo.IsUsable()) + { + PopPowerShell(RunspaceChangeAction.Exit); + } + } + + if (_shouldExit + && (CurrentFrame.FrameType & PowerShellFrameType.Remote) is not 0 + && (CurrentFrame.FrameType & PowerShellFrameType.Repl) is not 0 + && (CurrentFrame.FrameType & PowerShellFrameType.Nested) is 0) + { + _shouldExit = false; + PopPowerShell(); } } } @@ -602,7 +734,9 @@ private void DoOneRepl(CancellationToken cancellationToken) // the debugger (instead of using a Code launch configuration) via Wait-Debugger or // simply hitting a PSBreakpoint. We need to synchronize the state and stop the debug // context (and likely the debug server). - if (DebugContext.IsActive && !CurrentRunspace.Runspace.Debugger.InBreakpoint) + if (!DebugContext.IsDebuggingRemoteRunspace + && DebugContext.IsActive + && !CurrentRunspace.Runspace.Debugger.InBreakpoint) { StopDebugContext(); } @@ -644,6 +778,15 @@ private void DoOneRepl(CancellationToken cancellationToken) { // Do nothing, since we were just cancelled } + // Propagate exceptions thrown from the debugger when quitting. + catch (TerminateException) + { + throw; + } + catch (FlowControlException) + { + // Do nothing, a break or continue statement was used outside of a loop. + } catch (Exception e) { UI.WriteErrorLine($"An error occurred while running the REPL loop:{Environment.NewLine}{e}"); @@ -662,13 +805,14 @@ private void DoOneRepl(CancellationToken cancellationToken) private string GetPrompt(CancellationToken cancellationToken) { + Runspace.ThrowCancelledIfUnusable(); string prompt = DefaultPrompt; try { // TODO: Should we cache PSCommands like this as static members? var command = new PSCommand().AddCommand("prompt"); IReadOnlyList results = InvokePSCommand(command, executionOptions: null, cancellationToken); - if (results.Count > 0) + if (results?.Count > 0) { prompt = results[0]; } @@ -903,28 +1047,59 @@ private void StopDebugContext() } } + private readonly object _replFromAnotherThread = new(); + + internal void WaitForExternalDebuggerStops() + { + lock (_replFromAnotherThread) + { + } + } + private void OnDebuggerStopped(object sender, DebuggerStopEventArgs debuggerStopEventArgs) { // The debugger has officially started. We use this to later check if we should stop it. DebugContext.IsActive = true; - // If the debug server is NOT active, we need to synchronize state and start it. - if (!DebugContext.IsDebugServerActive) + // The local debugging architecture works mostly because we control the pipeline thread, + // but remote runspaces will trigger debugger stops on a separate thread. We lock here + // if we're on a different thread so in then event of a transport error, we can + // safely wind down REPL loops in a different thread. + bool isExternal = Environment.CurrentManagedThreadId != _pipelineThread.ManagedThreadId; + if (!isExternal) { - _languageServer?.SendNotification("powerShell/startDebugger"); + OnDebuggerStoppedImpl(sender, debuggerStopEventArgs); + return; } - DebugContext.SetDebuggerStopped(debuggerStopEventArgs); - - try + lock (_replFromAnotherThread) { - CurrentPowerShell.WaitForRemoteOutputIfNeeded(); - PushPowerShellAndRunLoop(CreateNestedPowerShell(CurrentRunspace), PowerShellFrameType.Debug | PowerShellFrameType.Nested); - CurrentPowerShell.ResumeRemoteOutputIfNeeded(); + OnDebuggerStoppedImpl(sender, debuggerStopEventArgs); } - finally + + void OnDebuggerStoppedImpl(object sender, DebuggerStopEventArgs debuggerStopEventArgs) { - DebugContext.SetDebuggerResumed(); + // If the debug server is NOT active, we need to synchronize state and start it. + if (!DebugContext.IsDebugServerActive) + { + _languageServer?.SendNotification("powerShell/startDebugger"); + } + + DebugContext.SetDebuggerStopped(debuggerStopEventArgs); + + try + { + CurrentPowerShell.WaitForRemoteOutputIfNeeded(); + PowerShellFrameType frameBase = CurrentFrame.FrameType & PowerShellFrameType.Remote; + PushPowerShellAndRunLoop( + CreateNestedPowerShell(CurrentRunspace), + frameBase | PowerShellFrameType.Debug | PowerShellFrameType.Nested | PowerShellFrameType.Repl); + CurrentPowerShell.ResumeRemoteOutputIfNeeded(); + } + finally + { + DebugContext.SetDebuggerResumed(); + } } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellDebugDisplay.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellDebugDisplay.cs new file mode 100644 index 000000000..2253f7375 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellDebugDisplay.cs @@ -0,0 +1,35 @@ +#if DEBUG +using System.Diagnostics; +using SMA = System.Management.Automation; + +[assembly: DebuggerDisplay("{Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility.PowerShellDebugDisplay.ToDebuggerString(this)}", Target = typeof(SMA.PowerShell))] +[assembly: DebuggerDisplay("{Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility.PSCommandDebugDisplay.ToDebuggerString(this)}", Target = typeof(SMA.PSCommand))] + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; + +internal static class PowerShellDebugDisplay +{ + public static string ToDebuggerString(SMA.PowerShell pwsh) + { + if (pwsh.Commands.Commands.Count == 0) + { + return "{}"; + } + + return $"{{{pwsh.Commands.Commands[0].CommandText}}}"; + } +} + +internal static class PSCommandDebugDisplay +{ + public static string ToDebuggerString(SMA.PSCommand command) + { + if (command.Commands.Count == 0) + { + return "{}"; + } + + return $"{{{command.Commands[0].CommandText}}}"; + } +} +#endif diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs index aa6684b4d..de2de8b28 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs @@ -37,6 +37,38 @@ static PowerShellExtensions() typeof(PowerShell).GetMethod("ResumeIncomingData", BindingFlags.Instance | BindingFlags.NonPublic)); } + public static PowerShell CloneForNewFrame(this PowerShell pwsh) + { + if (pwsh.IsNested) + { + return PowerShell.Create(RunspaceMode.CurrentRunspace); + } + + PowerShell newPwsh = PowerShell.Create(); + newPwsh.Runspace = pwsh.Runspace; + return newPwsh; + } + + public static void DisposeWhenCompleted(this PowerShell pwsh) + { + static void handler(object self, PSInvocationStateChangedEventArgs e) + { + if (e.InvocationStateInfo.State is + not PSInvocationState.Completed + and not PSInvocationState.Failed + and not PSInvocationState.Stopped) + { + return; + } + + PowerShell pwsh = (PowerShell)self; + pwsh.InvocationStateChanged -= handler; + pwsh.Dispose(); + } + + pwsh.InvocationStateChanged += handler; + } + public static Collection InvokeAndClear(this PowerShell pwsh, PSInvocationSettings invocationSettings = null) { try diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/RunspaceExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/RunspaceExtensions.cs index 0a5076e57..2cbe83a8e 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Utility/RunspaceExtensions.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/RunspaceExtensions.cs @@ -56,6 +56,17 @@ public static string GetRemotePrompt(this Runspace runspace, string basePrompt) return s_getRemotePromptFunc(runspace, basePrompt); } + public static void ThrowCancelledIfUnusable(this Runspace runspace) + => runspace.RunspaceStateInfo.ThrowCancelledIfUnusable(); + + public static void ThrowCancelledIfUnusable(this RunspaceStateInfo runspaceStateInfo) + { + if (!IsUsable(runspaceStateInfo)) + { + throw new OperationCanceledException(); + } + } + public static bool IsUsable(this RunspaceStateInfo runspaceStateInfo) { switch (runspaceStateInfo.State) From aa76735e65cb2a3e89a0b0acffeb6bc99588d688 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Tue, 12 Apr 2022 16:33:39 -0400 Subject: [PATCH 03/12] Use FilterText instead of Label for Assert If multiple Write-Host's exist then this test will fail. FilterText contains the module qualified name when dupes are present. --- .../LanguageServerProtocolMessageTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index 5214d8cf5..d868e9a71 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; @@ -963,7 +963,7 @@ public async Task CanSendCompletionAndCompletionResolveRequestAsync() }); CompletionItem completionItem = Assert.Single(completionItems, - completionItem1 => completionItem1.Label == "Write-Host"); + completionItem1 => completionItem1.FilterText == "Write-Host"); CompletionItem updatedCompletionItem = await PsesLanguageClient .SendRequest("completionItem/resolve", completionItem) From 64e9e10f148feecfc721572f44a83ffbed5fd31a Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Tue, 12 Apr 2022 16:36:25 -0400 Subject: [PATCH 04/12] Use Contains instead of Collection in assert If the testing machine has a pester template installed the collection will have an extra item. --- .../LanguageServerProtocolMessageTests.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index d868e9a71..94506d2c0 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -1103,9 +1103,8 @@ await PsesLanguageClient }) .Returning(CancellationToken.None).ConfigureAwait(true); - Assert.Collection(getProjectTemplatesResponse.Templates.OrderBy(t => t.Title), - template1 => Assert.Equal("AddPSScriptAnalyzerSettings", template1.Title), - template2 => Assert.Equal("New PowerShell Manifest Module", template2.Title)); + Assert.Contains(getProjectTemplatesResponse.Templates, t => t.Title is "AddPSScriptAnalyzerSettings"); + Assert.Contains(getProjectTemplatesResponse.Templates, t => t.Title is "New PowerShell Manifest Module"); } [SkippableFact] From bc0e68fcad042226831d5ea08db07872b5421410 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Tue, 12 Apr 2022 16:39:06 -0400 Subject: [PATCH 05/12] Add some safety to disposals --- .../Execution/SynchronousPowerShellTask.cs | 21 +++++++++++++------ .../PowerShell/Host/PsesInternalHost.cs | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs index e0a18c2f6..a30e4f991 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs @@ -333,7 +333,7 @@ private void StopDebuggerIfRemoteDebugSessionFailed() private void CancelNormalExecution() { - if (!_pwsh.Runspace.RunspaceStateInfo.IsUsable()) + if (_pwsh?.Runspace?.RunspaceStateInfo?.IsUsable() is false) { return; } @@ -341,23 +341,32 @@ private void CancelNormalExecution() // If we're signaled to exit a runspace then that'll trigger a stop, // if we block on that stop we'll never exit the runspace ( // and essentially deadlock). - if (_frame.SessionExiting) + if (_frame?.SessionExiting is true) { - _pwsh.BeginStop(null, null); + _pwsh?.BeginStop(null, null); return; } - _pwsh.Stop(); + try + { + _pwsh?.Stop(); + } + catch (NullReferenceException nre) + { + _logger.LogError( + nre, + "Null reference exception from PowerShell.Stop received."); + } } private void CancelDebugExecution() { - if (!_pwsh.Runspace.RunspaceStateInfo.IsUsable()) + if (_pwsh?.Runspace?.RunspaceStateInfo?.IsUsable() is false) { return; } - _pwsh.Runspace.Debugger.StopProcessCommand(); + _pwsh?.Runspace?.Debugger?.StopProcessCommand(); } } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index c1acccb43..05304cb10 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -619,7 +619,7 @@ private void PopPowerShellImpl(Action action = null) frame.Dispose(); } } - while (CurrentFrame.IsAwaitingPop); + while (_psFrameStack.Count > 0 && CurrentFrame.IsAwaitingPop); } private void RunTopLevelExecutionLoop() From 09954c8e75a66d0c6be4a89398f5938e9fd32d77 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Tue, 12 Apr 2022 16:40:40 -0400 Subject: [PATCH 06/12] Skip CLM E2E tests in unelevated process --- PowerShellEditorServices.build.ps1 | 5 +++++ .../LanguageServerProtocolMessageTests.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 716441da3..ae3618cc3 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -199,6 +199,11 @@ task TestE2E Build, SetupHelpForTests, { # Run E2E tests in ConstrainedLanguage mode. if (!$script:IsNix) { + if (-not [Security.Principal.WindowsIdentity]::GetCurrent().Owner.IsWellKnown("BuiltInAdministratorsSid")) { + Write-Warning 'Skipping E2E CLM tests as they must be ran in an elevated process.' + return + } + try { [System.Environment]::SetEnvironmentVariable("__PSLockdownPolicy", "0x80000007", [System.EnvironmentVariableTarget]::Machine); exec { & dotnet $script:dotnetTestArgs $script:NetRuntime.PS7 } diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index 94506d2c0..d5c733563 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; From 7b64f6df2b24b54bbec13da90cf315c4aee6f95d Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Mon, 18 Apr 2022 14:53:21 -0400 Subject: [PATCH 07/12] Add preset ExecutionOptions.ImmediateInteractive --- .../Handlers/LaunchAndAttachHandler.cs | 33 +++++++++---------- .../PowerShell/Execution/ExecutionOptions.cs | 7 ++++ 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs index 59fa75a7c..621e59837 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs @@ -251,7 +251,11 @@ public async Task Handle(PsesAttachRequestArguments request, Can try { - await _executionService.ExecutePSCommandAsync(enterPSSessionCommand, cancellationToken).ConfigureAwait(false); + await _executionService.ExecutePSCommandAsync( + enterPSSessionCommand, + cancellationToken, + PowerShellExecutionOptions.ImmediateInteractive) + .ConfigureAwait(false); } catch (Exception e) { @@ -292,13 +296,8 @@ void RunspaceChangedHandler(object s, RunspaceChangedEventArgs _) await _executionService.ExecutePSCommandAsync( enterPSHostProcessCommand, cancellationToken, - new PowerShellExecutionOptions() - { - MustRunInForeground = true, - InterruptCurrentForeground = true, - AddToHistory = false, - Priority = ExecutionPriority.Next, - }).ConfigureAwait(false); + PowerShellExecutionOptions.ImmediateInteractive) + .ConfigureAwait(false); } catch (Exception e) { @@ -320,7 +319,11 @@ await _executionService.ExecutePSCommandAsync( try { - await _executionService.ExecutePSCommandAsync(enterPSHostProcessCommand, cancellationToken).ConfigureAwait(false); + await _executionService.ExecutePSCommandAsync( + enterPSHostProcessCommand, + cancellationToken, + PowerShellExecutionOptions.ImmediateInteractive) + .ConfigureAwait(false); } catch (Exception e) { @@ -406,8 +409,8 @@ await _executionService.ExecutePSCommandAsync( _debugService.IsDebuggingRemoteRunspace = true; _debugStateService.WaitingForAttach = true; Task nonAwaitedTask = _executionService - .ExecutePSCommandAsync(debugRunspaceCmd, CancellationToken.None) - .ContinueWith(OnExecutionCompletedAsync); + .ExecutePSCommandAsync(debugRunspaceCmd, CancellationToken.None, PowerShellExecutionOptions.ImmediateInteractive) + .ContinueWith( OnExecutionCompletedAsync, TaskScheduler.Default); if (runspaceVersion.Version.Major >= 7) { @@ -469,12 +472,8 @@ private async Task OnExecutionCompletedAsync(Task executeTask) await _executionService.ExecutePSCommandAsync( new PSCommand().AddCommand("Exit-PSHostProcess"), CancellationToken.None, - new PowerShellExecutionOptions() - { - MustRunInForeground = true, - InterruptCurrentForeground = true, - Priority = ExecutionPriority.Next, - }).ConfigureAwait(false); + PowerShellExecutionOptions.ImmediateInteractive) + .ConfigureAwait(false); if (_debugStateService.IsRemoteAttach && _runspaceContext.CurrentRunspace.RunspaceOrigin != RunspaceOrigin.Local) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionOptions.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionOptions.cs index af5d9e7e9..27bb8935a 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionOptions.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionOptions.cs @@ -22,6 +22,13 @@ public record ExecutionOptions public record PowerShellExecutionOptions : ExecutionOptions { + internal static PowerShellExecutionOptions ImmediateInteractive = new() + { + Priority = ExecutionPriority.Next, + MustRunInForeground = true, + InterruptCurrentForeground = true, + }; + public bool WriteOutputToHost { get; init; } public bool WriteInputToHost { get; init; } public bool ThrowOnError { get; init; } = true; From d3ca7b5b9d221cc2a7fa18ecf9fa0f5e48b796ea Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Mon, 18 Apr 2022 14:54:25 -0400 Subject: [PATCH 08/12] Cleanup extra null conditionals --- .../PowerShell/Execution/SynchronousPowerShellTask.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs index a30e4f991..edca38770 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs @@ -333,7 +333,7 @@ private void StopDebuggerIfRemoteDebugSessionFailed() private void CancelNormalExecution() { - if (_pwsh?.Runspace?.RunspaceStateInfo?.IsUsable() is false) + if (_pwsh.Runspace.RunspaceStateInfo.IsUsable()) { return; } @@ -343,13 +343,13 @@ private void CancelNormalExecution() // and essentially deadlock). if (_frame?.SessionExiting is true) { - _pwsh?.BeginStop(null, null); + _pwsh.BeginStop(null, null); return; } try { - _pwsh?.Stop(); + _pwsh.Stop(); } catch (NullReferenceException nre) { @@ -361,12 +361,12 @@ private void CancelNormalExecution() private void CancelDebugExecution() { - if (_pwsh?.Runspace?.RunspaceStateInfo?.IsUsable() is false) + if (_pwsh.Runspace.RunspaceStateInfo.IsUsable()) { return; } - _pwsh?.Runspace?.Debugger?.StopProcessCommand(); + _pwsh.Runspace.Debugger.StopProcessCommand(); } } } From 07f7df81a01bd8eb117bb452246bf13bfb163453 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Mon, 18 Apr 2022 14:55:40 -0400 Subject: [PATCH 09/12] Add comment describing disconnect code path --- .../Services/PowerShell/Debugging/PowerShellDebugContext.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs b/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs index 59b57c54f..73ba20239 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs @@ -130,6 +130,8 @@ public void SetDebugResuming(DebuggerResumeAction debuggerResumeAction, bool isD if (debuggerResumeAction is DebuggerResumeAction.Stop) { + // If we're disconnecting we want to unwind all the way back to the default, local + // state. So we use UnwindCallStack here to ensure every context frame is cancelled. if (isDisconnect) { _psesHost.UnwindCallStack(); From 209f280d426d359ca59e5149b00e80d73a0896d7 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Mon, 18 Apr 2022 15:07:22 -0400 Subject: [PATCH 10/12] Add Is* properties for context frame types --- .../PowerShell/Context/PowerShellContextFrame.cs | 8 ++++++++ .../Services/PowerShell/Host/PsesInternalHost.cs | 10 ++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellContextFrame.cs b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellContextFrame.cs index 64da5087e..2db852c6e 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellContextFrame.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellContextFrame.cs @@ -39,6 +39,14 @@ public PowerShellContextFrame(SMA.PowerShell powerShell, RunspaceInfo runspaceIn public PowerShellFrameType FrameType { get; } + public bool IsRepl => (FrameType & PowerShellFrameType.Repl) is not 0; + + public bool IsRemote => (FrameType & PowerShellFrameType.Remote) is not 0; + + public bool IsNested => (FrameType & PowerShellFrameType.Nested) is not 0; + + public bool IsDebug => (FrameType & PowerShellFrameType.Debug) is not 0; + public bool IsAwaitingPop { get; set; } public bool SessionExiting { get; set; } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index 05304cb10..7b4ce49a9 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -193,7 +193,7 @@ public void PushRunspace(Runspace runspace) // TODO: Handle exit code if needed public override void SetShouldExit(int exitCode) { - if ((CurrentFrame.FrameType & PowerShellFrameType.Remote) is not 0) + if (CurrentFrame.IsRemote) { // PopRunspace also calls SetExit. PopRunspace(); @@ -262,7 +262,7 @@ public void SetExit() // Can't exit from the top level of PSES // since if you do, you lose all LSP services PowerShellContextFrame frame = CurrentFrame; - if ((frame.FrameType & PowerShellFrameType.Repl) is 0 || _psFrameStack.Count <= 1) + if (!frame.IsRepl || _psFrameStack.Count <= 1) { return; } @@ -525,7 +525,7 @@ private void PushPowerShellAndMaybeRunLoop(PowerShellContextFrame frame, bool sk { RunTopLevelExecutionLoop(); } - else if ((frame.FrameType & PowerShellFrameType.Debug) != 0) + else if (frame.IsDebug) { RunDebugExecutionLoop(); } @@ -701,9 +701,7 @@ private void RunExecutionLoop(bool isForDebug = false) } if (_shouldExit - && (CurrentFrame.FrameType & PowerShellFrameType.Remote) is not 0 - && (CurrentFrame.FrameType & PowerShellFrameType.Repl) is not 0 - && (CurrentFrame.FrameType & PowerShellFrameType.Nested) is 0) + && CurrentFrame is { IsRemote: true, IsRepl: true, IsNested: false }) { _shouldExit = false; PopPowerShell(); From 857f37c33e4c2e8659ab3ef2a4db871ca300dacd Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Mon, 18 Apr 2022 15:09:17 -0400 Subject: [PATCH 11/12] Use the new Is* props in a few more places --- .../Services/PowerShell/Debugging/PowerShellDebugContext.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs b/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs index 73ba20239..68c4becf8 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs @@ -142,7 +142,7 @@ public void SetDebugResuming(DebuggerResumeAction debuggerResumeAction, bool isD return; } - if ((_psesHost.CurrentFrame.FrameType & PowerShellFrameType.Repl) is not 0) + if (_psesHost.CurrentFrame.IsRepl) { _psesHost.CancelCurrentTask(); } @@ -171,7 +171,7 @@ public void ProcessDebuggerResult(DebuggerCommandResults debuggerResult) // If a debugging command like `c` is specified in a nested remote // debugging prompt we need to unwind the nested execution loop. - if ((_psesHost.CurrentFrame.FrameType & PowerShellFrameType.Remote) is not 0) + if (_psesHost.CurrentFrame.IsRemote) { _psesHost.ForceSetExit(); } From ef2c529ad7896e3b00cfabd6a50cc9e4bc26168d Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Mon, 18 Apr 2022 15:28:19 -0400 Subject: [PATCH 12/12] Fix new analyzers --- .../PowerShell/Debugging/PowerShellDebugContext.cs | 1 - .../Execution/SynchronousPowerShellTask.cs | 2 +- .../Services/PowerShell/Host/PsesInternalHost.cs | 14 +++----------- .../PowerShell/Utility/PowerShellDebugDisplay.cs | 3 +++ 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs b/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs index 68c4becf8..878caf18d 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Context; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs index edca38770..636f18ce6 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs @@ -261,7 +261,7 @@ private IReadOnlyList ExecuteInDebugger(CancellationToken cancellationT throw; } - using PSDataCollection errorOutputCollection = new PSDataCollection(); + using PSDataCollection errorOutputCollection = new(); errorOutputCollection.DataAdding += (object sender, DataAddingEventArgs args) => _psesHost.UI.WriteLine(args.ItemAdded?.ToString()); diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index fcfe9980a..1917e3432 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -312,15 +312,9 @@ public Task InvokeTaskOnPipelineThreadAsync( public void CancelCurrentTask() => _cancellationContext.CancelCurrentTask(); - public void CancelIdleParentTask() - { - _cancellationContext.CancelIdleParentTask(); - } + public void CancelIdleParentTask() => _cancellationContext.CancelIdleParentTask(); - public void UnwindCallStack() - { - _cancellationContext.CancelCurrentTaskStack(); - } + public void UnwindCallStack() => _cancellationContext.CancelCurrentTaskStack(); public Task ExecuteDelegateAsync( string representation, @@ -465,9 +459,7 @@ internal PowerShellContextFrame PushPowerShellForExecution() } private void PushPowerShellAndRunLoop(PowerShell pwsh, PowerShellFrameType frameType, RunspaceInfo newRunspaceInfo = null) - { - PushPowerShellAndMaybeRunLoop(pwsh, frameType, newRunspaceInfo, skipRunLoop: false); - } + => PushPowerShellAndMaybeRunLoop(pwsh, frameType, newRunspaceInfo, skipRunLoop: false); private void PushPowerShellAndMaybeRunLoop( PowerShell pwsh, diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellDebugDisplay.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellDebugDisplay.cs index 2253f7375..4a1536ff0 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellDebugDisplay.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellDebugDisplay.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + #if DEBUG using System.Diagnostics; using SMA = System.Management.Automation;