Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use public InternalHost from origin runspace #874

Merged

Conversation

SeeminglyScience
Copy link
Collaborator

@SeeminglyScience SeeminglyScience commented Mar 5, 2019

ConsoleHost from powershell.exe/pwsh still exists within the runspace created at process start. This change grabs the public reference to that host while initializing EditorServicesHost. We can then leverage that host so we can have a much closer to "default" experience.

Resolves #865, #384, PowerShell/vscode-powershell#140, PowerShell/vscode-powershell#1403

This adds support for the following host members:

$Host.UI

  • WriteProgress (including Write-Progress)

$Host.UI.RawUI

  • CursorSize (still doesn't work in xterm.js though)
  • WindowTitle
  • MaxPhysicalWindowSize
  • MaxWindowSize
  • ReadKey
  • GetBufferContents
  • ScrollBufferContents
  • SetBufferContents

TODO

  • Test RawUI members
  • Maybe write sync verison of ReadKey
  • Maybe avoid TerminalPSHost* breaking changes (constructors) I can't really think of any scenarios where folks would be creating their own TerminalPSHost* instances
  • Consider thread safety for progress record and ReadKey buffers Not sure it's necessary, open to suggestions though.

`ConsoleHost` from `powershell.exe`/`pwsh` still exists within the
runspace created at process start. This change grabs the public
reference to that host while initializing EditorServicesHost. We
can then leverage that host so we can have a much closer to
"default" experience.

This change adds support for the following host members

## $Host.UI

- WriteProgress (including `Write-Progress`)

## $Host.UI.RawUI

- CursorSize (still doesn't work in xterm.js though)
- WindowTitle
- MaxPhysicalWindowSize
- MaxWindowSize
- ReadKey
- GetBufferContents
- ScrollBufferContents
- SetBufferContents

## TODO

[ ] Test RawUI members
[ ] Maybe write sync verison of ReadKey
[ ] Maybe avoid TerminalPSHost* breaking changes (constructors)
- Add the XML documentation comments to ConsoleProxy because it's more
  likely to be used than the interface itself.

- Add a synchronous implementation of ReadKey to ConsoleProxy and use it
  in RawUI.ReadKey

- Fix Ctrl + C not returning as input in VSCode's terminal

- Use the exception message from ConsoleHost when ReadKeyOptions do not
  include either IncludeKeyUp nor IncludeKeyDown
@SeeminglyScience SeeminglyScience changed the title WIP: Use public InternalHost from origin runspace Use public InternalHost from origin runspace Mar 7, 2019
Copy link
Member

@TylerLeonhardt TylerLeonhardt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good!

@@ -184,11 +145,87 @@ public override Size MaxWindowSize
/// <returns>A KeyInfo struct with details about the current keypress.</returns>
public override KeyInfo ReadKey(ReadKeyOptions options)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SteveL-MSFT @daxian-dbw would you mind taking a look at this ReadKey implementation if you have a sec? This will be the implementation for $Host.UI.RawUI.ReadKey() in the Integrated Console.

I think it looks ok.

Copy link
Member

@TylerLeonhardt TylerLeonhardt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM I'd like to do 2 things before this goes in:

  1. test on non-Windows
  2. ask the community for complex Write-Progress's

@potatoqualitee
Copy link

Excellent!! Thank you so very very much. @okallstad has some advanced Write-Progress bars in his module, psInlineProgress

image

@TylerLeonhardt
Copy link
Member

@potatoqualitee I think they might go by a different name on GitHub? Looks like no one was tagged.

@potatoqualitee
Copy link

unfortunately, i dont know his github address :/ hopefully he'll notice the twitter ping

@wsmelton
Copy link

@potatoqualitee
Copy link

yay that is him 🎉

@SeeminglyScience
Copy link
Collaborator Author

@potatoqualitee Doesn't look like that uses Write-Progress. Actually that one probably already works without this PR.

@potatoqualitee
Copy link

daw sorry, i didnt check the code, i was just a lil familiar with his project. my own write-progresses are pretty straightforward

@TylerLeonhardt
Copy link
Member

Copy link
Member

@daxian-dbw daxian-dbw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No much experience in this area, so my questions are mainly for my own education ;)

info.Character,
info.ControlKeyState,
keyDown: false);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KeyUp is requested, but why are you returning a KeyDown, especially that this KeyDown has already been returned previously?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var result = new KeyInfo((int)key.Key, key.KeyChar, states, isDown);
if (isDown)
{
this.lastKeyDown = result;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The returned value from this local function is returned immediately at all call sites of this local function, which means the cached KeyDown returned below in if (includeUp && this.lastKeyDown != null) has already been returned previously.

Copy link
Collaborator Author

@SeeminglyScience SeeminglyScience Mar 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daxian-dbw

The returned value from this local function is returned immediately at all call sites of this local function, which means the cached KeyDown returned below in if (includeUp && this.lastKeyDown != null) has already been returned previously.

Yeah, that's an attempt match the behavior of ConsoleHost without dipping into native API's.

Here's an example to demonstrate what I'm trying emulate.

while ($true) {
    $Host.UI.RawUI.ReadKey('NoEcho, IncludeKeyDown')
    $Host.UI.RawUI.ReadKey('NoEcho, IncludeKeyUp')
}

With that running outside of PSES, every one key press will return two KeyInfo objects. One for down, and one for up. Example output:

VirtualKeyCode Character ControlKeyState KeyDown
-------------- --------- --------------- -------
            70         f       NumLockOn    True
            70         f       NumLockOn   False

Now obviously ConsoleHost does this a lot better using native API's directly, but this route saves a lot of complexity. The only difference in behavior is when holding down a key with both IncludeKeyUp and IncludeKeyDown set. In that scenario ConsoleHost will continuously return key down KeyInfo objects until they key is released where we will continuously return both key down and key up objects. That difference is negated a little bit though. Even ConsoleHost act the same under winpty as it doesn't translate input one to one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah on non-consolehost I think it typically only supports KeyDown events and not KeyUp at all.

If I tried that while loop on macOS, I only get KeyDown events.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify because the terms are confusing, when I sayConsoleHost I'm referring to Microsoft.PowerShell.ConsoleHost (the PSHost started by powershell.exe/pwsh) not conhost.exe

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah sorry. Yeah I meant non-conhost sorry.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha! Thanks for the clarification!

@TylerLeonhardt
Copy link
Member

I think I'm good with this. Tomorrow I'll test on non-Windows while we wait for more feedback.

@TylerLeonhardt
Copy link
Member

// Caller doesn't want CtrlC so throw a PipelineStoppedException to emulate
// a real stop. This will not show an exception to a script based caller and it
// will avoid having to return something like default(KeyInfo).
throw new PipelineStoppedException();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caller doesn't want CtrlC so throw a PipelineStoppedException to emulate a real stop.

@SeeminglyScience I'm a little confused about the comment on this throw statement. Can you please explain a bit?

Copy link
Collaborator Author

@SeeminglyScience SeeminglyScience Mar 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, so at that point we've received the input Ctrl + C. If the caller did not include AllowCtrlC in the options, then we should treat it like a break event instead and terminate the pipeline.

Emulate is probably not really the right word there, I chose it because we aren't going through PowerShell.Stop or Pipeline.Stop like a normal break event would.

Another route would be to not set Console.TreatControlCAsInput, and allow our break handler to call PowerShell.Stop like normal. This actually would be more similar to what ConsoleHost does as it will actually return default(KeyInfo), but triggers a pipeline stop.

The issue I have with this is that if RawUI.ReadKey is in an assignment statement, the pipeline stop won't actually happen until the next sequence point. PowerShell doesn't check if the current command is stopping between the RHS being evaluated and it's result being set to the LHS. So the LHS will actually be set to default(KeyInfo) instead of remaining unchanged. That doesn't happen if we throw instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another route would be to not set Console.TreatControlCAsInput

Thanks! My confusion was mainly around this case. It makes sense now.

So the LHS will actually be set to default(KeyInfo) instead of remaining unchanged.

Why does this matter if the pipeline stop will kick in at the next sequence point? Even if it's set to default(KeyInfo), the script execution gets killed and that assignment shouldn't have any real effect anymore, right?

Copy link
Collaborator Author

@SeeminglyScience SeeminglyScience Mar 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the script execution gets killed and that assignment shouldn't have any real effect anymore, right?

There's a few scenarios where it can linger:

  1. LHS is a global scoped variable (or script scoped variable in a module)
  2. LHS is a property or index expression
  3. Current command is a prompt evaluation (or is otherwise running within the top level scope of it's SessionState)
  4. If the assignment is in a try with a finally block (though it will only linger within the finally)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please provide a repro of the problems? For example, LHS is a global scoped variable and it gets assigned with default(KeyInfo)

I tried with this in pwsh console

function Test{
    $global:abc = $Host.UI.RawUI.ReadKey('NoEcho, IncludeKeyDown')
}

Test
<ctrl+c>

It seems ReadKey will get either the LeftCtrlPressed or c, but not default(KeyInfo).

Copy link
Collaborator Author

@SeeminglyScience SeeminglyScience Mar 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the easiest way:

$key = $null
# Drain "Enter" key up from accepting prompt.  Maybe comment
# this line for non-Windows.
$null = $Host.UI.RawUI.ReadKey('NoEcho, IncludeKeyUp')
$key = $Host.UI.RawUI.ReadKey('NoEcho, IncludeKeyUp')

Then press Ctrl + C and $key will hold:

VirtualKeyCode Character ControlKeyState KeyDown
-------------- --------- --------------- -------
             0                         0   False

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I can repro it now after removing PSReadline.
Interesting that if I change IncludeKeyUp to IncludeKeyDown, it doesn't repro anymore

$null = $Host.UI.RawUI.ReadKey('NoEcho, IncludeKeyDown')
$key = $Host.UI.RawUI.ReadKey('NoEcho, IncludeKeyDown')

VirtualKeyCode Character            ControlKeyState KeyDown
-------------- ---------            --------------- -------
            17           LeftCtrlPressed, NumLockOn    True

Copy link
Member

@daxian-dbw daxian-dbw Mar 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, my comment is not blocking on this PR. I'm just curious :)

Copy link
Collaborator Author

@SeeminglyScience SeeminglyScience Mar 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I can repro it now after removing PSReadline.

Huh that's surprising... mine was with PSRL as well. Though I'm on 2.0 beta 3 (or more likely one of the appveyor builds...)

Interesting that if I change IncludeKeyUp to IncludeKeyDown, it doesn't repro anymore

Wellll sorta. IncludeKeyDown makes it a lot harder to reproduce because it can trigger on a single press of Ctrl. So by the time you type C, you're actually already back to a prompt. But if you do something like this, it'll still show up

$key = $null
while ($true) {
    $key = $Host.UI.RawUI.ReadKey('NoEcho, IncludeKeyDown')
}

That will continously ReadKey until it registers Ctrl + C and a pipeline stop is triggered.

BTW, my comment is not blocking on this PR. I'm just curious :)

No worries, I could talk about it forever 🙂

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh that's surprising... mine was with PSRL as well. Though I'm on 2.0 beta 3 (or more likely one of the appveyor builds...)

Ha, I'm using 6.2.0-rc.1.

IncludeKeyDown makes it a lot harder to reproduce because it can trigger on a single press of Ctrl. So by the time you type C, you're actually already back to a prompt.

That makes perfect sense!

@@ -32,6 +32,7 @@ public abstract class EditorServicesPSHostUserInterface :
{
#region Private Fields

private readonly HashSet<ProgressKey> currentProgressMessages = new HashSet<ProgressKey>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thread safety pls 😄 ConcurrentBag

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to using ConcurrentDictionary<,>, but haven't tested it yet so hold off on merge pls

Copy link
Member

@TylerLeonhardt TylerLeonhardt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM just one nit

}
else
{
this.currentProgressMessages.TryAdd(new ProgressKey(sourceId, record), null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add a comment as to why you're adding null.

@potatoqualitee
Copy link

🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

PS Integrated Console incorrect width
6 participants