From 23eccfd641a3ebaf90fe9c7ad20c0299085ce251 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 22 Feb 2019 12:40:03 -0600 Subject: [PATCH] [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). --- .gitignore | 1 + .../host/msh/CommandLineParameterParser.cs | 42 ++++- .../host/msh/ConsoleHost.cs | 22 +-- .../CommandLineParameterParserStrings.resx | 6 + .../resources/ManagedEntranceStrings.resx | 14 +- .../commands/EnterPSHostProcessCommand.cs | 111 +++++++++++-- .../remoting/common/RemoteSessionNamedPipe.cs | 104 ++++++++++-- .../remoting/common/RunspaceConnectionInfo.cs | 45 +++++- .../fanin/OutOfProcTransportManager.cs | 4 +- .../resources/RemotingErrorIdStrings.resx | 9 ++ .../Enter-PSHostProcess.Tests.ps1 | 153 +++++++++++++----- test/xUnit/csharp/test_NamedPipe.cs | 58 +++++++ 12 files changed, 484 insertions(+), 85 deletions(-) create mode 100644 test/xUnit/csharp/test_NamedPipe.cs 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}"; + } + } +}