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" } } }