diff --git a/src/Microsoft.PowerShell.PSReadLine/ReadLine.cs b/src/Microsoft.PowerShell.PSReadLine/ReadLine.cs index e7ec6fb018..f3805676ad 100644 --- a/src/Microsoft.PowerShell.PSReadLine/ReadLine.cs +++ b/src/Microsoft.PowerShell.PSReadLine/ReadLine.cs @@ -881,6 +881,9 @@ namespace Microsoft.PowerShell _singleton.Render(); } + private const string PromptCommand = "prompt"; + private const string DefaultPrompt = "PS>"; + /// /// Gets the current prompt as possibly defined by the user through the /// prompt function, and returns a default prompt if no other is @@ -888,23 +891,45 @@ namespace Microsoft.PowerShell /// public static string GetPrompt() { + string newPrompt; var runspaceIsRemote = _singleton._mockableMethods.RunspaceIsRemote(_singleton._runspace); - System.Management.Automation.PowerShell ps; - if (!runspaceIsRemote) + + if ((_singleton._runspace.Debugger != null) && _singleton._runspace.Debugger.InBreakpoint) { - ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); + // Run prompt command in debugger API to ensure it is run correctly on the runspace. + // This handles remote runspace debugging and nested debugger scenarios. + PSDataCollection results = new PSDataCollection(); + var command = new PSCommand(); + command.AddCommand(PromptCommand); + _singleton._runspace.Debugger.ProcessCommand( + command, + results); + + newPrompt = (results.Count == 1) ? (results[0].BaseObject as string) : DefaultPrompt; } else { - ps = System.Management.Automation.PowerShell.Create(); - ps.Runspace = _singleton._runspace; + System.Management.Automation.PowerShell ps; + if (!runspaceIsRemote) + { + ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); + } + else + { + ps = System.Management.Automation.PowerShell.Create(); + ps.Runspace = _singleton._runspace; + } + using (ps) + { + ps.AddCommand(PromptCommand); + var result = ps.Invoke(); + newPrompt = result.Count == 1 ? result[0] : DefaultPrompt; + } } - string newPrompt; - using (ps) + + if (string.IsNullOrEmpty(newPrompt)) { - ps.AddCommand("prompt"); - var result = ps.Invoke(); - newPrompt = result.Count == 1 ? result[0] : "PS>"; + newPrompt = DefaultPrompt; } if (runspaceIsRemote) @@ -915,6 +940,7 @@ namespace Microsoft.PowerShell newPrompt = string.Format(CultureInfo.InvariantCulture, "[{0}]: {1}", connectionInfo.ComputerName, newPrompt); } } + return newPrompt; } diff --git a/src/System.Management.Automation/engine/hostifaces/HostUtilities.cs b/src/System.Management.Automation/engine/hostifaces/HostUtilities.cs index 31fb225eff..35fdd420ee 100644 --- a/src/System.Management.Automation/engine/hostifaces/HostUtilities.cs +++ b/src/System.Management.Automation/engine/hostifaces/HostUtilities.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using Microsoft.PowerShell.Commands; using System.Management.Automation.Host; @@ -863,6 +864,65 @@ namespace System.Management.Automation #region Public Access + #region Runspace Invoke + + /// + /// Helper method to invoke a PSCommand on a given runspace. This method correctly invokes the command for + /// these runspace cases: + /// 1. Local runspace. If the local runspace is busy it will invoke as a nested command. + /// 2. Remote runspace. + /// 3. Runspace that is stopped in the debugger at a breakpoint. + /// + /// Error and information streams are ignored and only the command result output is returned. + /// + /// This method is NOT thread safe. It does not support running commands from different threads on the + /// provided runspace. It assumes the thread invoking this method is the same that runs all other + /// commands on the provided runspace. + /// + /// Runspace to invoke the command on + /// Command to invoke + /// Collection of command output result objects + public static Collection InvokeOnRunspace(PSCommand command, Runspace runspace) + { + if (command == null) + { + throw new PSArgumentNullException("command"); + } + + if (runspace == null) + { + throw new PSArgumentNullException("runspace"); + } + + if ((runspace.Debugger != null) && runspace.Debugger.InBreakpoint) + { + // Use the Debugger API to run the command when a runspace is stopped in the debugger. + PSDataCollection output = new PSDataCollection(); + runspace.Debugger.ProcessCommand( + command, + output); + + return new Collection(output); + } + + // Otherwise run command directly in runspace. + PowerShell ps = PowerShell.Create(); + ps.Runspace = runspace; + ps.IsRunspaceOwner = false; + if (runspace.ConnectionInfo == null) + { + // Local runspace. Make a nested PowerShell object as needed. + ps.SetIsNested(runspace.GetCurrentlyRunningPipeline() != null); + } + using (ps) + { + ps.Commands = command; + return ps.Invoke(); + } + } + + #endregion + #region PSEdit Support /// @@ -878,10 +938,10 @@ namespace System.Management.Automation dir $file -File | foreach { $filePathName = $_.FullName -# Get file contents + # Get file contents $contentBytes = Get-Content -Path $filePathName -Raw -Encoding Byte -# Notify client for file open. + # Notify client for file open. New-Event -SourceIdentifier PSISERemoteSessionOpenFile -EventArguments @($filePathName, $contentBytes) > $null } } diff --git a/test/powershell/Host/HostUtilities.Tests.ps1 b/test/powershell/Host/HostUtilities.Tests.ps1 new file mode 100644 index 0000000000..8e4dd1995c --- /dev/null +++ b/test/powershell/Host/HostUtilities.Tests.ps1 @@ -0,0 +1,70 @@ +Describe "InvokeOnRunspace method argument error handling" -tags "Feature" { + + BeforeAll { + $command = [System.Management.Automation.PSCommand]::new() + $localRunspace = $host.Runspace + } + + It "Null argument exception should be thrown for null PSCommand argument" { + + try + { + [System.Management.Automation.HostUtilities]::InvokeOnRunspace($null, $localRunspace) + throw "InvokeOnRunspace method did not throw expected PSArgumentNullException exception" + } + catch + { + $_.FullyQualifiedErrorId | Should Be "PSArgumentNullException" + } + } + + It "Null argument exception should be thrown for null Runspace argument" { + + try + { + [System.Management.Automation.HostUtilities]::InvokeOnRunspace($command, $null) + throw "InvokeOnRunspace method did not throw expected PSArgumentNullException exception" + } + catch + { + $_.FullyQualifiedErrorId | Should Be "PSArgumentNullException" + } + } +} + +Describe "InvokeOnRunspace method as nested command" -tags "Feature" { + + It "Method should successfully invoke command as nested on busy runspace" { + + $command = [System.Management.Automation.PSCommand]::new() + $command.AddScript('"Hello!"') + $currentRunspace = $host.Runspace + + $results = [System.Management.Automation.HostUtilities]::InvokeOnRunspace($command, $currentRunspace) + + $results[0] | Should Be "Hello!" + } +} + +Describe "InvokeOnRunspace method on remote runspace" -tags "Feature" { + + BeforeAll { + $wc = [System.Management.Automation.Runspaces.WSManConnectionInfo]::new() + $remoteRunspace = [runspacefactory]::CreateRunspace($host, $wc) + $remoteRunspace.Open() + } + + AfterAll { + $remoteRunspace.Dispose(); + } + + It "Method should successfully invoke command on remote runspace" { + + $command = [System.Management.Automation.PSCommand]::new() + $command.AddScript('"Hello!"') + + $results = [System.Management.Automation.HostUtilities]::InvokeOnRunspace($command, $remoteRunspace) + + $results[0] | Should Be "Hello!" + } +}