using System; using System.IO; using System.Text; using System.Threading; using Renci.SshNet.Channels; using Renci.SshNet.Common; using Renci.SshNet.Messages.Connection; using Renci.SshNet.Messages.Transport; using System.Globalization; using Renci.SshNet.Abstractions; namespace Renci.SshNet { /// /// Represents SSH command that can be executed. /// public class SshCommand : IDisposable { private ISession _session; private readonly Encoding _encoding; private IChannelSession _channel; private CommandAsyncResult _asyncResult; private AsyncCallback _callback; private EventWaitHandle _sessionErrorOccuredWaitHandle; 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 (_result == null) { _result = new StringBuilder(); } if (OutputStream != null && OutputStream.Length > 0) { // do not dispose the StreamReader, as it would also dispose the stream var sr = new StreamReader(OutputStream, _encoding); _result.Append(sr.ReadToEnd()); } return _result.ToString(); } } private StringBuilder _error; /// /// Gets the command execution error. /// /// /// /// public string Error { get { if (_hasError) { if (_error == null) { _error = new StringBuilder(); } if (ExtendedOutputStream != null && ExtendedOutputStream.Length > 0) { // do not dispose the StreamReader, as it would also dispose the stream var sr = new StreamReader(ExtendedOutputStream, _encoding); _error.Append(sr.ReadToEnd()); } return _error.ToString(); } return string.Empty; } } /// /// Initializes a new instance of the class. /// /// The session. /// The command text. /// The encoding to use for the results. /// Either , is null. internal SshCommand(ISession session, string commandText, Encoding encoding) { if (session == null) throw new ArgumentNullException("session"); if (commandText == null) throw new ArgumentNullException("commandText"); if (encoding == null) throw new ArgumentNullException("encoding"); _session = session; CommandText = commandText; _encoding = encoding; CommandTimeout = Session.InfiniteTimeSpan; _sessionErrorOccuredWaitHandle = new AutoResetEvent(false); _session.Disconnected += Session_Disconnected; _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 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 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 (_asyncResult != null && !_asyncResult.EndCalled) { throw new InvalidOperationException("Asynchronous operation is already in progress."); } // Create new AsyncResult object _asyncResult = new CommandAsyncResult { AsyncWaitHandle = new ManualResetEvent(false), IsCompleted = false, AsyncState = state, }; // When command re-executed again, create a new channel if (_channel != null) { throw new SshException("Invalid operation."); } if (string.IsNullOrEmpty(CommandText)) throw new ArgumentException("CommandText property is empty."); var outputStream = OutputStream; if (outputStream != null) { outputStream.Dispose(); OutputStream = null; } var extendedOutputStream = ExtendedOutputStream; if (extendedOutputStream != null) { extendedOutputStream.Dispose(); ExtendedOutputStream = null; } // Initialize output streams OutputStream = new PipeStream(); ExtendedOutputStream = new PipeStream(); _result = null; _error = null; _callback = callback; _channel = CreateChannel(); _channel.Open(); _channel.SendExecRequest(CommandText); return _asyncResult; } /// /// Begins an asynchronous command execution. /// /// 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) { 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. /// is null. public string EndExecute(IAsyncResult asyncResult) { if (asyncResult == null) { throw new ArgumentNullException("asyncResult"); } var commandAsyncResult = asyncResult as CommandAsyncResult; if (commandAsyncResult == null || _asyncResult != commandAsyncResult) { throw new ArgumentException(string.Format("The {0} object was not returned from the corresponding asynchronous method on this class.", typeof(IAsyncResult).Name)); } lock (_endExecuteLock) { if (commandAsyncResult.EndCalled) { throw new ArgumentException("EndExecute can only be called once for each asynchronous operation."); } // wait for operation to complete (or time out) WaitOnHandle(_asyncResult.AsyncWaitHandle); UnsubscribeFromEventsAndDisposeChannel(_channel); _channel = null; commandAsyncResult.EndCalled = true; return Result; } } /// /// Executes command specified by property. /// /// Command execution result /// /// /// /// /// /// Client is not connected. /// Operation has timed out. public string Execute() { return EndExecute(BeginExecute(null, null)); } /// /// Cancels command execution in asynchronous scenarios. /// public void CancelAsync() { if (_channel != null && _channel.IsOpen && _asyncResult != null) { // TODO: check with Oleg if we shouldn't dispose the channel and uninitialize it ? _channel.Dispose(); } } /// /// Executes the specified command text. /// /// The command text. /// Command execution result /// Client is not connected. /// Operation has timed out. public string Execute(string commandText) { CommandText = commandText; return Execute(); } private IChannelSession CreateChannel() { var channel = _session.CreateChannelSession(); channel.DataReceived += Channel_DataReceived; channel.ExtendedDataReceived += Channel_ExtendedDataReceived; channel.RequestReceived += Channel_RequestReceived; channel.Closed += Channel_Closed; return channel; } private void Session_Disconnected(object sender, EventArgs e) { // If objected is disposed or being disposed don't handle this event if (_isDisposed) return; _exception = new SshConnectionException("An established connection was aborted by the software in your host machine.", DisconnectReason.ConnectionLost); _sessionErrorOccuredWaitHandle.Set(); } private void Session_ErrorOccured(object sender, ExceptionEventArgs e) { // If objected is disposed or being disposed don't handle this event if (_isDisposed) return; _exception = e.Exception; _sessionErrorOccuredWaitHandle.Set(); } private void Channel_Closed(object sender, ChannelEventArgs e) { var outputStream = OutputStream; if (outputStream != null) { outputStream.Flush(); } var extendedOutputStream = ExtendedOutputStream; if (extendedOutputStream != null) { extendedOutputStream.Flush(); } _asyncResult.IsCompleted = true; if (_callback != null) { // Execute callback on different thread ThreadAbstraction.ExecuteThread(() => _callback(_asyncResult)); } ((EventWaitHandle) _asyncResult.AsyncWaitHandle).Set(); } private void Channel_RequestReceived(object sender, ChannelRequestEventArgs e) { var exitStatusInfo = e.Info as ExitStatusRequestInfo; if (exitStatusInfo != null) { ExitStatus = (int) exitStatusInfo.ExitStatus; if (exitStatusInfo.WantReply) { var replyMessage = new ChannelSuccessMessage(_channel.LocalChannelNumber); _session.SendMessage(replyMessage); } } else { if (e.Info.WantReply) { var replyMessage = new ChannelFailureMessage(_channel.LocalChannelNumber); _session.SendMessage(replyMessage); } } } private void Channel_ExtendedDataReceived(object sender, ChannelExtendedDataEventArgs e) { if (ExtendedOutputStream != null) { ExtendedOutputStream.Write(e.Data, 0, e.Data.Length); ExtendedOutputStream.Flush(); } if (e.DataTypeCode == 1) { _hasError = true; } } private void Channel_DataReceived(object sender, ChannelDataEventArgs e) { if (OutputStream != null) { OutputStream.Write(e.Data, 0, e.Data.Length); OutputStream.Flush(); } if (_asyncResult != null) { lock (_asyncResult) { _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[] { _sessionErrorOccuredWaitHandle, waitHandle }; switch (WaitHandle.WaitAny(waitHandles, CommandTimeout)) { case 0: throw _exception; case WaitHandle.WaitTimeout: throw new SshOperationTimeoutException(string.Format(CultureInfo.CurrentCulture, "Command '{0}' has timed out.", CommandText)); } } /// /// Unsubscribes the current from channel events, and disposes /// the . /// /// The channel. /// /// Does nothing when is null. /// private void UnsubscribeFromEventsAndDisposeChannel(IChannel channel) { if (channel == null) return; // unsubscribe from events as we do not want to be signaled should these get fired // during the dispose of the channel channel.DataReceived -= Channel_DataReceived; channel.ExtendedDataReceived -= Channel_ExtendedDataReceived; channel.RequestReceived -= Channel_RequestReceived; channel.Closed -= Channel_Closed; // actually dispose the channel channel.Dispose(); } #region IDisposable Members private bool _isDisposed; /// /// 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 (_isDisposed) return; if (disposing) { // unsubscribe from session events to ensure other objects that we're going to dispose // are not accessed while disposing var session = _session; if (session != null) { session.Disconnected -= Session_Disconnected; session.ErrorOccured -= Session_ErrorOccured; _session = null; } // unsubscribe from channel events to ensure other objects that we're going to dispose // are not accessed while disposing var channel = _channel; if (channel != null) { UnsubscribeFromEventsAndDisposeChannel(channel); _channel = null; } var outputStream = OutputStream; if (outputStream != null) { outputStream.Dispose(); OutputStream = null; } var extendedOutputStream = ExtendedOutputStream; if (extendedOutputStream != null) { extendedOutputStream.Dispose(); ExtendedOutputStream = null; } var sessionErrorOccuredWaitHandle = _sessionErrorOccuredWaitHandle; if (sessionErrorOccuredWaitHandle != null) { sessionErrorOccuredWaitHandle.Dispose(); _sessionErrorOccuredWaitHandle = null; } _isDisposed = true; } } /// /// Releases unmanaged resources and performs other cleanup operations before the /// is reclaimed by garbage collection. /// ~SshCommand() { Dispose(false); } #endregion } }