-
Notifications
You must be signed in to change notification settings - Fork 237
PSReadLine integration #672
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
5a6eba6
1930afe
7e26e4e
ac44055
d2e1ceb
a507705
a870ee2
190cc0c
49db2ba
379eee4
e16c823
cc62dab
7f2b5b8
e19afe6
afdfb43
3575c79
6a3f7c9
cc10b91
86ab115
b51cc75
1682410
d68fb70
2968d1f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
Adds classes that manage the state of the prompt, nested contexts, and multiple ReadLine implementations of varying complexity. (cherry picked from commit 7ca8b9b)
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
using System.Threading; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need the copyright header here too There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||
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(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,248 @@ | ||
using System; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need a copyright header here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto |
||
|
||
internal InvocationEventQueue(PowerShellContext powerShellContext, PromptNest promptNest) | ||
{ | ||
_promptNest = promptNest; | ||
_powerShellContext = powerShellContext; | ||
_runspace = powerShellContext.CurrentRunspace.Runspace; | ||
CreateInvocationSubscriber(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a personal preference against doing significant logic in the constructor (educated in part by Misko Hevery's testable code principles and just general Dependency Inversion). What do you think of spinning out the runspace creation into a static factory method? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean creating the subscriber? Sure I don't have any issues with that. |
||
} | ||
|
||
/// <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())); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So there's actually a specific reason why I have a habit of using using (var pwsh = PowerShell.Create())
{
pwsh.AddCommand("Get-ChildItem")
.AddParameter("Path", "C:\\")
.Invoke();
} Personally I think it ends up being more readable, and There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, interesting point, I see now where you are coming from. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can explain how this works? I see that pwsh is the parameter to the func but it's There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not for that section, but for |
||
|
||
try | ||
{ | ||
return await request.Results; | ||
} | ||
finally | ||
{ | ||
await SetInvocationRequestAsync(null); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the |
||
} | ||
} | ||
|
||
/// <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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So while PSReadLine is running it has full control of the pipeline. While it's waiting for a key it will timeout once every 300 milliseconds and check for PowerShell events to process. When it does that, we use the subscriber to take over the pipeline thread. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also worth mentioning the reason why being on the pipeline thread is important. When there's no active pipeline you can start PowerShell asynchronously on any thread. But while there's an active pipeline you need to be on the same thread, and you can't call |
||
} | ||
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't read closely enough to see if we're using this a lot, but if we are, we should probably cache the |
||
.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)); | ||
} | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
using System.Threading; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Copyright header There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||
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. | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this new file needs a licensing header (at least that is needed in the main PowerShell repo). Applies to other changes as well