Skip to content

Commit 23eccfd

Browse files
TylerLeonhardtiSazonov
authored andcommitted
[feature] Add -CustomPipeName to pwsh and Enter-PSHostProcess (PowerShell#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).
1 parent 63acf68 commit 23eccfd

12 files changed

Lines changed: 485 additions & 86 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ Temporary Items
7777

7878
# TestsResults
7979
TestsResults*.xml
80+
ParallelXUnitResults.xml
8081

8182
# Resharper settings
8283
PowerShell.sln.DotSettings.user

src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ public override void WriteWarningLine(string message)
168168

169169
internal class CommandLineParameterParser
170170
{
171+
private const int MaxPipePathLengthLinux = 108;
172+
private const int MaxPipePathLengthMacOS = 104;
173+
171174
internal static string[] validParameters = {
172175
"version",
173176
"nologo",
@@ -189,7 +192,8 @@ internal class CommandLineParameterParser
189192
"settingsfile",
190193
"help",
191194
"workingdirectory",
192-
"removeworkingdirectorytrailingcharacter"
195+
"removeworkingdirectorytrailingcharacter",
196+
"custompipename"
193197
};
194198

195199
internal CommandLineParameterParser(PSHostUserInterface hostUI, string bannerText, string helpText)
@@ -341,6 +345,14 @@ internal bool ShowVersion
341345
}
342346
}
343347

348+
internal string CustomPipeName
349+
{
350+
get
351+
{
352+
return _customPipeName;
353+
}
354+
}
355+
344356
internal Serialization.DataFormat OutputFormat
345357
{
346358
get
@@ -773,6 +785,33 @@ private void ParseHelper(string[] args)
773785

774786
_configurationName = args[i];
775787
}
788+
else if (MatchSwitch(switchKey, "custompipename", "cus"))
789+
{
790+
++i;
791+
if (i >= args.Length)
792+
{
793+
WriteCommandLineError(
794+
CommandLineParameterParserStrings.MissingCustomPipeNameArgument);
795+
break;
796+
}
797+
798+
if (!Platform.IsWindows)
799+
{
800+
int maxNameLength = (Platform.IsLinux ? MaxPipePathLengthLinux : MaxPipePathLengthMacOS) - Path.GetTempPath().Length;
801+
if (args[i].Length > maxNameLength)
802+
{
803+
WriteCommandLineError(
804+
string.Format(
805+
CommandLineParameterParserStrings.CustomPipeNameTooLong,
806+
maxNameLength,
807+
args[i],
808+
args[i].Length));
809+
break;
810+
}
811+
}
812+
813+
_customPipeName = args[i];
814+
}
776815
else if (MatchSwitch(switchKey, "command", "c"))
777816
{
778817
if (!ParseCommand(args, ref i, noexitSeen, false))
@@ -1375,6 +1414,7 @@ private bool CollectArgs(string[] args, ref int i)
13751414
private string _helpText;
13761415
private bool _abortStartup;
13771416
private bool _skipUserInit;
1417+
private string _customPipeName;
13781418
#if STAMODE
13791419
// Win8: 182409 PowerShell 3.0 should run in STA mode by default
13801420
// -sta and -mta are mutually exclusive..so tracking them using nullable boolean

src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1369,6 +1369,12 @@ private uint Run(CommandLineParameterParser cpp, bool isPrestartWarned)
13691369
}
13701370
#endif
13711371

1372+
// If the debug pipe name was specified, create the custom IPC channel.
1373+
if (!string.IsNullOrEmpty(cpp.CustomPipeName))
1374+
{
1375+
RemoteSessionNamedPipeServer.CreateCustomNamedPipeServer(cpp.CustomPipeName);
1376+
}
1377+
13721378
// NTRAID#Windows Out Of Band Releases-915506-2005/09/09
13731379
// Removed HandleUnexpectedExceptions infrastructure
13741380
#if STAMODE
@@ -2884,16 +2890,12 @@ internal sealed class RunspaceCreationEventArgs : EventArgs
28842890
/// <summary>
28852891
/// Constructs RunspaceCreationEventArgs.
28862892
/// </summary>
2887-
/// <param name="initialCommand"></param>
2888-
/// <param name="skipProfiles"></param>
2889-
/// <param name="staMode"></param>
2890-
/// <param name="configurationName"></param>
2891-
/// <param name="initialCommandArgs"></param>
2892-
internal RunspaceCreationEventArgs(string initialCommand,
2893-
bool skipProfiles,
2894-
bool staMode,
2895-
string configurationName,
2896-
Collection<CommandParameter> initialCommandArgs)
2893+
internal RunspaceCreationEventArgs(
2894+
string initialCommand,
2895+
bool skipProfiles,
2896+
bool staMode,
2897+
string configurationName,
2898+
Collection<CommandParameter> initialCommandArgs)
28972899
{
28982900
InitialCommand = initialCommand;
28992901
SkipProfiles = skipProfiles;

src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,12 @@
186186
<data name="MissingConfigurationNameArgument" xml:space="preserve">
187187
<value>Cannot process the command because -Configuration requires an argument that is a remote endpoint configuration name. Specify this argument and try again.</value>
188188
</data>
189+
<data name="MissingCustomPipeNameArgument" xml:space="preserve">
190+
<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>
191+
</data>
192+
<data name="CustomPipeNameTooLong" xml:space="preserve">
193+
<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>
194+
</data>
189195
<data name="MissingSettingsFileArgument" xml:space="preserve">
190196
<value>Cannot process the command because -SettingsFile requires an argument that is a file path.</value>
191197
</data>

src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ Type 'help' to get help.</value>
128128
<value>Usage: pwsh[.exe] [[-File] &lt;filePath&gt; [args]]
129129
[-Command { - | &lt;script-block&gt; [-args &lt;arg-array&gt;]
130130
| &lt;string&gt; [&lt;CommandParameters&gt;] } ]
131-
[-ConfigurationName &lt;string&gt;] [-EncodedCommand &lt;Base64EncodedCommand&gt;]
131+
[-ConfigurationName &lt;string&gt;] [-CustomPipeName &lt;string&gt;]
132+
[-EncodedCommand &lt;Base64EncodedCommand&gt;]
132133
[-ExecutionPolicy &lt;ExecutionPolicy&gt;] [-InputFormat {Text | XML}]
133134
[-Interactive] [-NoExit] [-NoLogo] [-NonInteractive] [-NoProfile]
134135
[-OutputFormat {Text | XML}] [-Version] [-WindowStyle &lt;style&gt;]
@@ -174,6 +175,17 @@ All parameters are case-insensitive.</value>
174175

175176
Example: pwsh -ConfigurationName AdminRoles
176177

178+
-CustomPipeName
179+
Specifies the name to use for an additional IPC server (named pipe) used for debugging
180+
and other cross-process communication. This offers a predictable mechanism for connecting
181+
to other PowerShell instances. Typically used with the CustomPipeName parameter on Enter-PSHostProcess.
182+
183+
Example:
184+
# PowerShell instance 1
185+
pwsh -CustomPipeName mydebugpipe
186+
# PowerShell instance 2
187+
Enter-PSHostProcess -CustomPipeName mydebugpipe
188+
177189
-EncodedCommand | -e | -ec
178190
Accepts a base64 encoded string version of a command. Use this parameter to submit
179191
commands to PowerShell that require complex quotation marks or curly braces.

src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using System.Management.Automation.Internal;
1414
using System.Management.Automation.Remoting;
1515
using System.Management.Automation.Runspaces;
16+
using System.Text;
1617

1718
namespace Microsoft.PowerShell.Commands
1819
{
@@ -37,6 +38,7 @@ public sealed class EnterPSHostProcessCommand : PSCmdlet
3738
private const string ProcessParameterSet = "ProcessParameterSet";
3839
private const string ProcessNameParameterSet = "ProcessNameParameterSet";
3940
private const string ProcessIdParameterSet = "ProcessIdParameterSet";
41+
private const string PipeNameParameterSet = "PipePipeNameParameterSet";
4042
private const string PSHostProcessInfoParameterSet = "PSHostProcessInfoParameterSet";
4143

4244
private const string NamedPipeRunspaceName = "PSAttachRunspace";
@@ -91,6 +93,16 @@ public PSHostProcessInfo HostProcessInfo
9193
set;
9294
}
9395

96+
/// <summary>
97+
/// Gets or sets the custom named pipe name to connect to. This is usually used in conjunction with `pwsh -CustomPipeName`.
98+
/// </summary>
99+
[Parameter(Mandatory = true, ParameterSetName = EnterPSHostProcessCommand.PipeNameParameterSet)]
100+
public string CustomPipeName
101+
{
102+
get;
103+
set;
104+
}
105+
94106
/// <summary>
95107
/// Optional name of AppDomain in process to enter. If not specified then the default AppDomain is used.
96108
/// </summary>
@@ -129,25 +141,34 @@ protected override void EndProcessing()
129141
}
130142

131143
// Check selected process for existence, and whether it hosts PowerShell.
144+
Runspace namedPipeRunspace = null;
132145
switch (ParameterSetName)
133146
{
134147
case ProcessIdParameterSet:
135148
Process = GetProcessById(Id);
149+
VerifyProcess(Process);
150+
namedPipeRunspace = CreateNamedPipeRunspace(Process.Id, AppDomainName);
136151
break;
137152

138153
case ProcessNameParameterSet:
139154
Process = GetProcessByName(Name);
155+
VerifyProcess(Process);
156+
namedPipeRunspace = CreateNamedPipeRunspace(Process.Id, AppDomainName);
140157
break;
141158

142159
case PSHostProcessInfoParameterSet:
143160
Process = GetProcessByHostProcessInfo(HostProcessInfo);
144-
break;
145-
}
161+
VerifyProcess(Process);
146162

147-
VerifyProcess(Process);
163+
// Create named pipe runspace for selected process and open.
164+
namedPipeRunspace = CreateNamedPipeRunspace(Process.Id, AppDomainName);
165+
break;
148166

149-
// Create named pipe runspace for selected process and open.
150-
Runspace namedPipeRunspace = CreateNamedPipeRunspace(Process.Id, AppDomainName);
167+
case PipeNameParameterSet:
168+
VerifyPipeName(CustomPipeName);
169+
namedPipeRunspace = CreateNamedPipeRunspace(CustomPipeName);
170+
break;
171+
}
151172

152173
// Set runspace prompt. The runspace is closed on pop so we don't
153174
// have to reverse this change.
@@ -187,9 +208,20 @@ protected override void StopProcessing()
187208

188209
#region Private Methods
189210

211+
private Runspace CreateNamedPipeRunspace(string customPipeName)
212+
{
213+
NamedPipeConnectionInfo connectionInfo = new NamedPipeConnectionInfo(customPipeName);
214+
return CreateNamedPipeRunspace(connectionInfo);
215+
}
216+
190217
private Runspace CreateNamedPipeRunspace(int procId, string appDomainName)
191218
{
192219
NamedPipeConnectionInfo connectionInfo = new NamedPipeConnectionInfo(procId, appDomainName);
220+
return CreateNamedPipeRunspace(connectionInfo);
221+
}
222+
223+
private Runspace CreateNamedPipeRunspace(NamedPipeConnectionInfo connectionInfo)
224+
{
193225
TypeTable typeTable = TypeTable.LoadDefaultTypeFiles();
194226
RemoteRunspace remoteRunspace = RunspaceFactory.CreateRunspace(connectionInfo, this.Host, typeTable) as RemoteRunspace;
195227
remoteRunspace.Name = NamedPipeRunspaceName;
@@ -203,20 +235,40 @@ private Runspace CreateNamedPipeRunspace(int procId, string appDomainName)
203235
}
204236
catch (RuntimeException e)
205237
{
206-
string msgAppDomainName = (!string.IsNullOrEmpty(appDomainName)) ? appDomainName : NamedPipeUtils.DefaultAppDomainName;
207-
208238
// Unwrap inner exception for original error message, if any.
209239
string errorMessage = (e.InnerException != null) ? (e.InnerException.Message ?? string.Empty) : string.Empty;
210240

211-
ThrowTerminatingError(
212-
new ErrorRecord(
213-
new RuntimeException(
214-
StringUtil.Format(RemotingErrorIdStrings.EnterPSHostProcessCannotConnectToProcess,
215-
msgAppDomainName, procId, errorMessage),
216-
e.InnerException),
217-
"EnterPSHostProcessCannotConnectToProcess",
218-
ErrorCategory.OperationTimeout,
219-
this));
241+
if (connectionInfo.CustomPipeName != null)
242+
{
243+
ThrowTerminatingError(
244+
new ErrorRecord(
245+
new RuntimeException(
246+
StringUtil.Format(
247+
RemotingErrorIdStrings.EnterPSHostProcessCannotConnectToPipe,
248+
connectionInfo.CustomPipeName,
249+
errorMessage),
250+
e.InnerException),
251+
"EnterPSHostProcessCannotConnectToPipe",
252+
ErrorCategory.OperationTimeout,
253+
this));
254+
}
255+
else
256+
{
257+
string msgAppDomainName = connectionInfo.AppDomainName ?? NamedPipeUtils.DefaultAppDomainName;
258+
259+
ThrowTerminatingError(
260+
new ErrorRecord(
261+
new RuntimeException(
262+
StringUtil.Format(
263+
RemotingErrorIdStrings.EnterPSHostProcessCannotConnectToProcess,
264+
msgAppDomainName,
265+
connectionInfo.ProcessId,
266+
errorMessage),
267+
e.InnerException),
268+
"EnterPSHostProcessCannotConnectToProcess",
269+
ErrorCategory.OperationTimeout,
270+
this));
271+
}
220272
}
221273
finally
222274
{
@@ -345,6 +397,33 @@ private void VerifyProcess(Process process)
345397
}
346398
}
347399

400+
private void VerifyPipeName(string customPipeName)
401+
{
402+
// Named Pipes are represented differently on Windows vs macOS & Linux
403+
var sb = new StringBuilder(customPipeName.Length);
404+
if (Platform.IsWindows)
405+
{
406+
sb.Append(@"\\.\pipe\");
407+
}
408+
else
409+
{
410+
sb.Append(Path.GetTempPath()).Append("CoreFxPipe_");
411+
}
412+
413+
sb.Append(customPipeName);
414+
415+
string pipePath = sb.ToString();
416+
if (!File.Exists(pipePath))
417+
{
418+
ThrowTerminatingError(
419+
new ErrorRecord(
420+
new PSArgumentException(StringUtil.Format(RemotingErrorIdStrings.EnterPSHostProcessNoNamedPipeFound, customPipeName)),
421+
"EnterPSHostProcessNoNamedPipeFound",
422+
ErrorCategory.InvalidArgument,
423+
this));
424+
}
425+
}
426+
348427
#endregion
349428
}
350429

0 commit comments

Comments
 (0)