Fix SSH remoting connection hang with misconfigured endpoint (#15175)

This commit is contained in:
Paul Higinbotham 2021-04-13 15:36:36 -07:00 committed by GitHub
parent 9fd399ca89
commit 046e0fea03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 422 additions and 89 deletions

View File

@ -737,6 +737,30 @@ namespace Microsoft.PowerShell.Commands
set { base.KeyFilePath = value; }
}
/// <summary>
/// Gets and sets a value for the SSH subsystem to use for the remote connection.
/// </summary>
[Parameter(ParameterSetName = InvokeCommandCommand.SSHHostParameterSet)]
[Parameter(ParameterSetName = InvokeCommandCommand.FilePathSSHHostParameterSet)]
public override string Subsystem
{
get { return base.Subsystem; }
set { base.Subsystem = value; }
}
/// <summary>
/// Gets and sets a value in milliseconds that limits the time allowed for an SSH connection to be established.
/// </summary>
[Parameter(ParameterSetName = InvokeCommandCommand.SSHHostParameterSet)]
[Parameter(ParameterSetName = InvokeCommandCommand.FilePathSSHHostParameterSet)]
public override int ConnectingTimeout
{
get { return base.ConnectingTimeout; }
set { base.ConnectingTimeout = value; }
}
/// <summary>
/// This parameter specifies that SSH is used to establish the remote
/// connection and act as the remoting transport. By default WinRM is used

View File

@ -15,6 +15,7 @@ using System.Management.Automation.Language;
using System.Management.Automation.Remoting;
using System.Management.Automation.Remoting.Client;
using System.Management.Automation.Runspaces;
using System.Threading;
using Dbg = System.Management.Automation.Diagnostics;
@ -285,6 +286,7 @@ namespace Microsoft.PowerShell.Commands
public string KeyFilePath;
public int Port;
public string Subsystem;
public int ConnectingTimeout;
}
/// <summary>
@ -761,6 +763,20 @@ namespace Microsoft.PowerShell.Commands
set;
}
/// <summary>
/// Gets or sets a value for the SSH subsystem to use for the remote connection.
/// </summary>
[Parameter(ValueFromPipelineByPropertyName = true,
ParameterSetName = PSRemotingBaseCmdlet.SSHHostParameterSet)]
public virtual string Subsystem { get; set; }
/// <summary>
/// Gets or sets a value in milliseconds that limits the time allowed for an SSH connection to be established.
/// Default timeout value is infinite.
/// </summary>
[Parameter(ParameterSetName = PSRemotingBaseCmdlet.SSHHostParameterSet)]
public virtual int ConnectingTimeout { get; set; } = Timeout.Infinite;
/// <summary>
/// This parameter specifies that SSH is used to establish the remote
/// connection and act as the remoting transport. By default WinRM is used
@ -789,13 +805,6 @@ namespace Microsoft.PowerShell.Commands
set;
}
/// <summary>
/// This parameter specifies the SSH subsystem to use for the remote connection.
/// </summary>
[Parameter(ValueFromPipelineByPropertyName = true,
ParameterSetName = InvokeCommandCommand.SSHHostParameterSet)]
public virtual string Subsystem { get; set; }
#endregion
#endregion Properties
@ -856,6 +865,7 @@ namespace Microsoft.PowerShell.Commands
private const string IdentityFilePathAlias = "IdentityFilePath";
private const string PortParameter = "Port";
private const string SubsystemParameter = "Subsystem";
private const string ConnectingTimeoutParameter = "ConnectingTimeout";
#endregion
@ -902,7 +912,7 @@ namespace Microsoft.PowerShell.Commands
/// <returns>Array of SSHConnection objects.</returns>
internal SSHConnection[] ParseSSHConnectionHashTable()
{
List<SSHConnection> connections = new List<SSHConnection>();
List<SSHConnection> connections = new();
foreach (var item in this.SSHConnection)
{
if (item.ContainsKey(ComputerNameParameter) && item.ContainsKey(HostNameAlias))
@ -915,7 +925,7 @@ namespace Microsoft.PowerShell.Commands
throw new PSArgumentException(RemotingErrorIdStrings.SSHConnectionDuplicateKeyPath);
}
SSHConnection connectionInfo = new SSHConnection();
SSHConnection connectionInfo = new();
foreach (var key in item.Keys)
{
string paramName = key as string;
@ -955,6 +965,10 @@ namespace Microsoft.PowerShell.Commands
{
connectionInfo.Subsystem = GetSSHConnectionStringParameter(item[paramName]);
}
else if (paramName.Equals(ConnectingTimeoutParameter, StringComparison.OrdinalIgnoreCase))
{
connectionInfo.ConnectingTimeout = GetSSHConnectionIntParameter(item[paramName]);
}
else
{
throw new PSArgumentException(
@ -1448,9 +1462,9 @@ namespace Microsoft.PowerShell.Commands
{
ParseSshHostName(computerName, out string host, out string userName, out int port);
var sshConnectionInfo = new SSHConnectionInfo(userName, host, this.KeyFilePath, port, this.Subsystem);
var sshConnectionInfo = new SSHConnectionInfo(userName, host, KeyFilePath, port, Subsystem, ConnectingTimeout);
var typeTable = TypeTable.LoadDefaultTypeFiles();
var remoteRunspace = RunspaceFactory.CreateRunspace(sshConnectionInfo, this.Host, typeTable) as RemoteRunspace;
var remoteRunspace = RunspaceFactory.CreateRunspace(sshConnectionInfo, Host, typeTable) as RemoteRunspace;
var pipeline = CreatePipeline(remoteRunspace);
var operation = new ExecutionCmdletHelperComputerName(remoteRunspace, pipeline);
@ -1471,7 +1485,8 @@ namespace Microsoft.PowerShell.Commands
sshConnection.ComputerName,
sshConnection.KeyFilePath,
sshConnection.Port,
sshConnection.Subsystem);
sshConnection.Subsystem,
sshConnection.ConnectingTimeout);
var typeTable = TypeTable.LoadDefaultTypeFiles();
var remoteRunspace = RunspaceFactory.CreateRunspace(sshConnectionInfo, this.Host, typeTable) as RemoteRunspace;
var pipeline = CreatePipeline(remoteRunspace);

View File

@ -1262,7 +1262,7 @@ namespace Microsoft.PowerShell.Commands
private RemoteRunspace GetRunspaceForSSHSession()
{
ParseSshHostName(HostName, out string host, out string userName, out int port);
var sshConnectionInfo = new SSHConnectionInfo(userName, host, this.KeyFilePath, port, this.Subsystem);
var sshConnectionInfo = new SSHConnectionInfo(userName, host, KeyFilePath, port, Subsystem, ConnectingTimeout);
var typeTable = TypeTable.LoadDefaultTypeFiles();
// Use the class _tempRunspace field while the runspace is being opened so that StopProcessing can be handled at that time.

View File

@ -1092,7 +1092,8 @@ namespace Microsoft.PowerShell.Commands
host,
this.KeyFilePath,
port,
Subsystem);
Subsystem,
ConnectingTimeout);
var typeTable = TypeTable.LoadDefaultTypeFiles();
string rsName = GetRunspaceName(index, out int rsIdUnused);
index++;
@ -1118,7 +1119,8 @@ namespace Microsoft.PowerShell.Commands
sshConnection.ComputerName,
sshConnection.KeyFilePath,
sshConnection.Port,
sshConnection.Subsystem);
sshConnection.Subsystem,
sshConnection.ConnectingTimeout);
var typeTable = TypeTable.LoadDefaultTypeFiles();
string rsName = GetRunspaceName(index, out int rsIdUnused);
index++;

View File

@ -1907,6 +1907,20 @@ namespace System.Management.Automation.Runspaces
/// </summary>
public sealed class SSHConnectionInfo : RunspaceConnectionInfo
{
#region Constants
/// <summary>
/// Default value for subsystem.
/// </summary>
private const string DefaultSubsystem = "powershell";
/// <summary>
/// Default value is infinite timeout.
/// </summary>
private const int DefaultConnectingTimeoutTime = Timeout.Infinite;
#endregion
#region Properties
/// <summary>
@ -1945,6 +1959,16 @@ namespace System.Management.Automation.Runspaces
set;
}
/// <summary>
/// Gets or sets a time in milliseconds after which a connection attempt is terminated.
/// Default value (-1) never times out and a connection attempt waits indefinitely.
/// </summary>
public int ConnectingTimeout
{
get;
set;
}
#endregion
#region Constructors
@ -1968,11 +1992,12 @@ namespace System.Management.Automation.Runspaces
{
if (computerName == null) { throw new PSArgumentNullException(nameof(computerName)); }
this.UserName = userName;
this.ComputerName = computerName;
this.KeyFilePath = keyFilePath;
this.Port = 0;
this.Subsystem = DefaultSubsystem;
UserName = userName;
ComputerName = computerName;
KeyFilePath = keyFilePath;
Port = 0;
Subsystem = DefaultSubsystem;
ConnectingTimeout = DefaultConnectingTimeoutTime;
}
/// <summary>
@ -1989,8 +2014,7 @@ namespace System.Management.Automation.Runspaces
int port) : this(userName, computerName, keyFilePath)
{
ValidatePortInRange(port);
this.Port = port;
Port = port;
}
/// <summary>
@ -2006,12 +2030,29 @@ namespace System.Management.Automation.Runspaces
string computerName,
string keyFilePath,
int port,
string subsystem) : this(userName, computerName, keyFilePath)
string subsystem) : this(userName, computerName, keyFilePath, port)
{
ValidatePortInRange(port);
Subsystem = string.IsNullOrEmpty(subsystem) ? DefaultSubsystem : subsystem;
}
this.Port = port;
this.Subsystem = (string.IsNullOrEmpty(subsystem)) ? DefaultSubsystem : subsystem;
/// <summary>
/// Initializes a new instance of SSHConnectionInfo.
/// </summary>
/// <param name="userName">Name of user.</param>
/// <param name="computerName">Name of computer.</param>
/// <param name="keyFilePath">Path of key file.</param>
/// <param name="port">Port number for connection (default 22).</param>
/// <param name="subsystem">Subsystem to use (default 'powershell').</param>
/// <param name="connectingTimeout">Timeout time for terminating connection attempt.</param>
public SSHConnectionInfo(
string userName,
string computerName,
string keyFilePath,
int port,
string subsystem,
int connectingTimeout) : this(userName, computerName, keyFilePath, port, subsystem)
{
ConnectingTimeout = connectingTimeout;
}
#endregion
@ -2064,11 +2105,12 @@ namespace System.Management.Automation.Runspaces
internal override RunspaceConnectionInfo InternalCopy()
{
SSHConnectionInfo newCopy = new SSHConnectionInfo();
newCopy.ComputerName = this.ComputerName;
newCopy.UserName = this.UserName;
newCopy.KeyFilePath = this.KeyFilePath;
newCopy.Port = this.Port;
newCopy.Subsystem = this.Subsystem;
newCopy.ComputerName = ComputerName;
newCopy.UserName = UserName;
newCopy.KeyFilePath = KeyFilePath;
newCopy.Port = Port;
newCopy.Subsystem = Subsystem;
newCopy.ConnectingTimeout = ConnectingTimeout;
return newCopy;
}
@ -2187,15 +2229,6 @@ namespace System.Management.Automation.Runspaces
#endregion
#region Constants
/// <summary>
/// Default value for subsystem.
/// </summary>
private const string DefaultSubsystem = "powershell";
#endregion
#region SSH Process Creation
#if UNIX

View File

@ -591,7 +591,7 @@ namespace System.Management.Automation.Remoting.Client
// start the timer..so client can fail deterministically
_closeTimeOutTimer.Change(60 * 1000, Timeout.Infinite);
}
catch (IOException)
catch (Exception ex) when (ex is IOException || ex is ObjectDisposedException)
{
// Cannot communicate with server. Allow client to complete close operation.
shouldRaiseCloseCompleted = true;
@ -635,30 +635,7 @@ namespace System.Management.Automation.Remoting.Client
{
_cmdTransportManagers.Clear();
_closeTimeOutTimer.Dispose();
// Stop session processing thread.
try
{
_sessionMessageQueue.CompleteAdding();
}
catch (ObjectDisposedException)
{
// Object already disposed.
}
_sessionMessageQueue.Dispose();
// Stop command processing thread.
try
{
_commandMessageQueue.CompleteAdding();
}
catch (ObjectDisposedException)
{
// Object already disposed.
}
_commandMessageQueue.Dispose();
DisposeMessageQueue();
}
}
@ -1055,6 +1032,37 @@ namespace System.Management.Automation.Remoting.Client
}
#endregion
#region Protected Methods
protected void DisposeMessageQueue()
{
// Stop session processing thread.
try
{
_sessionMessageQueue.CompleteAdding();
}
catch (ObjectDisposedException)
{
// Object already disposed.
}
_sessionMessageQueue.Dispose();
// Stop command processing thread.
try
{
_commandMessageQueue.CompleteAdding();
}
catch (ObjectDisposedException)
{
// Object already disposed.
}
_commandMessageQueue.Dispose();
}
#endregion
}
internal class OutOfProcessClientSessionTransportManager : OutOfProcessClientSessionTransportManagerBase
@ -1584,6 +1592,7 @@ namespace System.Management.Automation.Remoting.Client
private StreamReader _stdOutReader;
private StreamReader _stdErrReader;
private bool _connectionEstablished;
private Timer _connectionTimer;
private const string _threadName = "SSHTransport Reader Thread";
@ -1641,6 +1650,49 @@ namespace System.Management.Automation.Remoting.Client
// Create reader thread and send first PSRP message.
StartReaderThread(_stdOutReader);
if (_connectionInfo.ConnectingTimeout < 0)
{
return;
}
// Start connection timeout timer if requested.
// Timer callback occurs only once after timeout time.
_connectionTimer = new Timer(
callback: (_) =>
{
if (_connectionEstablished)
{
return;
}
// Detect if SSH client process terminates prematurely.
bool sshTerminated = false;
try
{
using (var sshProcess = System.Diagnostics.Process.GetProcessById(_sshProcessId))
{
sshTerminated = sshProcess == null || sshProcess.Handle == IntPtr.Zero || sshProcess.HasExited;
}
}
catch
{
sshTerminated = true;
}
var errorMessage = StringUtil.Format(RemotingErrorIdStrings.SSHClientConnectTimeout, _connectionInfo.ConnectingTimeout / 1000);
if (sshTerminated)
{
errorMessage += RemotingErrorIdStrings.SSHClientConnectProcessTerminated;
}
// Report error and terminate connection attempt.
HandleSSHError(
new PSRemotingTransportException(errorMessage));
},
state: null,
dueTime: _connectionInfo.ConnectingTimeout,
period: Timeout.Infinite);
}
internal override void CloseAsync()
@ -1660,14 +1712,20 @@ namespace System.Management.Automation.Remoting.Client
private void CloseConnection()
{
// Ensure message queue is disposed.
DisposeMessageQueue();
var connectionTimer = Interlocked.Exchange(ref _connectionTimer, null);
connectionTimer?.Dispose();
var stdInWriter = Interlocked.Exchange(ref _stdInWriter, null);
if (stdInWriter != null) { stdInWriter.Dispose(); }
stdInWriter?.Dispose();
var stdOutReader = Interlocked.Exchange(ref _stdOutReader, null);
if (stdOutReader != null) { stdOutReader.Dispose(); }
stdOutReader?.Dispose();
var stdErrReader = Interlocked.Exchange(ref _stdErrReader, null);
if (stdErrReader != null) { stdErrReader.Dispose(); }
stdErrReader?.Dispose();
// The CloseConnection() method can be called multiple times from multiple places.
// Set the _sshProcessId to zero here so that we go through the work of finding
@ -1677,10 +1735,12 @@ namespace System.Management.Automation.Remoting.Client
{
try
{
var sshProcess = System.Diagnostics.Process.GetProcessById(sshProcessId);
if ((sshProcess != null) && (sshProcess.Handle != IntPtr.Zero) && !sshProcess.HasExited)
using (var sshProcess = System.Diagnostics.Process.GetProcessById(sshProcessId))
{
sshProcess.Kill();
if ((sshProcess != null) && (sshProcess.Handle != IntPtr.Zero) && !sshProcess.HasExited)
{
sshProcess.Kill();
}
}
}
catch (ArgumentException) { }

View File

@ -1625,6 +1625,13 @@ All WinRM sessions connected to PowerShell session configurations, such as Micro
<data name="SSHClientEndWithErrorMessage" xml:space="preserve">
<value>The SSH client session has ended with error message: {0}</value>
</data>
<data name="SSHClientConnectTimeout" xml:space="preserve">
<value>SSH connection attempt failed after time out: {0} seconds.</value>
</data>
<data name="SSHClientConnectProcessTerminated" xml:space="preserve">
<value>
SSH client process terminated before connection could be established.</value>
</data>
<data name="MissingRequiredSSHParameter" xml:space="preserve">
<value>The provided SSHConnection hashtable is missing the required ComputerName or HostName parameter.</value>
</data>

View File

@ -6,72 +6,202 @@ Describe "SSHRemoting Basic Tests" -tags CI {
# SSH remoting is set up to automatically authenticate current user via SSH keys
# All tests connect back to localhost machine
$script:TestConnectingTimeout = 5000 # Milliseconds
function RestartSSHDService
{
if ($IsWindows)
{
Write-Verbose -Verbose "Restarting Windows SSHD service..."
Restart-Service sshd
Write-Verbose -Verbose "SSHD service status: $(Get-Service sshd | Out-String)"
}
else
{
Write-Verbose -Verbose "Restarting Unix SSHD service..."
sudo service ssh restart
$status = sudo service ssh status
Write-Verbose -Verbose "SSHD service status: $status"
}
}
function TryNewPSSession
{
param(
[string[]] $HostName,
[string[]] $Name,
[int] $Port,
[string] $UserName,
[string] $KeyFilePath,
[string] $Subsystem
)
Write-Verbose -Verbose "Starting TryNewPSSession ..."
# Try creating a new SSH connection
$timeout = $script:TestConnectingTimeout
$connectionError = $null
$session = $null
$count = 0
while (($null -eq $session) -and ($count++ -lt 2))
{
$session = New-PSSession @PSBoundParameters -ConnectingTimeout $timeout -ErrorVariable connectionError -ErrorAction SilentlyContinue
if ($null -eq $session)
{
Write-Verbose -Verbose "SSH New-PSSession remoting connect failed."
if ($count -eq 1)
{
# Try restarting sshd service
RestartSSHDService
}
}
}
if ($null -eq $session)
{
$message = "New-PSSession unable to connect to SSH remoting endpoint after two attempts. Error: $($connectionError.Exception.Message)"
throw [System.Management.Automation.PSInvalidOperationException]::new($message)
}
Write-Verbose -Verbose "SSH New-PSSession remoting connect succeeded."
Write-Output $session
}
function TryNewPSSessionHash
{
param (
[hashtable[]] $SSHConnection,
[string[]] $Name
)
Write-Verbose -Verbose "Starting TryNewPSSessionHash ..."
foreach ($connect in $SSHConnection)
{
$connect.Add('ConnectingTimeout', $script:TestConnectingTimeout)
}
# Try creating a new SSH connection
$connectionError = $null
$session = $null
$count = 0
while (($null -eq $session) -and ($count++ -lt 2))
{
$session = New-PSSession @PSBoundParameters -ErrorVariable connectionError -ErrorAction SilentlyContinue
if ($null -eq $session)
{
Write-Verbose -Verbose "SSH New-PSSession remoting connect failed."
if ($count -eq 1)
{
# Try restarting sshd service
RestartSSHDService
}
}
}
if ($null -eq $session)
{
$message = "New-PSSession unable to connect to SSH remoting endpoint after two attempts. Error: $($connectionError.Exception.Message)"
throw [System.Management.Automation.PSInvalidOperationException]::new($message)
}
Write-Verbose -Verbose "SSH New-PSSession remoting connect succeeded."
Write-Output $session
}
function VerifySession {
param (
[System.Management.Automation.Runspaces.PSSession] $session
)
if ($null -eq $session)
{
return
}
Write-Verbose -Verbose "VerifySession called for session: $($session.Id)"
$session.State | Should -BeExactly 'Opened'
$session.ComputerName | Should -BeExactly 'localhost'
$session.Transport | Should -BeExactly 'SSH'
Write-Verbose -Verbose "Invoking whoami"
Invoke-Command -Session $session -ScriptBlock { whoami } | Should -BeExactly $(whoami)
Write-Verbose -Verbose "Invoking PSSenderInfo"
$psRemoteVersion = Invoke-Command -Session $session -ScriptBlock { $PSSenderInfo.ApplicationArguments.PSVersionTable.PSVersion }
$psRemoteVersion.Major | Should -BeExactly $PSVersionTable.PSVersion.Major
$psRemoteVersion.Minor | Should -BeExactly $PSVersionTable.PSVersion.Minor
Write-Verbose -Verbose "VerifySession complete"
}
Context "New-PSSession Tests" {
AfterEach {
Write-Verbose -Verbose "Starting New-PSSession AfterEach"
if ($script:session -ne $null) { Remove-PSSession -Session $script:session }
if ($script:sessions -ne $null) { Remove-PSSession -Session $script:sessions }
Write-Verbose -Verbose "AfterEach complete"
}
It "Verifies new connection with implicit current User" {
$script:session = New-PSSession -HostName localhost -ErrorVariable err
$err | Should -HaveCount 0
Write-Verbose -Verbose "It Starting: Verifies new connection with implicit current User"
$script:session = TryNewPSSession -HostName localhost
$script:session | Should -Not -BeNullOrEmpty
VerifySession $script:session
Write-Verbose -Verbose "It Complete"
}
It "Verifies new connection with explicit User parameter" {
$script:session = New-PSSession -HostName localhost -UserName (whoami) -ErrorVariable err
$err | Should -HaveCount 0
Write-Verbose -Verbose "It Starting: Verifies new connection with explicit User parameter"
$script:session = TryNewPSSession -HostName localhost -UserName (whoami)
$script:session | Should -Not -BeNullOrEmpty
VerifySession $script:session
Write-Verbose -Verbose "It Complete"
}
It "Verifies explicit Name parameter" {
Write-Verbose -Verbose "It Starting: Verifies explicit Name parameter"
$sessionName = 'TestSessionNameA'
$script:session = New-PSSession -HostName localhost -Name $sessionName -ErrorVariable err
$err | Should -HaveCount 0
$script:session = TryNewPSSession -HostName localhost -Name $sessionName
$script:session | Should -Not -BeNullOrEmpty
VerifySession $script:session
$script:session.Name | Should -BeExactly $sessionName
Write-Verbose -Verbose "It Complete"
}
It "Verifies explicit Port parameter" {
Write-Verbose -Verbose "It Starting: Verifies explicit Port parameter"
$portNum = 22
$script:session = New-PSSession -HostName localhost -Port $portNum -ErrorVariable err
$err | Should -HaveCount 0
$script:session = TryNewPSSession -HostName localhost -Port $portNum
$script:session | Should -Not -BeNullOrEmpty
VerifySession $script:session
Write-Verbose -Verbose "It Complete"
}
It "Verifies explicit Subsystem parameter" {
Write-Verbose -Verbose "It Starting: Verifies explicit Subsystem parameter"
$portNum = 22
$subSystem = 'powershell'
$script:session = New-PSSession -HostName localhost -Port $portNum -SubSystem $subSystem -ErrorVariable err
$err | Should -HaveCount 0
$script:session = TryNewPSSession -HostName localhost -Port $portNum -SubSystem $subSystem
$script:session | Should -Not -BeNullOrEmpty
VerifySession $script:session
Write-Verbose -Verbose "It Complete"
}
It "Verifies explicit KeyFilePath parameter" {
Write-Verbose -Verbose "It Starting: Verifies explicit KeyFilePath parameter"
$keyFilePath = "$HOME/.ssh/id_rsa"
$portNum = 22
$subSystem = 'powershell'
$script:session = New-PSSession -HostName localhost -Port $portNum -SubSystem $subSystem -KeyFilePath $keyFilePath -ErrorVariable err
$err | Should -HaveCount 0
$script:session = TryNewPSSession -HostName localhost -Port $portNum -SubSystem $subSystem -KeyFilePath $keyFilePath
$script:session | Should -Not -BeNullOrEmpty
VerifySession $script:session
Write-Verbose -Verbose "It Complete"
}
It "Verifies SSHConnection hash table parameters" {
Write-Verbose -Verbose "It Starting: Verifies SSHConnection hash table parameters"
$sshConnection = @(
@{
HostName = 'localhost'
@ -85,34 +215,92 @@ Describe "SSHRemoting Basic Tests" -tags CI {
KeyFilePath = "$HOME/.ssh/id_rsa"
Subsystem = 'powershell'
})
$script:sessions = New-PSSession -SSHConnection $sshConnection -Name 'Connection1','Connection2' -ErrorVariable err
$err | Should -HaveCount 0
$script:sessions = TryNewPSSessionHash -SSHConnection $sshConnection -Name 'Connection1','Connection2'
$script:sessions | Should -HaveCount 2
$script:sessions[0].Name | Should -BeLike 'Connection*'
$script:sessions[1].Name | Should -BeLike 'Connection*'
VerifySession $script:sessions[0]
VerifySession $script:sessions[1]
Write-Verbose -Verbose "It Complete"
}
}
function TryCreateRunspace
{
param (
[string] $UserName,
[string] $ComputerName,
[string] $KeyFilePath,
[int] $Port,
[string] $Subsystem
)
Write-Verbose -Verbose "Starting TryCreateRunspace ..."
$timeout = $script:TestConnectingTimeout
$connectionError = $null
$count = 0
$rs = $null
$ci = [System.Management.Automation.Runspaces.SSHConnectionInfo]::new($UserName, $ComputerName, $KeyFilePath, $Port, $Subsystem, $timeout)
while (($null -eq $rs) -and ($count++ -lt 2))
{
try
{
$rs = [runspacefactory]::CreateRunspace($host, $ci)
$null = $rs.Open()
}
catch
{
$connectionError = $_
$rs = $null
Write-Verbose -Verbose "SSH Runspace Open remoting connect failed."
if ($count -eq 1)
{
# Try restarting sshd service
RestartSSHDService
}
}
}
if ($null -eq $rs)
{
$message = "Runspace open unable to connect to SSH remoting endpoint after two attempts. Error: $($connectionError.Message)"
throw [System.Management.Automation.PSInvalidOperationException]::new($message)
}
Write-Verbose -Verbose "SSH Runspace Open remoting connect succeeded."
Write-Output $rs
}
function VerifyRunspace {
param (
[runspace] $rs
)
if ($null -eq $rs)
{
return
}
Write-Verbose -Verbose "VerifyRunspace called for runspace: $($rs.Id)"
$rs.RunspaceStateInfo.State | Should -BeExactly 'Opened'
$rs.RunspaceAvailability | Should -BeExactly 'Available'
$rs.RunspaceIsRemote | Should -BeTrue
$ps = [powershell]::Create()
try
{
Write-Verbose -Verbose "VerifyRunspace: Invoking PSSenderInfo"
$ps.Runspace = $rs
$psRemoteVersion = $ps.AddScript('$PSSenderInfo.ApplicationArguments.PSVersionTable.PSVersion').Invoke()
$psRemoteVersion.Major | Should -BeExactly $PSVersionTable.PSVersion.Major
$psRemoteVersion.Minor | Should -BeExactly $PSVersionTable.PSVersion.Minor
$ps.Commands.Clear()
Write-Verbose -Verbose "VerifyRunspace: Invoking whoami"
$ps.AddScript('whoami').Invoke() | Should -BeExactly $(whoami)
Write-Verbose -Verbose "VerifyRunspace complete"
}
finally
{
@ -123,7 +311,9 @@ Describe "SSHRemoting Basic Tests" -tags CI {
Context "SSH Remoting API Tests" {
AfterEach {
Write-Verbose -Verbose "Starting Runspace close AfterEach"
if ($script:rs -ne $null) { $script:rs.Dispose() }
Write-Verbose -Verbose "AfterEach complete"
}
$testCases = @(
@ -175,13 +365,15 @@ Describe "SSHRemoting Basic Tests" -tags CI {
$ComputerName,
$KeyFilePath,
$Port,
$SubSystem
$SubSystem,
$TestName
)
$ci = [System.Management.Automation.Runspaces.SSHConnectionInfo]::new($UserName, $ComputerName, $KeyFilePath, $Port, $Subsystem)
$script:rs = [runspacefactory]::CreateRunspace($host, $ci)
$script:rs.Open()
Write-Verbose -Verbose "It Starting: $TestName"
$script:rs = TryCreateRunspace -UserName $UserName -ComputerName $ComputerName -KeyFilePath $KeyFilePath -Port $Port -Subsystem $Subsystem
$script:rs | Should -Not -BeNullOrEmpty
VerifyRunspace $script:rs
Write-Verbose -Verbose "It Complete"
}
}
}