using System; using System.IO; using System.Text; using System.Threading; using Renci.SshNet.Channels; using Renci.SshNet.Common; using Renci.SshNet.Messages; using Renci.SshNet.Messages.Connection; using Renci.SshNet.Messages.Transport; using System.Globalization; namespace Renci.SshNet { /// /// Represents SSH command that can be executed. /// public partial class SshCommand : IDisposable { private readonly Session _session; private ChannelSession _channel; private CommandAsyncResult _asyncResult; private AsyncCallback _callback; private EventWaitHandle _sessionErrorOccuredWaitHandle = new AutoResetEvent(false); private Exception _exception; private bool _hasError; private readonly object _endExecuteLock = new object(); /// /// Gets the command text. /// public string CommandText { get; private set; } /// /// Gets or sets the command timeout. /// /// /// The command timeout. /// /// /// /// public TimeSpan CommandTimeout { get; set; } /// /// Gets the command exit status. /// /// /// /// public int ExitStatus { get; private set; } /// /// Gets the output stream. /// /// /// /// public Stream OutputStream { get; private set; } /// /// Gets the extended output stream. /// /// /// /// public Stream ExtendedOutputStream { get; private set; } private StringBuilder _result; /// /// Gets the command execution result. /// /// /// /// public string Result { get { if (this._result == null) { this._result = new StringBuilder(); } if (this.OutputStream != null && this.OutputStream.Length > 0) { using (var sr = new StreamReader(this.OutputStream, this._session.ConnectionInfo.Encoding)) { this._result.Append(sr.ReadToEnd()); } } return this._result.ToString(); } } private StringBuilder _error; /// /// Gets the command execution error. /// /// /// /// public string Error { get { if (this._hasError) { if (this._error == null) { this._error = new StringBuilder(); } if (this.ExtendedOutputStream != null && this.ExtendedOutputStream.Length > 0) { using (var sr = new StreamReader(this.ExtendedOutputStream, this._session.ConnectionInfo.Encoding)) { this._error.Append(sr.ReadToEnd()); } } return this._error.ToString(); } return string.Empty; } } /// /// Initializes a new instance of the class. /// /// The session. /// The command text. /// Either , is null. internal SshCommand(Session session, string commandText) { if (session == null) throw new ArgumentNullException("session"); if (commandText == null) throw new ArgumentNullException("commandText"); this._session = session; this.CommandText = commandText; this.CommandTimeout = new TimeSpan(0, 0, 0, 0, -1); this._session.Disconnected += Session_Disconnected; this._session.ErrorOccured += Session_ErrorOccured; } /// /// Begins an asynchronous command execution. /// /// /// An that represents the asynchronous command execution, which could still be pending. /// /// /// /// /// Asynchronous operation is already in progress. /// Invalid operation. /// CommandText property is empty. /// Client is not connected. /// Operation has timed out. /// Asynchronous operation is already in progress. /// CommandText property is empty. public IAsyncResult BeginExecute() { return this.BeginExecute(null, null); } /// /// Begins an asynchronous command execution. /// /// An optional asynchronous callback, to be called when the command execution is complete. /// /// An that represents the asynchronous command execution, which could still be pending. /// /// Asynchronous operation is already in progress. /// Invalid operation. /// CommandText property is empty. /// Client is not connected. /// Operation has timed out. /// Asynchronous operation is already in progress. /// CommandText property is empty. public IAsyncResult BeginExecute(AsyncCallback callback) { return this.BeginExecute(callback, null); } /// /// Begins an asynchronous command execution. /// /// An optional asynchronous callback, to be called when the command execution is complete. /// A user-provided object that distinguishes this particular asynchronous read request from other requests. /// /// An that represents the asynchronous command execution, which could still be pending. /// /// Asynchronous operation is already in progress. /// Invalid operation. /// CommandText property is empty. /// Client is not connected. /// Operation has timed out. /// Asynchronous operation is already in progress. /// CommandText property is empty. public IAsyncResult BeginExecute(AsyncCallback callback, object state) { // Prevent from executing BeginExecute before calling EndExecute if (this._asyncResult != null) { throw new InvalidOperationException("Asynchronous operation is already in progress."); } // Create new AsyncResult object this._asyncResult = new CommandAsyncResult(this) { AsyncWaitHandle = new ManualResetEvent(false), IsCompleted = false, AsyncState = state, }; // When command re-executed again, create a new channel if (this._channel != null) { throw new SshException("Invalid operation."); } this.CreateChannel(); if (string.IsNullOrEmpty(this.CommandText)) throw new ArgumentException("CommandText property is empty."); this._callback = callback; this._channel.Open(); // Send channel command request this._channel.SendExecRequest(this.CommandText); return _asyncResult; } /// /// Begins an asynchronous command execution. 22 /// /// The command text. /// An optional asynchronous callback, to be called when the command execution is complete. /// A user-provided object that distinguishes this particular asynchronous read request from other requests. /// /// An that represents the asynchronous command execution, which could still be pending. /// /// Client is not connected. /// Operation has timed out. public IAsyncResult BeginExecute(string commandText, AsyncCallback callback, object state) { this.CommandText = commandText; return BeginExecute(callback, state); } /// /// Waits for the pending asynchronous command execution to complete. /// /// The reference to the pending asynchronous request to finish. /// Command execution result. /// /// /// /// Either the IAsyncResult object did not come from the corresponding async method on this type, or EndExecute was called multiple times with the same IAsyncResult. /// Either the IAsyncResult object did not come from the corresponding async method on this type, or EndExecute was called multiple times with the same IAsyncResult. public string EndExecute(IAsyncResult asyncResult) { if (this._asyncResult == asyncResult && this._asyncResult != null) { lock (this._endExecuteLock) { if (this._asyncResult != null) { // Make sure that operation completed if not wait for it to finish this.WaitOnHandle(this._asyncResult.AsyncWaitHandle); if (this._channel.IsOpen) { this._channel.SendEof(); this._channel.Close(); } this._channel = null; this._asyncResult = null; return this.Result; } } } throw new ArgumentException("Either the IAsyncResult object did not come from the corresponding async method on this type, or EndExecute was called multiple times with the same IAsyncResult."); } /// /// Executes command specified by property. /// /// Command execution result /// /// /// /// /// /// Client is not connected. /// Operation has timed out. public string Execute() { return this.EndExecute(this.BeginExecute(null, null)); } /// /// Cancels command execution in asynchronous scenarios. /// public void CancelAsync() { if (this._channel != null && this._channel.IsOpen && this._asyncResult != null) { this._channel.Close(); } } /// /// Executes the specified command text. /// /// The command text. /// Command execution result /// Client is not connected. /// Operation has timed out. public string Execute(string commandText) { this.CommandText = commandText; return this.Execute(); } private void CreateChannel() { this._channel = this._session.CreateClientChannel(); this._channel.DataReceived += Channel_DataReceived; this._channel.ExtendedDataReceived += Channel_ExtendedDataReceived; this._channel.RequestReceived += Channel_RequestReceived; this._channel.Closed += Channel_Closed; // Dispose of streams if already exists if (this.OutputStream != null) { this.OutputStream.Dispose(); this.OutputStream = null; } if (this.ExtendedOutputStream != null) { this.ExtendedOutputStream.Dispose(); this.ExtendedOutputStream = null; } // Initialize output streams and StringBuilders this.OutputStream = new PipeStream(); this.ExtendedOutputStream = new PipeStream(); this._result = null; this._error = null; } private void Session_Disconnected(object sender, EventArgs e) { // If objected is disposed or being disposed don't handle this event if (this._isDisposed) return; this._exception = new SshConnectionException("An established connection was aborted by the software in your host machine.", DisconnectReason.ConnectionLost); this._sessionErrorOccuredWaitHandle.Set(); } private void Session_ErrorOccured(object sender, ExceptionEventArgs e) { // If objected is disposed or being disposed don't handle this event if (this._isDisposed) return; this._exception = e.Exception; this._sessionErrorOccuredWaitHandle.Set(); } private void Channel_Closed(object sender, ChannelEventArgs e) { if (this.OutputStream != null) { this.OutputStream.Flush(); } if (this.ExtendedOutputStream != null) { this.ExtendedOutputStream.Flush(); } this._asyncResult.IsCompleted = true; if (this._callback != null) { // Execute callback on different thread this.ExecuteThread(() => this._callback(this._asyncResult)); } ((EventWaitHandle)this._asyncResult.AsyncWaitHandle).Set(); } private void Channel_RequestReceived(object sender, ChannelRequestEventArgs e) { Message replyMessage = new ChannelFailureMessage(this._channel.LocalChannelNumber); if (e.Info is ExitStatusRequestInfo) { ExitStatusRequestInfo exitStatusInfo = e.Info as ExitStatusRequestInfo; this.ExitStatus = (int)exitStatusInfo.ExitStatus; replyMessage = new ChannelSuccessMessage(this._channel.LocalChannelNumber); } if (e.Info.WantReply) { this._session.SendMessage(replyMessage); } } private void Channel_ExtendedDataReceived(object sender, ChannelDataEventArgs e) { if (this.ExtendedOutputStream != null) { this.ExtendedOutputStream.Write(e.Data, 0, e.Data.Length); this.ExtendedOutputStream.Flush(); } if (e.DataTypeCode == 1) { this._hasError = true; } } private void Channel_DataReceived(object sender, ChannelDataEventArgs e) { if (this.OutputStream != null) { this.OutputStream.Write(e.Data, 0, e.Data.Length); this.OutputStream.Flush(); } if (this._asyncResult != null) { lock (this._asyncResult) { this._asyncResult.BytesReceived += e.Data.Length; } } } /// Command '{0}' has timed out. /// The actual command will be included in the exception message. private void WaitOnHandle(WaitHandle waitHandle) { var waitHandles = new[] { this._sessionErrorOccuredWaitHandle, waitHandle }; switch (WaitHandle.WaitAny(waitHandles, this.CommandTimeout)) { case 0: throw this._exception; case WaitHandle.WaitTimeout: throw new SshOperationTimeoutException(string.Format(CultureInfo.CurrentCulture, "Command '{0}' has timed out.", this.CommandText)); } } partial void ExecuteThread(Action action); #region IDisposable Members private bool _isDisposed; /// /// 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._isDisposed) { // If disposing equals true, dispose all managed // and unmanaged ResourceMessages. if (disposing) { this._session.Disconnected -= Session_Disconnected; this._session.ErrorOccured -= Session_ErrorOccured; // Dispose managed ResourceMessages. if (this.OutputStream != null) { this.OutputStream.Dispose(); this.OutputStream = null; } // Dispose managed ResourceMessages. if (this.ExtendedOutputStream != null) { this.ExtendedOutputStream.Dispose(); this.ExtendedOutputStream = null; } // Dispose managed ResourceMessages. if (this._sessionErrorOccuredWaitHandle != null) { this._sessionErrorOccuredWaitHandle.Dispose(); this._sessionErrorOccuredWaitHandle = null; } // Dispose managed ResourceMessages. if (this._channel != null) { this._channel.DataReceived -= Channel_DataReceived; this._channel.ExtendedDataReceived -= Channel_ExtendedDataReceived; this._channel.RequestReceived -= Channel_RequestReceived; this._channel.Closed -= Channel_Closed; this._channel.Dispose(); this._channel = null; } } // Note disposing has been done. this._isDisposed = true; } } /// /// Releases unmanaged resources and performs other cleanup operations before the /// is reclaimed by garbage collection. /// ~SshCommand() { // Do not re-create Dispose clean-up code here. // Calling Dispose(false) is optimal in terms of // readability and maintainability. Dispose(false); } #endregion } }