Skip to content

Commit 7ca8b9b

Browse files
Add infrastructure for managing context
Adds classes that manage the state of the prompt, nested contexts, and multiple ReadLine implementations of varying complexity.
1 parent aa893b2 commit 7ca8b9b

11 files changed

+1612
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace Microsoft.PowerShell.EditorServices.Session
2+
{
3+
/// <summary>
4+
/// Represents the different API's available for executing commands.
5+
/// </summary>
6+
internal enum ExecutionTarget
7+
{
8+
/// <summary>
9+
/// Indicates that the command should be invoked through the PowerShell debugger.
10+
/// </summary>
11+
Debugger,
12+
13+
/// <summary>
14+
/// Indicates that the command should be invoked via an instance of the PowerShell class.
15+
/// </summary>
16+
PowerShell,
17+
18+
/// <summary>
19+
/// Indicates that the command should be invoked through the PowerShell engine's event manager.
20+
/// </summary>
21+
InvocationEvent
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using System.Threading;
2+
using System.Threading.Tasks;
3+
4+
namespace Microsoft.PowerShell.EditorServices.Session
5+
{
6+
/// <summary>
7+
/// Provides methods for interacting with implementations of ReadLine.
8+
/// </summary>
9+
public interface IPromptContext
10+
{
11+
/// <summary>
12+
/// Read a string that has been input by the user.
13+
/// </summary>
14+
/// <param name="isCommandLine">Indicates if ReadLine should act like a command REPL.</param>
15+
/// <param name="cancellationToken">
16+
/// The cancellation token can be used to cancel reading user input.
17+
/// </param>
18+
/// <returns>
19+
/// A task object that represents the completion of reading input. The Result property will
20+
/// return the input string.
21+
/// </returns>
22+
Task<string> InvokeReadLine(bool isCommandLine, CancellationToken cancellationToken);
23+
24+
/// <summary>
25+
/// Performs any additional actions required to cancel the current ReadLine invocation.
26+
/// </summary>
27+
void AbortReadLine();
28+
29+
/// <summary>
30+
/// Creates a task that completes when the current ReadLine invocation has been aborted.
31+
/// </summary>
32+
/// <returns>
33+
/// A task object that represents the abortion of the current ReadLine invocation.
34+
/// </returns>
35+
Task AbortReadLineAsync();
36+
37+
/// <summary>
38+
/// Blocks until the current ReadLine invocation has exited.
39+
/// </summary>
40+
void WaitForReadLineExit();
41+
42+
/// <summary>
43+
/// Creates a task that completes when the current ReadLine invocation has exited.
44+
/// </summary>
45+
/// <returns>
46+
/// A task object that represents the exit of the current ReadLine invocation.
47+
/// </returns>
48+
Task WaitForReadLineExitAsync();
49+
50+
/// <summary>
51+
/// Adds the specified command to the history managed by the ReadLine implementation.
52+
/// </summary>
53+
/// <param name="command">The command to record.</param>
54+
void AddToHistory(string command);
55+
56+
/// <summary>
57+
/// Forces the prompt handler to trigger PowerShell event handling, reliquishing control
58+
/// of the pipeline thread during event processing.
59+
/// </summary>
60+
void ForcePSEventHandling();
61+
}
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Management.Automation.Runspaces;
4+
using System.Reflection;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
using System.Threading;
8+
9+
namespace Microsoft.PowerShell.EditorServices.Session
10+
{
11+
using System.Management.Automation;
12+
13+
/// <summary>
14+
/// Provides the ability to take over the current pipeline in a runspace.
15+
/// </summary>
16+
internal class InvocationEventQueue
17+
{
18+
private readonly PromptNest _promptNest;
19+
private readonly Runspace _runspace;
20+
private readonly PowerShellContext _powerShellContext;
21+
private InvocationRequest _invocationRequest;
22+
private Task _currentWaitTask;
23+
private SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
24+
25+
internal InvocationEventQueue(PowerShellContext powerShellContext, PromptNest promptNest)
26+
{
27+
_promptNest = promptNest;
28+
_powerShellContext = powerShellContext;
29+
_runspace = powerShellContext.CurrentRunspace.Runspace;
30+
CreateInvocationSubscriber();
31+
}
32+
33+
/// <summary>
34+
/// Executes a command on the main pipeline thread through
35+
/// eventing. A <see cref="PSEngineEvent.OnIdle" /> event subscriber will
36+
/// be created that creates a nested PowerShell instance for
37+
/// <see cref="PowerShellContext.ExecuteCommand" /> to utilize.
38+
/// </summary>
39+
/// <remarks>
40+
/// Avoid using this method directly if possible.
41+
/// <see cref="PowerShellContext.ExecuteCommand" /> will route commands
42+
/// through this method if required.
43+
/// </remarks>
44+
/// <typeparam name="TResult">The expected result type.</typeparam>
45+
/// <param name="psCommand">The <see cref="PSCommand" /> to be executed.</param>
46+
/// <param name="errorMessages">
47+
/// Error messages from PowerShell will be written to the <see cref="StringBuilder" />.
48+
/// </param>
49+
/// <param name="executionOptions">Specifies options to be used when executing this command.</param>
50+
/// <returns>
51+
/// An awaitable <see cref="Task" /> which will provide results once the command
52+
/// execution completes.
53+
/// </returns>
54+
internal async Task<IEnumerable<TResult>> ExecuteCommandOnIdle<TResult>(
55+
PSCommand psCommand,
56+
StringBuilder errorMessages,
57+
ExecutionOptions executionOptions)
58+
{
59+
var request = new PipelineExecutionRequest<TResult>(
60+
_powerShellContext,
61+
psCommand,
62+
errorMessages,
63+
executionOptions);
64+
65+
await SetInvocationRequestAsync(
66+
new InvocationRequest(
67+
pwsh => request.Execute().GetAwaiter().GetResult()));
68+
69+
try
70+
{
71+
return await request.Results;
72+
}
73+
finally
74+
{
75+
await SetInvocationRequestAsync(null);
76+
}
77+
}
78+
79+
/// <summary>
80+
/// Marshals a <see cref="Action{PowerShell}" /> to run on the pipeline thread. A new
81+
/// <see cref="PromptNestFrame" /> will be created for the invocation.
82+
/// </summary>
83+
/// <param name="invocationAction">
84+
/// The <see cref="Action{PowerShell}" /> to invoke on the pipeline thread. The nested
85+
/// <see cref="PowerShell" /> instance for the created <see cref="PromptNestFrame" />
86+
/// will be passed as an argument.
87+
/// </param>
88+
/// <returns>
89+
/// An awaitable <see cref="Task" /> that the caller can use to know when execution completes.
90+
/// </returns>
91+
internal async Task InvokeOnPipelineThread(Action<PowerShell> invocationAction)
92+
{
93+
var request = new InvocationRequest(pwsh =>
94+
{
95+
using (_promptNest.GetRunspaceHandle(CancellationToken.None, isReadLine: false))
96+
{
97+
pwsh.Runspace = _runspace;
98+
invocationAction(pwsh);
99+
}
100+
});
101+
102+
await SetInvocationRequestAsync(request);
103+
try
104+
{
105+
await request.Task;
106+
}
107+
finally
108+
{
109+
await SetInvocationRequestAsync(null);
110+
}
111+
}
112+
113+
private async Task WaitForExistingRequestAsync()
114+
{
115+
InvocationRequest existingRequest;
116+
await _lock.WaitAsync();
117+
try
118+
{
119+
existingRequest = _invocationRequest;
120+
if (existingRequest == null || existingRequest.Task.IsCompleted)
121+
{
122+
return;
123+
}
124+
}
125+
finally
126+
{
127+
_lock.Release();
128+
}
129+
130+
await existingRequest.Task;
131+
}
132+
133+
private async Task SetInvocationRequestAsync(InvocationRequest request)
134+
{
135+
await WaitForExistingRequestAsync();
136+
await _lock.WaitAsync();
137+
try
138+
{
139+
_invocationRequest = request;
140+
}
141+
finally
142+
{
143+
_lock.Release();
144+
}
145+
146+
_powerShellContext.ForcePSEventHandling();
147+
}
148+
149+
private void OnPowerShellIdle(object sender, EventArgs e)
150+
{
151+
if (!_lock.Wait(0))
152+
{
153+
return;
154+
}
155+
156+
InvocationRequest currentRequest = null;
157+
try
158+
{
159+
if (_invocationRequest == null || System.Console.KeyAvailable)
160+
{
161+
return;
162+
}
163+
164+
currentRequest = _invocationRequest;
165+
}
166+
finally
167+
{
168+
_lock.Release();
169+
}
170+
171+
_promptNest.PushPromptContext();
172+
try
173+
{
174+
currentRequest.Invoke(_promptNest.GetPowerShell());
175+
}
176+
finally
177+
{
178+
_promptNest.PopPromptContext();
179+
}
180+
}
181+
182+
private PSEventSubscriber CreateInvocationSubscriber()
183+
{
184+
PSEventSubscriber subscriber = _runspace.Events.SubscribeEvent(
185+
source: null,
186+
eventName: PSEngineEvent.OnIdle,
187+
sourceIdentifier: PSEngineEvent.OnIdle,
188+
data: null,
189+
handlerDelegate: OnPowerShellIdle,
190+
supportEvent: true,
191+
forwardEvent: false);
192+
193+
SetSubscriberExecutionThreadWithReflection(subscriber);
194+
195+
subscriber.Unsubscribed += OnInvokerUnsubscribed;
196+
197+
return subscriber;
198+
}
199+
200+
private void OnInvokerUnsubscribed(object sender, PSEventUnsubscribedEventArgs e)
201+
{
202+
CreateInvocationSubscriber();
203+
}
204+
205+
private void SetSubscriberExecutionThreadWithReflection(PSEventSubscriber subscriber)
206+
{
207+
// We need to create the PowerShell object in the same thread so we can get a nested
208+
// PowerShell. Without changes to PSReadLine directly, this is the only way to achieve
209+
// that consistently. The alternative is to make the subscriber a script block and have
210+
// that create and process the PowerShell object, but that puts us in a different
211+
// SessionState and is a lot slower.
212+
213+
// This should be safe as PSReadline should be waiting for pipeline input due to the
214+
// OnIdle event sent along with it.
215+
typeof(PSEventSubscriber)
216+
.GetProperty(
217+
"ShouldProcessInExecutionThread",
218+
BindingFlags.Instance | BindingFlags.NonPublic)
219+
.SetValue(subscriber, true);
220+
}
221+
222+
private class InvocationRequest : TaskCompletionSource<bool>
223+
{
224+
private readonly Action<PowerShell> _invocationAction;
225+
226+
internal InvocationRequest(Action<PowerShell> invocationAction)
227+
{
228+
_invocationAction = invocationAction;
229+
}
230+
231+
internal void Invoke(PowerShell pwsh)
232+
{
233+
try
234+
{
235+
_invocationAction(pwsh);
236+
237+
// Ensure the result is set in another thread otherwise the caller
238+
// may take over the pipeline thread.
239+
System.Threading.Tasks.Task.Run(() => SetResult(true));
240+
}
241+
catch (Exception e)
242+
{
243+
System.Threading.Tasks.Task.Run(() => SetException(e));
244+
}
245+
}
246+
}
247+
}
248+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System.Threading;
2+
using System.Threading.Tasks;
3+
using Microsoft.PowerShell.EditorServices.Console;
4+
5+
namespace Microsoft.PowerShell.EditorServices.Session
6+
{
7+
internal class LegacyReadLineContext : IPromptContext
8+
{
9+
private readonly ConsoleReadLine _legacyReadLine;
10+
11+
internal LegacyReadLineContext(PowerShellContext powerShellContext)
12+
{
13+
_legacyReadLine = new ConsoleReadLine(powerShellContext);
14+
}
15+
16+
public Task AbortReadLineAsync()
17+
{
18+
return Task.FromResult(true);
19+
}
20+
21+
public async Task<string> InvokeReadLine(bool isCommandLine, CancellationToken cancellationToken)
22+
{
23+
return await _legacyReadLine.InvokeLegacyReadLine(isCommandLine, cancellationToken);
24+
}
25+
26+
public Task WaitForReadLineExitAsync()
27+
{
28+
return Task.FromResult(true);
29+
}
30+
31+
public void AddToHistory(string command)
32+
{
33+
// Do nothing, history is managed completely by the PowerShell engine in legacy ReadLine.
34+
}
35+
36+
public void AbortReadLine()
37+
{
38+
// Do nothing, no additional actions are needed to cancel ReadLine.
39+
}
40+
41+
public void WaitForReadLineExit()
42+
{
43+
// Do nothing, ReadLine cancellation is instant or not appliciable.
44+
}
45+
46+
public void ForcePSEventHandling()
47+
{
48+
// Do nothing, the pipeline thread is not occupied by legacy ReadLine.
49+
}
50+
}
51+
}

0 commit comments

Comments
 (0)