using System;
using System.Globalization;
using System.Threading;
using Renci.SshNet.Abstractions;
using Renci.SshNet.Channels;
using Renci.SshNet.Common;
namespace Renci.SshNet
{
///
/// Base class for SSH subsystem implementations.
///
internal abstract class SubsystemSession : ISubsystemSession
{
///
/// Holds the number of system wait handles that are returned as the leading entries in the array returned
/// in .
///
private const int SystemWaitHandleCount = 3;
private readonly string _subsystemName;
private ISession _session;
private IChannelSession _channel;
private Exception _exception;
private EventWaitHandle _errorOccuredWaitHandle = new ManualResetEvent(initialState: false);
private EventWaitHandle _sessionDisconnectedWaitHandle = new ManualResetEvent(initialState: false);
private EventWaitHandle _channelClosedWaitHandle = new ManualResetEvent(initialState: false);
private bool _isDisposed;
///
/// Gets or set the number of seconds to wait for an operation to complete.
///
///
/// The number of seconds to wait for an operation to complete, or -1 to wait indefinitely.
///
public int OperationTimeout { get; private set; }
///
/// Occurs when an error occurred.
///
public event EventHandler ErrorOccurred;
///
/// Occurs when the server has disconnected from the session.
///
public event EventHandler Disconnected;
///
/// Gets the channel associated with this session.
///
///
/// The channel associated with this session.
///
internal IChannelSession Channel
{
get
{
EnsureNotDisposed();
return _channel;
}
}
///
/// Gets a value indicating whether this session is open.
///
///
/// true if this session is open; otherwise, false.
///
public bool IsOpen
{
get { return _channel is not null && _channel.IsOpen; }
}
///
/// Initializes a new instance of the class.
///
/// The session.
/// Name of the subsystem.
/// The number of milliseconds to wait for a given operation to complete, or -1 to wait indefinitely.
/// or is null.
protected SubsystemSession(ISession session, string subsystemName, int operationTimeout)
{
if (session is null)
{
throw new ArgumentNullException(nameof(session));
}
if (subsystemName is null)
{
throw new ArgumentNullException(nameof(subsystemName));
}
_session = session;
_subsystemName = subsystemName;
OperationTimeout = operationTimeout;
}
///
/// Connects the subsystem using a new SSH channel session.
///
/// The session is already connected.
/// The method was called after the session was disposed.
/// The channel session could not be opened, or the subsystem could not be executed.
public void Connect()
{
EnsureNotDisposed();
if (IsOpen)
{
throw new InvalidOperationException("The session is already connected.");
}
// reset waithandles in case we're reconnecting
_ = _errorOccuredWaitHandle.Reset();
_ = _sessionDisconnectedWaitHandle.Reset();
_ = _sessionDisconnectedWaitHandle.Reset();
_ = _channelClosedWaitHandle.Reset();
_session.ErrorOccured += Session_ErrorOccured;
_session.Disconnected += Session_Disconnected;
_channel = _session.CreateChannelSession();
_channel.DataReceived += Channel_DataReceived;
_channel.Exception += Channel_Exception;
_channel.Closed += Channel_Closed;
_channel.Open();
if (!_channel.SendSubsystemRequest(_subsystemName))
{
// close channel session
Disconnect();
// signal subsystem failure
throw new SshException(string.Format(CultureInfo.InvariantCulture,
"Subsystem '{0}' could not be executed.",
_subsystemName));
}
OnChannelOpen();
}
///
/// Disconnects the subsystem channel.
///
public void Disconnect()
{
UnsubscribeFromSessionEvents(_session);
var channel = _channel;
if (channel is not null)
{
_channel = null;
channel.DataReceived -= Channel_DataReceived;
channel.Exception -= Channel_Exception;
channel.Closed -= Channel_Closed;
channel.Dispose();
}
}
///
/// Sends data to the subsystem.
///
/// The data to be sent.
public void SendData(byte[] data)
{
EnsureNotDisposed();
EnsureSessionIsOpen();
_channel.SendData(data);
}
///
/// Called when channel is open.
///
protected abstract void OnChannelOpen();
///
/// Called when data is received.
///
/// The data.
protected abstract void OnDataReceived(byte[] data);
///
/// Raises the error.
///
/// The error.
protected void RaiseError(Exception error)
{
_exception = error;
DiagnosticAbstraction.Log("Raised exception: " + error);
_ = _errorOccuredWaitHandle?.Set();
SignalErrorOccurred(error);
}
private void Channel_DataReceived(object sender, ChannelDataEventArgs e)
{
try
{
OnDataReceived(e.Data);
}
catch (Exception ex)
{
RaiseError(ex);
}
}
private void Channel_Exception(object sender, ExceptionEventArgs e)
{
RaiseError(e.Exception);
}
private void Channel_Closed(object sender, ChannelEventArgs e)
{
_ = _channelClosedWaitHandle?.Set();
}
///
/// Waits a specified time for a given to get signaled.
///
/// The handle to wait for.
/// To number of milliseconds to wait for to get signaled, or -1 to wait indefinitely.
/// The connection was closed by the server.
/// The channel was closed.
/// The handle did not get signaled within the specified timeout.
public void WaitOnHandle(WaitHandle waitHandle, int millisecondsTimeout)
{
var waitHandles = new[]
{
_errorOccuredWaitHandle,
_sessionDisconnectedWaitHandle,
_channelClosedWaitHandle,
waitHandle
};
var result = WaitHandle.WaitAny(waitHandles, millisecondsTimeout);
switch (result)
{
case 0:
throw _exception;
case 1:
throw new SshException("Connection was closed by the server.");
case 2:
throw new SshException("Channel was closed.");
case 3:
break;
case WaitHandle.WaitTimeout:
throw new SshOperationTimeoutException("Operation has timed out.");
default:
throw new NotImplementedException(string.Format(CultureInfo.InvariantCulture, "WaitAny return value '{0}' is not implemented.", result));
}
}
///
/// Blocks the current thread until the specified gets signaled, using a
/// 32-bit signed integer to specify the time interval in milliseconds.
///
/// The handle to wait for.
/// To number of milliseconds to wait for to get signaled, or -1 to wait indefinitely.
///
/// true if received a signal within the specified timeout;
/// otherwise, false.
///
/// The connection was closed by the server.
/// The channel was closed.
///
/// The blocking wait is also interrupted when either the established channel is closed, the current
/// session is disconnected or an unexpected occurred while processing a channel
/// or session event.
///
public bool WaitOne(WaitHandle waitHandle, int millisecondsTimeout)
{
var waitHandles = new[]
{
_errorOccuredWaitHandle,
_sessionDisconnectedWaitHandle,
_channelClosedWaitHandle,
waitHandle
};
var result = WaitHandle.WaitAny(waitHandles, millisecondsTimeout);
switch (result)
{
case 0:
throw _exception;
case 1:
throw new SshException("Connection was closed by the server.");
case 2:
throw new SshException("Channel was closed.");
case 3:
return true;
case WaitHandle.WaitTimeout:
return false;
default:
throw new NotImplementedException(string.Format(CultureInfo.InvariantCulture, "WaitAny return value '{0}' is not implemented.", result));
}
}
///
/// Blocks the current thread until the specified gets signaled, using a
/// 32-bit signed integer to specify the time interval in milliseconds.
///
/// The first handle to wait for.
/// The second handle to wait for.
/// To number of milliseconds to wait for a to get signaled, or -1 to wait indefinitely.
///
/// 0 if received a signal within the specified timeout, and 1
/// if received a signal within the specified timeout.
///
/// The connection was closed by the server.
/// The channel was closed.
/// The handle did not get signaled within the specified timeout.
///
///
/// The blocking wait is also interrupted when either the established channel is closed, the current
/// session is disconnected or an unexpected occurred while processing a channel
/// or session event.
///
///
/// When both and are signaled during the call,
/// then 0 is returned.
///
///
public int WaitAny(WaitHandle waitHandle1, WaitHandle waitHandle2, int millisecondsTimeout)
{
var waitHandles = new[]
{
_errorOccuredWaitHandle,
_sessionDisconnectedWaitHandle,
_channelClosedWaitHandle,
waitHandle1,
waitHandle2
};
var result = WaitHandle.WaitAny(waitHandles, millisecondsTimeout);
switch (result)
{
case 0:
throw _exception;
case 1:
throw new SshException("Connection was closed by the server.");
case 2:
throw new SshException("Channel was closed.");
case 3:
return 0;
case 4:
return 1;
case WaitHandle.WaitTimeout:
throw new SshOperationTimeoutException("Operation has timed out.");
default:
throw new NotImplementedException(string.Format(CultureInfo.InvariantCulture, "WaitAny return value '{0}' is not implemented.", result));
}
}
///
/// Waits for any of the elements in the specified array to receive a signal, using a 32-bit signed
/// integer to specify the time interval.
///
/// A array - constructed using - containing the objects to wait for.
/// To number of milliseconds to wait for a to get signaled, or -1 to wait indefinitely.
///
/// The array index of the first non-system object that satisfied the wait.
///
/// The connection was closed by the server.
/// The channel was closed.
/// No object satified the wait and a time interval equivalent to has passed.
///
/// For the return value, the index of the first non-system object is considered to be zero.
///
public int WaitAny(WaitHandle[] waitHandles, int millisecondsTimeout)
{
var result = WaitHandle.WaitAny(waitHandles, millisecondsTimeout);
switch (result)
{
case 0:
throw _exception;
case 1:
throw new SshException("Connection was closed by the server.");
case 2:
throw new SshException("Channel was closed.");
case WaitHandle.WaitTimeout:
throw new SshOperationTimeoutException("Operation has timed out.");
default:
return result - SystemWaitHandleCount;
}
}
///
/// Creates a array that is composed of system objects and the specified
/// elements.
///
/// The first to wait for.
/// The second to wait for.
///
/// A array that is composed of system objects and the specified elements.
///
public WaitHandle[] CreateWaitHandleArray(WaitHandle waitHandle1, WaitHandle waitHandle2)
{
return new WaitHandle[]
{
_errorOccuredWaitHandle,
_sessionDisconnectedWaitHandle,
_channelClosedWaitHandle,
waitHandle1,
waitHandle2
};
}
///
/// Creates a array that is composed of system objects and the specified
/// elements.
///
/// A array containing the objects to wait for.
///
/// A array that is composed of system objects and the specified elements.
///
public WaitHandle[] CreateWaitHandleArray(params WaitHandle[] waitHandles)
{
var array = new WaitHandle[waitHandles.Length + SystemWaitHandleCount];
array[0] = _errorOccuredWaitHandle;
array[1] = _sessionDisconnectedWaitHandle;
array[2] = _channelClosedWaitHandle;
for (var i = 0; i < waitHandles.Length; i++)
{
array[i + SystemWaitHandleCount] = waitHandles[i];
}
return array;
}
private void Session_Disconnected(object sender, EventArgs e)
{
_ = _sessionDisconnectedWaitHandle?.Set();
SignalDisconnected();
}
private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
{
RaiseError(e.Exception);
}
private void SignalErrorOccurred(Exception error)
{
ErrorOccurred?.Invoke(this, new ExceptionEventArgs(error));
}
private void SignalDisconnected()
{
Disconnected?.Invoke(this, EventArgs.Empty);
}
private void EnsureSessionIsOpen()
{
if (!IsOpen)
{
throw new InvalidOperationException("The session is not open.");
}
}
///
/// Unsubscribes the current from session events.
///
/// The session.
///
/// Does nothing when is null.
///
private void UnsubscribeFromSessionEvents(ISession session)
{
if (session is null)
{
return;
}
session.Disconnected -= Session_Disconnected;
session.ErrorOccured -= Session_ErrorOccured;
}
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
///
/// Releases unmanaged and - optionally - managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected virtual void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}
if (disposing)
{
Disconnect();
_session = null;
var errorOccuredWaitHandle = _errorOccuredWaitHandle;
if (errorOccuredWaitHandle != null)
{
_errorOccuredWaitHandle = null;
errorOccuredWaitHandle.Dispose();
}
var sessionDisconnectedWaitHandle = _sessionDisconnectedWaitHandle;
if (sessionDisconnectedWaitHandle != null)
{
_sessionDisconnectedWaitHandle = null;
sessionDisconnectedWaitHandle.Dispose();
}
var channelClosedWaitHandle = _channelClosedWaitHandle;
if (channelClosedWaitHandle != null)
{
_channelClosedWaitHandle = null;
channelClosedWaitHandle.Dispose();
}
_isDisposed = true;
}
}
///
/// Finalizes an instance of the class.
///
~SubsystemSession()
{
Dispose(disposing: false);
}
private void EnsureNotDisposed()
{
if (_isDisposed)
{
throw new ObjectDisposedException(GetType().FullName);
}
}
}
}