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); } } } }