diff --git a/src/System.Management.Automation/engine/remoting/commands/InvokeCommandCommand.cs b/src/System.Management.Automation/engine/remoting/commands/InvokeCommandCommand.cs
index b60f3131f24..c86f7903a1a 100644
--- a/src/System.Management.Automation/engine/remoting/commands/InvokeCommandCommand.cs
+++ b/src/System.Management.Automation/engine/remoting/commands/InvokeCommandCommand.cs
@@ -737,6 +737,30 @@ public override string KeyFilePath
set { base.KeyFilePath = value; }
}
+ ///
+ /// Gets and sets a value for the SSH subsystem to use for the remote connection.
+ ///
+ [Parameter(ParameterSetName = InvokeCommandCommand.SSHHostParameterSet)]
+ [Parameter(ParameterSetName = InvokeCommandCommand.FilePathSSHHostParameterSet)]
+ public override string Subsystem
+ {
+ get { return base.Subsystem; }
+
+ set { base.Subsystem = value; }
+ }
+
+ ///
+ /// Gets and sets a value in milliseconds that limits the time allowed for an SSH connection to be established.
+ ///
+ [Parameter(ParameterSetName = InvokeCommandCommand.SSHHostParameterSet)]
+ [Parameter(ParameterSetName = InvokeCommandCommand.FilePathSSHHostParameterSet)]
+ public override int ConnectingTimeout
+ {
+ get { return base.ConnectingTimeout; }
+
+ set { base.ConnectingTimeout = value; }
+ }
+
///
/// This parameter specifies that SSH is used to establish the remote
/// connection and act as the remoting transport. By default WinRM is used
diff --git a/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs b/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs
index 9fafd34eec4..40be7ec2503 100644
--- a/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs
+++ b/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs
@@ -15,6 +15,7 @@
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 @@ internal struct SSHConnection
public string KeyFilePath;
public int Port;
public string Subsystem;
+ public int ConnectingTimeout;
}
///
@@ -761,6 +763,20 @@ public virtual string KeyFilePath
set;
}
+ ///
+ /// Gets or sets a value for the SSH subsystem to use for the remote connection.
+ ///
+ [Parameter(ValueFromPipelineByPropertyName = true,
+ ParameterSetName = PSRemotingBaseCmdlet.SSHHostParameterSet)]
+ public virtual string Subsystem { get; set; }
+
+ ///
+ /// Gets or sets a value in milliseconds that limits the time allowed for an SSH connection to be established.
+ /// Default timeout value is infinite.
+ ///
+ [Parameter(ParameterSetName = PSRemotingBaseCmdlet.SSHHostParameterSet)]
+ public virtual int ConnectingTimeout { get; set; } = Timeout.Infinite;
+
///
/// 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 @@ public virtual Hashtable[] SSHConnection
set;
}
- ///
- /// This parameter specifies the SSH subsystem to use for the remote connection.
- ///
- [Parameter(ValueFromPipelineByPropertyName = true,
- ParameterSetName = InvokeCommandCommand.SSHHostParameterSet)]
- public virtual string Subsystem { get; set; }
-
#endregion
#endregion Properties
@@ -856,6 +865,7 @@ internal static void ValidateSpecifiedAuthentication(PSCredential credential, st
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 @@ protected void ParseSshHostName(string hostname, out string host, out string use
/// Array of SSHConnection objects.
internal SSHConnection[] ParseSSHConnectionHashTable()
{
- List connections = new List();
+ List connections = new();
foreach (var item in this.SSHConnection)
{
if (item.ContainsKey(ComputerNameParameter) && item.ContainsKey(HostNameAlias))
@@ -915,7 +925,7 @@ internal SSHConnection[] ParseSSHConnectionHashTable()
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 @@ internal SSHConnection[] ParseSSHConnectionHashTable()
{
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 @@ protected void CreateHelpersForSpecifiedSSHComputerNames()
{
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 @@ protected void CreateHelpersForSpecifiedSSHHashComputerNames()
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);
diff --git a/src/System.Management.Automation/engine/remoting/commands/PushRunspaceCommand.cs b/src/System.Management.Automation/engine/remoting/commands/PushRunspaceCommand.cs
index cbc5f82742b..996b043d77e 100644
--- a/src/System.Management.Automation/engine/remoting/commands/PushRunspaceCommand.cs
+++ b/src/System.Management.Automation/engine/remoting/commands/PushRunspaceCommand.cs
@@ -1262,7 +1262,7 @@ private RemoteRunspace GetRunspaceForContainerSession()
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.
diff --git a/src/System.Management.Automation/engine/remoting/commands/newrunspacecommand.cs b/src/System.Management.Automation/engine/remoting/commands/newrunspacecommand.cs
index 625bb86fd64..d4a0a3e7508 100644
--- a/src/System.Management.Automation/engine/remoting/commands/newrunspacecommand.cs
+++ b/src/System.Management.Automation/engine/remoting/commands/newrunspacecommand.cs
@@ -1092,7 +1092,8 @@ private List CreateRunspacesForSSHHostParameterSet()
host,
this.KeyFilePath,
port,
- Subsystem);
+ Subsystem,
+ ConnectingTimeout);
var typeTable = TypeTable.LoadDefaultTypeFiles();
string rsName = GetRunspaceName(index, out int rsIdUnused);
index++;
@@ -1118,7 +1119,8 @@ private List CreateRunspacesForSSHHostHashParameterSet()
sshConnection.ComputerName,
sshConnection.KeyFilePath,
sshConnection.Port,
- sshConnection.Subsystem);
+ sshConnection.Subsystem,
+ sshConnection.ConnectingTimeout);
var typeTable = TypeTable.LoadDefaultTypeFiles();
string rsName = GetRunspaceName(index, out int rsIdUnused);
index++;
diff --git a/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs b/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs
index ea8a9c1a9d0..a16e7820bd1 100644
--- a/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs
+++ b/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs
@@ -1907,6 +1907,20 @@ internal override BaseClientSessionTransportManager CreateClientSessionTransport
///
public sealed class SSHConnectionInfo : RunspaceConnectionInfo
{
+ #region Constants
+
+ ///
+ /// Default value for subsystem.
+ ///
+ private const string DefaultSubsystem = "powershell";
+
+ ///
+ /// Default value is infinite timeout.
+ ///
+ private const int DefaultConnectingTimeoutTime = Timeout.Infinite;
+
+ #endregion
+
#region Properties
///
@@ -1945,6 +1959,16 @@ private string Subsystem
set;
}
+ ///
+ /// 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.
+ ///
+ public int ConnectingTimeout
+ {
+ get;
+ set;
+ }
+
#endregion
#region Constructors
@@ -1968,11 +1992,12 @@ public SSHConnectionInfo(
{
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;
}
///
@@ -1989,8 +2014,7 @@ public SSHConnectionInfo(
int port) : this(userName, computerName, keyFilePath)
{
ValidatePortInRange(port);
-
- this.Port = port;
+ Port = port;
}
///
@@ -2006,12 +2030,29 @@ public SSHConnectionInfo(
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;
+ ///
+ /// Initializes a new instance of SSHConnectionInfo.
+ ///
+ /// Name of user.
+ /// Name of computer.
+ /// Path of key file.
+ /// Port number for connection (default 22).
+ /// Subsystem to use (default 'powershell').
+ /// Timeout time for terminating connection attempt.
+ 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 @@ public override string CertificateThumbprint
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 @@ internal int StartSSHProcess(
#endregion
- #region Constants
-
- ///
- /// Default value for subsystem.
- ///
- private const string DefaultSubsystem = "powershell";
-
- #endregion
-
#region SSH Process Creation
#if UNIX
diff --git a/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs b/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs
index b8f6da13c17..ff206938879 100644
--- a/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs
+++ b/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs
@@ -591,7 +591,7 @@ internal override void CloseAsync()
// 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 @@ internal override void Dispose(bool isDisposing)
{
_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 @@ internal void OnCloseTimeOutTimerElapsed(object source)
}
#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 @@ internal sealed class SSHClientSessionTransportManager : OutOfProcessClientSessi
private StreamReader _stdOutReader;
private StreamReader _stdErrReader;
private bool _connectionEstablished;
+ private Timer _connectionTimer;
private const string _threadName = "SSHTransport Reader Thread";
@@ -1641,6 +1650,49 @@ internal override void CreateAsync()
// 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 @@ internal override void CloseAsync()
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 @@ private void CloseConnection()
{
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) { }
diff --git a/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx b/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx
index b4f9dc40a89..441228c9c44 100644
--- a/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx
+++ b/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx
@@ -1625,6 +1625,13 @@ All WinRM sessions connected to PowerShell session configurations, such as Micro
The SSH client session has ended with error message: {0}
+
+ SSH connection attempt failed after time out: {0} seconds.
+
+
+
+SSH client process terminated before connection could be established.
+
The provided SSHConnection hashtable is missing the required ComputerName or HostName parameter.
diff --git a/test/SSHRemoting/SSHRemoting.Basic.Tests.ps1 b/test/SSHRemoting/SSHRemoting.Basic.Tests.ps1
index 40fa0ec1027..7383cc2b8a1 100644
--- a/test/SSHRemoting/SSHRemoting.Basic.Tests.ps1
+++ b/test/SSHRemoting/SSHRemoting.Basic.Tests.ps1
@@ -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,14 +215,62 @@ 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 {
@@ -100,19 +278,29 @@ Describe "SSHRemoting Basic Tests" -tags CI {
[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"
}
}
}