Skip to content

Commit 1682410

Browse files
Attempt to fix issue with native apps and input
On Unix like platforms some native applications do not work properly if our event subscriber is active. I suspect this is due to PSReadLine querying cursor position prior to checking for events. I believe the cursor position response emitted is being read as input. I've attempted to fix this by hooking into PSHost.NotifyBeginApplication to temporarly remove the event subscriber, and PSHost.NotifyEndApplication to recreate it afterwards.
1 parent b51cc75 commit 1682410

File tree

3 files changed

+139
-29
lines changed

3 files changed

+139
-29
lines changed

src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs

+2
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ public override void NotifyBeginApplication()
271271
{
272272
Logger.Write(LogLevel.Verbose, "NotifyBeginApplication() called.");
273273
this.hostUserInterface.IsNativeApplicationRunning = true;
274+
this.powerShellContext.NotifyBeginApplication();
274275
}
275276

276277
/// <summary>
@@ -280,6 +281,7 @@ public override void NotifyEndApplication()
280281
{
281282
Logger.Write(LogLevel.Verbose, "NotifyEndApplication() called.");
282283
this.hostUserInterface.IsNativeApplicationRunning = false;
284+
this.powerShellContext.NotifyEndApplication();
283285
}
284286

285287
/// <summary>

src/PowerShellEditorServices/Session/InvocationEventQueue.cs

+112-29
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ namespace Microsoft.PowerShell.EditorServices.Session
1919
/// <summary>
2020
/// Provides the ability to take over the current pipeline in a runspace.
2121
/// </summary>
22-
internal class InvocationEventQueue
22+
internal class InvocationEventQueue : IDisposable
2323
{
2424
private const string ShouldProcessInExecutionThreadPropertyName = "ShouldProcessInExecutionThread";
2525

@@ -35,24 +35,125 @@ internal class InvocationEventQueue
3535

3636
private readonly PowerShellContext _powerShellContext;
3737

38+
private readonly ILogger _logger;
39+
40+
private bool _isDisposed;
41+
3842
private InvocationRequest _invocationRequest;
3943

40-
private SemaphoreSlim _lock = AsyncUtils.CreateSimpleLockingSemaphore();
44+
private PSEventSubscriber _onIdleSubscriber;
45+
46+
private SemaphoreSlim _pipelineRequestHandle = AsyncUtils.CreateSimpleLockingSemaphore();
47+
48+
private SemaphoreSlim _subscriberHandle = AsyncUtils.CreateSimpleLockingSemaphore();
4149

42-
private InvocationEventQueue(PowerShellContext powerShellContext, PromptNest promptNest)
50+
private InvocationEventQueue(
51+
PowerShellContext powerShellContext,
52+
PromptNest promptNest,
53+
ILogger logger)
4354
{
4455
_promptNest = promptNest;
4556
_powerShellContext = powerShellContext;
4657
_runspace = powerShellContext.CurrentRunspace.Runspace;
58+
_logger = logger;
59+
}
60+
61+
public void Dispose() => Dispose(true);
62+
63+
protected virtual void Dispose(bool disposing)
64+
{
65+
if (!_isDisposed)
66+
{
67+
return;
68+
}
69+
70+
if (disposing)
71+
{
72+
RemoveInvocationSubscriber();
73+
}
74+
75+
_isDisposed = true;
4776
}
4877

49-
internal static InvocationEventQueue Create(PowerShellContext powerShellContext, PromptNest promptNest)
78+
internal static InvocationEventQueue Create(
79+
PowerShellContext powerShellContext,
80+
PromptNest promptNest,
81+
ILogger logger)
5082
{
51-
var eventQueue = new InvocationEventQueue(powerShellContext, promptNest);
83+
var eventQueue = new InvocationEventQueue(powerShellContext, promptNest, logger);
5284
eventQueue.CreateInvocationSubscriber();
5385
return eventQueue;
5486
}
5587

88+
/// <summary>
89+
/// Creates a <see cref="PSEventSubscriber" /> subscribed the engine event
90+
/// <see cref="PSEngineEvent.OnIdle" /> that handles requests for pipeline thread access.
91+
/// </summary>
92+
/// <returns>
93+
/// The newly created <see cref="PSEventSubscriber" /> or an existing subscriber if
94+
/// creation already occurred.
95+
/// </returns>
96+
internal PSEventSubscriber CreateInvocationSubscriber()
97+
{
98+
_subscriberHandle.Wait();
99+
try
100+
{
101+
if (_onIdleSubscriber != null)
102+
{
103+
_logger.Write(
104+
LogLevel.Error,
105+
"An attempt to create the ReadLine OnIdle subscriber was made when one already exists.");
106+
return _onIdleSubscriber;
107+
}
108+
109+
_onIdleSubscriber = _runspace.Events.SubscribeEvent(
110+
source: null,
111+
eventName: PSEngineEvent.OnIdle,
112+
sourceIdentifier: PSEngineEvent.OnIdle,
113+
data: null,
114+
handlerDelegate: OnPowerShellIdle,
115+
supportEvent: true,
116+
forwardEvent: false);
117+
118+
SetSubscriberExecutionThreadWithReflection(_onIdleSubscriber);
119+
120+
_onIdleSubscriber.Unsubscribed += OnInvokerUnsubscribed;
121+
122+
return _onIdleSubscriber;
123+
}
124+
finally
125+
{
126+
_subscriberHandle.Release();
127+
}
128+
}
129+
130+
/// <summary>
131+
/// Unsubscribes the existing <see cref="PSEventSubscriber" /> handling pipeline thread
132+
/// access requests.
133+
/// </summary>
134+
internal void RemoveInvocationSubscriber()
135+
{
136+
_subscriberHandle.Wait();
137+
try
138+
{
139+
if (_onIdleSubscriber == null)
140+
{
141+
_logger.Write(
142+
LogLevel.Error,
143+
"An attempt to remove the ReadLine OnIdle subscriber was made before it was created.");
144+
return;
145+
}
146+
147+
_onIdleSubscriber.Unsubscribed -= OnInvokerUnsubscribed;
148+
_runspace.Events.UnsubscribeEvent(_onIdleSubscriber);
149+
_onIdleSubscriber = null;
150+
}
151+
finally
152+
{
153+
_subscriberHandle.Release();
154+
}
155+
}
156+
56157
/// <summary>
57158
/// Executes a command on the main pipeline thread through
58159
/// eventing. A <see cref="PSEngineEvent.OnIdle" /> event subscriber will
@@ -136,7 +237,7 @@ internal async Task InvokeOnPipelineThread(Action<PowerShell> invocationAction)
136237
private async Task WaitForExistingRequestAsync()
137238
{
138239
InvocationRequest existingRequest;
139-
await _lock.WaitAsync();
240+
await _pipelineRequestHandle.WaitAsync();
140241
try
141242
{
142243
existingRequest = _invocationRequest;
@@ -147,7 +248,7 @@ private async Task WaitForExistingRequestAsync()
147248
}
148249
finally
149250
{
150-
_lock.Release();
251+
_pipelineRequestHandle.Release();
151252
}
152253

153254
await existingRequest.Task;
@@ -156,22 +257,22 @@ private async Task WaitForExistingRequestAsync()
156257
private async Task SetInvocationRequestAsync(InvocationRequest request)
157258
{
158259
await WaitForExistingRequestAsync();
159-
await _lock.WaitAsync();
260+
await _pipelineRequestHandle.WaitAsync();
160261
try
161262
{
162263
_invocationRequest = request;
163264
}
164265
finally
165266
{
166-
_lock.Release();
267+
_pipelineRequestHandle.Release();
167268
}
168269

169270
_powerShellContext.ForcePSEventHandling();
170271
}
171272

172273
private void OnPowerShellIdle(object sender, EventArgs e)
173274
{
174-
if (!_lock.Wait(0))
275+
if (!_pipelineRequestHandle.Wait(0))
175276
{
176277
return;
177278
}
@@ -188,7 +289,7 @@ private void OnPowerShellIdle(object sender, EventArgs e)
188289
}
189290
finally
190291
{
191-
_lock.Release();
292+
_pipelineRequestHandle.Release();
192293
}
193294

194295
_promptNest.PushPromptContext();
@@ -202,24 +303,6 @@ private void OnPowerShellIdle(object sender, EventArgs e)
202303
}
203304
}
204305

205-
private PSEventSubscriber CreateInvocationSubscriber()
206-
{
207-
PSEventSubscriber subscriber = _runspace.Events.SubscribeEvent(
208-
source: null,
209-
eventName: PSEngineEvent.OnIdle,
210-
sourceIdentifier: PSEngineEvent.OnIdle,
211-
data: null,
212-
handlerDelegate: OnPowerShellIdle,
213-
supportEvent: true,
214-
forwardEvent: false);
215-
216-
SetSubscriberExecutionThreadWithReflection(subscriber);
217-
218-
subscriber.Unsubscribed += OnInvokerUnsubscribed;
219-
220-
return subscriber;
221-
}
222-
223306
private void OnInvokerUnsubscribed(object sender, PSEventUnsubscribedEventArgs e)
224307
{
225308
CreateInvocationSubscriber();

src/PowerShellEditorServices/Session/PowerShellContext.cs

+25
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,30 @@ await this.ExecuteCommand<object>(
962962
addToHistory: true);
963963
}
964964

965+
/// <summary>
966+
/// Called by the active <see cref="EditorServicesPSHost" /> to prepare for a native
967+
/// application execution.
968+
/// </summary>
969+
internal void NotifyBeginApplication()
970+
{
971+
// The OnIdle subscriber causes PSReadLine to query cursor position periodically. On
972+
// Unix based platforms this can cause native applications to read the cursor position
973+
// response query emitted to STDIN as input.
974+
this.InvocationEventQueue.RemoveInvocationSubscriber();
975+
}
976+
977+
/// <summary>
978+
/// Called by the active <see cref="EditorServicesPSHost" /> to cleanup after a native
979+
/// application execution.
980+
/// </summary>
981+
internal void NotifyEndApplication()
982+
{
983+
// The OnIdle subscriber causes PSReadLine to query cursor position periodically. On
984+
// Unix based platforms this can cause native applications to read the cursor position
985+
// response query emitted to STDIN as input.
986+
this.InvocationEventQueue.CreateInvocationSubscriber();
987+
}
988+
965989
/// <summary>
966990
/// Forces the <see cref="PromptContext" /> to trigger PowerShell event handling,
967991
/// reliquishing control of the pipeline thread during event processing.
@@ -1246,6 +1270,7 @@ private void ResumeDebugger(DebuggerResumeAction resumeAction, bool shouldWaitFo
12461270
public void Dispose()
12471271
{
12481272
this.PromptNest.Dispose();
1273+
this.InvocationEventQueue.Dispose();
12491274
this.SessionState = PowerShellContextState.Disposed;
12501275

12511276
// Clean up the active runspace

0 commit comments

Comments
 (0)