From 441538617202bb944cf803813cbccd0d1f3c144c Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Tue, 5 Aug 2025 09:31:30 +1000 Subject: [PATCH] Improve the `$using` expression support in `Invoke-Command` (#24025) 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 variables with special names, having `$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 (which has been deprecated) to support some of the newer features. --- .../remoting/commands/PSRemotingCmdlet.cs | 45 ++++++------------- .../Remoting/CustomConnection.Tests.ps1 | 33 +++++++++++++- .../Modules/HelpersCommon/HelpersCommon.psm1 | 3 +- 3 files changed, 46 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 5988884d16..8a5df65247 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 @@ namespace Microsoft.PowerShell.Commands // 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 @@ namespace Microsoft.PowerShell.Commands /// 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 @@ namespace Microsoft.PowerShell.Commands { // 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); + PSPrimitiveDictionary.TryPathGet( + psApplicationPrivateData, + out Version 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."); - } - - 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 @@ namespace Microsoft.PowerShell.Commands /// 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 2749f3bd28..a8d4652ae9 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,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 @@ -66,4 +70,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 80af8002bd..5c05198ff8 100644 --- a/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 +++ b/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 @@ -551,7 +551,8 @@ function Get-HelpNetworkTestCases # Command discovery does not follow symlinks to network locations for module qualified paths $networkBlockedError = "CommandNameNotAllowed,Microsoft.PowerShell.Commands.GetHelpCommand" - $scriptBlockedError = "ScriptsNotAllowed" + # This error may change as long as no test cases start failing for other reasons + $scriptBlockedError = "CommandNotFoundException" $formats = @( '//{0}/share/{1}'