From 52326bf5d531a4042a324e3cfd718dc7b78bdbfb Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Tue, 9 Jul 2024 12:23:11 +1000 Subject: [PATCH 1/5] Invoke-Command $using improvements Always use the v5.1 $using: logic when dealing with any connection type that is not based on the WSManConnectionInfo. This allows the caller to use the more advanced $using features like vars with special names, $using vars inside sub scopes and more. WSMan based connections might still communicate with older versions but as the oldest supported version is v3 on Server 2012 we don't need to keep the default as v2 to support some of the newer features. --- .../remoting/commands/PSRemotingCmdlet.cs | 45 ++++++------------- .../Remoting/CustomConnection.Tests.ps1 | 30 ++++++++++++- .../Modules/HelpersCommon/HelpersCommon.psm1 | 2 +- 3 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs b/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs index 5988884d16b..8a5df652470 100644 --- a/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs +++ b/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs @@ -1923,9 +1923,7 @@ internal Pipeline CreatePipeline(RemoteRunspace remoteRunspace) // the array-form using values if all UsingExpressions are in the same scope, otherwise, we handle the UsingExpression as // if the remote end is PSv2. string serverPsVersion = GetRemoteServerPsVersion(remoteRunspace); - System.Management.Automation.PowerShell powershellToUse = (serverPsVersion == PSv2) - ? GetPowerShellForPSv2() - : GetPowerShellForPSv3OrLater(serverPsVersion); + System.Management.Automation.PowerShell powershellToUse = GetPowerShellForPSv3OrLater(serverPsVersion); Pipeline pipeline = remoteRunspace.CreatePipeline(powershellToUse.Commands.Commands[0].CommandText, true); @@ -1946,9 +1944,9 @@ internal Pipeline CreatePipeline(RemoteRunspace remoteRunspace) /// private static string GetRemoteServerPsVersion(RemoteRunspace remoteRunspace) { - if (remoteRunspace.ConnectionInfo is NewProcessConnectionInfo) + if (remoteRunspace.ConnectionInfo is not WSManConnectionInfo) { - // This is for Start-Job. The remote end is actually a child local powershell process, so it must be PSv5 or later + // All transport types except for WSManConnectionInfo work with 5.1 or later. return PSv5OrLater; } @@ -1957,35 +1955,19 @@ private static string GetRemoteServerPsVersion(RemoteRunspace remoteRunspace) { // The remote runspace is not opened yet, or it's disconnected before the private data is retrieved. // In this case we cannot validate if the remote server is running PSv5 or later, so for safety purpose, - // we will handle the $using expressions as if the remote server is PSv2. - return PSv2; + // we will handle the $using expressions as if the remote server is PSv3Orv4. + return PSv3Orv4; } - // Unfortunately, the PSVersion value in the private data from PSv3 and PSv4 server is always 2.0. - // This got fixed in PSv5, so a PSv5 server will return 5.0. That means we need other way to tell - // if the remote server is PSv2 or PSv3+. After PSv3, remote runspace supports connect/disconnect, - // so we can use it to differentiate PSv2 from PSv3+. - if (remoteRunspace.CanDisconnect) - { - Version serverPsVersion = null; - PSPrimitiveDictionary.TryPathGet( - psApplicationPrivateData, - out serverPsVersion, - PSVersionInfo.PSVersionTableName, - PSVersionInfo.PSVersionName); - - if (serverPsVersion != null) - { - return serverPsVersion.Major >= 5 ? PSv5OrLater : PSv3Orv4; - } - - // The private data is available but we failed to get the server powershell version. - // This should never happen, but in case it happens, handle the $using expressions - // as if the remote server is PSv2. - Dbg.Assert(false, "Application private data is available but we failed to get the server powershell version. This should never happen."); - } + PSPrimitiveDictionary.TryPathGet( + psApplicationPrivateData, + out Version serverPsVersion, + PSVersionInfo.PSVersionTableName, + PSVersionInfo.PSVersionName); - return PSv2; + // PSv5 server will return 5.0 whereas older versions will always be 2.0. As we don't care about v2 + // anymore we can use a simple ternary check here to differenciate v5 using behaviour vs v3/4. + return serverPsVersion != null && serverPsVersion.Major >= 5 ? PSv5OrLater : PSv3Orv4; } /// @@ -2059,7 +2041,6 @@ private void WriteErrorCreateRemoteRunspaceFailed(Exception e, Uri uri) /// private const string PSv5OrLater = "PSv5OrLater"; private const string PSv3Orv4 = "PSv3Orv4"; - private const string PSv2 = "PSv2"; private System.Management.Automation.PowerShell _powershellV2; private System.Management.Automation.PowerShell _powershellV3; diff --git a/test/powershell/engine/Remoting/CustomConnection.Tests.ps1 b/test/powershell/engine/Remoting/CustomConnection.Tests.ps1 index 2749f3bd28e..31d434dbe18 100644 --- a/test/powershell/engine/Remoting/CustomConnection.Tests.ps1 +++ b/test/powershell/engine/Remoting/CustomConnection.Tests.ps1 @@ -27,14 +27,14 @@ function Start-PwshProcess Describe 'NamedPipe Custom Remote Connection Tests' -Tags 'Feature','RequireAdminOnWindows' { - BeforeAll { + BeforeEach { Import-Module -Name Microsoft.PowerShell.NamedPipeConnection -ErrorAction Stop $script:PwshProcId = Start-PwshProcess $script:session = $null } - AfterAll { + AfterEach { if ($null -ne $script:session) { Remove-PSSession -Session $script:session @@ -57,6 +57,7 @@ Describe 'NamedPipe Custom Remote Connection Tests' -Tags 'Feature','RequireAdmi # Skip this timeout test for non-Windows platforms, because dotNet named pipes do not honor the 'NumberOfServerInstances' # property and allows connection to a currently connected server. It 'Verifies timeout error when trying to connect to pwsh process with current connection' -Skip:(!$IsWindows) { + $script:session = New-NamedPipeSession -ProcessId $script:PwshProcId -ConnectingTimeout 10 -Name CustomNPConnection -ErrorAction Stop $brokenSession = New-NamedPipeSession -ProcessId $script:PwshProcId -ConnectingTimeout 2 -Name CustomNPConnection -ErrorAction Stop # Verify expected broken session @@ -66,4 +67,29 @@ Describe 'NamedPipe Custom Remote Connection Tests' -Tags 'Feature','RequireAdmi $brokenSession | Remove-PSSession } + + It 'Passes $using: with PSv5 compatibility in Invoke-Command' { + $script:session = New-NamedPipeSession -ProcessId $script:PwshProcId -ConnectingTimeout 10 -Name CustomNPConnection -ErrorAction Stop + + Function Test-Function { + 'foo' + } + + # The v2 engine will choke on a var with '-' in the name and the v3/v4 + # using logic will revert to the v2 branch if $using is in a new scope. + # By using a function and a new scope we can verify the v5 logic is + # used and not the v2-4 one. + $result = Invoke-Command -Session $script:session -ScriptBlock { + ${function:Test-Function} = ${using:function:Test-Function} + + Test-Function + + # Running in a new scope triggers the v2 logic if the v3/v4 branch + # was used. + & { (${using:function:Test-Function}).Trim() } + } + $result.Count | Should -Be 2 + $result[0] | Should -BeExactly foo + $result[1] | Should -BeExactly "'foo'" + } } diff --git a/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 b/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 index 80af8002bda..a3fe8a1856b 100644 --- a/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 +++ b/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 @@ -551,7 +551,7 @@ function Get-HelpNetworkTestCases # Command discovery does not follow symlinks to network locations for module qualified paths $networkBlockedError = "CommandNameNotAllowed,Microsoft.PowerShell.Commands.GetHelpCommand" - $scriptBlockedError = "ScriptsNotAllowed" + $scriptBlockedError = "CommandNotFoundException" $formats = @( '//{0}/share/{1}' From a8a2e1f5cbe626153ec6aa105aae62a1b2570219 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Wed, 30 Jul 2025 05:32:49 +1000 Subject: [PATCH 2/5] Remove stray test line that isn't needed --- test/powershell/engine/Remoting/CustomConnection.Tests.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/test/powershell/engine/Remoting/CustomConnection.Tests.ps1 b/test/powershell/engine/Remoting/CustomConnection.Tests.ps1 index 31d434dbe18..d1c72835b9b 100644 --- a/test/powershell/engine/Remoting/CustomConnection.Tests.ps1 +++ b/test/powershell/engine/Remoting/CustomConnection.Tests.ps1 @@ -57,7 +57,6 @@ Describe 'NamedPipe Custom Remote Connection Tests' -Tags 'Feature','RequireAdmi # Skip this timeout test for non-Windows platforms, because dotNet named pipes do not honor the 'NumberOfServerInstances' # property and allows connection to a currently connected server. It 'Verifies timeout error when trying to connect to pwsh process with current connection' -Skip:(!$IsWindows) { - $script:session = New-NamedPipeSession -ProcessId $script:PwshProcId -ConnectingTimeout 10 -Name CustomNPConnection -ErrorAction Stop $brokenSession = New-NamedPipeSession -ProcessId $script:PwshProcId -ConnectingTimeout 2 -Name CustomNPConnection -ErrorAction Stop # Verify expected broken session From f3d5d7cc60460e0161f8a93c2c25d39f1b68c662 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Wed, 30 Jul 2025 06:07:11 +1000 Subject: [PATCH 3/5] Explain what is happening with test and 2nd connection --- test/powershell/engine/Remoting/CustomConnection.Tests.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/powershell/engine/Remoting/CustomConnection.Tests.ps1 b/test/powershell/engine/Remoting/CustomConnection.Tests.ps1 index d1c72835b9b..a8d4652ae93 100644 --- a/test/powershell/engine/Remoting/CustomConnection.Tests.ps1 +++ b/test/powershell/engine/Remoting/CustomConnection.Tests.ps1 @@ -57,6 +57,10 @@ Describe 'NamedPipe Custom Remote Connection Tests' -Tags 'Feature','RequireAdmi # Skip this timeout test for non-Windows platforms, because dotNet named pipes do not honor the 'NumberOfServerInstances' # property and allows connection to a currently connected server. It 'Verifies timeout error when trying to connect to pwsh process with current connection' -Skip:(!$IsWindows) { + # We start an active connection to have it block the second connection attempt. + $script:session = New-NamedPipeSession -ProcessId $script:PwshProcId -ConnectingTimeout 10 -Name CustomNPConnection -ErrorAction Stop + + # The above connection means the named pipe server is busy and won't allow this second connection. $brokenSession = New-NamedPipeSession -ProcessId $script:PwshProcId -ConnectingTimeout 2 -Name CustomNPConnection -ErrorAction Stop # Verify expected broken session From 928ed83f8e74dcb0081d331e8c4c69be47d4d6a9 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Thu, 31 Jul 2025 04:36:15 +1000 Subject: [PATCH 4/5] Update test/tools/Modules/HelpersCommon/HelpersCommon.psm1 Co-authored-by: Travis Plunk --- test/tools/Modules/HelpersCommon/HelpersCommon.psm1 | 1 + 1 file changed, 1 insertion(+) diff --git a/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 b/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 index a3fe8a1856b..dea50aabd10 100644 --- a/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 +++ b/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 @@ -551,6 +551,7 @@ function Get-HelpNetworkTestCases # Command discovery does not follow symlinks to network locations for module qualified paths $networkBlockedError = "CommandNameNotAllowed,Microsoft.PowerShell.Commands.GetHelpCommand" + // This error may change as long as no test cases start failing for other reasons $scriptBlockedError = "CommandNotFoundException" $formats = @( From 6faefb4518fa07cbc2da22cdbcb1779ce6f1e043 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Thu, 31 Jul 2025 05:13:25 +1000 Subject: [PATCH 5/5] Fix wrong comment used --- test/tools/Modules/HelpersCommon/HelpersCommon.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 b/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 index dea50aabd10..5c05198ff8a 100644 --- a/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 +++ b/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 @@ -551,7 +551,7 @@ function Get-HelpNetworkTestCases # Command discovery does not follow symlinks to network locations for module qualified paths $networkBlockedError = "CommandNameNotAllowed,Microsoft.PowerShell.Commands.GetHelpCommand" - // This error may change as long as no test cases start failing for other reasons + # This error may change as long as no test cases start failing for other reasons $scriptBlockedError = "CommandNotFoundException" $formats = @(