Skip auto-loading PSReadLine on Windows if the NVDA screen reader is active (#10385)

This commit is contained in:
Dongbo Wang
2019-08-23 09:09:34 +05:00
committed by Ilya
parent 717b06fd2c
commit b77228906d
3 changed files with 110 additions and 39 deletions
@@ -23,8 +23,8 @@ using System.Runtime;
using System.Runtime.InteropServices;
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 @@ namespace Microsoft.PowerShell
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)]
[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 @@ namespace Microsoft.PowerShell
}
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,7 +587,7 @@ namespace Microsoft.PowerShell
}
// 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);
@@ -590,7 +595,7 @@ namespace Microsoft.PowerShell
if (this.runningCmd != null)
{
Microsoft.PowerShell.Commands.EnterPSSessionCommand.ContinueCommand(
EnterPSSessionCommand.ContinueCommand(
remoteRunspace,
this.runningCmd,
this,
@@ -858,7 +863,6 @@ namespace Microsoft.PowerShell
/// </summary>
/// <value></value>
/// <exception/>
public override System.Globalization.CultureInfo CurrentCulture
{
get
@@ -875,7 +879,6 @@ namespace Microsoft.PowerShell
/// </summary>
/// <value></value>
/// <exception/>
public override System.Globalization.CultureInfo CurrentUICulture
{
get
@@ -890,7 +893,6 @@ namespace Microsoft.PowerShell
/// <summary>
/// </summary>
/// <exception/>
public override void SetShouldExit(int exitCode)
{
lock (hostGlobalLock)
@@ -1320,7 +1322,6 @@ namespace Microsoft.PowerShell
/// <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 @@ namespace Microsoft.PowerShell
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 @@ namespace Microsoft.PowerShell
}
/// <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,6 +1546,7 @@ namespace Microsoft.PowerShell
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
@@ -1549,21 +1554,29 @@ namespace Microsoft.PowerShell
// 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 @@ namespace Microsoft.PowerShell
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
@@ -124,6 +124,9 @@ Copyright (c) Microsoft Corporation. All rights reserved.
https://aka.ms/powershell
Type 'help' to get help.</value>
</data>
<data name="PSReadLineDisabledWhenScreenReaderIsActive" xml:space="preserve">
<value>Warning: PowerShell detected that you might be using a screen reader and has disabled PSReadLine for compatibility purposes. If you want to re-enable it, run 'Import-Module PSReadLine'.</value>
</data>
<data name="UsageHelp" xml:space="preserve">
<value>Usage: pwsh[.exe] [-Login] [[-File] &lt;filePath&gt; [args]]
[-Command { - | &lt;script-block&gt; [-args &lt;arg-array&gt;]
@@ -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)
}
}