Skip to content

Commit b0afd8c

Browse files
Generalize the execution busy status to all PowerShell tasks (#1928)
We generalized the "execution busy status" from just editor commands to any running PowerShell task.
1 parent bbdf98d commit b0afd8c

File tree

3 files changed

+72
-73
lines changed

3 files changed

+72
-73
lines changed

src/PowerShellEditorServices/Services/Extension/ExtensionService.cs

+1-52
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,6 @@
1414

1515
namespace Microsoft.PowerShell.EditorServices.Services.Extension
1616
{
17-
/// Enumerates the possible execution results that can occur after
18-
/// executing a command or script.
19-
/// </summary>
20-
internal enum ExecutionStatus
21-
{
22-
/// <summary>
23-
/// Indicates that execution has not yet started.
24-
/// </summary>
25-
Pending,
26-
27-
/// <summary>
28-
/// Indicates that the command is executing.
29-
/// </summary>
30-
Running,
31-
32-
/// <summary>
33-
/// Indicates that execution has failed.
34-
/// </summary>
35-
Failed,
36-
37-
/// <summary>
38-
/// Indicates that execution was aborted by the user.
39-
/// </summary>
40-
Aborted,
41-
42-
/// <summary>
43-
/// Indicates that execution completed successfully.
44-
/// </summary>
45-
Completed
46-
}
47-
4817
/// <summary>
4918
/// Provides a high-level service which enables PowerShell scripts
5019
/// and modules to extend the behavior of the host editor.
@@ -154,7 +123,6 @@ internal Task InitializeAsync()
154123
/// <exception cref="KeyNotFoundException">The command being invoked was not registered.</exception>
155124
public Task InvokeCommandAsync(string commandName, EditorContext editorContext, CancellationToken cancellationToken)
156125
{
157-
_languageServer?.SendNotification("powerShell/executionStatusChanged", ExecutionStatus.Pending);
158126
if (editorCommands.TryGetValue(commandName, out EditorCommand editorCommand))
159127
{
160128
PSCommand executeCommand = new PSCommand()
@@ -163,7 +131,6 @@ public Task InvokeCommandAsync(string commandName, EditorContext editorContext,
163131
.AddParameter("ArgumentList", new object[] { editorContext });
164132

165133
// This API is used for editor command execution so it requires the foreground.
166-
_languageServer?.SendNotification("powerShell/executionStatusChanged", ExecutionStatus.Running);
167134
return ExecutionService.ExecutePSCommandAsync(
168135
executeCommand,
169136
cancellationToken,
@@ -173,27 +140,9 @@ public Task InvokeCommandAsync(string commandName, EditorContext editorContext,
173140
WriteOutputToHost = !editorCommand.SuppressOutput,
174141
AddToHistory = !editorCommand.SuppressOutput,
175142
ThrowOnError = false,
176-
}).ContinueWith((Task executeTask) =>
177-
{
178-
ExecutionStatus status = ExecutionStatus.Failed;
179-
if (executeTask.IsCompleted)
180-
{
181-
status = ExecutionStatus.Completed;
182-
}
183-
else if (executeTask.IsCanceled)
184-
{
185-
status = ExecutionStatus.Aborted;
186-
}
187-
else if (executeTask.IsFaulted)
188-
{
189-
status = ExecutionStatus.Failed;
190-
}
191-
192-
_languageServer?.SendNotification("powerShell/executionStatusChanged", status);
193-
}, TaskScheduler.Default);
143+
});
194144
}
195145

196-
_languageServer?.SendNotification("powerShell/executionStatusChanged", ExecutionStatus.Failed);
197146
throw new KeyNotFoundException($"Editor command not found: '{commandName}'");
198147
}
199148

src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs

+9-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,14 @@
1616

1717
namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution
1818
{
19-
internal class SynchronousPowerShellTask<TResult> : SynchronousTask<IReadOnlyList<TResult>>
19+
internal interface ISynchronousPowerShellTask
20+
{
21+
PowerShellExecutionOptions PowerShellExecutionOptions { get; }
22+
23+
void MaybeAddToHistory();
24+
}
25+
26+
internal class SynchronousPowerShellTask<TResult> : SynchronousTask<IReadOnlyList<TResult>>, ISynchronousPowerShellTask
2027
{
2128
private static readonly PowerShellExecutionOptions s_defaultPowerShellExecutionOptions = new();
2229

@@ -353,7 +360,7 @@ private void CancelNormalExecution()
353360
}
354361
}
355362

356-
internal void MaybeAddToHistory()
363+
public void MaybeAddToHistory()
357364
{
358365
// Do not add PSES internal commands to history. Also exclude input that came from the
359366
// REPL (e.g. PSReadLine) as it handles history itself in that scenario.

src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs

+62-19
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ public void SetExit()
295295

296296
internal void ForceSetExit() => _shouldExit = true;
297297

298+
private void SetBusy(bool busy) => _languageServer?.SendNotification("powerShell/executionBusyStatus", busy);
299+
298300
private bool CancelForegroundAndPrepend(ISynchronousTask task, bool isIdle = false)
299301
{
300302
// NOTE: This causes foreground tasks to act like they have `ExecutionPriority.Next`.
@@ -313,9 +315,9 @@ private bool CancelForegroundAndPrepend(ISynchronousTask task, bool isIdle = fal
313315

314316
_skipNextPrompt = true;
315317

316-
if (task is SynchronousPowerShellTask<PSObject> psTask)
318+
if (task is ISynchronousPowerShellTask t)
317319
{
318-
psTask.MaybeAddToHistory();
320+
t.MaybeAddToHistory();
319321
}
320322

321323
using (_taskQueue.BlockConsumers())
@@ -334,6 +336,32 @@ private bool CancelForegroundAndPrepend(ISynchronousTask task, bool isIdle = fal
334336
return true;
335337
}
336338

339+
// This handles executing the task while also notifying the client that the pipeline is
340+
// currently busy with a PowerShell task. The extension indicates this with a spinner.
341+
private void ExecuteTaskSynchronously(ISynchronousTask task, CancellationToken cancellationToken)
342+
{
343+
// TODO: Simplify this logic.
344+
bool busy = false;
345+
if (task is ISynchronousPowerShellTask t
346+
&& (t.PowerShellExecutionOptions.AddToHistory
347+
|| t.PowerShellExecutionOptions.FromRepl))
348+
{
349+
busy = true;
350+
SetBusy(true);
351+
}
352+
try
353+
{
354+
task.ExecuteSynchronously(cancellationToken);
355+
}
356+
finally
357+
{
358+
if (busy)
359+
{
360+
SetBusy(false);
361+
}
362+
}
363+
}
364+
337365
public Task<T> InvokeTaskOnPipelineThreadAsync<T>(SynchronousTask<T> task)
338366
{
339367
if (CancelForegroundAndPrepend(task))
@@ -769,8 +797,13 @@ private void RunExecutionLoop(bool isForDebug = false)
769797
{
770798
try
771799
{
772-
task.ExecuteSynchronously(cancellationScope.CancellationToken);
800+
ExecuteTaskSynchronously(task, cancellationScope.CancellationToken);
773801
}
802+
// Our flaky extension command test seems to be such because sometimes another
803+
// task gets queued, and since it runs in the foreground it cancels that task.
804+
// Interactively, this happens in the first loop (with DoOneRepl) which catches
805+
// the cancellation exception, but when under test that is a no-op, so it
806+
// happens in this second loop. Hence we need to catch it here too.
774807
catch (OperationCanceledException e)
775808
{
776809
_logger.LogDebug(e, "Task {Task} was canceled!", task);
@@ -935,19 +968,27 @@ private string InvokeReadLine(CancellationToken cancellationToken)
935968
}
936969
}
937970

971+
// TODO: Should we actually be directly invoking input versus queueing it as a task like everything else?
938972
private void InvokeInput(string input, CancellationToken cancellationToken)
939973
{
940-
PSCommand command = new PSCommand().AddScript(input, useLocalScope: false);
941-
InvokePSCommand(
942-
command,
943-
new PowerShellExecutionOptions
944-
{
945-
AddToHistory = true,
946-
ThrowOnError = false,
947-
WriteOutputToHost = true,
948-
FromRepl = true,
949-
},
950-
cancellationToken);
974+
SetBusy(true);
975+
try
976+
{
977+
InvokePSCommand(
978+
new PSCommand().AddScript(input, useLocalScope: false),
979+
new PowerShellExecutionOptions
980+
{
981+
AddToHistory = true,
982+
ThrowOnError = false,
983+
WriteOutputToHost = true,
984+
FromRepl = true,
985+
},
986+
cancellationToken);
987+
}
988+
finally
989+
{
990+
SetBusy(false);
991+
}
951992
}
952993

953994
private void AddRunspaceEventHandlers(Runspace runspace)
@@ -1076,16 +1117,18 @@ private void OnPowerShellIdle(CancellationToken idleCancellationToken)
10761117
while (!cancellationScope.CancellationToken.IsCancellationRequested
10771118
&& _taskQueue.TryTake(out ISynchronousTask task))
10781119
{
1120+
// Tasks which require the foreground cannot run under this idle handler, so the
1121+
// current foreground tasks gets canceled, the new task gets prepended, and this
1122+
// handler returns.
10791123
if (CancelForegroundAndPrepend(task, isIdle: true))
10801124
{
10811125
return;
10821126
}
10831127

1084-
// If we're executing a task, we don't need to run an extra pipeline later for events
1085-
// TODO: This may not be a PowerShell task, so ideally we can differentiate that here.
1086-
// For now it's mostly true and an easy assumption to make.
1087-
runPipelineForEventProcessing = false;
1088-
task.ExecuteSynchronously(cancellationScope.CancellationToken);
1128+
// If we're executing a PowerShell task, we don't need to run an extra pipeline
1129+
// later for events.
1130+
runPipelineForEventProcessing = task is not ISynchronousPowerShellTask;
1131+
ExecuteTaskSynchronously(task, cancellationScope.CancellationToken);
10891132
}
10901133
}
10911134

0 commit comments

Comments
 (0)