using System; using System.Globalization; using System.IO; using System.Text; using System.Threading; using Renci.SshNet.Abstractions; using Renci.SshNet.Channels; using Renci.SshNet.Common; using Renci.SshNet.Messages.Connection; using Renci.SshNet.Messages.Transport; namespace Renci.SshNet { /// /// Represents SSH command that can be executed. /// public class SshCommand : IDisposable { private readonly Encoding _encoding; private readonly object _endExecuteLock = new object(); private ISession _session; private IChannelSession _channel; private CommandAsyncResult _asyncResult; private AsyncCallback _callback; private EventWaitHandle _sessionErrorOccuredWaitHandle; private Exception _exception; private StringBuilder _result; private StringBuilder _error; private bool _hasError; private bool _isDisposed; /// /// 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; } /// /// Gets the command execution result. /// /// /// /// public string Result { get { _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(); } } /// /// Gets the command execution error. /// /// /// /// public string Error { get { if (_hasError) { _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 is null) { throw new ArgumentNullException(nameof(session)); } if (commandText is null) { throw new ArgumentNullException(nameof(commandText)); } if (encoding is null) { throw new ArgumentNullException(nameof(encoding)); } _session = session; CommandText = commandText; _encoding = encoding; CommandTimeout = Session.InfiniteTimeSpan; _sessionErrorOccuredWaitHandle = new AutoResetEvent(initialState: 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. public IAsyncResult BeginExecute() { return BeginExecute(callback: null, state: 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. public IAsyncResult BeginExecute(AsyncCallback callback) { return BeginExecute(callback, state: 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. 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(initialState: false), IsCompleted = false, AsyncState = state, }; // When command re-executed again, create a new channel if (_channel is not null) { throw new SshException("Invalid operation."); } if (string.IsNullOrEmpty(CommandText)) { throw new ArgumentException("CommandText property is empty."); } var outputStream = OutputStream; if (outputStream is not null) { outputStream.Dispose(); OutputStream = null; } var extendedOutputStream = ExtendedOutputStream; if (extendedOutputStream is not 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 is null) { throw new ArgumentNullException(nameof(asyncResult)); } if (asyncResult is not CommandAsyncResult commandAsyncResult || _asyncResult != commandAsyncResult) { throw new ArgumentException(string.Format("The {0} object was not returned from the corresponding asynchronous method on this class.", nameof(IAsyncResult))); } 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(callback: null, state: null)); } /// /// Cancels command execution in asynchronous scenarios. /// public void CancelAsync() { if (_channel is not null && _channel.IsOpen && _asyncResult is not 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. /// /// The result of the command execution. /// /// 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) { OutputStream?.Flush(); ExtendedOutputStream?.Flush(); _asyncResult.IsCompleted = true; if (_callback is not null) { // Execute callback on different thread ThreadAbstraction.ExecuteThread(() => _callback(_asyncResult)); } _ = ((EventWaitHandle) _asyncResult.AsyncWaitHandle).Set(); } private void Channel_RequestReceived(object sender, ChannelRequestEventArgs e) { if (e.Info is ExitStatusRequestInfo exitStatusInfo) { 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 }; var signaledElement = WaitHandle.WaitAny(waitHandles, CommandTimeout); switch (signaledElement) { case 0: throw _exception; case 1: // Specified waithandle was signaled break; case WaitHandle.WaitTimeout: throw new SshOperationTimeoutException(string.Format(CultureInfo.CurrentCulture, "Command '{0}' has timed out.", CommandText)); default: throw new SshException($"Unexpected element '{signaledElement.ToString(CultureInfo.InvariantCulture)}' signaled."); } } /// /// Unsubscribes the current from channel events, and disposes /// the . /// /// The channel. /// /// Does nothing when is null. /// private void UnsubscribeFromEventsAndDisposeChannel(IChannel channel) { if (channel is 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(); } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(disposing: 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; } } /// /// Finalizes an instance of the class. /// Releases unmanaged resources and performs other cleanup operations before the /// is reclaimed by garbage collection. /// ~SshCommand() { Dispose(disposing: false); } } }