Skip to content

Commit

Permalink
PSReadLine integration (PowerShell#672)
Browse files Browse the repository at this point in the history
* Add infrastructure for managing context

Adds classes that manage the state of the prompt, nested contexts,
and multiple ReadLine implementations of varying complexity.

(cherry picked from commit 7ca8b9b)

* Console related classes changes

Change ReadLine method to call out to PowerShellContext. This lets
the PowerShellContext determine which ReadLine implementation to use
based on available modules.

Also includes some changes to the System.Console proxy classes to
account for PSReadLine.

(cherry picked from commit 59bfa3b)

* 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.

(cherry picked from commit 21e6b5f)

* Rewrite direct SessionStateProxy calls

All interactions with the runspace must be done through
PowerShellContext now that nested PowerShell instances are
encountered frequently.

Also fix a bunch of race conditions that were made more obvious
with the changes.

(cherry picked from commit fa2faba)

* Pass feature flags to Start-EditorServicesHost

* Address feedback and fix travis build error

- Address feedback from @bergmeister

- Fix a few other similar mistakes I found

- Fix travis build failing due to missing documentation comment tag

* Fix all tests except ServiceLoadsProfileOnDemand

- Fix an issue where intellisense wouldn't finish if PSReadLine was not
  running

- Fix a crash that would occur if the PSHost was not set up for input
  like the one used in our tests

- Fix a compile error when building against PSv3/4

- Fix a hang that occurred when the PromptNest was disposed during a
  debug session

- Fix some XML documentation comment syntax errors

* Fix extra new lines outputted after each command

Removed a call to WriteOutput where it wasn't required.  This was
creating extra new lines which failed tests (and obviously didn't
look right).

* Remove unused field from InvocationEventQueue

And also fix spacing between the other fields.

* Remove copying of PDB's in build script

@rjmholt did a better job of this in a different PR that we can merge
into 2.0.0 later. It also doesn't make sense in this PR.

* Add AppVeyor tracking to branch 2.0.0

* Fix ambiguous method crash on CoreCLR

Simplify delegate creation in PSReadLineProxy and fix the immediate
ambiguous method crash the complicated code caused on CoreCLR.

* first round of feedback changes

* Some more feedback changes

* add a bunch of copyright headers I missed

* remove KeyAvailable query

* Get the latest PSReadLine module installed

* Add PSReadLine installation to build script

* the file should be downloaded as a .zip

* Address remaining feedback

* 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.

* Revert "Attempt to fix issue with native apps and input"

This reverts commit 1682410.

* Fix build failure
  • Loading branch information
SeeminglyScience authored and Robert Holt committed Oct 2, 2018
1 parent 73206f1 commit 0d0889e
Show file tree
Hide file tree
Showing 40 changed files with 3,403 additions and 466 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -29,6 +29,7 @@ registered_data.ini
.dotnet/
module/Plaster
module/PSScriptAnalyzer
module/PSReadLine
docs/_site/
docs/_repo/
docs/metadata/
Expand Down
19 changes: 19 additions & 0 deletions PowerShellEditorServices.build.ps1
Expand Up @@ -309,6 +309,25 @@ task RestorePsesModules -After Build {

Save-Module @splatParameters
}

# TODO: Replace this with adding a new module to Save when a new PSReadLine release comes out to the Gallery
if (-not (Test-Path $PSScriptRoot/module/PSReadLine))
{
Write-Host "`tInstalling module: PSReadLine"

# Download AppVeyor zip
$jobId = (Invoke-RestMethod https://ci.appveyor.com/api/projects/lzybkr/PSReadLine).build.jobs[0].jobId
Invoke-RestMethod https://ci.appveyor.com/api/buildjobs/$jobId/artifacts/bin%2FRelease%2FPSReadLine.zip -OutFile $PSScriptRoot/module/PSRL.zip

# Position PSReadLine
Expand-Archive $PSScriptRoot/module/PSRL.zip $PSScriptRoot/module/PSRL
Move-Item $PSScriptRoot/module/PSRL/PSReadLine $PSScriptRoot/module

# Clean up
Remove-Item -Force -Recurse $PSScriptRoot/module/PSRL.zip
Remove-Item -Force -Recurse $PSScriptRoot/module/PSRL
}

Write-Host "`n"
}

Expand Down
1 change: 1 addition & 0 deletions appveyor.yml
Expand Up @@ -6,6 +6,7 @@ skip_tags: true
branches:
only:
- master
- 2.0.0

environment:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true # Don't download unneeded packages
Expand Down
3 changes: 2 additions & 1 deletion module/PowerShellEditorServices/Start-EditorServices.ps1
Expand Up @@ -314,7 +314,8 @@ try {
-BundledModulesPath $BundledModulesPath `
-EnableConsoleRepl:$EnableConsoleRepl.IsPresent `
-DebugServiceOnly:$DebugServiceOnly.IsPresent `
-WaitForDebugger:$WaitForDebugger.IsPresent
-WaitForDebugger:$WaitForDebugger.IsPresent `
-FeatureFlags $FeatureFlags

# TODO: Verify that the service is started
Log "Start-EditorServicesHost returned $editorServicesHost"
Expand Down
6 changes: 4 additions & 2 deletions src/PowerShellEditorServices.Host/EditorServicesHost.cs
Expand Up @@ -365,7 +365,7 @@ public void WaitForCompletion()
bool enableConsoleRepl)
{
EditorSession editorSession = new EditorSession(this.logger);
PowerShellContext powerShellContext = new PowerShellContext(this.logger);
PowerShellContext powerShellContext = new PowerShellContext(this.logger, this.featureFlags.Contains("PSReadLine"));

EditorServicesPSHostUserInterface hostUserInterface =
enableConsoleRepl
Expand Down Expand Up @@ -405,7 +405,9 @@ public void WaitForCompletion()
bool enableConsoleRepl)
{
EditorSession editorSession = new EditorSession(this.logger);
PowerShellContext powerShellContext = new PowerShellContext(this.logger);
PowerShellContext powerShellContext = new PowerShellContext(
this.logger,
this.featureFlags.Contains("PSReadLine"));

EditorServicesPSHostUserInterface hostUserInterface =
enableConsoleRepl
Expand Down
27 changes: 26 additions & 1 deletion src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs
Expand Up @@ -112,6 +112,17 @@ protected Task LaunchScript(RequestContext<object> requestContext, string script

private async Task OnExecutionCompleted(Task executeTask)
{
try
{
await executeTask;
}
catch (Exception e)
{
Logger.Write(
LogLevel.Error,
"Exception occurred while awaiting debug launch task.\n\n" + e.ToString());
}

Logger.Write(LogLevel.Verbose, "Execution completed, terminating...");

_executionCompleted = true;
Expand Down Expand Up @@ -463,7 +474,7 @@ protected void Stop()
if (_executionCompleted == false)
{
_disconnectRequestContext = requestContext;
_editorSession.PowerShellContext.AbortExecution();
_editorSession.PowerShellContext.AbortExecution(shouldAbortDebugSession: true);

if (_isInteractiveDebugSession)
{
Expand Down Expand Up @@ -748,6 +759,20 @@ protected void Stop()
StackFrameDetails[] stackFrames =
_editorSession.DebugService.GetStackFrames();

// Handle a rare race condition where the adapter requests stack frames before they've
// begun building.
if (stackFrames == null)
{
await requestContext.SendResult(
new StackTraceResponseBody
{
StackFrames = new StackFrame[0],
TotalFrames = 0
});

return;
}

List<StackFrame> newStackFrames = new List<StackFrame>();

int startFrameIndex = stackTraceParams.StartFrame ?? 0;
Expand Down
Expand Up @@ -1543,6 +1543,15 @@ private static FileChange GetFileChangeDetails(Range changeRange, string insertS
catch (TaskCanceledException)
{
// If the task is cancelled, exit directly
foreach (var script in filesToAnalyze)
{
await PublishScriptDiagnostics(
script,
script.SyntaxMarkers,
correctionIndex,
eventSender);
}

return;
}

Expand Down
95 changes: 95 additions & 0 deletions src/PowerShellEditorServices/Console/ConsoleProxy.cs
@@ -0,0 +1,95 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.PowerShell.EditorServices.Console
{
/// <summary>
/// Provides asynchronous implementations of the <see cref="Console" /> API's as well as
/// synchronous implementations that work around platform specific issues.
/// </summary>
internal static class ConsoleProxy
{
private static IConsoleOperations s_consoleProxy;

static ConsoleProxy()
{
// Maybe we should just include the RuntimeInformation package for FullCLR?
#if CoreCLR
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
s_consoleProxy = new WindowsConsoleOperations();
return;
}

s_consoleProxy = new UnixConsoleOperations();
#else
s_consoleProxy = new WindowsConsoleOperations();
#endif
}

public static Task<ConsoleKeyInfo> ReadKeyAsync(CancellationToken cancellationToken) =>
s_consoleProxy.ReadKeyAsync(cancellationToken);

public static int GetCursorLeft() =>
s_consoleProxy.GetCursorLeft();

public static int GetCursorLeft(CancellationToken cancellationToken) =>
s_consoleProxy.GetCursorLeft(cancellationToken);

public static Task<int> GetCursorLeftAsync() =>
s_consoleProxy.GetCursorLeftAsync();

public static Task<int> GetCursorLeftAsync(CancellationToken cancellationToken) =>
s_consoleProxy.GetCursorLeftAsync(cancellationToken);

public static int GetCursorTop() =>
s_consoleProxy.GetCursorTop();

public static int GetCursorTop(CancellationToken cancellationToken) =>
s_consoleProxy.GetCursorTop(cancellationToken);

public static Task<int> GetCursorTopAsync() =>
s_consoleProxy.GetCursorTopAsync();

public static Task<int> GetCursorTopAsync(CancellationToken cancellationToken) =>
s_consoleProxy.GetCursorTopAsync(cancellationToken);

/// <summary>
/// On Unix platforms this method is sent to PSReadLine as a work around for issues
/// with the System.Console implementation for that platform. Functionally it is the
/// same as System.Console.ReadKey, with the exception that it will not lock the
/// standard input stream.
/// </summary>
/// <param name="intercept">
/// Determines whether to display the pressed key in the console window.
/// true to not display the pressed key; otherwise, false.
/// </param>
/// <param name="cancellationToken">
/// The <see cref="CancellationToken" /> that can be used to cancel the request.
/// </param>
/// <returns>
/// An object that describes the ConsoleKey constant and Unicode character, if any,
/// that correspond to the pressed console key. The ConsoleKeyInfo object also describes,
/// in a bitwise combination of ConsoleModifiers values, whether one or more Shift, Alt,
/// or Ctrl modifier keys was pressed simultaneously with the console key.
/// </returns>
internal static ConsoleKeyInfo UnixReadKey(bool intercept, CancellationToken cancellationToken)
{
try
{
return ((UnixConsoleOperations)s_consoleProxy).ReadKey(intercept, cancellationToken);
}
catch (OperationCanceledException)
{
return default(ConsoleKeyInfo);
}
}
}
}
58 changes: 32 additions & 26 deletions src/PowerShellEditorServices/Console/ConsoleReadLine.cs
Expand Up @@ -6,7 +6,6 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

Expand All @@ -20,27 +19,13 @@ namespace Microsoft.PowerShell.EditorServices.Console
internal class ConsoleReadLine
{
#region Private Field
private static IConsoleOperations s_consoleProxy;

private PowerShellContext powerShellContext;

#endregion

#region Constructors
static ConsoleReadLine()
{
// Maybe we should just include the RuntimeInformation package for FullCLR?
#if CoreCLR
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
s_consoleProxy = new WindowsConsoleOperations();
return;
}

s_consoleProxy = new UnixConsoleOperations();
#else
s_consoleProxy = new WindowsConsoleOperations();
#endif
}

public ConsoleReadLine(PowerShellContext powerShellContext)
Expand All @@ -54,20 +39,20 @@ public ConsoleReadLine(PowerShellContext powerShellContext)

public Task<string> ReadCommandLine(CancellationToken cancellationToken)
{
return this.ReadLine(true, cancellationToken);
return this.ReadLineAsync(true, cancellationToken);
}

public Task<string> ReadSimpleLine(CancellationToken cancellationToken)
{
return this.ReadLine(false, cancellationToken);
return this.ReadLineAsync(false, cancellationToken);
}

public async Task<SecureString> ReadSecureLine(CancellationToken cancellationToken)
{
SecureString secureString = new SecureString();

int initialPromptRow = Console.CursorTop;
int initialPromptCol = Console.CursorLeft;
int initialPromptRow = await ConsoleProxy.GetCursorTopAsync(cancellationToken);
int initialPromptCol = await ConsoleProxy.GetCursorLeftAsync(cancellationToken);
int previousInputLength = 0;

Console.TreatControlCAsInput = true;
Expand Down Expand Up @@ -114,7 +99,8 @@ public async Task<SecureString> ReadSecureLine(CancellationToken cancellationTok
}
else if (previousInputLength > 0 && currentInputLength < previousInputLength)
{
int row = Console.CursorTop, col = Console.CursorLeft;
int row = await ConsoleProxy.GetCursorTopAsync(cancellationToken);
int col = await ConsoleProxy.GetCursorLeftAsync(cancellationToken);

// Back up the cursor before clearing the character
col--;
Expand Down Expand Up @@ -146,10 +132,30 @@ public async Task<SecureString> ReadSecureLine(CancellationToken cancellationTok

private static async Task<ConsoleKeyInfo> ReadKeyAsync(CancellationToken cancellationToken)
{
return await s_consoleProxy.ReadKeyAsync(cancellationToken);
return await ConsoleProxy.ReadKeyAsync(cancellationToken);
}

private async Task<string> ReadLineAsync(bool isCommandLine, CancellationToken cancellationToken)
{
return await this.powerShellContext.InvokeReadLineAsync(isCommandLine, cancellationToken);
}

private async Task<string> ReadLine(bool isCommandLine, CancellationToken cancellationToken)
/// <summary>
/// Invokes a custom ReadLine method that is similar to but more basic than PSReadLine.
/// This method should be used when PSReadLine is disabled, either by user settings or
/// unsupported PowerShell versions.
/// </summary>
/// <param name="isCommandLine">
/// Indicates whether ReadLine should act like a command line.
/// </param>
/// <param name="cancellationToken">
/// The cancellation token that will be checked prior to completing the returned task.
/// </param>
/// <returns>
/// A task object representing the asynchronus operation. The Result property on
/// the task object returns the user input string.
/// </returns>
internal async Task<string> InvokeLegacyReadLineAsync(bool isCommandLine, CancellationToken cancellationToken)
{
string inputBeforeCompletion = null;
string inputAfterCompletion = null;
Expand All @@ -160,8 +166,8 @@ private async Task<string> ReadLine(bool isCommandLine, CancellationToken cancel

StringBuilder inputLine = new StringBuilder();

int initialCursorCol = Console.CursorLeft;
int initialCursorRow = Console.CursorTop;
int initialCursorCol = await ConsoleProxy.GetCursorLeftAsync(cancellationToken);
int initialCursorRow = await ConsoleProxy.GetCursorTopAsync(cancellationToken);

int initialWindowLeft = Console.WindowLeft;
int initialWindowTop = Console.WindowTop;
Expand Down Expand Up @@ -492,8 +498,8 @@ private async Task<string> ReadLine(bool isCommandLine, CancellationToken cancel
int consoleWidth)
{
return
((Console.CursorTop - promptStartRow) * consoleWidth) +
Console.CursorLeft - promptStartCol;
((ConsoleProxy.GetCursorTop() - promptStartRow) * consoleWidth) +
ConsoleProxy.GetCursorLeft() - promptStartCol;
}

private void CalculateCursorFromIndex(
Expand Down

0 comments on commit 0d0889e

Please sign in to comment.