You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

597 lines
24 KiB
C#

9 months ago
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
{
/// <summary>
/// Represents SSH command that can be executed.
/// </summary>
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();
/// <summary>
/// Gets the command text.
/// </summary>
public string CommandText { get; private set; }
/// <summary>
/// Gets or sets the command timeout.
/// </summary>
/// <value>
/// The command timeout.
/// </value>
/// <example>
/// <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateCommand Execute CommandTimeout" language="C#" title="Specify command execution timeout" />
/// </example>
public TimeSpan CommandTimeout { get; set; }
/// <summary>
/// Gets the command exit status.
/// </summary>
/// <example>
/// <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand RunCommand ExitStatus" language="C#" title="Get command execution exit status" />
/// </example>
public int ExitStatus { get; private set; }
/// <summary>
/// Gets the output stream.
/// </summary>
/// <example>
/// <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateCommand Execute OutputStream" language="C#" title="Use OutputStream to get command execution output" />
/// </example>
public Stream OutputStream { get; private set; }
/// <summary>
/// Gets the extended output stream.
/// </summary>
/// <example>
/// <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateCommand Execute ExtendedOutputStream" language="C#" title="Use ExtendedOutputStream to get command debug execution output" />
/// </example>
public Stream ExtendedOutputStream { get; private set; }
private StringBuilder _result;
/// <summary>
/// Gets the command execution result.
/// </summary>
/// <example>
/// <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand RunCommand Result" language="C#" title="Running simple command" />
/// </example>
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;
/// <summary>
/// Gets the command execution error.
/// </summary>
/// <example>
/// <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateCommand Error" language="C#" title="Display command execution error" />
/// </example>
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;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="SshCommand"/> class.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="commandText">The command text.</param>
/// <param name="encoding">The encoding to use for the results.</param>
/// <exception cref="ArgumentNullException">Either <paramref name="session"/>, <paramref name="commandText"/> is <c>null</c>.</exception>
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;
}
/// <summary>
/// Begins an asynchronous command execution.
/// </summary>
/// <returns>
/// An <see cref="System.IAsyncResult" /> that represents the asynchronous command execution, which could still be pending.
/// </returns>
/// <example>
/// <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateCommand BeginExecute IsCompleted EndExecute" language="C#" title="Asynchronous Command Execution" />
/// </example>
/// <exception cref="InvalidOperationException">Asynchronous operation is already in progress.</exception>
/// <exception cref="SshException">Invalid operation.</exception>
/// <exception cref="ArgumentException">CommandText property is empty.</exception>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
/// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
/// <exception cref="InvalidOperationException">Asynchronous operation is already in progress.</exception>
/// <exception cref="ArgumentException">CommandText property is empty.</exception>
public IAsyncResult BeginExecute()
{
return BeginExecute(null, null);
}
/// <summary>
/// Begins an asynchronous command execution.
/// </summary>
/// <param name="callback">An optional asynchronous callback, to be called when the command execution is complete.</param>
/// <returns>
/// An <see cref="System.IAsyncResult" /> that represents the asynchronous command execution, which could still be pending.
/// </returns>
/// <exception cref="InvalidOperationException">Asynchronous operation is already in progress.</exception>
/// <exception cref="SshException">Invalid operation.</exception>
/// <exception cref="ArgumentException">CommandText property is empty.</exception>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
/// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
/// <exception cref="InvalidOperationException">Asynchronous operation is already in progress.</exception>
/// <exception cref="ArgumentException">CommandText property is empty.</exception>
public IAsyncResult BeginExecute(AsyncCallback callback)
{
return BeginExecute(callback, null);
}
/// <summary>
/// Begins an asynchronous command execution.
/// </summary>
/// <param name="callback">An optional asynchronous callback, to be called when the command execution is complete.</param>
/// <param name="state">A user-provided object that distinguishes this particular asynchronous read request from other requests.</param>
/// <returns>
/// An <see cref="IAsyncResult" /> that represents the asynchronous command execution, which could still be pending.
/// </returns>
/// <exception cref="InvalidOperationException">Asynchronous operation is already in progress.</exception>
/// <exception cref="SshException">Invalid operation.</exception>
/// <exception cref="ArgumentException">CommandText property is empty.</exception>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
/// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
/// <exception cref="InvalidOperationException">Asynchronous operation is already in progress.</exception>
/// <exception cref="ArgumentException">CommandText property is empty.</exception>
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;
}
/// <summary>
/// Begins an asynchronous command execution.
/// </summary>
/// <param name="commandText">The command text.</param>
/// <param name="callback">An optional asynchronous callback, to be called when the command execution is complete.</param>
/// <param name="state">A user-provided object that distinguishes this particular asynchronous read request from other requests.</param>
/// <returns>
/// An <see cref="System.IAsyncResult" /> that represents the asynchronous command execution, which could still be pending.
/// </returns>
/// <exception cref="Renci.SshNet.Common.SshConnectionException">Client is not connected.</exception>
/// <exception cref="Renci.SshNet.Common.SshOperationTimeoutException">Operation has timed out.</exception>
public IAsyncResult BeginExecute(string commandText, AsyncCallback callback, object state)
{
CommandText = commandText;
return BeginExecute(callback, state);
}
/// <summary>
/// Waits for the pending asynchronous command execution to complete.
/// </summary>
/// <param name="asyncResult">The reference to the pending asynchronous request to finish.</param>
/// <returns>Command execution result.</returns>
/// <example>
/// <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateCommand BeginExecute IsCompleted EndExecute" language="C#" title="Asynchronous Command Execution" />
/// </example>
/// <exception cref="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.</exception>
/// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <c>null</c>.</exception>
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;
}
}
/// <summary>
/// Executes command specified by <see cref="CommandText"/> property.
/// </summary>
/// <returns>Command execution result</returns>
/// <example>
/// <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateCommand Execute" language="C#" title="Simple command execution" />
/// <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateCommand Error" language="C#" title="Display command execution error" />
/// <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateCommand Execute CommandTimeout" language="C#" title="Specify command execution timeout" />
/// </example>
/// <exception cref="Renci.SshNet.Common.SshConnectionException">Client is not connected.</exception>
/// <exception cref="Renci.SshNet.Common.SshOperationTimeoutException">Operation has timed out.</exception>
public string Execute()
{
return EndExecute(BeginExecute(null, null));
}
/// <summary>
/// Cancels command execution in asynchronous scenarios.
/// </summary>
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();
}
}
/// <summary>
/// Executes the specified command text.
/// </summary>
/// <param name="commandText">The command text.</param>
/// <returns>Command execution result</returns>
/// <exception cref="Renci.SshNet.Common.SshConnectionException">Client is not connected.</exception>
/// <exception cref="Renci.SshNet.Common.SshOperationTimeoutException">Operation has timed out.</exception>
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;
}
}
}
/// <exception cref="SshOperationTimeoutException">Command '{0}' has timed out.</exception>
/// <remarks>The actual command will be included in the exception message.</remarks>
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));
}
}
/// <summary>
/// Unsubscribes the current <see cref="SshCommand"/> from channel events, and disposes
/// the <see cref="IChannel"/>.
/// </summary>
/// <param name="channel">The channel.</param>
/// <remarks>
/// Does nothing when <paramref name="channel"/> is <c>null</c>.
/// </remarks>
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;
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
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;
}
}
/// <summary>
/// Releases unmanaged resources and performs other cleanup operations before the
/// <see cref="SshCommand"/> is reclaimed by garbage collection.
/// </summary>
~SshCommand()
{
Dispose(false);
}
#endregion
}
}