diff --git a/.gitignore b/.gitignore
index 47b0b0b10c..d328ed2984 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,6 +77,7 @@ Temporary Items
# TestsResults
TestsResults*.xml
+ParallelXUnitResults.xml
# Resharper settings
PowerShell.sln.DotSettings.user
diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs
index f2e0f5a4a4..c6deb0e0d3 100644
--- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs
+++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs
@@ -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
diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs
index 749c1ede11..c7113f3f1b 100644
--- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs
+++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs
@@ -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
///
/// Constructs RunspaceCreationEventArgs.
///
- ///
- ///
- ///
- ///
- ///
- internal RunspaceCreationEventArgs(string initialCommand,
- bool skipProfiles,
- bool staMode,
- string configurationName,
- Collection initialCommandArgs)
+ internal RunspaceCreationEventArgs(
+ string initialCommand,
+ bool skipProfiles,
+ bool staMode,
+ string configurationName,
+ Collection initialCommandArgs)
{
InitialCommand = initialCommand;
SkipProfiles = skipProfiles;
diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx
index 8c6eda44ca..be8eb1683f 100644
--- a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx
+++ b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx
@@ -186,6 +186,12 @@
Cannot process the command because -Configuration requires an argument that is a remote endpoint configuration name. Specify this argument and try again.
+
+ 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.
+
+
+ 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.
+
Cannot process the command because -SettingsFile requires an argument that is a file path.
diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx
index 72ae00245d..7ffba92699 100644
--- a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx
+++ b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx
@@ -128,7 +128,8 @@ Type 'help' to get help.
Usage: pwsh[.exe] [[-File] <filePath> [args]]
[-Command { - | <script-block> [-args <arg-array>]
| <string> [<CommandParameters>] } ]
- [-ConfigurationName <string>] [-EncodedCommand <Base64EncodedCommand>]
+ [-ConfigurationName <string>] [-CustomPipeName <string>]
+ [-EncodedCommand <Base64EncodedCommand>]
[-ExecutionPolicy <ExecutionPolicy>] [-InputFormat {Text | XML}]
[-Interactive] [-NoExit] [-NoLogo] [-NonInteractive] [-NoProfile]
[-OutputFormat {Text | XML}] [-Version] [-WindowStyle <style>]
@@ -174,6 +175,17 @@ All parameters are case-insensitive.
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.
diff --git a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs
index 654711ae49..38169d67b9 100644
--- a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs
+++ b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs
@@ -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;
}
+ ///
+ /// Gets or sets the custom named pipe name to connect to. This is usually used in conjunction with `pwsh -CustomPipeName`.
+ ///
+ [Parameter(Mandatory = true, ParameterSetName = EnterPSHostProcessCommand.PipeNameParameterSet)]
+ public string CustomPipeName
+ {
+ get;
+ set;
+ }
+
///
/// Optional name of AppDomain in process to enter. If not specified then the default AppDomain is used.
///
@@ -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
}
diff --git a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs
index 4c003f9a7b..464e91519f 100644
--- a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs
+++ b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs
@@ -343,7 +343,7 @@ namespace System.Management.Automation.Remoting
/// having correct access restrictions, and provides a listener
/// thread loop.
///
- 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
///
/// Returns the Named Pipe stream object.
///
- public NamedPipeServerStream Stream { get; }
+ internal NamedPipeServerStream Stream { get; }
///
/// Returns the Named Pipe name.
///
- public string PipeName { get; }
+ internal string PipeName { get; }
///
/// Returns true if listener is currently running.
///
- public bool IsListenerRunning { get; private set; }
+ internal bool IsListenerRunning { get; private set; }
///
/// Name of session configuration.
///
- public string ConfigurationName { get; set; }
+ internal string ConfigurationName { get; set; }
///
/// Accessor for the named pipe reader.
///
- public StreamReader TextReader { get; private set; }
+ internal StreamReader TextReader { get; private set; }
///
/// Accessor for the named pipe writer.
///
- public StreamWriter TextWriter { get; private set; }
+ internal StreamWriter TextWriter { get; private set; }
///
/// Returns true if object is currently disposed.
///
- public bool IsDisposed { get; private set; }
+ internal bool IsDisposed { get; private set; }
///
/// Buffer size for PSRP fragmentor.
@@ -419,7 +424,7 @@ namespace System.Management.Automation.Remoting
/// Event raised when the named pipe server listening thread
/// ends.
///
- public event EventHandler ListenerEnded;
+ internal event EventHandler ListenerEnded;
#endregion
@@ -429,7 +434,7 @@ namespace System.Management.Automation.Remoting
/// Creates a RemoteSessionNamedPipeServer with the current process and AppDomain information.
///
/// RemoteSessionNamedPipeServer.
- 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
+ ///
+ /// Creates the custom named pipe server with the given pipename.
+ ///
+ /// The name of the pipe to create.
+ 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
+
///
/// 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.
///
/// Connection callback.
- public void StartListening(
+ internal void StartListening(
Action 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.
diff --git a/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs b/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs
index a349a4fb1d..17a1faa08d 100644
--- a/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs
+++ b/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs
@@ -1722,12 +1722,21 @@ namespace System.Management.Automation.Runspaces
}
}
+ ///
+ /// Gets or sets the custom named pipe name to connect to. This is usually used in conjunction with pwsh -CustomPipeName.
+ ///
+ public string CustomPipeName
+ {
+ get;
+ set;
+ }
+
#endregion
#region Constructors
///
- /// Constructor.
+ /// Initializes a new instance of the class.
///
public NamedPipeConnectionInfo()
{
@@ -1735,7 +1744,7 @@ namespace System.Management.Automation.Runspaces
}
///
- /// Constructor.
+ /// Initializes a new instance of the class.
///
/// Process Id to connect to.
public NamedPipeConnectionInfo(
@@ -1744,7 +1753,7 @@ namespace System.Management.Automation.Runspaces
{ }
///
- /// Constructor.
+ /// Initializes a new instance of the class.
///
/// Process Id to connect to.
/// Application domain name to connect to, or default AppDomain if blank.
@@ -1755,7 +1764,7 @@ namespace System.Management.Automation.Runspaces
{ }
///
- /// Constructor.
+ /// Initializes a new instance of the class.
///
/// Process Id to connect to.
/// Name of application domain to connect to. Connection is to default application domain if blank.
@@ -1770,6 +1779,33 @@ namespace System.Management.Automation.Runspaces
OpenTimeout = openTimeout;
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Pipe name to connect to.
+ public NamedPipeConnectionInfo(
+ string customPipeName) :
+ this(customPipeName, _defaultOpenTimeout)
+ { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Pipe name to connect to.
+ /// Open time out in Milliseconds.
+ 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;
}
diff --git a/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs b/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs
index 96e55c7c76..7cb6143fc2 100644
--- a/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs
+++ b/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs
@@ -1884,7 +1884,9 @@ namespace System.Management.Automation.Remoting.Client
///
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);
diff --git a/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx b/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx
index ef1c0554f9..e100c8babc 100644
--- a/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx
+++ b/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx
@@ -1422,6 +1422,12 @@ All WinRM sessions connected to PowerShell session configurations, such as Micro
No process was found with Name: {0}.
+
+ No named pipe was found with CustomPipeName: {0}.
+
+
+ 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.
+
The current host does not support the Enter-PSHostProcess cmdlet.
@@ -1440,6 +1446,9 @@ All WinRM sessions connected to PowerShell session configurations, such as Micro
Unable to connect to application domain name {0} of process {1}. Error: {2}.
+
+ Unable to connect to pipe with name {0}. Error: {1}.
+
PowerShell plugin cannot process the Connect operation as required negotiation information is either missing or not complete.
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Enter-PSHostProcess.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Enter-PSHostProcess.Tests.ps1
index 88122c05b4..75d96ac886 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Core/Enter-PSHostProcess.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Enter-PSHostProcess.Tests.ps1
@@ -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
+ }
+ }
}
}
diff --git a/test/xUnit/csharp/test_NamedPipe.cs b/test/xUnit/csharp/test_NamedPipe.cs
new file mode 100644
index 0000000000..6bf55b813e
--- /dev/null
+++ b/test/xUnit/csharp/test_NamedPipe.cs
@@ -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(() =>
+ 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}";
+ }
+ }
+}