using System; using System.Linq; using System.IO; using System.Threading; using Renci.SshNet.Channels; using Renci.SshNet.Common; using System.Collections.Generic; namespace Renci.SshNet { /// /// Represents instance of the SSH shell object /// public partial class Shell : IDisposable { private readonly Session _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(Session session, Stream input, Stream output, Stream extendedOutput, string terminalName, uint columns, uint rows, uint width, uint height, IDictionary terminalModes, int bufferSize) { this._session = session; this._input = input; this._outputStream = output; this._extendedOutputStream = extendedOutput; this._terminalName = terminalName; this._columns = columns; this._rows = rows; this._width = width; this._height = height; this._terminalModes = terminalModes; this._bufferSize = bufferSize; } /// /// Starts this shell. /// /// Shell is started. public void Start() { if (this.IsStarted) { throw new SshException("Shell is started."); } if (this.Starting != null) { this.Starting(this, new EventArgs()); } this._channel = this._session.CreateClientChannel(); this._channel.DataReceived += Channel_DataReceived; this._channel.ExtendedDataReceived += Channel_ExtendedDataReceived; this._channel.Closed += Channel_Closed; this._session.Disconnected += Session_Disconnected; this._session.ErrorOccured += Session_ErrorOccured; this._channel.Open(); this._channel.SendPseudoTerminalRequest(this._terminalName, this._columns, this._rows, this._width, this._height, this._terminalModes); this._channel.SendShellRequest(); this._channelClosedWaitHandle = new AutoResetEvent(false); // Start input stream listener this._dataReaderTaskCompleted = new ManualResetEvent(false); this.ExecuteThread(() => { try { var buffer = new byte[this._bufferSize]; while (this._channel.IsOpen) { var asyncResult = this._input.BeginRead(buffer, 0, buffer.Length, delegate(IAsyncResult result) { // If input stream is closed and disposed already dont finish reading the stream if (this._input == null) return; var read = this._input.EndRead(result); if (read > 0) { this._channel.SendData(buffer.Take(read).ToArray()); } }, null); EventWaitHandle.WaitAny(new WaitHandle[] { asyncResult.AsyncWaitHandle, this._channelClosedWaitHandle }); if (asyncResult.IsCompleted) continue; break; } } catch (Exception exp) { this.RaiseError(new ExceptionEventArgs(exp)); } finally { this._dataReaderTaskCompleted.Set(); } }); this.IsStarted = true; if (this.Started != null) { this.Started(this, new EventArgs()); } } /// /// Stops this shell. /// /// Shell is not started. public void Stop() { if (!this.IsStarted) { throw new SshException("Shell is not started."); } // If channel is open then close it to cause Channel_Closed method to be called if (this._channel != null && this._channel.IsOpen) { this._channel.SendEof(); this._channel.Close(); } } private void Session_ErrorOccured(object sender, ExceptionEventArgs e) { this.RaiseError(e); } private void RaiseError(ExceptionEventArgs e) { var handler = this.ErrorOccurred; if (handler != null) { handler(this, e); } } private void Session_Disconnected(object sender, EventArgs e) { this.Stop(); } private void Channel_ExtendedDataReceived(object sender, ChannelDataEventArgs e) { if (this._extendedOutputStream != null) { this._extendedOutputStream.Write(e.Data, 0, e.Data.Length); } } private void Channel_DataReceived(object sender, ChannelDataEventArgs e) { if (this._outputStream != null) { this._outputStream.Write(e.Data, 0, e.Data.Length); } } private void Channel_Closed(object sender, ChannelEventArgs e) { if (this.Stopping != null) { // Handle event on different thread this.ExecuteThread(() => this.Stopping(this, new EventArgs())); } if (this._channel.IsOpen) { this._channel.SendEof(); this._channel.Close(); } this._channelClosedWaitHandle.Set(); this._input.Dispose(); this._input = null; this._dataReaderTaskCompleted.WaitOne(this._session.ConnectionInfo.Timeout); this._dataReaderTaskCompleted.Dispose(); this._dataReaderTaskCompleted = null; this._channel.DataReceived -= Channel_DataReceived; this._channel.ExtendedDataReceived -= Channel_ExtendedDataReceived; this._channel.Closed -= Channel_Closed; this._session.Disconnected -= Session_Disconnected; this._session.ErrorOccured -= Session_ErrorOccured; if (this.Stopped != null) { // Handle event on different thread this.ExecuteThread(() => this.Stopped(this, new EventArgs())); } this._channel = null; } partial void ExecuteThread(Action action); #region IDisposable Members private bool _disposed; /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged ResourceMessages. /// 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 ResourceMessages. protected virtual void Dispose(bool disposing) { // Check to see if Dispose has already been called. if (!this._disposed) { // If disposing equals true, dispose all managed // and unmanaged ResourceMessages. if (disposing) { if (this._channelClosedWaitHandle != null) { this._channelClosedWaitHandle.Dispose(); this._channelClosedWaitHandle = null; } if (this._channel != null) { this._channel.Dispose(); this._channel = null; } if (this._dataReaderTaskCompleted != null) { this._dataReaderTaskCompleted.Dispose(); this._dataReaderTaskCompleted = null; } } // Note disposing has been done. this._disposed = true; } } /// /// Releases unmanaged resources and performs other cleanup operations before the /// is reclaimed by garbage collection. /// ~Shell() { // Do not re-create Dispose clean-up code here. // Calling Dispose(false) is optimal in terms of // readability and maintainability. Dispose(false); } #endregion } }