using System; using System.Collections.Generic; using System.IO; using System.Threading; using Renci.SshNet.Abstractions; using Renci.SshNet.Channels; using Renci.SshNet.Common; namespace Renci.SshNet { /// /// Represents instance of the SSH shell object. /// public class Shell : IDisposable { private readonly ISession _session; private readonly string _terminalName; private readonly uint _columns; private readonly uint _rows; private readonly uint _width; private readonly uint _height; private readonly IDictionary _terminalModes; private readonly Stream _outputStream; private readonly Stream _extendedOutputStream; private readonly int _bufferSize; private EventWaitHandle _dataReaderTaskCompleted; private IChannelSession _channel; private EventWaitHandle _channelClosedWaitHandle; private Stream _input; /// /// Gets a value indicating whether this shell is started. /// /// /// true if started is started; otherwise, false. /// public bool IsStarted { get; private set; } /// /// Occurs when shell is starting. /// public event EventHandler Starting; /// /// Occurs when shell is started. /// public event EventHandler Started; /// /// Occurs when shell is stopping. /// public event EventHandler Stopping; /// /// Occurs when shell is stopped. /// public event EventHandler Stopped; /// /// Occurs when an error occurred. /// public event EventHandler ErrorOccurred; /// /// Initializes a new instance of the class. /// /// The session. /// The input. /// The output. /// The extended output. /// Name of the terminal. /// The columns. /// The rows. /// The width. /// The height. /// The terminal modes. /// Size of the buffer for output stream. internal Shell(ISession session, Stream input, Stream output, Stream extendedOutput, string terminalName, uint columns, uint rows, uint width, uint height, IDictionary terminalModes, int bufferSize) { _session = session; _input = input; _outputStream = output; _extendedOutputStream = extendedOutput; _terminalName = terminalName; _columns = columns; _rows = rows; _width = width; _height = height; _terminalModes = terminalModes; _bufferSize = bufferSize; } /// /// Starts this shell. /// /// Shell is started. public void Start() { if (IsStarted) { throw new SshException("Shell is started."); } Starting?.Invoke(this, EventArgs.Empty); _channel = _session.CreateChannelSession(); _channel.DataReceived += Channel_DataReceived; _channel.ExtendedDataReceived += Channel_ExtendedDataReceived; _channel.Closed += Channel_Closed; _session.Disconnected += Session_Disconnected; _session.ErrorOccured += Session_ErrorOccured; _channel.Open(); _ = _channel.SendPseudoTerminalRequest(_terminalName, _columns, _rows, _width, _height, _terminalModes); _ = _channel.SendShellRequest(); _channelClosedWaitHandle = new AutoResetEvent(initialState: false); // Start input stream listener _dataReaderTaskCompleted = new ManualResetEvent(initialState: false); ThreadAbstraction.ExecuteThread(() => { try { var buffer = new byte[_bufferSize]; while (_channel.IsOpen) { var readTask = _input.ReadAsync(buffer, 0, buffer.Length); var readWaitHandle = ((IAsyncResult) readTask).AsyncWaitHandle; if (WaitHandle.WaitAny(new[] {readWaitHandle, _channelClosedWaitHandle}) == 0) { var read = readTask.GetAwaiter().GetResult(); _channel.SendData(buffer, 0, read); continue; } break; } } catch (Exception exp) { RaiseError(new ExceptionEventArgs(exp)); } finally { _ = _dataReaderTaskCompleted.Set(); } }); IsStarted = true; Started?.Invoke(this, EventArgs.Empty); } /// /// Stops this shell. /// /// Shell is not started. public void Stop() { if (!IsStarted) { throw new SshException("Shell is not started."); } _channel?.Dispose(); } private void Session_ErrorOccured(object sender, ExceptionEventArgs e) { RaiseError(e); } private void RaiseError(ExceptionEventArgs e) { ErrorOccurred?.Invoke(this, e); } private void Session_Disconnected(object sender, EventArgs e) { Stop(); } private void Channel_ExtendedDataReceived(object sender, ChannelExtendedDataEventArgs e) { _extendedOutputStream?.Write(e.Data, 0, e.Data.Length); } private void Channel_DataReceived(object sender, ChannelDataEventArgs e) { _outputStream?.Write(e.Data, 0, e.Data.Length); } private void Channel_Closed(object sender, ChannelEventArgs e) { if (Stopping is not null) { // Handle event on different thread ThreadAbstraction.ExecuteThread(() => Stopping(this, EventArgs.Empty)); } _channel.Dispose(); _ = _channelClosedWaitHandle.Set(); _input.Dispose(); _input = null; _ = _dataReaderTaskCompleted.WaitOne(_session.ConnectionInfo.Timeout); _dataReaderTaskCompleted.Dispose(); _dataReaderTaskCompleted = null; _channel.DataReceived -= Channel_DataReceived; _channel.ExtendedDataReceived -= Channel_ExtendedDataReceived; _channel.Closed -= Channel_Closed; UnsubscribeFromSessionEvents(_session); if (Stopped != null) { // Handle event on different thread ThreadAbstraction.ExecuteThread(() => Stopped(this, EventArgs.Empty)); } _channel = null; } /// /// 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; } private bool _disposed; /// /// 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 (_disposed) { return; } if (disposing) { UnsubscribeFromSessionEvents(_session); var channelClosedWaitHandle = _channelClosedWaitHandle; if (channelClosedWaitHandle is not null) { channelClosedWaitHandle.Dispose(); _channelClosedWaitHandle = null; } var channel = _channel; if (channel is not null) { channel.Dispose(); _channel = null; } var dataReaderTaskCompleted = _dataReaderTaskCompleted; if (dataReaderTaskCompleted is not null) { dataReaderTaskCompleted.Dispose(); _dataReaderTaskCompleted = null; } _disposed = true; } } /// /// Finalizes an instance of the class. /// ~Shell() { Dispose(disposing: false); } } }