From ec85f59e5a119582d5f484976350bf8e4595d847 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 12 Oct 2021 10:42:01 -0700 Subject: [PATCH 1/4] Implement PowerShell engine OnIdle events --- .../PowerShell/Host/PsesInternalHost.cs | 52 ++++++++++++++++--- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index 12fbbfdb7..3eeed44a6 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -55,6 +55,8 @@ internal class PsesInternalHost : PSHost, IHostSupportsInteractiveSession, IRuns private readonly IdempotentLatch _isRunningLatch = new(); + private EngineIntrinsics _mainRunspaceEngineIntrinsics; + private bool _shouldExit = false; private string _localComputerName; @@ -345,17 +347,18 @@ public Task SetInitialWorkingDirectoryAsync(string path, CancellationToken cance private void Run() { - (PowerShell pwsh, RunspaceInfo localRunspaceInfo) = CreateInitialPowerShellSession(); + (PowerShell pwsh, RunspaceInfo localRunspaceInfo, EngineIntrinsics engineIntrinsics) = CreateInitialPowerShellSession(); + _mainRunspaceEngineIntrinsics = engineIntrinsics; _localComputerName = localRunspaceInfo.SessionDetails.ComputerName; _runspaceStack.Push(new RunspaceFrame(pwsh.Runspace, localRunspaceInfo)); PushPowerShellAndRunLoop(pwsh, PowerShellFrameType.Normal, localRunspaceInfo); } - private (PowerShell, RunspaceInfo) CreateInitialPowerShellSession() + private (PowerShell, RunspaceInfo, EngineIntrinsics) CreateInitialPowerShellSession() { - PowerShell pwsh = CreateInitialPowerShell(_hostInfo, _readLineProvider); + (PowerShell pwsh, EngineIntrinsics engineIntrinsics) = CreateInitialPowerShell(_hostInfo, _readLineProvider); RunspaceInfo localRunspaceInfo = RunspaceInfo.CreateFromLocalPowerShell(_logger, pwsh); - return (pwsh, localRunspaceInfo); + return (pwsh, localRunspaceInfo, engineIntrinsics); } private void PushPowerShellAndRunLoop(SMA.PowerShell pwsh, PowerShellFrameType frameType, RunspaceInfo newRunspaceInfo = null) @@ -611,7 +614,7 @@ private static PowerShell CreatePowerShellForRunspace(Runspace runspace) return pwsh; } - public PowerShell CreateInitialPowerShell( + public (PowerShell, EngineIntrinsics) CreateInitialPowerShell( HostStartupInfo hostStartupInfo, ReadLineProvider readLineProvider) { @@ -647,7 +650,7 @@ public PowerShell CreateInitialPowerShell( } } - return pwsh; + return (pwsh, engineIntrinsics); } private Runspace CreateInitialRunspace(InitialSessionState initialSessionState) @@ -666,7 +669,27 @@ private Runspace CreateInitialRunspace(InitialSessionState initialSessionState) private void OnPowerShellIdle() { - if (_taskQueue.Count == 0) + IReadOnlyList eventSubscribers = _mainRunspaceEngineIntrinsics.Events.Subscribers; + + // Go through pending event subscribers and: + // - if we have any subscribers, ensure we process any events + // - if we have any idle events, generate an idle event and process that + bool runPipelineForEventProcessing = false; + foreach (PSEventSubscriber subscriber in eventSubscribers) + { + runPipelineForEventProcessing = true; + + if (string.Equals(subscriber.SourceIdentifier, PSEngineEvent.OnIdle, StringComparison.OrdinalIgnoreCase)) + { + // PowerShell thinks we're in a call (the ReadLine call) rather than idle, + // but we know we're sitting in the prompt. + // So we need to generate the idle event ourselves + _mainRunspaceEngineIntrinsics.Events.GenerateEvent(PSEngineEvent.OnIdle, sender: null, args: null, extraData: null); + break; + } + } + + if (!runPipelineForEventProcessing && _taskQueue.Count == 0) { return; } @@ -686,9 +709,21 @@ private void OnPowerShellIdle() return; } + // If we're executing a task, we don't need to run an extra pipeline later for events + // TODO: This may not be a PowerShell task, so ideally we can differentiate that here. + // For now it's mostly true and an easy assumption to make. + runPipelineForEventProcessing = false; task.ExecuteSynchronously(cancellationScope.CancellationToken); } } + + // We didn't end up executinng anything in the background, + // so we need to run a small artificial pipeline instead + // to force event processing + if (runPipelineForEventProcessing) + { + InvokePSCommand(new PSCommand().AddScript("0", useLocalScope: true), PowerShellExecutionOptions.Default, CancellationToken.None); + } } private void OnCancelKeyPress(object sender, ConsoleCancelEventArgs args) @@ -769,7 +804,8 @@ private Task PopOrReinitializeRunspaceAsync() // If our main runspace was corrupted, // we must re-initialize our state. // TODO: Use runspace.ResetRunspaceState() here instead - (PowerShell pwsh, RunspaceInfo runspaceInfo) = CreateInitialPowerShellSession(); + (PowerShell pwsh, RunspaceInfo runspaceInfo, EngineIntrinsics engineIntrinsics) = CreateInitialPowerShellSession(); + _mainRunspaceEngineIntrinsics = engineIntrinsics; PushPowerShell(new PowerShellContextFrame(pwsh, runspaceInfo, PowerShellFrameType.Normal)); _logger.LogError($"Top level runspace entered state '{oldRunspaceState.State}' for reason '{oldRunspaceState.Reason}' and was reinitialized." From e4730a12fe0c6ec819d04bef0bad3d74574e416d Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 12 Oct 2021 17:19:58 -0700 Subject: [PATCH 2/4] Improve artificial idle event comment --- .../Services/PowerShell/Host/PsesInternalHost.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index 3eeed44a6..ada728d1e 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -681,9 +681,9 @@ private void OnPowerShellIdle() if (string.Equals(subscriber.SourceIdentifier, PSEngineEvent.OnIdle, StringComparison.OrdinalIgnoreCase)) { - // PowerShell thinks we're in a call (the ReadLine call) rather than idle, - // but we know we're sitting in the prompt. - // So we need to generate the idle event ourselves + // We control the pipeline thread, so it's not possible for PowerShell to generate events while we're here. + // But we know we're sitting waiting for the prompt, so we generate the idle event ourselves + // and that will flush idle event subscribers in PowerShell so we can service them _mainRunspaceEngineIntrinsics.Events.GenerateEvent(PSEngineEvent.OnIdle, sender: null, args: null, extraData: null); break; } From fe6a142bdd6bc882dba34518ec871365d58210af Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 12 Oct 2021 17:20:21 -0700 Subject: [PATCH 3/4] Fix typo --- .../Services/PowerShell/Host/PsesInternalHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index ada728d1e..09805bd84 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -717,7 +717,7 @@ private void OnPowerShellIdle() } } - // We didn't end up executinng anything in the background, + // We didn't end up executing anything in the background, // so we need to run a small artificial pipeline instead // to force event processing if (runPipelineForEventProcessing) From 08df557eca01f9f405a8beea6a28e1ff0fe87e48 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 12 Oct 2021 17:24:19 -0700 Subject: [PATCH 4/4] Change BlockingConcurrentDequeue to provide IsEmpty property --- .../Services/PowerShell/Execution/BlockingConcurrentDeque.cs | 2 +- .../Services/PowerShell/Host/PsesInternalHost.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/BlockingConcurrentDeque.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/BlockingConcurrentDeque.cs index afaf59cc9..79339411b 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Execution/BlockingConcurrentDeque.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/BlockingConcurrentDeque.cs @@ -36,7 +36,7 @@ public BlockingConcurrentDeque() }; } - public int Count => _queues[0].Count + _queues[1].Count; + public bool IsEmpty => _queues[0].Count == 0 && _queues[1].Count == 0; public void Prepend(T item) { diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index 09805bd84..56ab05dda 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -689,7 +689,7 @@ private void OnPowerShellIdle() } } - if (!runPipelineForEventProcessing && _taskQueue.Count == 0) + if (!runPipelineForEventProcessing && _taskQueue.IsEmpty) { return; }