mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
Skip auto-loading PSReadLine on Windows if the NVDA screen reader is active (#10385)
This commit is contained in:
@@ -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] <filePath> [args]]
|
||||
[-Command { - | <script-block> [-args <arg-array>]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user