[feature] Add -CustomPipeName to pwsh and Enter-PSHostProcess (#8889)

This allows a user to start PowerShell up with the name of the named pipe that is used for cross process communication (I.e. Enter-PSHostProcess).
This commit is contained in:
Tyler James Leonhardt 2019-02-22 12:40:03 -06:00 committed by Ilya
parent 63acf6812f
commit 23eccfd641
12 changed files with 484 additions and 85 deletions

1
.gitignore vendored
View file

@ -77,6 +77,7 @@ Temporary Items
# TestsResults
TestsResults*.xml
ParallelXUnitResults.xml
# Resharper settings
PowerShell.sln.DotSettings.user

View file

@ -168,6 +168,9 @@ namespace Microsoft.PowerShell
internal class CommandLineParameterParser
{
private const int MaxPipePathLengthLinux = 108;
private const int MaxPipePathLengthMacOS = 104;
internal static string[] validParameters = {
"version",
"nologo",
@ -189,7 +192,8 @@ namespace Microsoft.PowerShell
"settingsfile",
"help",
"workingdirectory",
"removeworkingdirectorytrailingcharacter"
"removeworkingdirectorytrailingcharacter",
"custompipename"
};
internal CommandLineParameterParser(PSHostUserInterface hostUI, string bannerText, string helpText)
@ -341,6 +345,14 @@ namespace Microsoft.PowerShell
}
}
internal string CustomPipeName
{
get
{
return _customPipeName;
}
}
internal Serialization.DataFormat OutputFormat
{
get
@ -773,6 +785,33 @@ namespace Microsoft.PowerShell
_configurationName = args[i];
}
else if (MatchSwitch(switchKey, "custompipename", "cus"))
{
++i;
if (i >= args.Length)
{
WriteCommandLineError(
CommandLineParameterParserStrings.MissingCustomPipeNameArgument);
break;
}
if (!Platform.IsWindows)
{
int maxNameLength = (Platform.IsLinux ? MaxPipePathLengthLinux : MaxPipePathLengthMacOS) - Path.GetTempPath().Length;
if (args[i].Length > maxNameLength)
{
WriteCommandLineError(
string.Format(
CommandLineParameterParserStrings.CustomPipeNameTooLong,
maxNameLength,
args[i],
args[i].Length));
break;
}
}
_customPipeName = args[i];
}
else if (MatchSwitch(switchKey, "command", "c"))
{
if (!ParseCommand(args, ref i, noexitSeen, false))
@ -1375,6 +1414,7 @@ namespace Microsoft.PowerShell
private string _helpText;
private bool _abortStartup;
private bool _skipUserInit;
private string _customPipeName;
#if STAMODE
// Win8: 182409 PowerShell 3.0 should run in STA mode by default
// -sta and -mta are mutually exclusive..so tracking them using nullable boolean

View file

@ -1369,6 +1369,12 @@ namespace Microsoft.PowerShell
}
#endif
// If the debug pipe name was specified, create the custom IPC channel.
if (!string.IsNullOrEmpty(cpp.CustomPipeName))
{
RemoteSessionNamedPipeServer.CreateCustomNamedPipeServer(cpp.CustomPipeName);
}
// NTRAID#Windows Out Of Band Releases-915506-2005/09/09
// Removed HandleUnexpectedExceptions infrastructure
#if STAMODE
@ -2884,16 +2890,12 @@ namespace Microsoft.PowerShell
/// <summary>
/// Constructs RunspaceCreationEventArgs.
/// </summary>
/// <param name="initialCommand"></param>
/// <param name="skipProfiles"></param>
/// <param name="staMode"></param>
/// <param name="configurationName"></param>
/// <param name="initialCommandArgs"></param>
internal RunspaceCreationEventArgs(string initialCommand,
bool skipProfiles,
bool staMode,
string configurationName,
Collection<CommandParameter> initialCommandArgs)
internal RunspaceCreationEventArgs(
string initialCommand,
bool skipProfiles,
bool staMode,
string configurationName,
Collection<CommandParameter> initialCommandArgs)
{
InitialCommand = initialCommand;
SkipProfiles = skipProfiles;

View file

@ -186,6 +186,12 @@
<data name="MissingConfigurationNameArgument" xml:space="preserve">
<value>Cannot process the command because -Configuration requires an argument that is a remote endpoint configuration name. Specify this argument and try again.</value>
</data>
<data name="MissingCustomPipeNameArgument" xml:space="preserve">
<value>Cannot process the command because -CustomPipeName requires an argument that is a name of the pipe you want to use. Specify this argument and try again.</value>
</data>
<data name="CustomPipeNameTooLong" xml:space="preserve">
<value>Cannot process the command because -CustomPipeName specified is too long. Pipe names on this platform can be up to {0} characters long. Your pipe name '{1}' is {2} characters.</value>
</data>
<data name="MissingSettingsFileArgument" xml:space="preserve">
<value>Cannot process the command because -SettingsFile requires an argument that is a file path.</value>
</data>

View file

@ -128,7 +128,8 @@ Type 'help' to get help.</value>
<value>Usage: pwsh[.exe] [[-File] &lt;filePath&gt; [args]]
[-Command { - | &lt;script-block&gt; [-args &lt;arg-array&gt;]
| &lt;string&gt; [&lt;CommandParameters&gt;] } ]
[-ConfigurationName &lt;string&gt;] [-EncodedCommand &lt;Base64EncodedCommand&gt;]
[-ConfigurationName &lt;string&gt;] [-CustomPipeName &lt;string&gt;]
[-EncodedCommand &lt;Base64EncodedCommand&gt;]
[-ExecutionPolicy &lt;ExecutionPolicy&gt;] [-InputFormat {Text | XML}]
[-Interactive] [-NoExit] [-NoLogo] [-NonInteractive] [-NoProfile]
[-OutputFormat {Text | XML}] [-Version] [-WindowStyle &lt;style&gt;]
@ -174,6 +175,17 @@ All parameters are case-insensitive.</value>
Example: pwsh -ConfigurationName AdminRoles
-CustomPipeName
Specifies the name to use for an additional IPC server (named pipe) used for debugging
and other cross-process communication. This offers a predictable mechanism for connecting
to other PowerShell instances. Typically used with the CustomPipeName parameter on Enter-PSHostProcess.
Example:
# PowerShell instance 1
pwsh -CustomPipeName mydebugpipe
# PowerShell instance 2
Enter-PSHostProcess -CustomPipeName mydebugpipe
-EncodedCommand | -e | -ec
Accepts a base64 encoded string version of a command. Use this parameter to submit
commands to PowerShell that require complex quotation marks or curly braces.

View file

@ -13,6 +13,7 @@ using System.Management.Automation.Host;
using System.Management.Automation.Internal;
using System.Management.Automation.Remoting;
using System.Management.Automation.Runspaces;
using System.Text;
namespace Microsoft.PowerShell.Commands
{
@ -37,6 +38,7 @@ namespace Microsoft.PowerShell.Commands
private const string ProcessParameterSet = "ProcessParameterSet";
private const string ProcessNameParameterSet = "ProcessNameParameterSet";
private const string ProcessIdParameterSet = "ProcessIdParameterSet";
private const string PipeNameParameterSet = "PipePipeNameParameterSet";
private const string PSHostProcessInfoParameterSet = "PSHostProcessInfoParameterSet";
private const string NamedPipeRunspaceName = "PSAttachRunspace";
@ -91,6 +93,16 @@ namespace Microsoft.PowerShell.Commands
set;
}
/// <summary>
/// Gets or sets the custom named pipe name to connect to. This is usually used in conjunction with `pwsh -CustomPipeName`.
/// </summary>
[Parameter(Mandatory = true, ParameterSetName = EnterPSHostProcessCommand.PipeNameParameterSet)]
public string CustomPipeName
{
get;
set;
}
/// <summary>
/// Optional name of AppDomain in process to enter. If not specified then the default AppDomain is used.
/// </summary>
@ -129,26 +141,35 @@ namespace Microsoft.PowerShell.Commands
}
// Check selected process for existence, and whether it hosts PowerShell.
Runspace namedPipeRunspace = null;
switch (ParameterSetName)
{
case ProcessIdParameterSet:
Process = GetProcessById(Id);
VerifyProcess(Process);
namedPipeRunspace = CreateNamedPipeRunspace(Process.Id, AppDomainName);
break;
case ProcessNameParameterSet:
Process = GetProcessByName(Name);
VerifyProcess(Process);
namedPipeRunspace = CreateNamedPipeRunspace(Process.Id, AppDomainName);
break;
case PSHostProcessInfoParameterSet:
Process = GetProcessByHostProcessInfo(HostProcessInfo);
VerifyProcess(Process);
// Create named pipe runspace for selected process and open.
namedPipeRunspace = CreateNamedPipeRunspace(Process.Id, AppDomainName);
break;
case PipeNameParameterSet:
VerifyPipeName(CustomPipeName);
namedPipeRunspace = CreateNamedPipeRunspace(CustomPipeName);
break;
}
VerifyProcess(Process);
// Create named pipe runspace for selected process and open.
Runspace namedPipeRunspace = CreateNamedPipeRunspace(Process.Id, AppDomainName);
// Set runspace prompt. The runspace is closed on pop so we don't
// have to reverse this change.
PrepareRunspace(namedPipeRunspace);
@ -187,9 +208,20 @@ namespace Microsoft.PowerShell.Commands
#region Private Methods
private Runspace CreateNamedPipeRunspace(string customPipeName)
{
NamedPipeConnectionInfo connectionInfo = new NamedPipeConnectionInfo(customPipeName);
return CreateNamedPipeRunspace(connectionInfo);
}
private Runspace CreateNamedPipeRunspace(int procId, string appDomainName)
{
NamedPipeConnectionInfo connectionInfo = new NamedPipeConnectionInfo(procId, appDomainName);
return CreateNamedPipeRunspace(connectionInfo);
}
private Runspace CreateNamedPipeRunspace(NamedPipeConnectionInfo connectionInfo)
{
TypeTable typeTable = TypeTable.LoadDefaultTypeFiles();
RemoteRunspace remoteRunspace = RunspaceFactory.CreateRunspace(connectionInfo, this.Host, typeTable) as RemoteRunspace;
remoteRunspace.Name = NamedPipeRunspaceName;
@ -203,20 +235,40 @@ namespace Microsoft.PowerShell.Commands
}
catch (RuntimeException e)
{
string msgAppDomainName = (!string.IsNullOrEmpty(appDomainName)) ? appDomainName : NamedPipeUtils.DefaultAppDomainName;
// Unwrap inner exception for original error message, if any.
string errorMessage = (e.InnerException != null) ? (e.InnerException.Message ?? string.Empty) : string.Empty;
ThrowTerminatingError(
new ErrorRecord(
new RuntimeException(
StringUtil.Format(RemotingErrorIdStrings.EnterPSHostProcessCannotConnectToProcess,
msgAppDomainName, procId, errorMessage),
e.InnerException),
"EnterPSHostProcessCannotConnectToProcess",
ErrorCategory.OperationTimeout,
this));
if (connectionInfo.CustomPipeName != null)
{
ThrowTerminatingError(
new ErrorRecord(
new RuntimeException(
StringUtil.Format(
RemotingErrorIdStrings.EnterPSHostProcessCannotConnectToPipe,
connectionInfo.CustomPipeName,
errorMessage),
e.InnerException),
"EnterPSHostProcessCannotConnectToPipe",
ErrorCategory.OperationTimeout,
this));
}
else
{
string msgAppDomainName = connectionInfo.AppDomainName ?? NamedPipeUtils.DefaultAppDomainName;
ThrowTerminatingError(
new ErrorRecord(
new RuntimeException(
StringUtil.Format(
RemotingErrorIdStrings.EnterPSHostProcessCannotConnectToProcess,
msgAppDomainName,
connectionInfo.ProcessId,
errorMessage),
e.InnerException),
"EnterPSHostProcessCannotConnectToProcess",
ErrorCategory.OperationTimeout,
this));
}
}
finally
{
@ -345,6 +397,33 @@ namespace Microsoft.PowerShell.Commands
}
}
private void VerifyPipeName(string customPipeName)
{
// Named Pipes are represented differently on Windows vs macOS & Linux
var sb = new StringBuilder(customPipeName.Length);
if (Platform.IsWindows)
{
sb.Append(@"\\.\pipe\");
}
else
{
sb.Append(Path.GetTempPath()).Append("CoreFxPipe_");
}
sb.Append(customPipeName);
string pipePath = sb.ToString();
if (!File.Exists(pipePath))
{
ThrowTerminatingError(
new ErrorRecord(
new PSArgumentException(StringUtil.Format(RemotingErrorIdStrings.EnterPSHostProcessNoNamedPipeFound, customPipeName)),
"EnterPSHostProcessNoNamedPipeFound",
ErrorCategory.InvalidArgument,
this));
}
}
#endregion
}

View file

@ -343,7 +343,7 @@ namespace System.Management.Automation.Remoting
/// having correct access restrictions, and provides a listener
/// thread loop.
/// </summary>
internal sealed class RemoteSessionNamedPipeServer : IDisposable
public sealed class RemoteSessionNamedPipeServer : IDisposable
{
#region Members
@ -352,12 +352,17 @@ namespace System.Management.Automation.Remoting
private const string _threadName = "IPC Listener Thread";
private const int _namedPipeBufferSizeForRemoting = 32768;
private const int _maxPipePathLengthLinux = 108;
private const int _maxPipePathLengthMacOS = 104;
// Singleton server.
private static object s_syncObject;
internal static RemoteSessionNamedPipeServer IPCNamedPipeServer;
internal static bool IPCNamedPipeServerEnabled;
// Optional custom server.
private static RemoteSessionNamedPipeServer _customNamedPipeServer;
// Access mask constant taken from PipeSecurity access rights and is equivalent to
// PipeAccessRights.FullControl.
// See: https://msdn.microsoft.com/library/vstudio/bb348408(v=vs.100).aspx
@ -371,37 +376,37 @@ namespace System.Management.Automation.Remoting
/// <summary>
/// Returns the Named Pipe stream object.
/// </summary>
public NamedPipeServerStream Stream { get; }
internal NamedPipeServerStream Stream { get; }
/// <summary>
/// Returns the Named Pipe name.
/// </summary>
public string PipeName { get; }
internal string PipeName { get; }
/// <summary>
/// Returns true if listener is currently running.
/// </summary>
public bool IsListenerRunning { get; private set; }
internal bool IsListenerRunning { get; private set; }
/// <summary>
/// Name of session configuration.
/// </summary>
public string ConfigurationName { get; set; }
internal string ConfigurationName { get; set; }
/// <summary>
/// Accessor for the named pipe reader.
/// </summary>
public StreamReader TextReader { get; private set; }
internal StreamReader TextReader { get; private set; }
/// <summary>
/// Accessor for the named pipe writer.
/// </summary>
public StreamWriter TextWriter { get; private set; }
internal StreamWriter TextWriter { get; private set; }
/// <summary>
/// Returns true if object is currently disposed.
/// </summary>
public bool IsDisposed { get; private set; }
internal bool IsDisposed { get; private set; }
/// <summary>
/// Buffer size for PSRP fragmentor.
@ -419,7 +424,7 @@ namespace System.Management.Automation.Remoting
/// Event raised when the named pipe server listening thread
/// ends.
/// </summary>
public event EventHandler<ListenerEndedEventArgs> ListenerEnded;
internal event EventHandler<ListenerEndedEventArgs> ListenerEnded;
#endregion
@ -429,7 +434,7 @@ namespace System.Management.Automation.Remoting
/// Creates a RemoteSessionNamedPipeServer with the current process and AppDomain information.
/// </summary>
/// <returns>RemoteSessionNamedPipeServer.</returns>
public static RemoteSessionNamedPipeServer CreateRemoteSessionNamedPipeServer()
internal static RemoteSessionNamedPipeServer CreateRemoteSessionNamedPipeServer()
{
string appDomainName = NamedPipeUtils.GetCurrentAppDomainName();
@ -552,6 +557,7 @@ namespace System.Management.Automation.Remoting
IPCNamedPipeServerEnabled = true;
CreateIPCNamedPipeServerSingleton();
#if !CORECLR // There is only one AppDomain per application in CoreCLR, which would be the default
CreateAppDomainUnloadHandler();
#endif
@ -600,6 +606,70 @@ namespace System.Management.Automation.Remoting
#region Public Methods
/// <summary>
/// Creates the custom named pipe server with the given pipename.
/// </summary>
/// <param name="pipeName">The name of the pipe to create.</param>
public static void CreateCustomNamedPipeServer(string pipeName)
{
lock (s_syncObject)
{
if (_customNamedPipeServer != null && !_customNamedPipeServer.IsDisposed)
{
if (pipeName == _customNamedPipeServer.PipeName)
{
// we shouldn't recreate the server object if we're using the same pipeName
return;
}
// Dispose of the current pipe server so we can create a new one with the new pipeName
_customNamedPipeServer.Dispose();
}
if (!Platform.IsWindows)
{
int maxNameLength = (Platform.IsLinux ? _maxPipePathLengthLinux : _maxPipePathLengthMacOS) - Path.GetTempPath().Length;
if (pipeName.Length > maxNameLength)
{
throw new InvalidOperationException(
string.Format(
RemotingErrorIdStrings.CustomPipeNameTooLong,
maxNameLength,
pipeName,
pipeName.Length));
}
}
try
{
try
{
_customNamedPipeServer = new RemoteSessionNamedPipeServer(pipeName);
}
catch (IOException)
{
// Expected when named pipe server for this process already exists.
// This can happen if process has multiple AppDomains hosting PowerShell (SMA.dll).
return;
}
// Listener ended callback, used to create listening new pipe server.
_customNamedPipeServer.ListenerEnded += OnCustomNamedPipeServerEnded;
// Start the pipe server listening thread, and provide client connection callback.
_customNamedPipeServer.StartListening(ClientConnectionCallback);
}
catch (Exception)
{
_customNamedPipeServer = null;
}
}
}
#endregion
#region Private Methods
/// <summary>
/// Starts named pipe server listening thread. When a client connects this thread
/// makes a callback to implement the client communication. When the thread ends
@ -607,7 +677,7 @@ namespace System.Management.Automation.Remoting
/// and a new listening thread started to handle subsequent client connections.
/// </summary>
/// <param name="clientConnectCallback">Connection callback.</param>
public void StartListening(
internal void StartListening(
Action<RemoteSessionNamedPipeServer> clientConnectCallback)
{
if (clientConnectCallback == null)
@ -632,10 +702,6 @@ namespace System.Management.Automation.Remoting
}
}
#endregion
#region Private Methods
internal static CommonSecurityDescriptor GetServerPipeSecurity()
{
#if UNIX
@ -925,6 +991,14 @@ namespace System.Management.Automation.Remoting
}
}
private static void OnCustomNamedPipeServerEnded(object sender, ListenerEndedEventArgs args)
{
if (args.RestartListener && sender is RemoteSessionNamedPipeServer server)
{
CreateCustomNamedPipeServer(server.PipeName);
}
}
private static void ClientConnectionCallback(RemoteSessionNamedPipeServer pipeServer)
{
// Create server mediator object and begin remote session with client.

View file

@ -1722,12 +1722,21 @@ namespace System.Management.Automation.Runspaces
}
}
/// <summary>
/// Gets or sets the custom named pipe name to connect to. This is usually used in conjunction with pwsh -CustomPipeName.
/// </summary>
public string CustomPipeName
{
get;
set;
}
#endregion
#region Constructors
/// <summary>
/// Constructor.
/// Initializes a new instance of the <see cref="NamedPipeConnectionInfo"/> class.
/// </summary>
public NamedPipeConnectionInfo()
{
@ -1735,7 +1744,7 @@ namespace System.Management.Automation.Runspaces
}
/// <summary>
/// Constructor.
/// Initializes a new instance of the <see cref="NamedPipeConnectionInfo"/> class.
/// </summary>
/// <param name="processId">Process Id to connect to.</param>
public NamedPipeConnectionInfo(
@ -1744,7 +1753,7 @@ namespace System.Management.Automation.Runspaces
{ }
/// <summary>
/// Constructor.
/// Initializes a new instance of the <see cref="NamedPipeConnectionInfo"/> class.
/// </summary>
/// <param name="processId">Process Id to connect to.</param>
/// <param name="appDomainName">Application domain name to connect to, or default AppDomain if blank.</param>
@ -1755,7 +1764,7 @@ namespace System.Management.Automation.Runspaces
{ }
/// <summary>
/// Constructor.
/// Initializes a new instance of the <see cref="NamedPipeConnectionInfo"/> class.
/// </summary>
/// <param name="processId">Process Id to connect to.</param>
/// <param name="appDomainName">Name of application domain to connect to. Connection is to default application domain if blank.</param>
@ -1770,6 +1779,33 @@ namespace System.Management.Automation.Runspaces
OpenTimeout = openTimeout;
}
/// <summary>
/// Initializes a new instance of the <see cref="NamedPipeConnectionInfo"/> class.
/// </summary>
/// <param name="customPipeName">Pipe name to connect to.</param>
public NamedPipeConnectionInfo(
string customPipeName) :
this(customPipeName, _defaultOpenTimeout)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="NamedPipeConnectionInfo"/> class.
/// </summary>
/// <param name="customPipeName">Pipe name to connect to.</param>
/// <param name="openTimeout">Open time out in Milliseconds.</param>
public NamedPipeConnectionInfo(
string customPipeName,
int openTimeout)
{
if (customPipeName == null)
{
throw new PSArgumentNullException(nameof(customPipeName));
}
CustomPipeName = customPipeName;
OpenTimeout = openTimeout;
}
#endregion
#region Overrides
@ -1842,6 +1878,7 @@ namespace System.Management.Automation.Runspaces
newCopy.ProcessId = this.ProcessId;
newCopy._appDomainName = _appDomainName;
newCopy.OpenTimeout = this.OpenTimeout;
newCopy.CustomPipeName = this.CustomPipeName;
return newCopy;
}

View file

@ -1884,7 +1884,9 @@ namespace System.Management.Automation.Remoting.Client
/// </summary>
internal override void CreateAsync()
{
_clientPipe = new RemoteSessionNamedPipeClient(_connectionInfo.ProcessId, _connectionInfo.AppDomainName);
_clientPipe = string.IsNullOrEmpty(_connectionInfo.CustomPipeName) ?
new RemoteSessionNamedPipeClient(_connectionInfo.ProcessId, _connectionInfo.AppDomainName) :
new RemoteSessionNamedPipeClient(_connectionInfo.CustomPipeName);
// Wait for named pipe to connect.
_clientPipe.Connect(_connectionInfo.OpenTimeout);

View file

@ -1422,6 +1422,12 @@ All WinRM sessions connected to PowerShell session configurations, such as Micro
<data name="EnterPSHostProcessNoProcessFoundWithName" xml:space="preserve">
<value>No process was found with Name: {0}.</value>
</data>
<data name="EnterPSHostProcessNoNamedPipeFound" xml:space="preserve">
<value>No named pipe was found with CustomPipeName: {0}.</value>
</data>
<data name="CustomPipeNameTooLong" xml:space="preserve">
<value>Cannot process the command because the pipeName specified is too long. Pipe names on this platform can be up to {0} characters long. Your pipe name '{1}' is {2} characters.</value>
</data>
<data name="HostDoesNotSupportIASession" xml:space="preserve">
<value>The current host does not support the Enter-PSHostProcess cmdlet.</value>
</data>
@ -1440,6 +1446,9 @@ All WinRM sessions connected to PowerShell session configurations, such as Micro
<data name="EnterPSHostProcessCannotConnectToProcess" xml:space="preserve">
<value>Unable to connect to application domain name {0} of process {1}. Error: {2}.</value>
</data>
<data name="EnterPSHostProcessCannotConnectToPipe" xml:space="preserve">
<value>Unable to connect to pipe with name {0}. Error: {1}.</value>
</data>
<data name="WSManPluginConnectNoNegotiationData" xml:space="preserve">
<value>PowerShell plugin cannot process the Connect operation as required negotiation information is either missing or not complete.</value>
</data>

View file

@ -2,54 +2,133 @@
# Licensed under the MIT License.
Describe "Enter-PSHostProcess tests" -Tag Feature {
BeforeAll {
$pwsh_started = New-TemporaryFile
$si = [System.Diagnostics.ProcessStartInfo]::new()
$si.FileName = "pwsh"
$si.Arguments = "-noexit -command 'pwsh' > '$pwsh_started'"
$si.RedirectStandardInput = $true
$si.RedirectStandardOutput = $true
$si.RedirectStandardError = $true
$pwsh = [System.Diagnostics.Process]::Start($si)
Context "By Process Id" {
BeforeAll {
$params = @{
FilePath = "pwsh"
PassThru = $true
RedirectStandardOutput = "TestDrive:\pwsh_out.log"
RedirectStandardError = "TestDrive:\pwsh_err.log"
}
$pwsh = Start-Process @params
if ($IsWindows) {
$params = @{
FilePath = "powershell"
PassThru = $true
RedirectStandardOutput = "TestDrive:\powershell_out.log"
RedirectStandardError = "TestDrive:\powershell_err.log"
}
$powershell = Start-Process @params
}
if ($IsWindows) {
$powershell_started = New-TemporaryFile
$si.FileName = "powershell"
$si.Arguments = "-noexit -command 'powershell' >'$powershell_started'"
$powershell = [System.Diagnostics.Process]::Start($si)
}
}
AfterAll {
$pwsh | Stop-Process
AfterAll {
$pwsh | Stop-Process
Remove-Item $pwsh_started -Force -ErrorAction SilentlyContinue
if ($IsWindows) {
$powershell | Stop-Process
}
}
if ($IsWindows) {
$powershell | Stop-Process
Remove-Item $powershell_started -Force -ErrorAction SilentlyContinue
It "Can enter and exit another PSHost" {
Wait-UntilTrue { (Get-PSHostProcessInfo -Id $pwsh.Id) -ne $null }
"Enter-PSHostProcess -Id $($pwsh.Id) -ErrorAction Stop
`$pid
Exit-PSHostProcess" | pwsh -c - | Should -Be $pwsh.Id
}
It "Can enter and exit another Windows PowerShell PSHost" -Skip:(!$IsWindows) {
Wait-UntilTrue { (Get-PSHostProcessInfo -Id $powershell.Id) -ne $null }
"Enter-PSHostProcess -Id $($powershell.Id) -ErrorAction Stop
`$pid
Exit-PSHostProcess" | pwsh -c - | Should -Be $powershell.Id
}
It "Can enter using NamedPipeConnectionInfo" {
try {
$npInfo = [System.Management.Automation.Runspaces.NamedPipeConnectionInfo]::new($pwsh.Id)
$rs = [runspacefactory]::CreateRunspace($npInfo)
$rs.Open()
$ps = [powershell]::Create()
$ps.Runspace = $rs
$ps.AddScript('$pid').Invoke() | Should -Be $pwsh.Id
} finally {
$rs.Dispose()
}
}
}
It "Can enter and exit another PSHost" {
Wait-UntilTrue { Test-Path $pwsh_started }
Context "By CustomPipeName" {
BeforeAll {
# Helper function to get the correct path for the named pipe.
function Get-PipePath {
param (
$PipeName
)
if ($IsWindows) {
return "\\.\pipe\$PipeName"
}
"$([System.IO.Path]::GetTempPath())CoreFxPipe_$PipeName"
}
"enter-pshostprocess -id $($pwsh.Id)`n`$pid`nexit-pshostprocess" | pwsh -c - | Should -Be $pwsh.Id
}
$pipeName = [System.IO.Path]::GetRandomFileName()
$params = @{
FilePath = "pwsh"
ArgumentList = @("-CustomPipeName",$pipeName)
PassThru = $true
RedirectStandardOutput = "TestDrive:\pwsh_out.log"
RedirectStandardError = "TestDrive:\pwsh_err.log"
}
$pwsh = Start-Process @params
It "Can enter and exit another Windows PowerShell PSHost" -Skip:(!$IsWindows) {
Wait-UntilTrue { Test-Path $powershell_started }
$pipePath = Get-PipePath -PipeName $pipeName
}
"enter-pshostprocess -id $($powershell.Id)`n`$pid`nexit-pshostprocess" | pwsh -c - | Should -Be $powershell.Id
}
AfterAll {
$pwsh | Stop-Process
}
It "Can enter using NamedPipeConnectionInfo" {
$npInfo = [System.Management.Automation.Runspaces.NamedPipeConnectionInfo]::new($pwsh.Id)
$rs = [runspacefactory]::CreateRunspace($npInfo)
$rs.Open()
$ps = [powershell]::Create()
$ps.Runspace = $rs
$ps.AddScript('$pid').Invoke() | Should -Be $pwsh.Id
$rs.Dispose()
It "Can enter using CustomPipeName" {
Wait-UntilTrue { Test-Path $pipePath }
"Enter-PSHostProcess -CustomPipeName $pipeName -ErrorAction Stop
`$pid
Exit-PSHostProcess" | pwsh -c - | Should -Be $pwsh.Id
}
It "Can enter, exit, and re-enter using CustomPipeName" {
Wait-UntilTrue { Test-Path $pipePath }
"Enter-PSHostProcess -CustomPipeName $pipeName -ErrorAction Stop
`$pid
Exit-PSHostProcess" | pwsh -c - | Should -Be $pwsh.Id
"Enter-PSHostProcess -CustomPipeName $pipeName -ErrorAction Stop
`$pid
Exit-PSHostProcess" | pwsh -c - | Should -Be $pwsh.Id
}
It "Should throw if CustomPipeName does not exist" {
Wait-UntilTrue { Test-Path $pipePath }
{ Enter-PSHostProcess -CustomPipeName badpipename } | Should -Throw -ExpectedMessage "No named pipe was found with CustomPipeName: badpipename."
}
It "Should throw if CustomPipeName is too long on Linux or macOS" {
$longPipeName = "DoggoipsumwaggywagssmolborkingdoggowithalongsnootforpatsdoingmeafrightenporgoYapperporgolongwatershoobcloudsbigolpupperlengthboy"
if (!$IsWindows) {
"`$pid" | pwsh -CustomPipeName $longPipeName -c -
# 64 is the ExitCode for BadCommandLineParameter
$LASTEXITCODE | Should -Be 64
} else {
"`$pid" | pwsh -CustomPipeName $longPipeName -c -
$LASTEXITCODE | Should -Be 0
}
}
}
}

View file

@ -0,0 +1,58 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.IO;
using System.Management.Automation;
using System.Management.Automation.Remoting;
using Xunit;
namespace PSTests.Parallel
{
public class NamedPipeTests
{
[Fact]
public void TestCustomPipeNameCreation()
{
string pipeNameForFirstCall = Path.GetRandomFileName();
string pipeNameForSecondCall = Path.GetRandomFileName();
RemoteSessionNamedPipeServer.CreateCustomNamedPipeServer(pipeNameForFirstCall);
Assert.True(File.Exists(GetPipePath(pipeNameForFirstCall)));
// The second call to this method would override the first named pipe.
RemoteSessionNamedPipeServer.CreateCustomNamedPipeServer(pipeNameForSecondCall);
Assert.True(File.Exists(GetPipePath(pipeNameForSecondCall)));
// Previous pipe should have been cleaned up.
Assert.False(File.Exists(GetPipePath(pipeNameForFirstCall)));
}
[Fact]
public void TestCustomPipeNameCreationTooLongOnNonWindows()
{
var longPipeName = "DoggoipsumwaggywagssmolborkingdoggowithalongsnootforpatsdoingmeafrightenporgoYapperporgolongwatershoobcloudsbigolpupperlengthboy";
if (!Platform.IsWindows)
{
Assert.Throws<InvalidOperationException>(() =>
RemoteSessionNamedPipeServer.CreateCustomNamedPipeServer(longPipeName));
}
else
{
RemoteSessionNamedPipeServer.CreateCustomNamedPipeServer(longPipeName);
Assert.True(File.Exists(GetPipePath(longPipeName)));
}
}
private static string GetPipePath(string pipeName)
{
if (Platform.IsWindows)
{
return $@"\\.\pipe\{pipeName}";
}
return $@"{Path.GetTempPath()}CoreFxPipe_{pipeName}";
}
}
}