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