diff --git a/src/PowerShellEditorServices/Session/ExecutionTarget.cs b/src/PowerShellEditorServices/Session/ExecutionTarget.cs
new file mode 100644
index 000000000..3a11f48c9
--- /dev/null
+++ b/src/PowerShellEditorServices/Session/ExecutionTarget.cs
@@ -0,0 +1,23 @@
+namespace Microsoft.PowerShell.EditorServices.Session
+{
+ ///
+ /// Represents the different API's available for executing commands.
+ ///
+ internal enum ExecutionTarget
+ {
+ ///
+ /// Indicates that the command should be invoked through the PowerShell debugger.
+ ///
+ Debugger,
+
+ ///
+ /// Indicates that the command should be invoked via an instance of the PowerShell class.
+ ///
+ PowerShell,
+
+ ///
+ /// Indicates that the command should be invoked through the PowerShell engine's event manager.
+ ///
+ InvocationEvent
+ }
+}
diff --git a/src/PowerShellEditorServices/Session/IPromptContext.cs b/src/PowerShellEditorServices/Session/IPromptContext.cs
new file mode 100644
index 000000000..cabc3cf48
--- /dev/null
+++ b/src/PowerShellEditorServices/Session/IPromptContext.cs
@@ -0,0 +1,62 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerShell.EditorServices.Session
+{
+ ///
+ /// Provides methods for interacting with implementations of ReadLine.
+ ///
+ public interface IPromptContext
+ {
+ ///
+ /// Read a string that has been input by the user.
+ ///
+ /// Indicates if ReadLine should act like a command REPL.
+ ///
+ /// The cancellation token can be used to cancel reading user input.
+ ///
+ ///
+ /// A task object that represents the completion of reading input. The Result property will
+ /// return the input string.
+ ///
+ Task InvokeReadLine(bool isCommandLine, CancellationToken cancellationToken);
+
+ ///
+ /// Performs any additional actions required to cancel the current ReadLine invocation.
+ ///
+ void AbortReadLine();
+
+ ///
+ /// Creates a task that completes when the current ReadLine invocation has been aborted.
+ ///
+ ///
+ /// A task object that represents the abortion of the current ReadLine invocation.
+ ///
+ Task AbortReadLineAsync();
+
+ ///
+ /// Blocks until the current ReadLine invocation has exited.
+ ///
+ void WaitForReadLineExit();
+
+ ///
+ /// Creates a task that completes when the current ReadLine invocation has exited.
+ ///
+ ///
+ /// A task object that represents the exit of the current ReadLine invocation.
+ ///
+ Task WaitForReadLineExitAsync();
+
+ ///
+ /// Adds the specified command to the history managed by the ReadLine implementation.
+ ///
+ /// The command to record.
+ void AddToHistory(string command);
+
+ ///
+ /// Forces the prompt handler to trigger PowerShell event handling, reliquishing control
+ /// of the pipeline thread during event processing.
+ ///
+ void ForcePSEventHandling();
+ }
+}
diff --git a/src/PowerShellEditorServices/Session/InvocationEventQueue.cs b/src/PowerShellEditorServices/Session/InvocationEventQueue.cs
new file mode 100644
index 000000000..a3a72316d
--- /dev/null
+++ b/src/PowerShellEditorServices/Session/InvocationEventQueue.cs
@@ -0,0 +1,248 @@
+using System;
+using System.Collections.Generic;
+using System.Management.Automation.Runspaces;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using System.Threading;
+
+namespace Microsoft.PowerShell.EditorServices.Session
+{
+ using System.Management.Automation;
+
+ ///
+ /// Provides the ability to take over the current pipeline in a runspace.
+ ///
+ internal class InvocationEventQueue
+ {
+ private readonly PromptNest _promptNest;
+ private readonly Runspace _runspace;
+ private readonly PowerShellContext _powerShellContext;
+ private InvocationRequest _invocationRequest;
+ private Task _currentWaitTask;
+ private SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
+
+ internal InvocationEventQueue(PowerShellContext powerShellContext, PromptNest promptNest)
+ {
+ _promptNest = promptNest;
+ _powerShellContext = powerShellContext;
+ _runspace = powerShellContext.CurrentRunspace.Runspace;
+ CreateInvocationSubscriber();
+ }
+
+ ///
+ /// Executes a command on the main pipeline thread through
+ /// eventing. A event subscriber will
+ /// be created that creates a nested PowerShell instance for
+ /// to utilize.
+ ///
+ ///
+ /// Avoid using this method directly if possible.
+ /// will route commands
+ /// through this method if required.
+ ///
+ /// The expected result type.
+ /// The to be executed.
+ ///
+ /// Error messages from PowerShell will be written to the .
+ ///
+ /// Specifies options to be used when executing this command.
+ ///
+ /// An awaitable which will provide results once the command
+ /// execution completes.
+ ///
+ internal async Task> ExecuteCommandOnIdle(
+ PSCommand psCommand,
+ StringBuilder errorMessages,
+ ExecutionOptions executionOptions)
+ {
+ var request = new PipelineExecutionRequest(
+ _powerShellContext,
+ psCommand,
+ errorMessages,
+ executionOptions);
+
+ await SetInvocationRequestAsync(
+ new InvocationRequest(
+ pwsh => request.Execute().GetAwaiter().GetResult()));
+
+ try
+ {
+ return await request.Results;
+ }
+ finally
+ {
+ await SetInvocationRequestAsync(null);
+ }
+ }
+
+ ///
+ /// Marshals a to run on the pipeline thread. A new
+ /// will be created for the invocation.
+ ///
+ ///
+ /// The to invoke on the pipeline thread. The nested
+ /// instance for the created
+ /// will be passed as an argument.
+ ///
+ ///
+ /// An awaitable that the caller can use to know when execution completes.
+ ///
+ internal async Task InvokeOnPipelineThread(Action invocationAction)
+ {
+ var request = new InvocationRequest(pwsh =>
+ {
+ using (_promptNest.GetRunspaceHandle(CancellationToken.None, isReadLine: false))
+ {
+ pwsh.Runspace = _runspace;
+ invocationAction(pwsh);
+ }
+ });
+
+ await SetInvocationRequestAsync(request);
+ try
+ {
+ await request.Task;
+ }
+ finally
+ {
+ await SetInvocationRequestAsync(null);
+ }
+ }
+
+ private async Task WaitForExistingRequestAsync()
+ {
+ InvocationRequest existingRequest;
+ await _lock.WaitAsync();
+ try
+ {
+ existingRequest = _invocationRequest;
+ if (existingRequest == null || existingRequest.Task.IsCompleted)
+ {
+ return;
+ }
+ }
+ finally
+ {
+ _lock.Release();
+ }
+
+ await existingRequest.Task;
+ }
+
+ private async Task SetInvocationRequestAsync(InvocationRequest request)
+ {
+ await WaitForExistingRequestAsync();
+ await _lock.WaitAsync();
+ try
+ {
+ _invocationRequest = request;
+ }
+ finally
+ {
+ _lock.Release();
+ }
+
+ _powerShellContext.ForcePSEventHandling();
+ }
+
+ private void OnPowerShellIdle(object sender, EventArgs e)
+ {
+ if (!_lock.Wait(0))
+ {
+ return;
+ }
+
+ InvocationRequest currentRequest = null;
+ try
+ {
+ if (_invocationRequest == null || System.Console.KeyAvailable)
+ {
+ return;
+ }
+
+ currentRequest = _invocationRequest;
+ }
+ finally
+ {
+ _lock.Release();
+ }
+
+ _promptNest.PushPromptContext();
+ try
+ {
+ currentRequest.Invoke(_promptNest.GetPowerShell());
+ }
+ finally
+ {
+ _promptNest.PopPromptContext();
+ }
+ }
+
+ private PSEventSubscriber CreateInvocationSubscriber()
+ {
+ PSEventSubscriber subscriber = _runspace.Events.SubscribeEvent(
+ source: null,
+ eventName: PSEngineEvent.OnIdle,
+ sourceIdentifier: PSEngineEvent.OnIdle,
+ data: null,
+ handlerDelegate: OnPowerShellIdle,
+ supportEvent: true,
+ forwardEvent: false);
+
+ SetSubscriberExecutionThreadWithReflection(subscriber);
+
+ subscriber.Unsubscribed += OnInvokerUnsubscribed;
+
+ return subscriber;
+ }
+
+ private void OnInvokerUnsubscribed(object sender, PSEventUnsubscribedEventArgs e)
+ {
+ CreateInvocationSubscriber();
+ }
+
+ private void SetSubscriberExecutionThreadWithReflection(PSEventSubscriber subscriber)
+ {
+ // We need to create the PowerShell object in the same thread so we can get a nested
+ // PowerShell. Without changes to PSReadLine directly, this is the only way to achieve
+ // that consistently. The alternative is to make the subscriber a script block and have
+ // that create and process the PowerShell object, but that puts us in a different
+ // SessionState and is a lot slower.
+
+ // This should be safe as PSReadline should be waiting for pipeline input due to the
+ // OnIdle event sent along with it.
+ typeof(PSEventSubscriber)
+ .GetProperty(
+ "ShouldProcessInExecutionThread",
+ BindingFlags.Instance | BindingFlags.NonPublic)
+ .SetValue(subscriber, true);
+ }
+
+ private class InvocationRequest : TaskCompletionSource
+ {
+ private readonly Action _invocationAction;
+
+ internal InvocationRequest(Action invocationAction)
+ {
+ _invocationAction = invocationAction;
+ }
+
+ internal void Invoke(PowerShell pwsh)
+ {
+ try
+ {
+ _invocationAction(pwsh);
+
+ // Ensure the result is set in another thread otherwise the caller
+ // may take over the pipeline thread.
+ System.Threading.Tasks.Task.Run(() => SetResult(true));
+ }
+ catch (Exception e)
+ {
+ System.Threading.Tasks.Task.Run(() => SetException(e));
+ }
+ }
+ }
+ }
+}
diff --git a/src/PowerShellEditorServices/Session/LegacyReadLineContext.cs b/src/PowerShellEditorServices/Session/LegacyReadLineContext.cs
new file mode 100644
index 000000000..5548f5d19
--- /dev/null
+++ b/src/PowerShellEditorServices/Session/LegacyReadLineContext.cs
@@ -0,0 +1,51 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.PowerShell.EditorServices.Console;
+
+namespace Microsoft.PowerShell.EditorServices.Session
+{
+ internal class LegacyReadLineContext : IPromptContext
+ {
+ private readonly ConsoleReadLine _legacyReadLine;
+
+ internal LegacyReadLineContext(PowerShellContext powerShellContext)
+ {
+ _legacyReadLine = new ConsoleReadLine(powerShellContext);
+ }
+
+ public Task AbortReadLineAsync()
+ {
+ return Task.FromResult(true);
+ }
+
+ public async Task InvokeReadLine(bool isCommandLine, CancellationToken cancellationToken)
+ {
+ return await _legacyReadLine.InvokeLegacyReadLine(isCommandLine, cancellationToken);
+ }
+
+ public Task WaitForReadLineExitAsync()
+ {
+ return Task.FromResult(true);
+ }
+
+ public void AddToHistory(string command)
+ {
+ // Do nothing, history is managed completely by the PowerShell engine in legacy ReadLine.
+ }
+
+ public void AbortReadLine()
+ {
+ // Do nothing, no additional actions are needed to cancel ReadLine.
+ }
+
+ public void WaitForReadLineExit()
+ {
+ // Do nothing, ReadLine cancellation is instant or not appliciable.
+ }
+
+ public void ForcePSEventHandling()
+ {
+ // Do nothing, the pipeline thread is not occupied by legacy ReadLine.
+ }
+ }
+}
diff --git a/src/PowerShellEditorServices/Session/PSReadLinePromptContext.cs b/src/PowerShellEditorServices/Session/PSReadLinePromptContext.cs
new file mode 100644
index 000000000..2fe44f68f
--- /dev/null
+++ b/src/PowerShellEditorServices/Session/PSReadLinePromptContext.cs
@@ -0,0 +1,216 @@
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using System;
+using System.Management.Automation.Runspaces;
+using Microsoft.PowerShell.EditorServices.Console;
+
+namespace Microsoft.PowerShell.EditorServices.Session {
+ using System.Management.Automation;
+
+ internal class PSReadLinePromptContext : IPromptContext {
+ private const string ReadLineScript = @"
+ [System.Diagnostics.DebuggerHidden()]
+ [System.Diagnostics.DebuggerStepThrough()]
+ param()
+ return [Microsoft.PowerShell.PSConsoleReadLine]::ReadLine(
+ $Host.Runspace,
+ $ExecutionContext,
+ $args[0])";
+
+ // private const string ReadLineScript = @"
+ // [System.Diagnostics.DebuggerHidden()]
+ // [System.Diagnostics.DebuggerStepThrough()]
+ // param(
+ // [Parameter(Mandatory)]
+ // [Threading.CancellationToken] $CancellationToken,
+
+ // [ValidateNotNull()]
+ // [runspace] $Runspace = $Host.Runspace,
+
+ // [ValidateNotNull()]
+ // [System.Management.Automation.EngineIntrinsics] $EngineIntrinsics = $ExecutionContext
+ // )
+ // end {
+ // if ($CancellationToken.IsCancellationRequested) {
+ // return [string]::Empty
+ // }
+
+ // return [Microsoft.PowerShell.PSConsoleReadLine]::ReadLine(
+ // $Runspace,
+ // $EngineIntrinsics,
+ // $CancellationToken)
+ // }";
+
+ private const string ReadLineInitScript = @"
+ [System.Diagnostics.DebuggerHidden()]
+ [System.Diagnostics.DebuggerStepThrough()]
+ param()
+ end {
+ $module = Get-Module -ListAvailable PSReadLine | Select-Object -First 1
+ if (-not $module -or $module.Version -lt ([version]'2.0.0')) {
+ return
+ }
+
+ Import-Module -ModuleInfo $module
+ return 'Microsoft.PowerShell.PSConsoleReadLine' -as [type]
+ }";
+
+ private readonly PowerShellContext _powerShellContext;
+
+ private PromptNest _promptNest;
+
+ private InvocationEventQueue _invocationEventQueue;
+
+ private ConsoleReadLine _consoleReadLine;
+
+ private CancellationTokenSource _readLineCancellationSource;
+
+ private PSReadLineProxy _readLineProxy;
+
+ internal PSReadLinePromptContext(
+ PowerShellContext powerShellContext,
+ PromptNest promptNest,
+ InvocationEventQueue invocationEventQueue,
+ PSReadLineProxy readLineProxy)
+ {
+ _promptNest = promptNest;
+ _powerShellContext = powerShellContext;
+ _invocationEventQueue = invocationEventQueue;
+ _consoleReadLine = new ConsoleReadLine(powerShellContext);
+ _readLineProxy = readLineProxy;
+
+ #if CoreCLR
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return;
+ }
+
+ _readLineProxy.OverrideReadKey(
+ intercept => ConsoleProxy.UnixReadKey(
+ intercept,
+ _readLineCancellationSource.Token));
+ #endif
+ }
+
+ internal static bool TryGetPSReadLineProxy(Runspace runspace, out PSReadLineProxy readLineProxy)
+ {
+ readLineProxy = null;
+ using (var pwsh = PowerShell.Create())
+ {
+ pwsh.Runspace = runspace;
+ var psReadLineType = pwsh
+ .AddScript(ReadLineInitScript)
+ .Invoke()
+ .FirstOrDefault();
+
+ if (psReadLineType == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ readLineProxy = new PSReadLineProxy(psReadLineType);
+ }
+ catch (InvalidOperationException)
+ {
+ // The Type we got back from PowerShell doesn't have the members we expected.
+ // Could be an older version, a custom build, or something a newer version with
+ // breaking changes.
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public async Task InvokeReadLine(bool isCommandLine, CancellationToken cancellationToken)
+ {
+ _readLineCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ var localTokenSource = _readLineCancellationSource;
+ if (localTokenSource.Token.IsCancellationRequested)
+ {
+ throw new TaskCanceledException();
+ }
+
+ try
+ {
+ if (!isCommandLine)
+ {
+ return await _consoleReadLine.InvokeLegacyReadLine(
+ false,
+ _readLineCancellationSource.Token);
+ }
+
+ var result = (await _powerShellContext.ExecuteCommand(
+ new PSCommand()
+ .AddScript(ReadLineScript)
+ .AddArgument(_readLineCancellationSource.Token),
+ null,
+ new ExecutionOptions()
+ {
+ WriteErrorsToHost = false,
+ WriteOutputToHost = false,
+ InterruptCommandPrompt = false,
+ AddToHistory = false,
+ IsReadLine = isCommandLine
+ }))
+ .FirstOrDefault();
+
+ return cancellationToken.IsCancellationRequested
+ ? string.Empty
+ : result;
+ }
+ finally
+ {
+ _readLineCancellationSource = null;
+ }
+ }
+
+ public void AbortReadLine()
+ {
+ if (_readLineCancellationSource == null)
+ {
+ return;
+ }
+
+ _readLineCancellationSource.Cancel();
+
+ WaitForReadLineExit();
+ }
+
+ public async Task AbortReadLineAsync() {
+ if (_readLineCancellationSource == null)
+ {
+ return;
+ }
+
+ _readLineCancellationSource.Cancel();
+
+ await WaitForReadLineExitAsync();
+ }
+
+ public void WaitForReadLineExit()
+ {
+ using (_promptNest.GetRunspaceHandle(CancellationToken.None, isReadLine: true))
+ { }
+ }
+
+ public async Task WaitForReadLineExitAsync () {
+ using (await _promptNest.GetRunspaceHandleAsync(CancellationToken.None, isReadLine: true))
+ { }
+ }
+
+ public void AddToHistory(string command)
+ {
+ _readLineProxy.AddToHistory(command);
+ }
+
+ public void ForcePSEventHandling()
+ {
+ _readLineProxy.ForcePSEventHandling();
+ }
+ }
+}
diff --git a/src/PowerShellEditorServices/Session/PSReadLineProxy.cs b/src/PowerShellEditorServices/Session/PSReadLineProxy.cs
new file mode 100644
index 000000000..165cd7e71
--- /dev/null
+++ b/src/PowerShellEditorServices/Session/PSReadLineProxy.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Reflection;
+
+namespace Microsoft.PowerShell.EditorServices.Session
+{
+ internal class PSReadLineProxy
+ {
+ private const string AddToHistoryMethodName = "AddToHistory";
+
+ private const string SetKeyHandlerMethodName = "SetKeyHandler";
+
+ private const string ReadKeyOverrideFieldName = "_readKeyOverride";
+
+ private const string VirtualTerminalTypeName = "Microsoft.PowerShell.Internal.VirtualTerminal";
+
+ private const string ForcePSEventHandlingMethodName = "ForcePSEventHandling";
+
+ private static readonly Type[] s_setKeyHandlerTypes = new Type[4]
+ {
+ typeof(string[]),
+ typeof(Action),
+ typeof(string),
+ typeof(string)
+ };
+
+ private static readonly Type[] s_addToHistoryTypes = new Type[1] { typeof(string) };
+
+ private readonly FieldInfo _readKeyOverrideField;
+
+ internal PSReadLineProxy(Type psConsoleReadLine)
+ {
+ ForcePSEventHandling =
+ (Action)GetMethod(
+ psConsoleReadLine,
+ ForcePSEventHandlingMethodName,
+ Type.EmptyTypes,
+ BindingFlags.Static | BindingFlags.NonPublic)
+ .CreateDelegate(typeof(Action));
+
+ AddToHistory =
+ (Action)GetMethod(
+ psConsoleReadLine,
+ AddToHistoryMethodName,
+ s_addToHistoryTypes)
+ .CreateDelegate(typeof(Action));
+
+ SetKeyHandler =
+ (Action, string, string>)GetMethod(
+ psConsoleReadLine,
+ SetKeyHandlerMethodName,
+ s_setKeyHandlerTypes)
+ .CreateDelegate(typeof(Action, string, string>));
+
+ _readKeyOverrideField = psConsoleReadLine.GetTypeInfo().Assembly
+ .GetType(VirtualTerminalTypeName)
+ ?.GetField(ReadKeyOverrideFieldName, BindingFlags.Static | BindingFlags.NonPublic);
+
+ if (_readKeyOverrideField == null)
+ {
+ throw new InvalidOperationException();
+ }
+ }
+
+ internal Action AddToHistory { get; }
+
+ internal Action, object>, string, string> SetKeyHandler { get; }
+
+ internal Action ForcePSEventHandling { get; }
+
+ internal void OverrideReadKey(Func readKeyFunc)
+ {
+ _readKeyOverrideField.SetValue(null, readKeyFunc);
+ }
+
+ private static MethodInfo GetMethod(
+ Type psConsoleReadLine,
+ string name,
+ Type[] types,
+ BindingFlags flags = BindingFlags.Public | BindingFlags.Static)
+ {
+ // Shouldn't need this compiler directive after switching to netstandard2.0
+ #if CoreCLR
+ var method = psConsoleReadLine.GetMethod(
+ name,
+ flags);
+ #else
+ var method = psConsoleReadLine.GetMethod(
+ name,
+ flags,
+ null,
+ types,
+ types.Length == 0 ? new ParameterModifier[0] : new[] { new ParameterModifier(types.Length) });
+ #endif
+
+ if (method == null)
+ {
+ throw new InvalidOperationException();
+ }
+
+ return method;
+ }
+ }
+}
diff --git a/src/PowerShellEditorServices/Session/PipelineExecutionRequest.cs b/src/PowerShellEditorServices/Session/PipelineExecutionRequest.cs
new file mode 100644
index 000000000..ce9781229
--- /dev/null
+++ b/src/PowerShellEditorServices/Session/PipelineExecutionRequest.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerShell.EditorServices.Session
+{
+ internal interface IPipelineExecutionRequest
+ {
+ Task Execute();
+
+ Task WaitTask { get; }
+ }
+
+ ///
+ /// Contains details relating to a request to execute a
+ /// command on the PowerShell pipeline thread.
+ ///
+ /// The expected result type of the execution.
+ internal class PipelineExecutionRequest : IPipelineExecutionRequest
+ {
+ private PowerShellContext _powerShellContext;
+ private PSCommand _psCommand;
+ private StringBuilder _errorMessages;
+ private ExecutionOptions _executionOptions;
+ private TaskCompletionSource> _resultsTask;
+
+ public Task> Results
+ {
+ get { return this._resultsTask.Task; }
+ }
+
+ public Task WaitTask { get { return Results; } }
+
+ public PipelineExecutionRequest(
+ PowerShellContext powerShellContext,
+ PSCommand psCommand,
+ StringBuilder errorMessages,
+ bool sendOutputToHost)
+ : this(
+ powerShellContext,
+ psCommand,
+ errorMessages,
+ new ExecutionOptions()
+ {
+ WriteOutputToHost = sendOutputToHost
+ })
+ { }
+
+
+ public PipelineExecutionRequest(
+ PowerShellContext powerShellContext,
+ PSCommand psCommand,
+ StringBuilder errorMessages,
+ ExecutionOptions executionOptions)
+ {
+ _powerShellContext = powerShellContext;
+ _psCommand = psCommand;
+ _errorMessages = errorMessages;
+ _executionOptions = executionOptions;
+ _resultsTask = new TaskCompletionSource>();
+ }
+
+ public async Task Execute()
+ {
+ var results =
+ await _powerShellContext.ExecuteCommand(
+ _psCommand,
+ _errorMessages,
+ _executionOptions);
+
+ var unusedTask = Task.Run(() => _resultsTask.SetResult(results));
+ // TODO: Deal with errors?
+ }
+ }
+}
diff --git a/src/PowerShellEditorServices/Session/PromptNest.cs b/src/PowerShellEditorServices/Session/PromptNest.cs
new file mode 100644
index 000000000..91813544b
--- /dev/null
+++ b/src/PowerShellEditorServices/Session/PromptNest.cs
@@ -0,0 +1,559 @@
+using System.Collections.Concurrent;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.PowerShell.EditorServices.Utility;
+
+namespace Microsoft.PowerShell.EditorServices.Session
+{
+ using System;
+ using System.Management.Automation;
+
+ ///
+ /// Represents the stack of contexts in which PowerShell commands can be invoked.
+ ///
+ internal class PromptNest : IDisposable
+ {
+ private ConcurrentStack _frameStack;
+
+ private PromptNestFrame _readLineFrame;
+
+ private IHostInput _consoleReader;
+
+ private PowerShellContext _powerShellContext;
+
+ private IVersionSpecificOperations _versionSpecificOperations;
+
+ private bool _isDisposed;
+
+ private object _syncObject = new object();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The to track prompt status for.
+ ///
+ ///
+ /// The instance for the first frame.
+ ///
+ ///
+ /// The input handler.
+ ///
+ ///
+ /// The for the calling
+ /// instance.
+ ///
+ ///
+ /// This constructor should only be called when
+ /// is set to the initial runspace.
+ ///
+ internal PromptNest(
+ PowerShellContext powerShellContext,
+ PowerShell initialPowerShell,
+ IHostInput consoleReader,
+ IVersionSpecificOperations versionSpecificOperations)
+ {
+ _versionSpecificOperations = versionSpecificOperations;
+ _consoleReader = consoleReader;
+ _powerShellContext = powerShellContext;
+ _frameStack = new ConcurrentStack();
+ _frameStack.Push(
+ new PromptNestFrame(
+ initialPowerShell,
+ NewHandleQueue()));
+
+ var readLineShell = PowerShell.Create();
+ readLineShell.Runspace = powerShellContext.CurrentRunspace.Runspace;
+ _readLineFrame = new PromptNestFrame(
+ readLineShell,
+ new AsyncQueue());
+
+ ReleaseRunspaceHandleImpl(isReadLine: true);
+ }
+
+ ///
+ /// Gets a value indicating whether the current frame was created by a debugger stop event.
+ ///
+ internal bool IsInDebugger => CurrentFrame.FrameType.HasFlag(PromptNestFrameType.Debug);
+
+ ///
+ /// Gets a value indicating whether the current frame was created for an out of process runspace.
+ ///
+ internal bool IsRemote => CurrentFrame.FrameType.HasFlag(PromptNestFrameType.Remote);
+
+ ///
+ /// Gets a value indicating whether the current frame was created by PSHost.EnterNestedPrompt().
+ ///
+ internal bool IsNestedPrompt => CurrentFrame.FrameType.HasFlag(PromptNestFrameType.NestedPrompt);
+
+ ///
+ /// Gets a value indicating the current number of frames managed by this PromptNest.
+ ///
+ internal int NestedPromptLevel => _frameStack.Count;
+
+ private PromptNestFrame CurrentFrame
+ {
+ get
+ {
+ _frameStack.TryPeek(out PromptNestFrame currentFrame);
+ return _isDisposed ? _readLineFrame : currentFrame;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ lock (_syncObject)
+ {
+ if (_isDisposed || !disposing)
+ {
+ return;
+ }
+
+ while (NestedPromptLevel > 1)
+ {
+ _consoleReader?.StopCommandLoop();
+ var currentFrame = CurrentFrame;
+ if (currentFrame.FrameType.HasFlag(PromptNestFrameType.Debug))
+ {
+ _versionSpecificOperations.StopCommandInDebugger(_powerShellContext);
+ currentFrame.ThreadController.StartThreadExit(DebuggerResumeAction.Stop);
+ currentFrame.WaitForFrameExit(CancellationToken.None);
+ continue;
+ }
+
+ if (currentFrame.FrameType.HasFlag(PromptNestFrameType.NestedPrompt))
+ {
+ _powerShellContext.ExitAllNestedPrompts();
+ continue;
+ }
+
+ currentFrame.PowerShell.BeginStop(null, null);
+ currentFrame.WaitForFrameExit(CancellationToken.None);
+ }
+
+ _consoleReader?.StopCommandLoop();
+ _readLineFrame.Dispose();
+ CurrentFrame.Dispose();
+ _frameStack.Clear();
+ _powerShellContext = null;
+ _consoleReader = null;
+ _isDisposed = true;
+ }
+ }
+
+ ///
+ /// Gets the for the current frame.
+ ///
+ ///
+ /// The for the current frame, or
+ /// if the current frame does not have one.
+ ///
+ internal ThreadController GetThreadController()
+ {
+ if (_isDisposed)
+ {
+ return null;
+ }
+
+ return CurrentFrame.IsThreadController ? CurrentFrame.ThreadController : null;
+ }
+
+ ///
+ /// Create a new and set it as the current frame.
+ ///
+ internal void PushPromptContext()
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ PushPromptContext(PromptNestFrameType.Normal);
+ }
+
+ ///
+ /// Create a new and set it as the current frame.
+ ///
+ /// The frame type.
+ internal void PushPromptContext(PromptNestFrameType frameType)
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ _frameStack.Push(
+ new PromptNestFrame(
+ frameType.HasFlag(PromptNestFrameType.Remote)
+ ? PowerShell.Create()
+ : PowerShell.Create(RunspaceMode.CurrentRunspace),
+ NewHandleQueue(),
+ frameType));
+ }
+
+ ///
+ /// Dispose of the current and revert to the previous frame.
+ ///
+ internal void PopPromptContext()
+ {
+ PromptNestFrame currentFrame;
+ lock (_syncObject)
+ {
+ if (_isDisposed || _frameStack.Count == 1)
+ {
+ return;
+ }
+
+ _frameStack.TryPop(out currentFrame);
+ }
+
+ currentFrame.Dispose();
+ }
+
+ ///
+ /// Get the instance for the current
+ /// .
+ ///
+ /// Indicates whether this is for a PSReadLine command.
+ /// The instance for the current frame.
+ internal PowerShell GetPowerShell(bool isReadLine = false)
+ {
+ if (_isDisposed)
+ {
+ return null;
+ }
+
+ // Typically we want to run PSReadLine on the current nest frame.
+ // The exception is when the current frame is remote, in which
+ // case we need to run it in it's own frame because we can't take
+ // over a remote pipeline through event invocation.
+ if (NestedPromptLevel > 1 && !IsRemote)
+ {
+ return CurrentFrame.PowerShell;
+ }
+
+ return isReadLine ? _readLineFrame.PowerShell : CurrentFrame.PowerShell;
+ }
+
+ ///
+ /// Get the for the current .
+ ///
+ ///
+ /// The that can be used to cancel the request.
+ ///
+ /// Indicates whether this is for a PSReadLine command.
+ /// The for the current frame.
+ internal RunspaceHandle GetRunspaceHandle(CancellationToken cancellationToken, bool isReadLine)
+ {
+ if (_isDisposed)
+ {
+ return null;
+ }
+
+ // Also grab the main runspace handle if this is for a ReadLine pipeline and the runspace
+ // is in process.
+ if (isReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess())
+ {
+ GetRunspaceHandleImpl(cancellationToken, isReadLine: false);
+ }
+
+ return GetRunspaceHandleImpl(cancellationToken, isReadLine);
+ }
+
+
+ ///
+ /// Get the for the current .
+ ///
+ ///
+ /// The that will be checked prior to
+ /// completing the returned task.
+ ///
+ /// Indicates whether this is for a PSReadLine command.
+ ///
+ /// A object representing the asynchronous operation.
+ /// The property will return the
+ /// for the current frame.
+ ///
+ internal async Task GetRunspaceHandleAsync(CancellationToken cancellationToken, bool isReadLine)
+ {
+ if (_isDisposed)
+ {
+ return null;
+ }
+
+ // Also grab the main runspace handle if this is for a ReadLine pipeline and the runspace
+ // is in process.
+ if (isReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess())
+ {
+ await GetRunspaceHandleImplAsync(cancellationToken, isReadLine: false);
+ }
+
+ return await GetRunspaceHandleImplAsync(cancellationToken, isReadLine);
+ }
+
+ ///
+ /// Releases control of the aquired via the
+ /// .
+ ///
+ ///
+ /// The representing the control to release.
+ ///
+ internal void ReleaseRunspaceHandle(RunspaceHandle runspaceHandle)
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ ReleaseRunspaceHandleImpl(runspaceHandle.IsReadLine);
+ if (runspaceHandle.IsReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess())
+ {
+ ReleaseRunspaceHandleImpl(isReadLine: false);
+ }
+ }
+
+ ///
+ /// Releases control of the aquired via the
+ /// .
+ ///
+ ///
+ /// The representing the control to release.
+ ///
+ ///
+ /// A object representing the release of the
+ /// .
+ ///
+ internal async Task ReleaseRunspaceHandleAsync(RunspaceHandle runspaceHandle)
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ await ReleaseRunspaceHandleImplAsync(runspaceHandle.IsReadLine);
+ if (runspaceHandle.IsReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess())
+ {
+ await ReleaseRunspaceHandleImplAsync(isReadLine: false);
+ }
+ }
+
+ ///
+ /// Determines if the current frame is unavailable for commands.
+ ///
+ ///
+ /// A value indicating whether the current frame is unavailable for commands.
+ ///
+ internal bool IsMainThreadBusy()
+ {
+ return !_isDisposed && CurrentFrame.Queue.IsEmpty;
+ }
+
+ ///
+ /// Determines if a PSReadLine command is currently running.
+ ///
+ ///
+ /// A value indicating whether a PSReadLine command is currently running.
+ ///
+ internal bool IsReadLineBusy()
+ {
+ return !_isDisposed && _readLineFrame.Queue.IsEmpty;
+ }
+
+ ///
+ /// Blocks until the current frame has been disposed.
+ ///
+ ///
+ /// A delegate that when invoked initates the exit of the current frame.
+ ///
+ internal void WaitForCurrentFrameExit(Action initiator)
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ var currentFrame = CurrentFrame;
+ try
+ {
+ initiator.Invoke(currentFrame);
+ }
+ finally
+ {
+ currentFrame.WaitForFrameExit(CancellationToken.None);
+ }
+ }
+
+ ///
+ /// Blocks until the current frame has been disposed.
+ ///
+ internal void WaitForCurrentFrameExit()
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ CurrentFrame.WaitForFrameExit(CancellationToken.None);
+ }
+
+ ///
+ /// Blocks until the current frame has been disposed.
+ ///
+ ///
+ /// The used the exit the block prior to
+ /// the current frame being disposed.
+ ///
+ internal void WaitForCurrentFrameExit(CancellationToken cancellationToken)
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ CurrentFrame.WaitForFrameExit(cancellationToken);
+ }
+
+ ///
+ /// Creates a task that is completed when the current frame has been disposed.
+ ///
+ ///
+ /// A delegate that when invoked initates the exit of the current frame.
+ ///
+ ///
+ /// A object representing the current frame being disposed.
+ ///
+ internal async Task WaitForCurrentFrameExitAsync(Func initiator)
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ var currentFrame = CurrentFrame;
+ try
+ {
+ await initiator.Invoke(currentFrame);
+ }
+ finally
+ {
+ await currentFrame.WaitForFrameExitAsync(CancellationToken.None);
+ }
+ }
+
+ ///
+ /// Creates a task that is completed when the current frame has been disposed.
+ ///
+ ///
+ /// A delegate that when invoked initates the exit of the current frame.
+ ///
+ ///
+ /// A object representing the current frame being disposed.
+ ///
+ internal async Task WaitForCurrentFrameExitAsync(Action initiator)
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ var currentFrame = CurrentFrame;
+ try
+ {
+ initiator.Invoke(currentFrame);
+ }
+ finally
+ {
+ await currentFrame.WaitForFrameExitAsync(CancellationToken.None);
+ }
+ }
+
+ ///
+ /// Creates a task that is completed when the current frame has been disposed.
+ ///
+ ///
+ /// A object representing the current frame being disposed.
+ ///
+ internal async Task WaitForCurrentFrameExitAsync()
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ await WaitForCurrentFrameExitAsync(CancellationToken.None);
+ }
+
+ ///
+ /// Creates a task that is completed when the current frame has been disposed.
+ ///
+ ///
+ /// The used the exit the block prior to the current frame being disposed.
+ ///
+ ///
+ /// A object representing the current frame being disposed.
+ ///
+ internal async Task WaitForCurrentFrameExitAsync(CancellationToken cancellationToken)
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ await CurrentFrame.WaitForFrameExitAsync(cancellationToken);
+ }
+
+ private AsyncQueue NewHandleQueue()
+ {
+ var queue = new AsyncQueue();
+ queue.Enqueue(new RunspaceHandle(_powerShellContext));
+ return queue;
+ }
+
+ private RunspaceHandle GetRunspaceHandleImpl(CancellationToken cancellationToken, bool isReadLine)
+ {
+ if (isReadLine)
+ {
+ return _readLineFrame.Queue.Dequeue(cancellationToken);
+ }
+
+ return CurrentFrame.Queue.Dequeue(cancellationToken);
+ }
+
+ private async Task GetRunspaceHandleImplAsync(CancellationToken cancellationToken, bool isReadLine)
+ {
+ if (isReadLine)
+ {
+ return await _readLineFrame.Queue.DequeueAsync(cancellationToken);
+ }
+
+ return await CurrentFrame.Queue.DequeueAsync(cancellationToken);
+ }
+
+ private void ReleaseRunspaceHandleImpl(bool isReadLine)
+ {
+ if (isReadLine)
+ {
+ _readLineFrame.Queue.Enqueue(new RunspaceHandle(_powerShellContext, true));
+ return;
+ }
+
+ CurrentFrame.Queue.Enqueue(new RunspaceHandle(_powerShellContext, false));
+ }
+
+ private async Task ReleaseRunspaceHandleImplAsync(bool isReadLine)
+ {
+ if (isReadLine)
+ {
+ await _readLineFrame.Queue.EnqueueAsync(new RunspaceHandle(_powerShellContext, true));
+ return;
+ }
+
+ await CurrentFrame.Queue.EnqueueAsync(new RunspaceHandle(_powerShellContext, false));
+ }
+ }
+}
diff --git a/src/PowerShellEditorServices/Session/PromptNestFrame.cs b/src/PowerShellEditorServices/Session/PromptNestFrame.cs
new file mode 100644
index 000000000..7ced26e45
--- /dev/null
+++ b/src/PowerShellEditorServices/Session/PromptNestFrame.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.PowerShell.EditorServices.Utility;
+
+namespace Microsoft.PowerShell.EditorServices.Session
+{
+ using System.Management.Automation;
+
+ ///
+ /// Represents a single frame in the .
+ ///
+ internal class PromptNestFrame : IDisposable
+ {
+ private const PSInvocationState IndisposableStates = PSInvocationState.Stopping | PSInvocationState.Running;
+
+ private SemaphoreSlim _frameExited = new SemaphoreSlim(initialCount: 0);
+
+ private bool _isDisposed = false;
+
+ ///
+ /// Gets the instance.
+ ///
+ internal PowerShell PowerShell { get; }
+
+ ///
+ /// Gets the queue that controls command invocation order.
+ ///
+ internal AsyncQueue Queue { get; }
+
+ ///
+ /// Gets the frame type.
+ ///
+ internal PromptNestFrameType FrameType { get; }
+
+ ///
+ /// Gets the .
+ ///
+ internal ThreadController ThreadController { get; }
+
+ ///
+ /// Gets a value indicating whether the frame requires command invocations
+ /// to be routed to a specific thread.
+ ///
+ internal bool IsThreadController { get; }
+
+ internal PromptNestFrame(PowerShell powerShell, AsyncQueue handleQueue)
+ : this(powerShell, handleQueue, PromptNestFrameType.Normal)
+ { }
+
+ internal PromptNestFrame(
+ PowerShell powerShell,
+ AsyncQueue handleQueue,
+ PromptNestFrameType frameType)
+ {
+ PowerShell = powerShell;
+ Queue = handleQueue;
+ FrameType = frameType;
+ IsThreadController = (frameType & (PromptNestFrameType.Debug | PromptNestFrameType.NestedPrompt)) != 0;
+ if (!IsThreadController)
+ {
+ return;
+ }
+
+ ThreadController = new ThreadController(this);
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ if (IndisposableStates.HasFlag(PowerShell.InvocationStateInfo.State))
+ {
+ PowerShell.BeginStop(
+ asyncResult =>
+ {
+ PowerShell.Runspace = null;
+ PowerShell.Dispose();
+ },
+ null);
+ }
+ else
+ {
+ PowerShell.Runspace = null;
+ PowerShell.Dispose();
+ }
+
+ _frameExited.Release();
+ }
+
+ _isDisposed = true;
+ }
+
+ ///
+ /// Blocks until the frame has been disposed.
+ ///
+ ///
+ /// The that will exit the block when cancelled.
+ ///
+ internal void WaitForFrameExit(CancellationToken cancellationToken)
+ {
+ _frameExited.Wait(cancellationToken);
+ _frameExited.Release();
+ }
+
+ ///
+ /// Creates a task object that is completed when the frame has been disposed.
+ ///
+ ///
+ /// The that will be checked prior to completing
+ /// the returned task.
+ ///
+ ///
+ /// A object that represents this frame being disposed.
+ ///
+ internal async Task WaitForFrameExitAsync(CancellationToken cancellationToken)
+ {
+ await _frameExited.WaitAsync(cancellationToken);
+ _frameExited.Release();
+ }
+ }
+}
diff --git a/src/PowerShellEditorServices/Session/PromptNestFrameType.cs b/src/PowerShellEditorServices/Session/PromptNestFrameType.cs
new file mode 100644
index 000000000..55cf550b7
--- /dev/null
+++ b/src/PowerShellEditorServices/Session/PromptNestFrameType.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace Microsoft.PowerShell.EditorServices.Session
+{
+ [Flags]
+ internal enum PromptNestFrameType
+ {
+ Normal = 0,
+
+ NestedPrompt = 1,
+
+ Debug = 2,
+
+ Remote = 4
+ }
+}
diff --git a/src/PowerShellEditorServices/Session/ThreadController.cs b/src/PowerShellEditorServices/Session/ThreadController.cs
new file mode 100644
index 000000000..95fc85bb5
--- /dev/null
+++ b/src/PowerShellEditorServices/Session/ThreadController.cs
@@ -0,0 +1,126 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using System.Management.Automation.Runspaces;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.PowerShell.EditorServices.Utility;
+
+namespace Microsoft.PowerShell.EditorServices.Session
+{
+ ///
+ /// Provides the ability to route PowerShell command invocations to a specific thread.
+ ///
+ internal class ThreadController
+ {
+ private PromptNestFrame _nestFrame;
+
+ internal AsyncQueue PipelineRequestQueue { get; }
+
+ internal TaskCompletionSource FrameExitTask { get; }
+
+ internal int ManagedThreadId { get; }
+
+ internal bool IsPipelineThread { get; }
+
+ ///
+ /// Initializes an new instance of the ThreadController class. This constructor should only
+ /// ever been called from the thread it is meant to control.
+ ///
+ /// The parent PromptNestFrame object.
+ internal ThreadController(PromptNestFrame nestFrame)
+ {
+ _nestFrame = nestFrame;
+ PipelineRequestQueue = new AsyncQueue();
+ FrameExitTask = new TaskCompletionSource();
+ ManagedThreadId = Thread.CurrentThread.ManagedThreadId;
+
+ // If the debugger stop is triggered on a thread with no default runspace we
+ // shouldn't attempt to route commands to it.
+ IsPipelineThread = Runspace.DefaultRunspace != null;
+ }
+
+ ///
+ /// Determines if the caller is already on the thread that this object maintains.
+ ///
+ ///
+ /// A value indicating if the caller is already on the thread maintained by this object.
+ ///
+ internal bool IsCurrentThread()
+ {
+ return Thread.CurrentThread.ManagedThreadId == ManagedThreadId;
+ }
+
+ ///
+ /// Requests the invocation of a PowerShell command on the thread maintained by this object.
+ ///
+ /// The execution request to send.
+ ///
+ /// A task object representing the asynchronous operation. The Result property will return
+ /// the output of the command invocation.
+ ///
+ internal async Task> RequestPipelineExecution(
+ PipelineExecutionRequest executionRequest)
+ {
+ await PipelineRequestQueue.EnqueueAsync(executionRequest);
+ return await executionRequest.Results;
+ }
+
+ ///
+ /// Retrieves the first currently queued execution request. If there are no pending
+ /// execution requests then the task will be completed when one is requested.
+ ///
+ ///
+ /// A task object representing the asynchronous operation. The Result property will return
+ /// the retrieved pipeline execution request.
+ ///
+ internal async Task TakeExecutionRequest()
+ {
+ return await PipelineRequestQueue.DequeueAsync();
+ }
+
+ ///
+ /// Marks the thread to be exited.
+ ///
+ ///
+ /// The resume action for the debugger. If the frame is not a debugger frame this parameter
+ /// is ignored.
+ ///
+ internal void StartThreadExit(DebuggerResumeAction action)
+ {
+ StartThreadExit(action, waitForExit: false);
+ }
+
+ ///
+ /// Marks the thread to be exited.
+ ///
+ ///
+ /// The resume action for the debugger. If the frame is not a debugger frame this parameter
+ /// is ignored.
+ ///
+ ///
+ /// Indicates whether the method should block until the exit is completed.
+ ///
+ internal void StartThreadExit(DebuggerResumeAction action, bool waitForExit)
+ {
+ Task.Run(() => FrameExitTask.TrySetResult(action));
+ if (!waitForExit)
+ {
+ return;
+ }
+
+ _nestFrame.WaitForFrameExit(CancellationToken.None);
+ }
+
+ ///
+ /// Creates a task object that completes when the thread has be marked for exit.
+ ///
+ ///
+ /// A task object representing the frame receiving a request to exit. The Result property
+ /// will return the DebuggerResumeAction supplied with the request.
+ ///
+ internal async Task Exit()
+ {
+ return await FrameExitTask.Task.ConfigureAwait(false);
+ }
+ }
+}