diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs index 9ba3d11899d..3e657818e99 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs @@ -23,8 +23,8 @@ 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 @@ 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)] + [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,7 +587,7 @@ 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); @@ -590,7 +595,7 @@ public void PushRunspace(Runspace newRunspace) if (this.runningCmd != null) { - Microsoft.PowerShell.Commands.EnterPSSessionCommand.ContinueCommand( + EnterPSSessionCommand.ContinueCommand( remoteRunspace, this.runningCmd, this, @@ -858,7 +863,6 @@ public override PSObject PrivateData /// /// /// - public override System.Globalization.CultureInfo CurrentCulture { get @@ -875,7 +879,6 @@ public override System.Globalization.CultureInfo CurrentCulture /// /// /// - public override System.Globalization.CultureInfo CurrentUICulture { get @@ -890,7 +893,6 @@ public override System.Globalization.CultureInfo CurrentUICulture /// /// /// - public override void SetShouldExit(int exitCode) { lock (hostGlobalLock) @@ -1320,7 +1322,6 @@ internal TextWriter ConsoleTextWriter /// /// The process exit code to be returned by Main. /// - 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; } - /// - /// This method is retained to make V1 tests compatible with V2 as signature of this method - /// is slightly changed in v2. - /// - /// - /// - /// - /// - /// - [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); - } - /// /// Loops over the Host's sole Runspace; opens the runspace, initializes it, then recycles it if the Runspace fails. /// @@ -1504,12 +1488,32 @@ private void CreateRunspace(object runspaceCreationArgs) } /// - /// 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. /// - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - private void InitializeRunspace(string initialCommand, bool skipProfiles, Collection 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 @@ 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 @@ -1549,21 +1554,29 @@ private void DoCreateRunspace(string initialCommand, bool skipProfiles, bool sta // 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 defaultImportModulesList = null; + ReadOnlyCollection 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 diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx index 2b27cb19a4b..c82668c8eae 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx +++ b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx @@ -124,6 +124,9 @@ Copyright (c) Microsoft Corporation. All rights reserved. https://aka.ms/powershell Type 'help' to get help. + + 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'. + Usage: pwsh[.exe] [-Login] [[-File] <filePath> [args]] [-Command { - | <script-block> [-args <arg-array>] diff --git a/test/powershell/Host/ScreenReader.Tests.ps1 b/test/powershell/Host/ScreenReader.Tests.ps1 new file mode 100644 index 00000000000..c8bc31b5b95 --- /dev/null +++ b/test/powershell/Host/ScreenReader.Tests.ps1 @@ -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) + } +}