Skip to content

Commit

Permalink
Add infrastructure for managing context
Browse files Browse the repository at this point in the history
Adds classes that manage the state of the prompt, nested contexts,
and multiple ReadLine implementations of varying complexity.
  • Loading branch information
SeeminglyScience committed Jun 2, 2018
1 parent aa893b2 commit 7ca8b9b
Show file tree
Hide file tree
Showing 11 changed files with 1,612 additions and 0 deletions.
23 changes: 23 additions & 0 deletions src/PowerShellEditorServices/Session/ExecutionTarget.cs
@@ -0,0 +1,23 @@
namespace Microsoft.PowerShell.EditorServices.Session
{
/// <summary>
/// Represents the different API's available for executing commands.
/// </summary>
internal enum ExecutionTarget
{
/// <summary>
/// Indicates that the command should be invoked through the PowerShell debugger.
/// </summary>
Debugger,

/// <summary>
/// Indicates that the command should be invoked via an instance of the PowerShell class.
/// </summary>
PowerShell,

/// <summary>
/// Indicates that the command should be invoked through the PowerShell engine's event manager.
/// </summary>
InvocationEvent
}
}
62 changes: 62 additions & 0 deletions src/PowerShellEditorServices/Session/IPromptContext.cs
@@ -0,0 +1,62 @@
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.PowerShell.EditorServices.Session
{
/// <summary>
/// Provides methods for interacting with implementations of ReadLine.
/// </summary>
public interface IPromptContext
{
/// <summary>
/// Read a string that has been input by the user.
/// </summary>
/// <param name="isCommandLine">Indicates if ReadLine should act like a command REPL.</param>
/// <param name="cancellationToken">
/// The cancellation token can be used to cancel reading user input.
/// </param>
/// <returns>
/// A task object that represents the completion of reading input. The Result property will
/// return the input string.
/// </returns>
Task<string> InvokeReadLine(bool isCommandLine, CancellationToken cancellationToken);

/// <summary>
/// Performs any additional actions required to cancel the current ReadLine invocation.
/// </summary>
void AbortReadLine();

/// <summary>
/// Creates a task that completes when the current ReadLine invocation has been aborted.
/// </summary>
/// <returns>
/// A task object that represents the abortion of the current ReadLine invocation.
/// </returns>
Task AbortReadLineAsync();

/// <summary>
/// Blocks until the current ReadLine invocation has exited.
/// </summary>
void WaitForReadLineExit();

/// <summary>
/// Creates a task that completes when the current ReadLine invocation has exited.
/// </summary>
/// <returns>
/// A task object that represents the exit of the current ReadLine invocation.
/// </returns>
Task WaitForReadLineExitAsync();

/// <summary>
/// Adds the specified command to the history managed by the ReadLine implementation.
/// </summary>
/// <param name="command">The command to record.</param>
void AddToHistory(string command);

/// <summary>
/// Forces the prompt handler to trigger PowerShell event handling, reliquishing control
/// of the pipeline thread during event processing.
/// </summary>
void ForcePSEventHandling();
}
}
248 changes: 248 additions & 0 deletions 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;

/// <summary>
/// Provides the ability to take over the current pipeline in a runspace.
/// </summary>
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();
}

/// <summary>
/// Executes a command on the main pipeline thread through
/// eventing. A <see cref="PSEngineEvent.OnIdle" /> event subscriber will
/// be created that creates a nested PowerShell instance for
/// <see cref="PowerShellContext.ExecuteCommand" /> to utilize.
/// </summary>
/// <remarks>
/// Avoid using this method directly if possible.
/// <see cref="PowerShellContext.ExecuteCommand" /> will route commands
/// through this method if required.
/// </remarks>
/// <typeparam name="TResult">The expected result type.</typeparam>
/// <param name="psCommand">The <see cref="PSCommand" /> to be executed.</param>
/// <param name="errorMessages">
/// Error messages from PowerShell will be written to the <see cref="StringBuilder" />.
/// </param>
/// <param name="executionOptions">Specifies options to be used when executing this command.</param>
/// <returns>
/// An awaitable <see cref="Task" /> which will provide results once the command
/// execution completes.
/// </returns>
internal async Task<IEnumerable<TResult>> ExecuteCommandOnIdle<TResult>(
PSCommand psCommand,
StringBuilder errorMessages,
ExecutionOptions executionOptions)
{
var request = new PipelineExecutionRequest<TResult>(
_powerShellContext,
psCommand,
errorMessages,
executionOptions);

await SetInvocationRequestAsync(
new InvocationRequest(
pwsh => request.Execute().GetAwaiter().GetResult()));

try
{
return await request.Results;
}
finally
{
await SetInvocationRequestAsync(null);
}
}

/// <summary>
/// Marshals a <see cref="Action{PowerShell}" /> to run on the pipeline thread. A new
/// <see cref="PromptNestFrame" /> will be created for the invocation.
/// </summary>
/// <param name="invocationAction">
/// The <see cref="Action{PowerShell}" /> to invoke on the pipeline thread. The nested
/// <see cref="PowerShell" /> instance for the created <see cref="PromptNestFrame" />
/// will be passed as an argument.
/// </param>
/// <returns>
/// An awaitable <see cref="Task" /> that the caller can use to know when execution completes.
/// </returns>
internal async Task InvokeOnPipelineThread(Action<PowerShell> 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<bool>
{
private readonly Action<PowerShell> _invocationAction;

internal InvocationRequest(Action<PowerShell> 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));
}
}
}
}
}
51 changes: 51 additions & 0 deletions 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<string> 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.
}
}
}

0 comments on commit 7ca8b9b

Please sign in to comment.