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)
+ }
+}