PSReadLine icon indicating copy to clipboard operation
PSReadLine copied to clipboard

Windows: $host.UI.RawUI.KeyAvailable has false positives.

Open mklement0 opened this issue 6 years ago • 9 comments

Environment data

PS version: 7.0.0-preview.1
PSReadline version: 2.0.0-beta4
os: 10.0.18362.1 (WinBuild.160101.0800)
PS file version: 7.0.0.0
BufferWidth: 180
BufferHeight: 9999

Steps to reproduce or exception report

Note: The problem surfaces on Windows only.

Run the following code with and without PSReadLine imported:

# Show a spinner until the user presses a key.
$i=0; while (-not $host.UI.RawUI.KeyAvailable) { Write-Host -NoNewline ("`r{0}" -f '/―\|'[($i++ % 4)]); start-sleep -ms 200 }

Expected: A spinner should appear and keep spinning until the user presses a key.

Actual, with PSReadLine imported: The while loop is either never entered (1st invocation immediately after start of the PS session) or stops after one iteration even if no key is pressed (subsequent invocations).

Once you run Remove-Module PSReadLine, the behavior is as expected in regular console windows; interestingly, the problem persists in a Windows Terminal window.

For consoles / terminals only, using [Console]::KeyAvailable in lieu of $host.UI.RawUI.KeyAvailable is an effective workaround.

mklement0 avatar Jul 06 '19 17:07 mklement0

Duplicate of #901 and #291.

lzybkr avatar Jul 06 '19 18:07 lzybkr

Quoted from https://github.com/PowerShell/PSReadLine/issues/291#issuecomment-342685388

It seems reasonable for PSReadLine to consume the final KeyUp before returning to the caller.

First, this is a Windows only issue because on Linux/macOS, the KeyAvailable and ReadKey implementation in ConsoleHost use Console.ReadKey directly.

I looked into consuming the final KeyUp events before returning from PSConsoleReadLine.ReadLine using the Win32 APIs GetNumberOfConsoleInputEvents, PeekConsoleInput and GetConsoleInput. The problem is, after Console.ReadKey(), there is a delay till the unread KeyUp events can be queried by those APIs.

On my dev machine, the delay is about 100 ms. With Thread.Sleep(100) after Console.ReadKey(), the APIs GetNumberOfConsoleInputEvents, PeekConsoleInput and GetConsoleInput can return the unread KeyUp keys, while without Thread.Sleep or sleeping for a shorter time span, those 3 Win32 APIs shows 0 event read. This explains the reported symptom: The while loop is either never entered or stops after one iteration even if no key is pressed. It stops after one iteration because the first time $host.UI.RawUI.KeyAvailable is call, the unread KeyUp events haven't shown up to the Win32 APIs yet, and the second call to the property, they show up.

Given the delay, I don't think there is a reliable way to consume the KeyUp events before returning to the caller. @lzybkr any suggestion?

daxian-dbw avatar Nov 11 '19 21:11 daxian-dbw

@daxian-dbw 100ms? Maybe @DHowett-MSFT has an idea why that might be.

Previous alternatives I've considered - but not really good options:

  • Change .Net to return on key up.
  • Use P/Invoke directly on Windows.

And a new idea based your investigation:

  • Consume the keyboard event in the background. This is might be a horrible idea - it would introduce a race condition unless you can lock the correct handle (CONIO$)?

lzybkr avatar Nov 11 '19 21:11 lzybkr

Is there a work around for this? $Host.UI.RawUI.KeyAvailable always returns true and if I ReadKey after this it breaks in my script. Is there anyway to defect a key press other than KeyAvailable?

Stewartarmbrecht avatar Jun 16 '20 00:06 Stewartarmbrecht

We have https://github.com/PowerShell/PowerShell/issues/7971 - Read-Host is slow on Linux. Perhaps it is related to this.

iSazonov avatar Jun 16 '20 03:06 iSazonov

Here is my workaround for this. Apparently FlushInputBuffer() is not quick enough so I need to throw in a quick sleep at the beginning. I have found 100ms does the trick reliably, I was able to go down as low as 75ms but it was a hit or miss. Tested in PowerShell 5.1 and 7.0.3.

start-sleep -milliseconds 100; 
$host.ui.RawUI.FlushInputBuffer(); 
$i=0; while(-not $host.UI.RawUI.KeyAvailable) { Write-Host -NoNewline ("`r{0}" -f '/?\|'[($i++ % 4)]); start-sleep -Milliseconds 200 }

forevanyeung avatar Dec 21 '20 21:12 forevanyeung

Using [Console]::KeyAvailable worked for me

jeremyhagan avatar Aug 27 '21 04:08 jeremyhagan

Interestingly, in Windows Terminal (but not in regular console windows) the problem also surfaces when PSReadLine is not loaded.

mklement0 avatar Sep 21 '21 15:09 mklement0