-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Skip auto-loading PSReadLine on Windows if the NVDA screen reader is active #10385
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
Changes from all commits
d42db83
17b1c11
3ba280f
1c16f62
a1242f5
1d068a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,8 +23,8 @@ | |
| using System.Runtime.InteropServices; | ||
daxian-dbw marked this conversation as resolved.
Show resolved
Hide resolved
daxian-dbw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| using System.Text; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.PowerShell.Telemetry; | ||
| using Microsoft.PowerShell.Commands; | ||
|
|
||
| using ConsoleHandle = Microsoft.Win32.SafeHandles.SafeFileHandle; | ||
| using Dbg = System.Management.Automation.Diagnostics; | ||
|
|
@@ -55,6 +55,11 @@ internal sealed partial class ConsoleHost | |
| internal const int ExitCodeCtrlBreak = 128 + 21; // SIGBREAK | ||
| internal const int ExitCodeInitFailure = 70; // Internal Software Error | ||
| internal const int ExitCodeBadCommandLineParameter = 64; // Command Line Usage Error | ||
| private const uint SPI_GETSCREENREADER = 0x0046; | ||
|
|
||
| [DllImport("user32.dll", SetLastError = true)] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could remove
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer to keep it, even it's not used in this case. People copy code, and this is a good practice for PInvoke. |
||
| [return: MarshalAs(UnmanagedType.Bool)] | ||
| private static extern bool SystemParametersInfo(uint uiAction, uint uiParam, ref bool pvParam, uint fWinIni); | ||
|
|
||
| // NTRAID#Windows Out Of Band Releases-915506-2005/09/09 | ||
| // Removed HandleUnexpectedExceptions infrastructure | ||
|
|
@@ -158,7 +163,7 @@ internal static int Start( | |
| } | ||
|
|
||
| s_cpp = new CommandLineParameterParser( | ||
| (s_theConsoleHost != null) ? s_theConsoleHost.UI : (new NullHostUserInterface()), | ||
| (s_theConsoleHost != null) ? s_theConsoleHost.UI : new NullHostUserInterface(), | ||
| bannerText, helpText); | ||
|
|
||
| s_cpp.Parse(args); | ||
|
|
@@ -582,15 +587,15 @@ public void PushRunspace(Runspace newRunspace) | |
| } | ||
|
|
||
| // Connect a disconnected command. | ||
| this.runningCmd = Microsoft.PowerShell.Commands.EnterPSSessionCommand.ConnectRunningPipeline(remoteRunspace); | ||
| this.runningCmd = EnterPSSessionCommand.ConnectRunningPipeline(remoteRunspace); | ||
|
|
||
| // Push runspace. | ||
| _runspaceRef.Override(remoteRunspace, hostGlobalLock, out _isRunspacePushed); | ||
| RunspacePushed.SafeInvoke(this, EventArgs.Empty); | ||
|
|
||
| if (this.runningCmd != null) | ||
| { | ||
| Microsoft.PowerShell.Commands.EnterPSSessionCommand.ContinueCommand( | ||
| EnterPSSessionCommand.ContinueCommand( | ||
| remoteRunspace, | ||
| this.runningCmd, | ||
| this, | ||
|
|
@@ -858,7 +863,6 @@ public override PSObject PrivateData | |
| /// </summary> | ||
| /// <value></value> | ||
| /// <exception/> | ||
|
|
||
| public override System.Globalization.CultureInfo CurrentCulture | ||
| { | ||
| get | ||
|
|
@@ -875,7 +879,6 @@ public override System.Globalization.CultureInfo CurrentCulture | |
| /// </summary> | ||
| /// <value></value> | ||
| /// <exception/> | ||
|
|
||
| public override System.Globalization.CultureInfo CurrentUICulture | ||
| { | ||
| get | ||
|
|
@@ -890,7 +893,6 @@ public override System.Globalization.CultureInfo CurrentUICulture | |
| /// <summary> | ||
| /// </summary> | ||
| /// <exception/> | ||
|
|
||
| public override void SetShouldExit(int exitCode) | ||
| { | ||
| lock (hostGlobalLock) | ||
|
|
@@ -1320,7 +1322,6 @@ internal TextWriter ConsoleTextWriter | |
| /// <returns> | ||
| /// The process exit code to be returned by Main. | ||
| /// </returns> | ||
|
|
||
| private uint Run(CommandLineParameterParser cpp, bool isPrestartWarned) | ||
| { | ||
| Dbg.Assert(cpp != null, "CommandLine parameter parser cannot be null."); | ||
|
|
@@ -1382,23 +1383,6 @@ private uint Run(CommandLineParameterParser cpp, bool isPrestartWarned) | |
| return exitCode; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// This method is retained to make V1 tests compatible with V2 as signature of this method | ||
| /// is slightly changed in v2. | ||
| /// </summary> | ||
| /// <param name="bannerText"></param> | ||
| /// <param name="helpText"></param> | ||
| /// <param name="isPrestartWarned"></param> | ||
| /// <param name="args"></param> | ||
| /// <returns></returns> | ||
| [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] | ||
| private uint Run(string bannerText, string helpText, bool isPrestartWarned, string[] args) | ||
| { | ||
| s_cpp = new CommandLineParameterParser(this.UI, bannerText, helpText); | ||
| s_cpp.Parse(args); | ||
| return Run(s_cpp, isPrestartWarned); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Loops over the Host's sole Runspace; opens the runspace, initializes it, then recycles it if the Runspace fails. | ||
| /// </summary> | ||
|
|
@@ -1504,12 +1488,32 @@ private void CreateRunspace(object runspaceCreationArgs) | |
| } | ||
|
|
||
| /// <summary> | ||
| /// This method is here only to make V1 tests compatible with V2. DO NOT USE THIS FUNCTION! Use DoCreateRunspace instead. | ||
| /// Check if a screen reviewer utility is running. | ||
| /// When a screen reader is running, we don't auto-load the PSReadLine module at startup, | ||
| /// since PSReadLine is not accessibility-firendly enough as of today. | ||
| /// </summary> | ||
| [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] | ||
| private void InitializeRunspace(string initialCommand, bool skipProfiles, Collection<CommandParameter> initialCommandArgs) | ||
| private bool IsScreenReaderActive() | ||
| { | ||
| DoCreateRunspace(initialCommand, skipProfiles, staMode: false, configurationName: null, initialCommandArgs: initialCommandArgs); | ||
| if (_screenReaderActive.HasValue) | ||
| { | ||
| return _screenReaderActive.Value; | ||
| } | ||
|
|
||
| _screenReaderActive = false; | ||
| if (Platform.IsWindowsDesktop) | ||
| { | ||
| // Note: this API can detect if a third-party screen reader is active, such as NVDA, but not the in-box Windows Narrator. | ||
| // Quoted from https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfoa about the | ||
| // accessibility parameter 'SPI_GETSCREENREADER': | ||
| // "Narrator, the screen reader that is included with Windows, does not set the SPI_SETSCREENREADER or SPI_GETSCREENREADER flags." | ||
| bool enabled = false; | ||
| if (SystemParametersInfo(SPI_GETSCREENREADER, 0, ref enabled, 0)) | ||
| { | ||
| _screenReaderActive = enabled; | ||
| } | ||
| } | ||
|
|
||
| return _screenReaderActive.Value; | ||
| } | ||
|
|
||
| private bool LoadPSReadline() | ||
|
|
@@ -1542,28 +1546,37 @@ private void DoCreateRunspace(string initialCommand, bool skipProfiles, bool sta | |
| bool psReadlineFailed = false; | ||
|
|
||
| // Load PSReadline by default unless there is no use: | ||
| // - screen reader is active, such as NVDA, indicating non-visual access | ||
| // - we're running a command/file and just exiting | ||
| // - stdin is redirected by a parent process | ||
| // - we're not interactive | ||
| // - we're explicitly reading from stdin (the '-' argument) | ||
| // It's also important to have a scenario where PSReadline is not loaded so it can be updated, e.g. | ||
| // powershell -command "Update-Module PSReadline" | ||
| // This should work just fine as long as no other instances of PowerShell are running. | ||
| ReadOnlyCollection<Microsoft.PowerShell.Commands.ModuleSpecification> defaultImportModulesList = null; | ||
| ReadOnlyCollection<ModuleSpecification> defaultImportModulesList = null; | ||
| if (LoadPSReadline()) | ||
| { | ||
| // Create and open Runspace with PSReadline. | ||
| defaultImportModulesList = DefaultInitialSessionState.Modules; | ||
| DefaultInitialSessionState.ImportPSModule(new[] { "PSReadLine" }); | ||
| consoleRunspace = RunspaceFactory.CreateRunspace(this, DefaultInitialSessionState); | ||
| try | ||
| if (IsScreenReaderActive()) | ||
| { | ||
| OpenConsoleRunspace(consoleRunspace, staMode); | ||
| s_theConsoleHost.UI.WriteLine(ManagedEntranceStrings.PSReadLineDisabledWhenScreenReaderIsActive); | ||
| s_theConsoleHost.UI.WriteLine(); | ||
| } | ||
| catch (Exception) | ||
| else | ||
| { | ||
| consoleRunspace = null; | ||
| psReadlineFailed = true; | ||
| // Create and open Runspace with PSReadline. | ||
| defaultImportModulesList = DefaultInitialSessionState.Modules; | ||
| DefaultInitialSessionState.ImportPSModule(new[] { "PSReadLine" }); | ||
| consoleRunspace = RunspaceFactory.CreateRunspace(this, DefaultInitialSessionState); | ||
| try | ||
| { | ||
| OpenConsoleRunspace(consoleRunspace, staMode); | ||
| } | ||
| catch (Exception) | ||
| { | ||
| consoleRunspace = null; | ||
| psReadlineFailed = true; | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -2834,6 +2847,7 @@ private class ConsoleHostStartupException : Exception | |
| private bool _setShouldExitCalled; | ||
| private bool _isRunningPromptLoop; | ||
| private bool _wasInitialCommandEncoded; | ||
| private bool? _screenReaderActive; | ||
|
|
||
| // hostGlobalLock is used to sync public method calls (in case multiple threads call into the host) and access to | ||
| // state that persists across method calls, like progress data. It's internal because the ui object also | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. | ||
|
|
||
| Describe "Validate start of console host" -Tag CI { | ||
| BeforeAll { | ||
| if (-not $IsWindows) { | ||
| return | ||
| } | ||
|
|
||
| $csharp_source = @' | ||
| using System; | ||
| using System.Runtime.InteropServices; | ||
|
|
||
| public class ScreenReaderTestUtility { | ||
| private const uint SPI_SETSCREENREADER = 0x0047; | ||
|
|
||
| [DllImport("user32.dll", SetLastError = true)] | ||
| [return: MarshalAs(UnmanagedType.Bool)] | ||
| private static extern bool SystemParametersInfo(uint uiAction, uint uiParam, IntPtr pvParam, uint fWinIni); | ||
|
|
||
| public static bool ActivateScreenReader() { | ||
| return SystemParametersInfo(SPI_SETSCREENREADER, 1u, IntPtr.Zero, 0); | ||
| } | ||
|
|
||
| public static bool DeactivateScreenReader() { | ||
| return SystemParametersInfo(SPI_SETSCREENREADER, 0u, IntPtr.Zero, 0); | ||
| } | ||
| } | ||
| '@ | ||
| $utilType = "ScreenReaderTestUtility" -as [type] | ||
| if (-not $utilType) { | ||
| $utilType = Add-Type -TypeDefinition $csharp_source -PassThru | ||
| } | ||
|
|
||
| ## Make the screen reader status active. | ||
| $utilType::ActivateScreenReader() | ||
| } | ||
|
|
||
| AfterAll { | ||
| if ($IsWindows) { | ||
| ## Make the screen reader status in-active. | ||
| $utilType::DeactivateScreenReader() | ||
| } | ||
| } | ||
|
|
||
| It "PSReadLine should not be auto-loaded when screen reader status is active" -Skip:(-not $IsWindows) { | ||
| $output = pwsh -noprofile -noexit -c "Get-Module PSReadLine; exit" | ||
| $output.Length | Should -BeExactly 2 | ||
|
|
||
| ## The warning message about screen reader should be returned, but the PSReadLine module should not be loaded. | ||
| $output[0] | Should -BeLike "Warning:*'Import-Module PSReadLine'." | ||
| $output[1] | Should -BeExactly ([string]::Empty) | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.