using System; using System.IO; using System.Threading; using Renci.SshNet.Channels; using Renci.SshNet.Common; using System.Collections.Generic; using Renci.SshNet.Abstractions; namespace Renci.SshNet { /// /// Represents instance of the SSH shell object /// public class Shell : IDisposable { private readonly ISession _session; private IChannelSession _channel; private EventWaitHandle _channelClosedWaitHandle; private Stream _input; 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 EventWaitHandle _dataReaderTaskCompleted; private readonly Stream _outputStream; private readonly Stream _extendedOutputStream; private readonly int _bufferSize; /// /// 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."); } if (Starting != null) { Starting(this, new EventArgs()); } _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(false); // Start input stream listener _dataReaderTaskCompleted = new ManualResetEvent(false); ThreadAbstraction.ExecuteThread(() => { try { var buffer = new byte[_bufferSize]; while (_channel.IsOpen) { #if FEATURE_STREAM_TAP var readTask = _input.ReadAsync(buffer, 0, buffer.Length); var readWaitHandle = ((IAsyncResult) readTask).AsyncWaitHandle; if (WaitHandle.WaitAny(new[] {readWaitHandle, _channelClosedWaitHandle}) == 0) { var read = readTask.Result; _channel.SendData(buffer, 0, read); continue; } #elif FEATURE_STREAM_APM var asyncResult = _input.BeginRead(buffer, 0, buffer.Length, result => { // If input stream is closed and disposed already don't finish reading the stream if (_input == null) return; var read = _input.EndRead(result); _channel.SendData(buffer, 0, read); }, null); WaitHandle.WaitAny(new[] { asyncResult.AsyncWaitHandle, _channelClosedWaitHandle }); if (asyncResult.IsCompleted) continue; #else #error Async receive is not implemented. #endif break; } } catch (Exception exp) { RaiseError(new ExceptionEventArgs(exp)); } finally { _dataReaderTaskCompleted.Set(); } }); IsStarted = true; if (Started != null) { Started(this, new EventArgs()); } } /// /// Stops this shell. /// /// Shell is not started. public void Stop() { if (!IsStarted) { throw new SshException("Shell is not started."); } if (_channel != null) { _channel.Dispose(); } } private void Session_ErrorOccured(object sender, ExceptionEventArgs e) { RaiseError(e); } private void RaiseError(ExceptionEventArgs e) { var handler = ErrorOccurred; if (handler != null) { handler(this, e); } } private void Session_Disconnected(object sender, EventArgs e) { Stop(); } private void Channel_ExtendedDataReceived(object sender, ChannelExtendedDataEventArgs e) { if (_extendedOutputStream != null) { _extendedOutputStream.Write(e.Data, 0, e.Data.Length); } } private void Channel_DataReceived(object sender, ChannelDataEventArgs e) { if (_outputStream != null) { _outputStream.Write(e.Data, 0, e.Data.Length); } } private void Channel_Closed(object sender, ChannelEventArgs e) { if (Stopping != null) { // Handle event on different thread ThreadAbstraction.ExecuteThread(() => Stopping(this, new EventArgs())); } _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, new EventArgs())); } _channel = null; } /// /// Unsubscribes the current from session events. /// /// The session. /// /// Does nothing when is null. /// private void UnsubscribeFromSessionEvents(ISession session) { if (session == null) return; session.Disconnected -= Session_Disconnected; session.ErrorOccured -= Session_ErrorOccured; } #region IDisposable Members private bool _disposed; /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(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 != null) { channelClosedWaitHandle.Dispose(); _channelClosedWaitHandle = null; } var channel = _channel; if (channel != null) { channel.Dispose(); _channel = null; } var dataReaderTaskCompleted = _dataReaderTaskCompleted; if (dataReaderTaskCompleted != null) { dataReaderTaskCompleted.Dispose(); _dataReaderTaskCompleted = null; } _disposed = true; } } /// /// Releases unmanaged resources and performs other cleanup operations before the /// is reclaimed by garbage collection. /// ~Shell() { Dispose(false); } #endregion } }