Skip to content

Commit 21e6b5f

Browse files
Rewrite command invocation operations for PSRL
Refactor PowerShellContext to have a more robust system for tracking the context in which commands are invoked. This is a significant change in that all interactions with the runspace must be done through methods in PowerShellContext. These changes also greatly increase stability.
1 parent 59bfa3b commit 21e6b5f

10 files changed

+832
-294
lines changed

src/PowerShellEditorServices.Host/EditorServicesHost.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ private EditorSession CreateSession(
354354
bool enableConsoleRepl)
355355
{
356356
EditorSession editorSession = new EditorSession(this.logger);
357-
PowerShellContext powerShellContext = new PowerShellContext(this.logger);
357+
PowerShellContext powerShellContext = new PowerShellContext(this.logger, this.featureFlags.Contains("PSReadLine"));
358358

359359
EditorServicesPSHostUserInterface hostUserInterface =
360360
enableConsoleRepl
@@ -394,7 +394,9 @@ private EditorSession CreateDebugSession(
394394
bool enableConsoleRepl)
395395
{
396396
EditorSession editorSession = new EditorSession(this.logger);
397-
PowerShellContext powerShellContext = new PowerShellContext(this.logger);
397+
PowerShellContext powerShellContext = new PowerShellContext(
398+
this.logger,
399+
this.featureFlags.Contains("PSReadLine"));
398400

399401
EditorServicesPSHostUserInterface hostUserInterface =
400402
enableConsoleRepl
@@ -444,4 +446,4 @@ private void CurrentDomain_UnhandledException(
444446

445447
#endregion
446448
}
447-
}
449+
}

src/PowerShellEditorServices/Session/ExecutionOptions.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ namespace Microsoft.PowerShell.EditorServices
1010
/// </summary>
1111
public class ExecutionOptions
1212
{
13+
private bool? _shouldExecuteInOriginalRunspace;
14+
1315
#region Properties
1416

1517
/// <summary>
@@ -38,6 +40,39 @@ public class ExecutionOptions
3840
/// </summary>
3941
public bool InterruptCommandPrompt { get; set; }
4042

43+
/// <summary>
44+
/// Gets or sets a value indicating whether the text of the command
45+
/// should be written to the host as if it was ran interactively.
46+
/// </summary>
47+
public bool WriteInputToHost { get; set; }
48+
49+
/// <summary>
50+
/// Gets or sets a value indicating whether the command to
51+
/// be executed is a console input prompt, such as the
52+
/// PSConsoleHostReadLine function.
53+
/// </summary>
54+
internal bool IsReadLine { get; set; }
55+
56+
/// <summary>
57+
/// Gets or sets a value indicating whether the command should
58+
/// be invoked in the original runspace. In the majority of cases
59+
/// this should remain unset.
60+
/// </summary>
61+
internal bool ShouldExecuteInOriginalRunspace
62+
{
63+
get
64+
{
65+
return
66+
_shouldExecuteInOriginalRunspace.HasValue
67+
? _shouldExecuteInOriginalRunspace.Value
68+
: IsReadLine;
69+
}
70+
set
71+
{
72+
_shouldExecuteInOriginalRunspace = value;
73+
}
74+
}
75+
4176
#endregion
4277

4378
#region Constructors
@@ -50,6 +85,7 @@ public ExecutionOptions()
5085
{
5186
this.WriteOutputToHost = true;
5287
this.WriteErrorsToHost = true;
88+
this.WriteInputToHost = false;
5389
this.AddToHistory = false;
5490
this.InterruptCommandPrompt = false;
5591
}

src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class EditorServicesPSHost : PSHost, IHostSupportsInteractiveSession
2626
private Guid instanceId = Guid.NewGuid();
2727
private EditorServicesPSHostUserInterface hostUserInterface;
2828
private IHostSupportsInteractiveSession hostSupportsInteractiveSession;
29+
private PowerShellContext powerShellContext;
2930

3031
#endregion
3132

@@ -55,6 +56,7 @@ public EditorServicesPSHost(
5556
this.hostDetails = hostDetails;
5657
this.hostUserInterface = hostUserInterface;
5758
this.hostSupportsInteractiveSession = powerShellContext;
59+
this.powerShellContext = powerShellContext;
5860
}
5961

6062
#endregion
@@ -78,7 +80,7 @@ public override string Name
7880
}
7981

8082
/// <summary>
81-
///
83+
///
8284
/// </summary>
8385
public override PSObject PrivateData
8486
{
@@ -126,15 +128,15 @@ public override PSHostUserInterface UI
126128
/// </summary>
127129
public override void EnterNestedPrompt()
128130
{
129-
Logger.Write(LogLevel.Verbose, "EnterNestedPrompt() called.");
131+
this.powerShellContext.EnterNestedPrompt();
130132
}
131133

132134
/// <summary>
133135
///
134136
/// </summary>
135137
public override void ExitNestedPrompt()
136138
{
137-
Logger.Write(LogLevel.Verbose, "ExitNestedPrompt() called.");
139+
this.powerShellContext.ExitNestedPrompt();
138140
}
139141

140142
/// <summary>

src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs

Lines changed: 85 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -632,10 +632,20 @@ public Collection<int> PromptForChoice(
632632

633633
#region Private Methods
634634

635-
private async Task WritePromptStringToHost()
635+
private Coordinates lastPromptLocation;
636+
637+
private async Task WritePromptStringToHost(CancellationToken cancellationToken)
636638
{
639+
if (this.lastPromptLocation != null &&
640+
this.lastPromptLocation.X == await ConsoleProxy.GetCursorLeftAsync(cancellationToken) &&
641+
this.lastPromptLocation.Y == await ConsoleProxy.GetCursorTopAsync(cancellationToken))
642+
{
643+
return;
644+
}
645+
637646
PSCommand promptCommand = new PSCommand().AddScript("prompt");
638647

648+
cancellationToken.ThrowIfCancellationRequested();
639649
string promptString =
640650
(await this.powerShellContext.ExecuteCommand<PSObject>(promptCommand, false, false))
641651
.Select(pso => pso.BaseObject)
@@ -665,8 +675,13 @@ private async Task WritePromptStringToHost()
665675
promptString);
666676
}
667677

678+
cancellationToken.ThrowIfCancellationRequested();
679+
668680
// Write the prompt string
669681
this.WriteOutput(promptString, false);
682+
this.lastPromptLocation = new Coordinates(
683+
await ConsoleProxy.GetCursorLeftAsync(cancellationToken),
684+
await ConsoleProxy.GetCursorTopAsync(cancellationToken));
670685
}
671686

672687
private void WriteDebuggerBanner(DebuggerStopEventArgs eventArgs)
@@ -686,14 +701,23 @@ private void WriteDebuggerBanner(DebuggerStopEventArgs eventArgs)
686701

687702
private async Task StartReplLoop(CancellationToken cancellationToken)
688703
{
689-
do
704+
while (!cancellationToken.IsCancellationRequested)
690705
{
691706
string commandString = null;
707+
int originalCursorTop = 0;
692708

693-
await this.WritePromptStringToHost();
709+
try
710+
{
711+
await this.WritePromptStringToHost(cancellationToken);
712+
}
713+
catch (OperationCanceledException)
714+
{
715+
break;
716+
}
694717

695718
try
696719
{
720+
originalCursorTop = await ConsoleProxy.GetCursorTopAsync(cancellationToken);
697721
commandString = await this.ReadCommandLine(cancellationToken);
698722
}
699723
catch (PipelineStoppedException)
@@ -718,29 +742,29 @@ private async Task StartReplLoop(CancellationToken cancellationToken)
718742

719743
Logger.WriteException("Caught exception while reading command line", e);
720744
}
721-
722-
if (commandString != null)
745+
finally
723746
{
724-
if (!string.IsNullOrWhiteSpace(commandString))
725-
{
726-
var unusedTask =
727-
this.powerShellContext
728-
.ExecuteScriptString(
729-
commandString,
730-
false,
731-
true,
732-
true)
733-
.ConfigureAwait(false);
734-
735-
break;
736-
}
737-
else
747+
if (!cancellationToken.IsCancellationRequested &&
748+
originalCursorTop == await ConsoleProxy.GetCursorTopAsync(cancellationToken))
738749
{
739-
this.WriteOutput(string.Empty);
750+
this.WriteLine();
740751
}
741752
}
753+
754+
if (!string.IsNullOrWhiteSpace(commandString))
755+
{
756+
var unusedTask =
757+
this.powerShellContext
758+
.ExecuteScriptString(
759+
commandString,
760+
false,
761+
true,
762+
true)
763+
.ConfigureAwait(false);
764+
765+
break;
766+
}
742767
}
743-
while (!cancellationToken.IsCancellationRequested);
744768
}
745769

746770
private InputPromptHandler CreateInputPromptHandler()
@@ -835,6 +859,12 @@ private void WaitForPromptCompletion<TResult>(
835859

836860
private void PowerShellContext_DebuggerStop(object sender, System.Management.Automation.DebuggerStopEventArgs e)
837861
{
862+
if (!this.IsCommandLoopRunning)
863+
{
864+
((IHostInput)this).StartCommandLoop();
865+
return;
866+
}
867+
838868
// Cancel any existing prompt first
839869
this.CancelCommandPrompt();
840870

@@ -850,45 +880,50 @@ private void PowerShellContext_DebuggerResumed(object sender, System.Management.
850880
private void PowerShellContext_ExecutionStatusChanged(object sender, ExecutionStatusChangedEventArgs eventArgs)
851881
{
852882
// The command loop should only be manipulated if it's already started
853-
if (this.IsCommandLoopRunning)
883+
if (eventArgs.ExecutionStatus == ExecutionStatus.Aborted)
854884
{
855-
if (eventArgs.ExecutionStatus == ExecutionStatus.Aborted)
885+
// When aborted, cancel any lingering prompts
886+
if (this.activePromptHandler != null)
856887
{
857-
// When aborted, cancel any lingering prompts
858-
if (this.activePromptHandler != null)
859-
{
860-
this.activePromptHandler.CancelPrompt();
861-
this.WriteOutput(string.Empty);
862-
}
888+
this.activePromptHandler.CancelPrompt();
889+
this.WriteOutput(string.Empty);
863890
}
864-
else if (
865-
eventArgs.ExecutionOptions.WriteOutputToHost ||
866-
eventArgs.ExecutionOptions.InterruptCommandPrompt)
891+
}
892+
else if (
893+
eventArgs.ExecutionOptions.WriteOutputToHost ||
894+
eventArgs.ExecutionOptions.InterruptCommandPrompt)
895+
{
896+
// Any command which writes output to the host will affect
897+
// the display of the prompt
898+
if (eventArgs.ExecutionStatus != ExecutionStatus.Running)
867899
{
868-
// Any command which writes output to the host will affect
869-
// the display of the prompt
870-
if (eventArgs.ExecutionStatus != ExecutionStatus.Running)
871-
{
872-
// Execution has completed, start the input prompt
873-
this.ShowCommandPrompt();
874-
}
875-
else
876-
{
877-
// A new command was started, cancel the input prompt
878-
this.CancelCommandPrompt();
879-
this.WriteOutput(string.Empty);
880-
}
900+
// Execution has completed, start the input prompt
901+
this.ShowCommandPrompt();
902+
((IHostInput)this).StartCommandLoop();
881903
}
882-
else if (
883-
eventArgs.ExecutionOptions.WriteErrorsToHost &&
884-
(eventArgs.ExecutionStatus == ExecutionStatus.Failed ||
885-
eventArgs.HadErrors))
904+
else
886905
{
906+
// A new command was started, cancel the input prompt
907+
((IHostInput)this).StopCommandLoop();
887908
this.CancelCommandPrompt();
888909
this.WriteOutput(string.Empty);
889-
this.ShowCommandPrompt();
890910
}
891911
}
912+
else if (
913+
eventArgs.ExecutionOptions.WriteErrorsToHost &&
914+
(eventArgs.ExecutionStatus == ExecutionStatus.Failed ||
915+
eventArgs.HadErrors))
916+
{
917+
// this.CancelCommandPrompt();
918+
// this.WriteOutput(string.Empty);
919+
// this.ShowCommandPrompt();
920+
// ((IHostInput)this).StopCommandLoop();
921+
// this.CancelCommandPrompt();
922+
// ((IHostInput)this).StartCommandLoop();
923+
// this.ShowCommandPrompt();
924+
this.WriteOutput(string.Empty, true);
925+
var unusedTask = this.WritePromptStringToHost(CancellationToken.None);
926+
}
892927
}
893928

894929
#endregion

src/PowerShellEditorServices/Session/IVersionSpecificOperations.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
using System.Collections.Generic;
77
using System.Management.Automation;
8+
using System.Management.Automation.Host;
89
using System.Management.Automation.Runspaces;
910

1011
namespace Microsoft.PowerShell.EditorServices.Session
@@ -21,6 +22,12 @@ IEnumerable<TResult> ExecuteCommandInDebugger<TResult>(
2122
PSCommand psCommand,
2223
bool sendOutputToHost,
2324
out DebuggerResumeAction? debuggerResumeAction);
25+
26+
void StopCommandInDebugger(PowerShellContext powerShellContext);
27+
28+
bool IsDebuggerStopped(PromptNest promptNest, Runspace runspace);
29+
30+
void ExitNestedPrompt(PSHost host);
2431
}
2532
}
2633

src/PowerShellEditorServices/Session/PowerShell3Operations.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Collections.Generic;
88
using System.Linq;
99
using System.Management.Automation;
10+
using System.Management.Automation.Host;
1011
using System.Management.Automation.Runspaces;
1112

1213
namespace Microsoft.PowerShell.EditorServices.Session
@@ -69,6 +70,28 @@ public IEnumerable<TResult> ExecuteCommandInDebugger<TResult>(
6970

7071
return executionResult;
7172
}
73+
74+
public void StopCommandInDebugger(PowerShellContext powerShellContext)
75+
{
76+
// TODO: Possibly save the pipeline to a field and initiate stop here. Or just throw.
77+
}
78+
79+
public bool IsDebuggerStopped(PromptNest promptNest, Runspace runspace)
80+
{
81+
return promptNest.IsInDebugger;
82+
}
83+
84+
public void ExitNestedPrompt(PSHost host)
85+
{
86+
try
87+
{
88+
host.ExitNestedPrompt();
89+
}
90+
// FlowControlException is not accessible in PSv3
91+
catch (Exception)
92+
{
93+
}
94+
}
7295
}
7396
}
7497

0 commit comments

Comments
 (0)