mirror of
https://github.com/PowerShell/PowerShell.git
synced 2024-11-27 20:05:34 +08:00
Fix SSH remoting connection hang with misconfigured endpoint (#15175)
This commit is contained in:
parent
9fd399ca89
commit
046e0fea03
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -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++;
|
||||
|
@ -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
|
||||
|
@ -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) { }
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user