From 6b917a045fa816b8b94ed391a6a67c5d7135cbc6 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Sat, 10 Nov 2018 07:31:08 -0800 Subject: [PATCH 01/14] initial code to add UNIX path to named pipes --- .../System.Management.Automation.csproj | 1 - .../engine/InitialSessionState.cs | 6 +- .../commands/EnterPSHostProcessCommand.cs | 24 ++++++- .../remoting/common/RemoteSessionNamedPipe.cs | 66 +++++++++++++++++-- .../fanin/OutOfProcTransportManager.cs | 2 +- .../server/OutOfProcServerMediator.cs | 2 + 6 files changed, 89 insertions(+), 12 deletions(-) diff --git a/src/System.Management.Automation/System.Management.Automation.csproj b/src/System.Management.Automation/System.Management.Automation.csproj index 10f2ae45ae7..a4d5414d04d 100644 --- a/src/System.Management.Automation/System.Management.Automation.csproj +++ b/src/System.Management.Automation/System.Management.Automation.csproj @@ -138,7 +138,6 @@ - diff --git a/src/System.Management.Automation/engine/InitialSessionState.cs b/src/System.Management.Automation/engine/InitialSessionState.cs index d449aa09b47..54bdd8b895b 100644 --- a/src/System.Management.Automation/engine/InitialSessionState.cs +++ b/src/System.Management.Automation/engine/InitialSessionState.cs @@ -5243,9 +5243,6 @@ private static void InitializeCoreCmdletsAndProviders( #if !UNIX {"Disable-PSRemoting", new SessionStateCmdletEntry("Disable-PSRemoting", typeof(DisablePSRemotingCommand), helpFile) }, {"Enable-PSRemoting", new SessionStateCmdletEntry("Enable-PSRemoting", typeof(EnablePSRemotingCommand), helpFile) }, - {"Get-PSHostProcessInfo", new SessionStateCmdletEntry("Get-PSHostProcessInfo", typeof(GetPSHostProcessInfoCommand), helpFile) }, - {"Enter-PSHostProcess", new SessionStateCmdletEntry("Enter-PSHostProcess", typeof(EnterPSHostProcessCommand), helpFile) }, - {"Exit-PSHostProcess", new SessionStateCmdletEntry("Exit-PSHostProcess", typeof(ExitPSHostProcessCommand), helpFile) }, {"Disable-PSSessionConfiguration", new SessionStateCmdletEntry("Disable-PSSessionConfiguration", typeof(DisablePSSessionConfigurationCommand), helpFile) }, {"Enable-PSSessionConfiguration", new SessionStateCmdletEntry("Enable-PSSessionConfiguration", typeof(EnablePSSessionConfigurationCommand), helpFile) }, {"Get-PSSessionCapability", new SessionStateCmdletEntry("Get-PSSessionCapability", typeof(GetPSSessionCapabilityCommand), helpFile) }, @@ -5260,7 +5257,9 @@ private static void InitializeCoreCmdletsAndProviders( {"Connect-PSSession", new SessionStateCmdletEntry("Connect-PSSession", typeof(ConnectPSSessionCommand), helpFile) }, {"Disconnect-PSSession", new SessionStateCmdletEntry("Disconnect-PSSession", typeof(DisconnectPSSessionCommand), helpFile) }, #endif + {"Enter-PSHostProcess", new SessionStateCmdletEntry("Enter-PSHostProcess", typeof(EnterPSHostProcessCommand), helpFile) }, {"Enter-PSSession", new SessionStateCmdletEntry("Enter-PSSession", typeof(EnterPSSessionCommand), helpFile) }, + {"Exit-PSHostProcess", new SessionStateCmdletEntry("Exit-PSHostProcess", typeof(ExitPSHostProcessCommand), helpFile) }, {"Exit-PSSession", new SessionStateCmdletEntry("Exit-PSSession", typeof(ExitPSSessionCommand), helpFile) }, {"Export-ModuleMember", new SessionStateCmdletEntry("Export-ModuleMember", typeof(ExportModuleMemberCommand), helpFile) }, {"ForEach-Object", new SessionStateCmdletEntry("ForEach-Object", typeof(ForEachObjectCommand), helpFile) }, @@ -5270,6 +5269,7 @@ private static void InitializeCoreCmdletsAndProviders( {"Get-History", new SessionStateCmdletEntry("Get-History", typeof(GetHistoryCommand), helpFile) }, {"Get-Job", new SessionStateCmdletEntry("Get-Job", typeof(GetJobCommand), helpFile) }, {"Get-Module", new SessionStateCmdletEntry("Get-Module", typeof(GetModuleCommand), helpFile) }, + {"Get-PSHostProcessInfo", new SessionStateCmdletEntry("Get-PSHostProcessInfo", typeof(GetPSHostProcessInfoCommand), helpFile) }, {"Get-PSSession", new SessionStateCmdletEntry("Get-PSSession", typeof(GetPSSessionCommand), helpFile) }, {"Import-Module", new SessionStateCmdletEntry("Import-Module", typeof(ImportModuleCommand), helpFile) }, {"Invoke-Command", new SessionStateCmdletEntry("Invoke-Command", typeof(InvokeCommandCommand), helpFile) }, diff --git a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs index 39b7b0962f3..be5663d0ac3 100644 --- a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs +++ b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; +using System.IO; using System.Management.Automation; using System.Management.Automation.Host; using System.Management.Automation.Internal; @@ -393,7 +394,12 @@ public sealed class GetPSHostProcessInfoCommand : PSCmdlet private const string ProcessParameterSet = "ProcessParameterSet"; private const string ProcessIdParameterSet = "ProcessIdParameterSet"; private const string ProcessNameParameterSet = "ProcessNameParameterSet"; + +#if UNIX + private static readonly string NamedPipePath = Path.GetTempPath(); +#else private const string NamedPipePath = @"\\.\pipe\"; +#endif #endregion @@ -524,10 +530,18 @@ internal static IReadOnlyCollection GetAppDomainNamesFromProc var procAppDomainInfo = new List(); // Get all named pipe 'files' on local machine. - List directories; List namedPipes; +#if !UNIX + List directories; Utils.NativeEnumerateDirectory(NamedPipePath, out directories, out namedPipes); - +#else + namedPipes = new List(); + var namedPipeDirectory = new DirectoryInfo(NamedPipePath); + foreach(var pipeFileInfo in namedPipeDirectory.EnumerateFiles(NamedPipeUtils.NamedPipeNamePrefixSearch)) + { + namedPipes.Add(Path.Combine(pipeFileInfo.DirectoryName, pipeFileInfo.Name)); + } +#endif // Collect all PowerShell named pipes for given process Ids. foreach (string namedPipe in namedPipes) { @@ -535,9 +549,13 @@ internal static IReadOnlyCollection GetAppDomainNamesFromProc if (startIndex > -1) { // This is a PowerShell named pipe. Parse the process Id, AppDomain name, and process name. +#if !UNIX int pStartTimeIndex = namedPipe.IndexOf(".", startIndex, StringComparison.OrdinalIgnoreCase); if (pStartTimeIndex > -1) { +#else + int pStartTimeIndex = 0; +#endif int pIdIndex = namedPipe.IndexOf(".", pStartTimeIndex + 1, StringComparison.OrdinalIgnoreCase); if (pIdIndex > -1) { @@ -576,7 +594,9 @@ internal static IReadOnlyCollection GetAppDomainNamesFromProc } } } +#if !UNIX } +#endif } } diff --git a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs index fc027aebd29..72b507ac72d 100644 --- a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs +++ b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs @@ -8,6 +8,7 @@ using System.IO; using System.IO.Pipes; using System.Threading; +using System.Threading.Tasks; using System.Security.AccessControl; using System.Security.Principal; using System.Runtime.InteropServices; @@ -25,8 +26,13 @@ internal static class NamedPipeUtils #region Strings internal const string DefaultAppDomainName = "DefaultAppDomain"; +#if UNIX + internal const string NamedPipeNamePrefix = "PSHost."; + internal const string NamedPipeNamePrefixSearch = "CoreFxPipe_PSHost*"; +#else internal const string NamedPipeNamePrefix = "PSHost."; internal const string NamedPipeNamePrefixSearch = "PSHost*"; +#endif #endregion @@ -91,12 +97,20 @@ internal static string CreateProcessPipeName( { appDomainName = DefaultAppDomainName; } - +#if UNIX + // there is a limit of 104 characters in total including the temp path to the named pipe file + // on non-Windows systems, so we'll exclude the StartTime + return NamedPipeNamePrefix + + proc.Id.ToString(CultureInfo.InvariantCulture) + "." + + CleanAppDomainNameForPipeName(appDomainName) + "." + + proc.ProcessName; +#else return NamedPipeNamePrefix + proc.StartTime.ToFileTime().ToString(CultureInfo.InvariantCulture) + "." + proc.Id.ToString(CultureInfo.InvariantCulture) + "." + CleanAppDomainNameForPipeName(appDomainName) + "." + proc.ProcessName; +#endif } private static string CleanAppDomainNameForPipeName(string appDomainName) @@ -444,6 +458,7 @@ private NamedPipeServerStream CreateNamedPipe( if (namespaceName == null) { throw new PSArgumentNullException("namespaceName"); } if (coreName == null) { throw new PSArgumentNullException("coreName"); } +#if !UNIX string fullPipeName = @"\\" + serverName + @"\" + namespaceName + @"\" + coreName; // Create optional security attributes based on provided PipeSecurity. @@ -494,6 +509,16 @@ private NamedPipeServerStream CreateNamedPipe( pipeHandle.Dispose(); throw; } +#else + return new NamedPipeServerStream( + pipeName: coreName, + direction: PipeDirection.InOut, + maxNumberOfServerInstances: 1, + transmissionMode: PipeTransmissionMode.Byte, + options: PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly, + inBufferSize: _namedPipeBufferSizeForRemoting, + outBufferSize: _namedPipeBufferSizeForRemoting); +#endif } static RemoteSessionNamedPipeServer() @@ -502,10 +527,7 @@ static RemoteSessionNamedPipeServer() // All PowerShell instances will start with the named pipe // and listener created and running. - if (Platform.IsWindows) - { - IPCNamedPipeServerEnabled = true; - } + IPCNamedPipeServerEnabled = true; CreateIPCNamedPipeServerSingleton(); #if !CORECLR // There is only one AppDomain per application in CoreCLR, which would be the default @@ -590,6 +612,11 @@ public void StartListening( internal static CommonSecurityDescriptor GetServerPipeSecurity() { + if (!Platform.IsWindows) + { + return null; + } + // Built-in Admin SID SecurityIdentifier adminSID = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null); DiscretionaryAcl dacl = new DiscretionaryAcl(false, false, 1); @@ -658,7 +685,11 @@ private void ProcessListeningThread(object state) try { +#if UNIX + userName = System.Environment.UserName; +#else userName = WindowsIdentity.GetCurrent().Name; +#endif } catch (System.Security.SecurityException) { } @@ -1005,7 +1036,9 @@ internal sealed class RemoteSessionNamedPipeClient : NamedPipeClientBase { #region Members +#if !UNIX private volatile bool _connecting; +#endif #endregion @@ -1046,7 +1079,11 @@ internal RemoteSessionNamedPipeClient( throw new PSArgumentNullException("pipeName"); } +#if UNIX + _pipeName = pipeName; +#else _pipeName = @"\\.\pipe\" + pipeName; +#endif // Defer creating the .Net NamedPipeClientStream object until we connect. // _clientPipeStream == null. @@ -1082,7 +1119,9 @@ internal RemoteSessionNamedPipeClient( /// public override void AbortConnect() { +#if !UNIX _connecting = false; +#endif } #endregion @@ -1091,6 +1130,7 @@ public override void AbortConnect() protected override NamedPipeClientStream DoConnect(int timeout) { +#if !UNIX // Repeatedly attempt connection to pipe until timeout expires. int startTime = Environment.TickCount; int elapsedTime = 0; @@ -1112,6 +1152,22 @@ protected override NamedPipeClientStream DoConnect(int timeout) _connecting = false; throw new TimeoutException(RemotingErrorIdStrings.ConnectNamedPipeTimeout); +#else + NamedPipeClientStream namedPipeClientStream = new NamedPipeClientStream( + serverName: ".", + pipeName: _pipeName, + direction: PipeDirection.InOut, + options: PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly); + try + { + namedPipeClientStream.Connect(timeout); + } + catch (TimeoutException) + { + throw new TimeoutException(RemotingErrorIdStrings.ConnectNamedPipeTimeout); + } + return namedPipeClientStream; +#endif } #endregion diff --git a/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs b/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs index 1cf899b48f1..69aca329a64 100644 --- a/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs +++ b/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs @@ -1821,7 +1821,7 @@ private void ProcessReaderThread(object state) { if (e is ArgumentOutOfRangeException) { - Dbg.Assert(false, "Need to adjust transport fragmentor to accomodate read buffer size."); + Dbg.Assert(false, "Need to adjust transport fragmentor to accommodate read buffer size."); } string errorMsg = (e.Message != null) ? e.Message : string.Empty; diff --git a/src/System.Management.Automation/engine/remoting/server/OutOfProcServerMediator.cs b/src/System.Management.Automation/engine/remoting/server/OutOfProcServerMediator.cs index 313b7eee56e..f80fd8c30fe 100644 --- a/src/System.Management.Automation/engine/remoting/server/OutOfProcServerMediator.cs +++ b/src/System.Management.Automation/engine/remoting/server/OutOfProcServerMediator.cs @@ -590,6 +590,7 @@ private NamedPipeProcessMediator( originalStdOut = new OutOfProcessTextWriter(namedPipeServer.TextWriter); originalStdErr = new NamedPipeErrorTextWriter(namedPipeServer.TextWriter); +#if !UNIX // Flow impersonation if requested. WindowsIdentity currentIdentity = null; try @@ -599,6 +600,7 @@ private NamedPipeProcessMediator( catch (System.Security.SecurityException) { } _windowsIdentityToImpersonate = ((currentIdentity != null) && (currentIdentity.ImpersonationLevel == TokenImpersonationLevel.Impersonation)) ? currentIdentity : null; +#endif } #endregion From 9b54fd25beaa288a8c6a01434093a53f538f92fe Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Sun, 11 Nov 2018 15:32:16 -0800 Subject: [PATCH 02/14] fix codefactor and test failure --- .../commands/EnterPSHostProcessCommand.cs | 6 +++--- .../remoting/common/RemoteSessionNamedPipe.cs | 17 ++++++++--------- .../engine/Basic/DefaultCommands.Tests.ps1 | 6 +++--- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs index be5663d0ac3..c8fe6747876 100644 --- a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs +++ b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs @@ -2,17 +2,17 @@ // Licensed under the MIT License. using System; -using System.Diagnostics; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Management.Automation; using System.Management.Automation.Host; using System.Management.Automation.Internal; -using System.Management.Automation.Runspaces; using System.Management.Automation.Remoting; -using System.Diagnostics.CodeAnalysis; +using System.Management.Automation.Runspaces; namespace Microsoft.PowerShell.Commands { diff --git a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs index 72b507ac72d..76daf68527b 100644 --- a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs +++ b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs @@ -1,19 +1,19 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.Management.Automation.Tracing; -using System.Management.Automation.Internal; -using System.Management.Automation.Remoting.Server; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.IO.Pipes; -using System.Threading; -using System.Threading.Tasks; +using System.Management.Automation.Internal; +using System.Management.Automation.Remoting.Server; +using System.Management.Automation.Tracing; +using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; -using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Win32.SafeHandles; -using System.Diagnostics.CodeAnalysis; using Dbg = System.Diagnostics.Debug; namespace System.Management.Automation.Remoting @@ -26,11 +26,10 @@ internal static class NamedPipeUtils #region Strings internal const string DefaultAppDomainName = "DefaultAppDomain"; -#if UNIX internal const string NamedPipeNamePrefix = "PSHost."; +#if UNIX internal const string NamedPipeNamePrefixSearch = "CoreFxPipe_PSHost*"; #else - internal const string NamedPipeNamePrefix = "PSHost."; internal const string NamedPipeNamePrefixSearch = "PSHost*"; #endif diff --git a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 index cc60c28641f..dfe7bd81f12 100644 --- a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 +++ b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 @@ -221,9 +221,9 @@ Describe "Verify approved aliases list" -Tags "CI" { "Cmdlet", "Enable-PSSessionConfiguration", , $($FullCLR -or $CoreWindows ) "Cmdlet", "Enable-RunspaceDebug", , $($FullCLR -or $CoreWindows -or $CoreUnix) "Cmdlet", "Enable-WSManCredSSP", , $($FullCLR -or $CoreWindows ) -"Cmdlet", "Enter-PSHostProcess", , $($FullCLR -or $CoreWindows ) +"Cmdlet", "Enter-PSHostProcess", , $($FullCLR -or $CoreWindows -or $CoreUnix) "Cmdlet", "Enter-PSSession", , $($FullCLR -or $CoreWindows -or $CoreUnix) -"Cmdlet", "Exit-PSHostProcess", , $($FullCLR -or $CoreWindows ) +"Cmdlet", "Exit-PSHostProcess", , $($FullCLR -or $CoreWindows -or $CoreUnix) "Cmdlet", "Exit-PSSession", , $($FullCLR -or $CoreWindows -or $CoreUnix) "Cmdlet", "Export-Alias", , $($FullCLR -or $CoreWindows -or $CoreUnix) "Cmdlet", "Export-Clixml", , $($FullCLR -or $CoreWindows -or $CoreUnix) @@ -279,7 +279,7 @@ Describe "Verify approved aliases list" -Tags "CI" { "Cmdlet", "Get-PSBreakpoint", , $($FullCLR -or $CoreWindows -or $CoreUnix) "Cmdlet", "Get-PSCallStack", , $($FullCLR -or $CoreWindows -or $CoreUnix) "Cmdlet", "Get-PSDrive", , $($FullCLR -or $CoreWindows -or $CoreUnix) -"Cmdlet", "Get-PSHostProcessInfo", , $($FullCLR -or $CoreWindows ) +"Cmdlet", "Get-PSHostProcessInfo", , $($FullCLR -or $CoreWindows -or $CoreUnix) "Cmdlet", "Get-PSProvider", , $($FullCLR -or $CoreWindows -or $CoreUnix) "Cmdlet", "Get-PSSession", , $($FullCLR -or $CoreWindows -or $CoreUnix) "Cmdlet", "Get-PSSessionCapability", , $($FullCLR -or $CoreWindows ) From 589c644b41912e0d73c634f3c3fc42656b9184ef Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL)" Date: Sun, 11 Nov 2018 16:02:42 -0800 Subject: [PATCH 03/14] remove unnecessary legacy Windows specific doe --- .../remoting/commands/EnterPSHostProcessCommand.cs | 6 +++--- .../engine/remoting/common/RemoteSessionNamedPipe.cs | 11 +++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs index c8fe6747876..96ba5dd7585 100644 --- a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs +++ b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs @@ -531,17 +531,17 @@ internal static IReadOnlyCollection GetAppDomainNamesFromProc // Get all named pipe 'files' on local machine. List namedPipes; -#if !UNIX +#if REMOVEME List directories; Utils.NativeEnumerateDirectory(NamedPipePath, out directories, out namedPipes); -#else +#endif namedPipes = new List(); var namedPipeDirectory = new DirectoryInfo(NamedPipePath); foreach(var pipeFileInfo in namedPipeDirectory.EnumerateFiles(NamedPipeUtils.NamedPipeNamePrefixSearch)) { namedPipes.Add(Path.Combine(pipeFileInfo.DirectoryName, pipeFileInfo.Name)); } -#endif + // Collect all PowerShell named pipes for given process Ids. foreach (string namedPipe in namedPipes) { diff --git a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs index 76daf68527b..a9786deef15 100644 --- a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs +++ b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs @@ -457,7 +457,7 @@ private NamedPipeServerStream CreateNamedPipe( if (namespaceName == null) { throw new PSArgumentNullException("namespaceName"); } if (coreName == null) { throw new PSArgumentNullException("coreName"); } -#if !UNIX +#if REMOVEME string fullPipeName = @"\\" + serverName + @"\" + namespaceName + @"\" + coreName; // Create optional security attributes based on provided PipeSecurity. @@ -1035,7 +1035,7 @@ internal sealed class RemoteSessionNamedPipeClient : NamedPipeClientBase { #region Members -#if !UNIX +#if REMOVEME private volatile bool _connecting; #endif @@ -1078,9 +1078,8 @@ internal RemoteSessionNamedPipeClient( throw new PSArgumentNullException("pipeName"); } -#if UNIX _pipeName = pipeName; -#else +#if REMOVEME _pipeName = @"\\.\pipe\" + pipeName; #endif @@ -1118,7 +1117,7 @@ internal RemoteSessionNamedPipeClient( /// public override void AbortConnect() { -#if !UNIX +#if REMOVEME _connecting = false; #endif } @@ -1129,7 +1128,7 @@ public override void AbortConnect() protected override NamedPipeClientStream DoConnect(int timeout) { -#if !UNIX +#if REMOVEME // Repeatedly attempt connection to pipe until timeout expires. int startTime = Environment.TickCount; int elapsedTime = 0; From 2504dd98eae42944d404d2153245cafddea4d846 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Sun, 11 Nov 2018 17:08:09 -0800 Subject: [PATCH 04/14] [feature] cleanup existing Windows specific code and update tests --- .../engine/Utils.cs | 33 -------- .../commands/EnterPSHostProcessCommand.cs | 9 +- .../remoting/common/RemoteSessionNamedPipe.cs | 84 +++---------------- test/powershell/Host/Logging.Tests.ps1 | 14 ++-- 4 files changed, 22 insertions(+), 118 deletions(-) diff --git a/src/System.Management.Automation/engine/Utils.cs b/src/System.Management.Automation/engine/Utils.cs index de4bdc5b0bb..95ad4381465 100644 --- a/src/System.Management.Automation/engine/Utils.cs +++ b/src/System.Management.Automation/engine/Utils.cs @@ -902,39 +902,6 @@ internal static bool IsAdministrator() #endif } - internal static void NativeEnumerateDirectory(string directory, out List directories, out List files) - { - IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1); - NativeMethods.WIN32_FIND_DATA findData; - - files = new List(); - directories = new List(); - - IntPtr findHandle; - - findHandle = NativeMethods.FindFirstFile(directory + "\\*", out findData); - if (findHandle != INVALID_HANDLE_VALUE) - { - do - { - if ((findData.dwFileAttributes & NativeMethods.FileAttributes.Directory) != 0) - { - if ((!String.Equals(".", findData.cFileName, StringComparison.OrdinalIgnoreCase)) && - (!String.Equals("..", findData.cFileName, StringComparison.OrdinalIgnoreCase))) - { - directories.Add(directory + "\\" + findData.cFileName); - } - } - else - { - files.Add(directory + "\\" + findData.cFileName); - } - } - while (NativeMethods.FindNextFile(findHandle, out findData)); - NativeMethods.FindClose(findHandle); - } - } - internal static bool IsReservedDeviceName(string destinationPath) { #if !UNIX diff --git a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs index 96ba5dd7585..4144e72114c 100644 --- a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs +++ b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs @@ -530,14 +530,9 @@ internal static IReadOnlyCollection GetAppDomainNamesFromProc var procAppDomainInfo = new List(); // Get all named pipe 'files' on local machine. - List namedPipes; -#if REMOVEME - List directories; - Utils.NativeEnumerateDirectory(NamedPipePath, out directories, out namedPipes); -#endif - namedPipes = new List(); + List namedPipes = new List(); var namedPipeDirectory = new DirectoryInfo(NamedPipePath); - foreach(var pipeFileInfo in namedPipeDirectory.EnumerateFiles(NamedPipeUtils.NamedPipeNamePrefixSearch)) + foreach (var pipeFileInfo in namedPipeDirectory.EnumerateFiles(NamedPipeUtils.NamedPipeNamePrefixSearch)) { namedPipes.Add(Path.Combine(pipeFileInfo.DirectoryName, pipeFileInfo.Name)); } diff --git a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs index a9786deef15..414749be18e 100644 --- a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs +++ b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs @@ -457,7 +457,7 @@ private NamedPipeServerStream CreateNamedPipe( if (namespaceName == null) { throw new PSArgumentNullException("namespaceName"); } if (coreName == null) { throw new PSArgumentNullException("coreName"); } -#if REMOVEME +#if !UNIX string fullPipeName = @"\\" + serverName + @"\" + namespaceName + @"\" + coreName; // Create optional security attributes based on provided PipeSecurity. @@ -1035,9 +1035,7 @@ internal sealed class RemoteSessionNamedPipeClient : NamedPipeClientBase { #region Members -#if REMOVEME private volatile bool _connecting; -#endif #endregion @@ -1079,9 +1077,6 @@ internal RemoteSessionNamedPipeClient( } _pipeName = pipeName; -#if REMOVEME - _pipeName = @"\\.\pipe\" + pipeName; -#endif // Defer creating the .Net NamedPipeClientStream object until we connect. // _clientPipeStream == null. @@ -1117,9 +1112,7 @@ internal RemoteSessionNamedPipeClient( /// public override void AbortConnect() { -#if REMOVEME _connecting = false; -#endif } #endregion @@ -1128,88 +1121,35 @@ public override void AbortConnect() protected override NamedPipeClientStream DoConnect(int timeout) { -#if REMOVEME // Repeatedly attempt connection to pipe until timeout expires. int startTime = Environment.TickCount; int elapsedTime = 0; _connecting = true; + NamedPipeClientStream namedPipeClientStream = new NamedPipeClientStream( + serverName: ".", + pipeName: _pipeName, + direction: PipeDirection.InOut, + options: PipeOptions.Asynchronous); + + namedPipeClientStream.Connect(); + do { - // Wait in 100 mSec increments. - if (!NamedPipeNative.WaitNamedPipe(_pipeName, 100)) + if (!namedPipeClientStream.IsConnected) { + Thread.Sleep(100); elapsedTime = unchecked(Environment.TickCount - startTime); continue; } _connecting = false; - return OpenNamedPipe(); + return namedPipeClientStream; } while (_connecting && (elapsedTime < timeout)); _connecting = false; throw new TimeoutException(RemotingErrorIdStrings.ConnectNamedPipeTimeout); -#else - NamedPipeClientStream namedPipeClientStream = new NamedPipeClientStream( - serverName: ".", - pipeName: _pipeName, - direction: PipeDirection.InOut, - options: PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly); - try - { - namedPipeClientStream.Connect(timeout); - } - catch (TimeoutException) - { - throw new TimeoutException(RemotingErrorIdStrings.ConnectNamedPipeTimeout); - } - return namedPipeClientStream; -#endif - } - - #endregion - - #region Private Methods - - /// - /// Helper method to open a named pipe via native APIs and return in - /// .Net NamedPipeClientStream wrapper object. - /// - private NamedPipeClientStream OpenNamedPipe() - { - // Create pipe flags. - uint pipeFlags = NamedPipeNative.FILE_FLAG_OVERLAPPED; - - // Get handle to pipe. - SafePipeHandle pipeHandle = NamedPipeNative.CreateFile( - _pipeName, - NamedPipeNative.GENERIC_READ | NamedPipeNative.GENERIC_WRITE, - 0, - IntPtr.Zero, - NamedPipeNative.OPEN_EXISTING, - pipeFlags, - IntPtr.Zero); - - int lastError = Marshal.GetLastWin32Error(); - if (pipeHandle.IsInvalid) - { - throw new System.ComponentModel.Win32Exception(lastError); - } - - try - { - return new NamedPipeClientStream( - PipeDirection.InOut, - true, // IsAsync - true, // IsConnected - pipeHandle); - } - catch (Exception) - { - pipeHandle.Dispose(); - throw; - } } #endregion diff --git a/test/powershell/Host/Logging.Tests.ps1 b/test/powershell/Host/Logging.Tests.ps1 index 98e7d23d97e..25a9754945c 100644 --- a/test/powershell/Host/Logging.Tests.ps1 +++ b/test/powershell/Host/Logging.Tests.ps1 @@ -178,12 +178,13 @@ Creating Scriptblock text \(1 of 1\):#012{0}(#012)*ScriptBlock ID: [0-9a-z\-]*#0 $items | Should -Not -Be $null $items.Length | Should -BeGreaterThan 1 $items[0].EventId | Should -BeExactly 'Perftrack_ConsoleStartupStart:PowershellConsoleStartup.WinStart.Informational' - $items[1].EventId | Should -BeExactly 'Perftrack_ConsoleStartupStop:PowershellConsoleStartup.WinStop.Informational' + $items[1].EventId | Should -BeExactly 'NamedPipeIPC_ServerListenerStarted:NamedPipe.Open.Informational' + $items[2].EventId | Should -BeExactly 'Perftrack_ConsoleStartupStop:PowershellConsoleStartup.WinStop.Informational' # if there are more items than expected... - if ($items.Length -gt 2) + if ($items.Length -gt 3) { # Force reporting of the first unexpected item to help diagnosis - $items[2] | Should -Be $null + $items[3] | Should -Be $null } } @@ -288,12 +289,13 @@ Path:.* $items | Should -Not -Be $null $items.Count | Should -BeGreaterThan 1 $items[0].EventId | Should -BeExactly 'Perftrack_ConsoleStartupStart:PowershellConsoleStartup.WinStart.Informational' - $items[1].EventId | Should -BeExactly 'Perftrack_ConsoleStartupStop:PowershellConsoleStartup.WinStop.Informational' + $items[1].EventId | Should -BeExactly 'NamedPipeIPC_ServerListenerStarted:NamedPipe.Open.Informational' + $items[2].EventId | Should -BeExactly 'Perftrack_ConsoleStartupStop:PowershellConsoleStartup.WinStop.Informational' # if there are more items than expected... - if ($items.Count -gt 2) + if ($items.Count -gt 3) { # Force reporting of the first unexpected item to help diagnosis - $items[2] | Should -Be $null + $items[3] | Should -Be $null } } catch { From 33f6254030c11af131530a58d9737b03387b778e Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Sun, 11 Nov 2018 22:41:32 -0800 Subject: [PATCH 05/14] address Ilya's feedback, some minor cleanup --- .../commands/EnterPSHostProcessCommand.cs | 9 ++++---- .../remoting/common/RemoteSessionNamedPipe.cs | 23 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs index 4144e72114c..99246f9ef6f 100644 --- a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs +++ b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs @@ -544,17 +544,18 @@ internal static IReadOnlyCollection GetAppDomainNamesFromProc if (startIndex > -1) { // This is a PowerShell named pipe. Parse the process Id, AppDomain name, and process name. + // UNIX is limited to a 104 character named pipe file path so we must exclude starttime to shorten the length. #if !UNIX - int pStartTimeIndex = namedPipe.IndexOf(".", startIndex, StringComparison.OrdinalIgnoreCase); + int pStartTimeIndex = namedPipe.IndexOf('.', startIndex); if (pStartTimeIndex > -1) { #else int pStartTimeIndex = 0; #endif - int pIdIndex = namedPipe.IndexOf(".", pStartTimeIndex + 1, StringComparison.OrdinalIgnoreCase); + int pIdIndex = namedPipe.IndexOf('.', pStartTimeIndex + 1); if (pIdIndex > -1) { - int pAppDomainIndex = namedPipe.IndexOf(".", pIdIndex + 1, StringComparison.OrdinalIgnoreCase); + int pAppDomainIndex = namedPipe.IndexOf('.', pIdIndex + 1); if (pAppDomainIndex > -1) { string idString = namedPipe.Substring(pIdIndex + 1, (pAppDomainIndex - pIdIndex - 1)); @@ -578,7 +579,7 @@ internal static IReadOnlyCollection GetAppDomainNamesFromProc } } - int pNameIndex = namedPipe.IndexOf(".", pAppDomainIndex + 1, StringComparison.OrdinalIgnoreCase); + int pNameIndex = namedPipe.IndexOf('.', pAppDomainIndex + 1); if (pNameIndex > -1) { string appDomainName = namedPipe.Substring(pAppDomainIndex + 1, (pNameIndex - pAppDomainIndex - 1)); diff --git a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs index 414749be18e..cb7180090ae 100644 --- a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs +++ b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs @@ -100,15 +100,15 @@ internal static string CreateProcessPipeName( // there is a limit of 104 characters in total including the temp path to the named pipe file // on non-Windows systems, so we'll exclude the StartTime return NamedPipeNamePrefix + - proc.Id.ToString(CultureInfo.InvariantCulture) + "." + - CleanAppDomainNameForPipeName(appDomainName) + "." + - proc.ProcessName; + proc.Id.ToString(CultureInfo.InvariantCulture) + "." + + CleanAppDomainNameForPipeName(appDomainName) + "." + + proc.ProcessName; #else return NamedPipeNamePrefix + - proc.StartTime.ToFileTime().ToString(CultureInfo.InvariantCulture) + "." + - proc.Id.ToString(CultureInfo.InvariantCulture) + "." + - CleanAppDomainNameForPipeName(appDomainName) + "." + - proc.ProcessName; + proc.StartTime.ToFileTime().ToString(CultureInfo.InvariantCulture) + "." + + proc.Id.ToString(CultureInfo.InvariantCulture) + "." + + CleanAppDomainNameForPipeName(appDomainName) + "." + + proc.ProcessName; #endif } @@ -611,11 +611,9 @@ public void StartListening( internal static CommonSecurityDescriptor GetServerPipeSecurity() { - if (!Platform.IsWindows) - { - return null; - } - +#if UNIX + return null; +#else // Built-in Admin SID SecurityIdentifier adminSID = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null); DiscretionaryAcl dacl = new DiscretionaryAcl(false, false, 1); @@ -644,6 +642,7 @@ internal static CommonSecurityDescriptor GetServerPipeSecurity() } return securityDesc; +#endif } /// From 60f2f7d313a3852561aba52cba622733840e5abc Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 12 Nov 2018 09:58:32 -0800 Subject: [PATCH 06/14] [feature] use same model as AMSI uninitialize to cleanup named pipes on exit when enumerating pshostprocessinfo, check if they are valid, otherwise exclude and try to remove --- .../engine/hostifaces/LocalConnection.cs | 8 ++++++- .../commands/EnterPSHostProcessCommand.cs | 21 +++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/System.Management.Automation/engine/hostifaces/LocalConnection.cs b/src/System.Management.Automation/engine/hostifaces/LocalConnection.cs index 54737851650..9a2d8e91168 100644 --- a/src/System.Management.Automation/engine/hostifaces/LocalConnection.cs +++ b/src/System.Management.Automation/engine/hostifaces/LocalConnection.cs @@ -823,7 +823,7 @@ private void DoCloseHelper() // When closing the primary runspace, ensure all other local runspaces are closed. var closeAllOpenRunspaces = isPrimaryRunspace && haveOpenRunspaces; - // Stop all transcriptions and unitialize AMSI if we're the last runspace to exit or we are exiting the primary runspace. + // Stop all transcriptions and un-initialize AMSI if we're the last runspace to exit or we are exiting the primary runspace. if (!haveOpenRunspaces) { ExecutionContext executionContext = this.GetExecutionContext; @@ -836,6 +836,12 @@ private void DoCloseHelper() } } + if (s_IPCNamedPipeServer != null) + { + s_IPCNamedPipeServer.Dispose(); + s_IPCNamedPipeServer = null; + } + AmsiUtils.Uninitialize(); } diff --git a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs index 99246f9ef6f..c48ca743277 100644 --- a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs +++ b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs @@ -585,8 +585,25 @@ internal static IReadOnlyCollection GetAppDomainNamesFromProc string appDomainName = namedPipe.Substring(pAppDomainIndex + 1, (pNameIndex - pAppDomainIndex - 1)); string pName = namedPipe.Substring(pNameIndex + 1); - procAppDomainInfo.Add( - new PSHostProcessInfo(pName, id, appDomainName)); + // If SMA.dll did not exit cleanly, there could be left over pipes + // so we check if the process exists and matches the name, otherwise remove. + var process = System.Diagnostics.Process.GetProcessById(id); + if (process.ProcessName.Equals(pName, StringComparison.Ordinal)) + { + procAppDomainInfo.Add(new PSHostProcessInfo(pName, id, appDomainName)); + } + else + { + try + { + var pipeFile = new FileInfo(namedPipe); + pipeFile.Delete(); + } + catch (Exception) + { + // best effort + } + } } } } From f2a9c04228005e3146769c01a8822dc0efda21c1 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 12 Nov 2018 10:29:25 -0800 Subject: [PATCH 07/14] [feature] address Paul's feedback --- .../engine/remoting/commands/EnterPSHostProcessCommand.cs | 8 -------- .../engine/remoting/common/RemoteSessionNamedPipe.cs | 7 +++++-- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs index c48ca743277..6ffab97ff84 100644 --- a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs +++ b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs @@ -543,15 +543,9 @@ internal static IReadOnlyCollection GetAppDomainNamesFromProc int startIndex = namedPipe.IndexOf(NamedPipeUtils.NamedPipeNamePrefix, StringComparison.OrdinalIgnoreCase); if (startIndex > -1) { - // This is a PowerShell named pipe. Parse the process Id, AppDomain name, and process name. - // UNIX is limited to a 104 character named pipe file path so we must exclude starttime to shorten the length. -#if !UNIX int pStartTimeIndex = namedPipe.IndexOf('.', startIndex); if (pStartTimeIndex > -1) { -#else - int pStartTimeIndex = 0; -#endif int pIdIndex = namedPipe.IndexOf('.', pStartTimeIndex + 1); if (pIdIndex > -1) { @@ -607,9 +601,7 @@ internal static IReadOnlyCollection GetAppDomainNamesFromProc } } } -#if !UNIX } -#endif } } diff --git a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs index cb7180090ae..d35b3601dac 100644 --- a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs +++ b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs @@ -97,9 +97,12 @@ internal static string CreateProcessPipeName( appDomainName = DefaultAppDomainName; } #if UNIX - // there is a limit of 104 characters in total including the temp path to the named pipe file - // on non-Windows systems, so we'll exclude the StartTime + // The starttime is there to prevent another process easily guessing the pipe name + // and squatting on it. + // There is a limit of 104 characters in total including the temp path to the named pipe file + // on non-Windows systems, so we'll convert the starttime to hex and just take the first 4 characters. return NamedPipeNamePrefix + + proc.StartTime.ToFileTime().ToString("X4").Substring(1,4) + "." + proc.Id.ToString(CultureInfo.InvariantCulture) + "." + CleanAppDomainNameForPipeName(appDomainName) + "." + proc.ProcessName; From 37d076c07ec85a7deb0925f8f854bfdfc7725b5e Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 12 Nov 2018 13:44:44 -0800 Subject: [PATCH 08/14] [feature] address Paul and Tyler's feedback --- .../commands/EnterPSHostProcessCommand.cs | 39 +++++++++++++------ .../remoting/common/RemoteSessionNamedPipe.cs | 24 +++++++----- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs index 6ffab97ff84..1a8471ac9d8 100644 --- a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs +++ b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs @@ -536,6 +536,7 @@ internal static IReadOnlyCollection GetAppDomainNamesFromProc { namedPipes.Add(Path.Combine(pipeFileInfo.DirectoryName, pipeFileInfo.Name)); } + int currentPid = System.Diagnostics.Process.GetCurrentProcess().Id; // Collect all PowerShell named pipes for given process Ids. foreach (string namedPipe in namedPipes) @@ -579,23 +580,37 @@ internal static IReadOnlyCollection GetAppDomainNamesFromProc string appDomainName = namedPipe.Substring(pAppDomainIndex + 1, (pNameIndex - pAppDomainIndex - 1)); string pName = namedPipe.Substring(pNameIndex + 1); - // If SMA.dll did not exit cleanly, there could be left over pipes - // so we check if the process exists and matches the name, otherwise remove. - var process = System.Diagnostics.Process.GetProcessById(id); - if (process.ProcessName.Equals(pName, StringComparison.Ordinal)) - { - procAppDomainInfo.Add(new PSHostProcessInfo(pName, id, appDomainName)); - } - else + Process process = null; + + // Only return instances that aren't ourselves since you cannot enter yourself + if (id != currentPid) { try { - var pipeFile = new FileInfo(namedPipe); - pipeFile.Delete(); + process = System.Diagnostics.Process.GetProcessById(id); } catch (Exception) { - // best effort + // Do nothing if the process no longer exists + } + + if (process == null) + { + try + { + // If the process is gone, try removing the PSHost named pipe + var pipeFile = new FileInfo(namedPipe); + pipeFile.Delete(); + } + catch (Exception) + { + // best effort to cleanup + } + } + else if (process.ProcessName.Equals(pName, StringComparison.Ordinal)) + { + // only add if the process name matches + procAppDomainInfo.Add(new PSHostProcessInfo(pName, id, appDomainName)); } } } @@ -686,7 +701,7 @@ internal PSHostProcessInfo(string processName, int processId, string appDomainNa MainWindowTitle = String.Empty; try { - var proc = System.Diagnostics.Process.GetProcessById(processId); + var proc = Process.GetProcessById(processId); MainWindowTitle = proc.MainWindowTitle ?? string.Empty; } catch (ArgumentException) { } diff --git a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs index d35b3601dac..bf50b386830 100644 --- a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs +++ b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs @@ -32,6 +32,8 @@ internal static class NamedPipeUtils #else internal const string NamedPipeNamePrefixSearch = "PSHost*"; #endif + // On non-Windows, .NET named pipes are limited to up to 104 characters + internal const int MaxNamedPipeNameSize = 104; #endregion @@ -96,23 +98,25 @@ internal static string CreateProcessPipeName( { appDomainName = DefaultAppDomainName; } + + System.Text.StringBuilder pipeNameBuilder = new System.Text.StringBuilder(MaxNamedPipeNameSize); + pipeNameBuilder.Append(NamedPipeNamePrefix); #if UNIX // The starttime is there to prevent another process easily guessing the pipe name // and squatting on it. // There is a limit of 104 characters in total including the temp path to the named pipe file // on non-Windows systems, so we'll convert the starttime to hex and just take the first 4 characters. - return NamedPipeNamePrefix + - proc.StartTime.ToFileTime().ToString("X4").Substring(1,4) + "." + - proc.Id.ToString(CultureInfo.InvariantCulture) + "." + - CleanAppDomainNameForPipeName(appDomainName) + "." + - proc.ProcessName; + pipeNameBuilder.Append(proc.StartTime.ToFileTime().ToString("X4").Substring(1,4)); #else - return NamedPipeNamePrefix + - proc.StartTime.ToFileTime().ToString(CultureInfo.InvariantCulture) + "." + - proc.Id.ToString(CultureInfo.InvariantCulture) + "." + - CleanAppDomainNameForPipeName(appDomainName) + "." + - proc.ProcessName; + pipeNameBuilder.Append(proc.StartTime.ToFileTime().ToString(CultureInfo.InvariantCulture)); #endif + pipeNameBuilder.Append('.'); + pipeNameBuilder.Append(proc.Id.ToString(CultureInfo.InvariantCulture)); + pipeNameBuilder.Append('.'); + pipeNameBuilder.Append(CleanAppDomainNameForPipeName(appDomainName)); + pipeNameBuilder.Append('.'); + pipeNameBuilder.Append(proc.ProcessName); + return pipeNameBuilder.ToString(); } private static string CleanAppDomainNameForPipeName(string appDomainName) From 28ed3054c85fe64d2f0baa9b53829963a2f74ca0 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 12 Nov 2018 14:26:38 -0800 Subject: [PATCH 09/14] [feature] added tests --- .../Enter-PSHostProcess.Tests.ps1 | 22 +++++++++++++ .../Get-PSHostProcessInfo.Tests.ps1 | 32 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 test/powershell/Modules/Microsoft.PowerShell.Core/Enter-PSHostProcess.Tests.ps1 create mode 100644 test/powershell/Modules/Microsoft.PowerShell.Core/Get-PSHostProcessInfo.Tests.ps1 diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Enter-PSHostProcess.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Enter-PSHostProcess.Tests.ps1 new file mode 100644 index 00000000000..668e58fac3c --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Enter-PSHostProcess.Tests.ps1 @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +Describe "Enter-PSHostProcess tests" -Tag Feature { + BeforeAll { + $si = [System.Diagnostics.ProcessStartInfo]::new() + $si.FileName = "pwsh" + $si.Arguments = "-noexit" + $si.RedirectStandardInput = $true + $si.RedirectStandardOutput = $true + $si.RedirectStandardError = $true + $pwsh = [System.Diagnostics.Process]::Start($si) + } + + AfterAll { + $pwsh | Stop-Process + } + + It "Can enter and exit another PSHost" { + "enter-pshostprocess -id $($pwsh.Id)`n`$pid`nexit-pshostprocess" | pwsh -c - | Should -Be $pwsh.Id + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Get-PSHostProcessInfo.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Get-PSHostProcessInfo.Tests.ps1 new file mode 100644 index 00000000000..9fa50988200 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Get-PSHostProcessInfo.Tests.ps1 @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +Describe "Get-PSHostProcessInfo tests" -Tag CI { + BeforeAll { + $si = [System.Diagnostics.ProcessStartInfo]::new() + $si.FileName = "pwsh" + $si.Arguments = "-noexit" + $si.RedirectStandardInput = $true + $si.RedirectStandardOutput = $true + $si.RedirectStandardError = $true + $pwsh = [System.Diagnostics.Process]::Start($si) + } + + AfterAll { + $pwsh | Stop-Process + } + + It "Should not return own self" { + Get-PSHostProcessInfo | Select-Object -ExpandProperty ProcessId | Should -Not -Contain $pid + } + + It "Should list info for other PowerShell hosted processes" { + # Creation of the named pipe is async + Wait-UntilTrue { + Get-PSHostProcessInfo | Where-Object { $_.ProcessId -eq $pwsh.Id } + } + $pshosts = Get-PSHostProcessInfo + $pshosts.Count | Should -BeGreaterOrEqual 1 + $pshosts | Select-Object -ExpandProperty ProcessId | Should -Contain $pwsh.Id + } +} From c32a6576a1ef654ed2038ea5fdad0993c5f3884d Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 12 Nov 2018 16:34:46 -0800 Subject: [PATCH 10/14] [feature] address additional comments from Paul and Tyler --- .../engine/hostifaces/LocalConnection.cs | 6 ---- .../remoting/common/RemoteSessionNamedPipe.cs | 36 +++++++++++-------- .../Enter-PSHostProcess.Tests.ps1 | 10 ++++++ 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/System.Management.Automation/engine/hostifaces/LocalConnection.cs b/src/System.Management.Automation/engine/hostifaces/LocalConnection.cs index 9a2d8e91168..3d93afc65e7 100644 --- a/src/System.Management.Automation/engine/hostifaces/LocalConnection.cs +++ b/src/System.Management.Automation/engine/hostifaces/LocalConnection.cs @@ -836,12 +836,6 @@ private void DoCloseHelper() } } - if (s_IPCNamedPipeServer != null) - { - s_IPCNamedPipeServer.Dispose(); - s_IPCNamedPipeServer = null; - } - AmsiUtils.Uninitialize(); } diff --git a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs index bf50b386830..18a08bcb1a0 100644 --- a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs +++ b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs @@ -25,7 +25,7 @@ internal static class NamedPipeUtils { #region Strings - internal const string DefaultAppDomainName = "DefaultAppDomain"; + internal const string DefaultAppDomainName = "Default"; internal const string NamedPipeNamePrefix = "PSHost."; #if UNIX internal const string NamedPipeNamePrefixSearch = "CoreFxPipe_PSHost*"; @@ -100,22 +100,30 @@ internal static string CreateProcessPipeName( } System.Text.StringBuilder pipeNameBuilder = new System.Text.StringBuilder(MaxNamedPipeNameSize); - pipeNameBuilder.Append(NamedPipeNamePrefix); + pipeNameBuilder.Append(NamedPipeNamePrefix) #if UNIX - // The starttime is there to prevent another process easily guessing the pipe name - // and squatting on it. - // There is a limit of 104 characters in total including the temp path to the named pipe file - // on non-Windows systems, so we'll convert the starttime to hex and just take the first 4 characters. - pipeNameBuilder.Append(proc.StartTime.ToFileTime().ToString("X4").Substring(1,4)); + // The starttime is there to prevent another process easily guessing the pipe name + // and squatting on it. + // There is a limit of 104 characters in total including the temp path to the named pipe file + // on non-Windows systems, so we'll convert the starttime to hex and just take the first 8 characters. + .Append(proc.StartTime.ToFileTime().ToString("X8").Substring(1,8)) #else - pipeNameBuilder.Append(proc.StartTime.ToFileTime().ToString(CultureInfo.InvariantCulture)); + .Append(proc.StartTime.ToFileTime().ToString(CultureInfo.InvariantCulture)) #endif - pipeNameBuilder.Append('.'); - pipeNameBuilder.Append(proc.Id.ToString(CultureInfo.InvariantCulture)); - pipeNameBuilder.Append('.'); - pipeNameBuilder.Append(CleanAppDomainNameForPipeName(appDomainName)); - pipeNameBuilder.Append('.'); - pipeNameBuilder.Append(proc.ProcessName); + .Append('.') + .Append(proc.Id.ToString(CultureInfo.InvariantCulture)) + .Append('.') + .Append(CleanAppDomainNameForPipeName(appDomainName)) + .Append('.') + .Append(proc.ProcessName); +#if UNIX + int charsToTrim = pipeNameBuilder.Length - MaxNamedPipeNameSize; + if (charsToTrim > 0) + { + pipeNameBuilder.Remove(MaxNamedPipeNameSize + 1, charsToTrim); + } +#endif + return pipeNameBuilder.ToString(); } 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 668e58fac3c..7249bf17960 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/Enter-PSHostProcess.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Enter-PSHostProcess.Tests.ps1 @@ -19,4 +19,14 @@ Describe "Enter-PSHostProcess tests" -Tag Feature { It "Can enter and exit another PSHost" { "enter-pshostprocess -id $($pwsh.Id)`n`$pid`nexit-pshostprocess" | pwsh -c - | Should -Be $pwsh.Id } + + 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() + } } From 6cbc4983b5ffdbbb43d99fb7e0ab5e4ac4b55f12 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Tue, 13 Nov 2018 18:18:38 -0800 Subject: [PATCH 11/14] [feature] address Paul and Dongbo's feedback --- .../commands/EnterPSHostProcessCommand.cs | 50 ++++++++++--------- .../remoting/common/RemoteSessionNamedPipe.cs | 11 ++-- .../Get-PSHostProcessInfo.Tests.ps1 | 4 +- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs index 1a8471ac9d8..54bd3e07e39 100644 --- a/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs +++ b/src/System.Management.Automation/engine/remoting/commands/EnterPSHostProcessCommand.cs @@ -396,6 +396,8 @@ public sealed class GetPSHostProcessInfoCommand : PSCmdlet private const string ProcessNameParameterSet = "ProcessNameParameterSet"; #if UNIX + // CoreFx uses the system temp path to store the file used for named pipes and is not settable. + // This member is only used by Get-PSHostProcessInfo to know where to look for the named pipe files. private static readonly string NamedPipePath = Path.GetTempPath(); #else private const string NamedPipePath = @"\\.\pipe\"; @@ -536,7 +538,6 @@ internal static IReadOnlyCollection GetAppDomainNamesFromProc { namedPipes.Add(Path.Combine(pipeFileInfo.DirectoryName, pipeFileInfo.Name)); } - int currentPid = System.Diagnostics.Process.GetCurrentProcess().Id; // Collect all PowerShell named pipes for given process Ids. foreach (string namedPipe in namedPipes) @@ -573,6 +574,11 @@ internal static IReadOnlyCollection GetAppDomainNamesFromProc if (!found) { continue; } } } + else + { + // Process id is not valid so we'll skip + continue; + } int pNameIndex = namedPipe.IndexOf('.', pAppDomainIndex + 1); if (pNameIndex > -1) @@ -582,37 +588,33 @@ internal static IReadOnlyCollection GetAppDomainNamesFromProc Process process = null; - // Only return instances that aren't ourselves since you cannot enter yourself - if (id != currentPid) + try + { + process = System.Diagnostics.Process.GetProcessById(id); + } + catch (Exception) + { + // Do nothing if the process no longer exists + } + + if (process == null) { try { - process = System.Diagnostics.Process.GetProcessById(id); + // If the process is gone, try removing the PSHost named pipe + var pipeFile = new FileInfo(namedPipe); + pipeFile.Delete(); } catch (Exception) { - // Do nothing if the process no longer exists - } - - if (process == null) - { - try - { - // If the process is gone, try removing the PSHost named pipe - var pipeFile = new FileInfo(namedPipe); - pipeFile.Delete(); - } - catch (Exception) - { - // best effort to cleanup - } - } - else if (process.ProcessName.Equals(pName, StringComparison.Ordinal)) - { - // only add if the process name matches - procAppDomainInfo.Add(new PSHostProcessInfo(pName, id, appDomainName)); + // best effort to cleanup } } + else if (process.ProcessName.Equals(pName, StringComparison.Ordinal)) + { + // only add if the process name matches + procAppDomainInfo.Add(new PSHostProcessInfo(pName, id, appDomainName)); + } } } } diff --git a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs index 18a08bcb1a0..6a3c82c99b0 100644 --- a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs +++ b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs @@ -25,7 +25,7 @@ internal static class NamedPipeUtils { #region Strings - internal const string DefaultAppDomainName = "Default"; + internal const string DefaultAppDomainName = "None"; internal const string NamedPipeNamePrefix = "PSHost."; #if UNIX internal const string NamedPipeNamePrefixSearch = "CoreFxPipe_PSHost*"; @@ -101,15 +101,11 @@ internal static string CreateProcessPipeName( System.Text.StringBuilder pipeNameBuilder = new System.Text.StringBuilder(MaxNamedPipeNameSize); pipeNameBuilder.Append(NamedPipeNamePrefix) -#if UNIX // The starttime is there to prevent another process easily guessing the pipe name // and squatting on it. // There is a limit of 104 characters in total including the temp path to the named pipe file // on non-Windows systems, so we'll convert the starttime to hex and just take the first 8 characters. .Append(proc.StartTime.ToFileTime().ToString("X8").Substring(1,8)) -#else - .Append(proc.StartTime.ToFileTime().ToString(CultureInfo.InvariantCulture)) -#endif .Append('.') .Append(proc.Id.ToString(CultureInfo.InvariantCulture)) .Append('.') @@ -120,6 +116,9 @@ internal static string CreateProcessPipeName( int charsToTrim = pipeNameBuilder.Length - MaxNamedPipeNameSize; if (charsToTrim > 0) { + // TODO: In the case the pipe name is truncated, the user cannot connect to it using the cmdlet + // unless we add a `-Force` type switch as it attempts to validate the current process name + // matches the process name in the pipe name pipeNameBuilder.Remove(MaxNamedPipeNameSize + 1, charsToTrim); } #endif @@ -127,6 +126,8 @@ internal static string CreateProcessPipeName( return pipeNameBuilder.ToString(); } + + private static string CleanAppDomainNameForPipeName(string appDomainName) { // Pipe names cannot contain the ':' character. Remove unwanted characters. diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Get-PSHostProcessInfo.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Get-PSHostProcessInfo.Tests.ps1 index 9fa50988200..6f7d636fed6 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/Get-PSHostProcessInfo.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Get-PSHostProcessInfo.Tests.ps1 @@ -16,8 +16,8 @@ Describe "Get-PSHostProcessInfo tests" -Tag CI { $pwsh | Stop-Process } - It "Should not return own self" { - Get-PSHostProcessInfo | Select-Object -ExpandProperty ProcessId | Should -Not -Contain $pid + It "Should return own self" { + Get-PSHostProcessInfo | Select-Object -ExpandProperty ProcessId | Should -Contain $pid } It "Should list info for other PowerShell hosted processes" { From 6d46bad8cd1a95608abe8ffa031d5c7b4f94bb56 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 13 Nov 2018 23:42:09 -0800 Subject: [PATCH 12/14] Remove extra new lines --- .../engine/remoting/common/RemoteSessionNamedPipe.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs index 6a3c82c99b0..c96ca2174e0 100644 --- a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs +++ b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs @@ -126,8 +126,6 @@ internal static string CreateProcessPipeName( return pipeNameBuilder.ToString(); } - - private static string CleanAppDomainNameForPipeName(string appDomainName) { // Pipe names cannot contain the ':' character. Remove unwanted characters. From f08bf2ae36c1cb46355512bac400ec15ca820f61 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL)" Date: Wed, 14 Nov 2018 13:03:56 -0800 Subject: [PATCH 13/14] [feature] revert defaultappdomainname and starttime format on Windows as it's needed to interop with Windows PowerShell --- .../engine/remoting/common/RemoteSessionNamedPipe.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs index c96ca2174e0..c9287dd0a85 100644 --- a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs +++ b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs @@ -25,11 +25,14 @@ internal static class NamedPipeUtils { #region Strings - internal const string DefaultAppDomainName = "None"; + internal const string NamedPipeNamePrefix = "PSHost."; #if UNIX + internal const string DefaultAppDomainName = "None"; + // This `CoreFxPipe` prefix is defined by CoreFx internal const string NamedPipeNamePrefixSearch = "CoreFxPipe_PSHost*"; #else + internal const string DefaultAppDomainName = "DefaultAppDomain"; internal const string NamedPipeNamePrefixSearch = "PSHost*"; #endif // On non-Windows, .NET named pipes are limited to up to 104 characters @@ -105,7 +108,11 @@ internal static string CreateProcessPipeName( // and squatting on it. // There is a limit of 104 characters in total including the temp path to the named pipe file // on non-Windows systems, so we'll convert the starttime to hex and just take the first 8 characters. +#if UNIX .Append(proc.StartTime.ToFileTime().ToString("X8").Substring(1,8)) +#else + .Append(proc.StartTime.ToFileTime().ToString(CultureInfo.InvariantCulture)) +#endif .Append('.') .Append(proc.Id.ToString(CultureInfo.InvariantCulture)) .Append('.') From 1f26306aac0d023782b95459791b0ce79ddec641 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL)" Date: Wed, 14 Nov 2018 17:54:05 -0800 Subject: [PATCH 14/14] [feature] added tests to validate interop with Windows PowerShell --- .../Enter-PSHostProcess.Tests.ps1 | 13 +++++++++++ .../Get-PSHostProcessInfo.Tests.ps1 | 23 +++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) 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 7249bf17960..89cbd54e366 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/Enter-PSHostProcess.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Enter-PSHostProcess.Tests.ps1 @@ -10,16 +10,29 @@ Describe "Enter-PSHostProcess tests" -Tag Feature { $si.RedirectStandardOutput = $true $si.RedirectStandardError = $true $pwsh = [System.Diagnostics.Process]::Start($si) + + if ($IsWindows) { + $si.FileName = "powershell" + $powershell = [System.Diagnostics.Process]::Start($si) + } } AfterAll { $pwsh | Stop-Process + + if ($IsWindows) { + $powershell | Stop-Process + } } It "Can enter and exit another PSHost" { "enter-pshostprocess -id $($pwsh.Id)`n`$pid`nexit-pshostprocess" | pwsh -c - | Should -Be $pwsh.Id } + It "Can enter and exit another Windows PowerShell PSHost" -Skip:(!$IsWindows) { + "enter-pshostprocess -id $($powershell.Id)`n`$pid`nexit-pshostprocess" | pwsh -c - | Should -Be $powershell.Id + } + It "Can enter using NamedPipeConnectionInfo" { $npInfo = [System.Management.Automation.Runspaces.NamedPipeConnectionInfo]::new($pwsh.Id) $rs = [runspacefactory]::CreateRunspace($npInfo) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Get-PSHostProcessInfo.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Get-PSHostProcessInfo.Tests.ps1 index 6f7d636fed6..ba6849046b3 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/Get-PSHostProcessInfo.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Get-PSHostProcessInfo.Tests.ps1 @@ -10,14 +10,23 @@ Describe "Get-PSHostProcessInfo tests" -Tag CI { $si.RedirectStandardOutput = $true $si.RedirectStandardError = $true $pwsh = [System.Diagnostics.Process]::Start($si) + + if ($IsWindows) { + $si.FileName = "powershell" + $powershell = [System.Diagnostics.Process]::Start($si) + } } AfterAll { $pwsh | Stop-Process + + if ($IsWindows) { + $powershell | Stop-Process + } } It "Should return own self" { - Get-PSHostProcessInfo | Select-Object -ExpandProperty ProcessId | Should -Contain $pid + (Get-PSHostProcessInfo).ProcessId | Should -Contain $pid } It "Should list info for other PowerShell hosted processes" { @@ -27,6 +36,16 @@ Describe "Get-PSHostProcessInfo tests" -Tag CI { } $pshosts = Get-PSHostProcessInfo $pshosts.Count | Should -BeGreaterOrEqual 1 - $pshosts | Select-Object -ExpandProperty ProcessId | Should -Contain $pwsh.Id + $pshosts.ProcessId | Should -Contain $pwsh.Id + } + + It "Should list Windows PowerShell process" -Skip:(!$IsWindows) { + # Creation of the named pipe is async + Wait-UntilTrue { + Get-PSHostProcessInfo | Where-Object { $_.ProcessId -eq $powershell.Id } + } + $psProcess = Get-PSHostProcessInfo | Where-Object { $_.ProcessName -eq "powershell" } + $psProcess.Count | Should -BeGreaterOrEqual 1 + $psProcess.ProcessId | Should -Contain $powershell.id } }