|
@@ -1,5 +1,6 @@
|
|
|
-using System;
|
|
|
|
|
-using System.Globalization;
|
|
|
|
|
|
|
+#nullable enable
|
|
|
|
|
+using System;
|
|
|
|
|
+using System.Diagnostics;
|
|
|
using System.IO;
|
|
using System.IO;
|
|
|
using System.Runtime.ExceptionServices;
|
|
using System.Runtime.ExceptionServices;
|
|
|
using System.Text;
|
|
using System.Text;
|
|
@@ -18,24 +19,30 @@ namespace Renci.SshNet
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
public class SshCommand : IDisposable
|
|
public class SshCommand : IDisposable
|
|
|
{
|
|
{
|
|
|
|
|
+ private static readonly object CompletedResult = new();
|
|
|
|
|
+
|
|
|
|
|
+ private readonly ISession _session;
|
|
|
private readonly Encoding _encoding;
|
|
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 EventWaitHandle _commandCancelledWaitHandle;
|
|
|
|
|
- private Exception _exception;
|
|
|
|
|
- private string _result;
|
|
|
|
|
- private string _error;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// The result of the command: an exception, <see cref="CompletedResult"/>
|
|
|
|
|
+ /// or <see langword="null"/>.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private object? _result;
|
|
|
|
|
+
|
|
|
|
|
+ private IChannelSession? _channel;
|
|
|
|
|
+ private CommandAsyncResult? _asyncResult;
|
|
|
|
|
+ private AsyncCallback? _callback;
|
|
|
|
|
+ private string? _stdOut;
|
|
|
|
|
+ private string? _stdErr;
|
|
|
private bool _hasError;
|
|
private bool _hasError;
|
|
|
private bool _isDisposed;
|
|
private bool _isDisposed;
|
|
|
- private bool _isCancelled;
|
|
|
|
|
- private ChannelInputStream _inputStream;
|
|
|
|
|
|
|
+ private ChannelInputStream? _inputStream;
|
|
|
private TimeSpan _commandTimeout;
|
|
private TimeSpan _commandTimeout;
|
|
|
|
|
|
|
|
|
|
+ private int _exitStatus;
|
|
|
|
|
+ private volatile bool _haveExitStatus; // volatile to prevent re-ordering of reads/writes of _exitStatus.
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Gets the command text.
|
|
/// Gets the command text.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
@@ -62,23 +69,43 @@ namespace Renci.SshNet
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
- /// Gets the command exit status.
|
|
|
|
|
|
|
+ /// Gets the number representing the exit status of the command, if applicable,
|
|
|
|
|
+ /// otherwise <see langword="null"/>.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
- public int ExitStatus { get; private set; }
|
|
|
|
|
|
|
+ /// <remarks>
|
|
|
|
|
+ /// The value is not <see langword="null"/> when an exit status code has been returned
|
|
|
|
|
+ /// from the server. If the command terminated due to a signal, <see cref="ExitSignal"/>
|
|
|
|
|
+ /// may be not <see langword="null"/> instead.
|
|
|
|
|
+ /// </remarks>
|
|
|
|
|
+ /// <seealso cref="ExitSignal"/>
|
|
|
|
|
+ public int? ExitStatus
|
|
|
|
|
+ {
|
|
|
|
|
+ get
|
|
|
|
|
+ {
|
|
|
|
|
+ return _haveExitStatus ? _exitStatus : null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Gets the name of the signal due to which the command
|
|
|
|
|
+ /// terminated violently, if applicable, otherwise <see langword="null"/>.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <remarks>
|
|
|
|
|
+ /// The value (if it exists) is supplied by the server and is usually one of the
|
|
|
|
|
+ /// following, as described in https://datatracker.ietf.org/doc/html/rfc4254#section-6.10:
|
|
|
|
|
+ /// ABRT, ALRM, FPE, HUP, ILL, INT, KILL, PIPE, QUIT, SEGV, TER, USR1, USR2.
|
|
|
|
|
+ /// </remarks>
|
|
|
|
|
+ public string? ExitSignal { get; private set; }
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Gets the output stream.
|
|
/// Gets the output stream.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
-#pragma warning disable CA1859 // Use concrete types when possible for improved performance
|
|
|
|
|
public Stream OutputStream { get; private set; }
|
|
public Stream OutputStream { get; private set; }
|
|
|
-#pragma warning restore CA1859 // Use concrete types when possible for improved performance
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Gets the extended output stream.
|
|
/// Gets the extended output stream.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
-#pragma warning disable CA1859 // Use concrete types when possible for improved performance
|
|
|
|
|
public Stream ExtendedOutputStream { get; private set; }
|
|
public Stream ExtendedOutputStream { get; private set; }
|
|
|
-#pragma warning restore CA1859 // Use concrete types when possible for improved performance
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Creates and returns the input stream for the command.
|
|
/// Creates and returns the input stream for the command.
|
|
@@ -109,21 +136,19 @@ namespace Renci.SshNet
|
|
|
{
|
|
{
|
|
|
get
|
|
get
|
|
|
{
|
|
{
|
|
|
- if (_result is not null)
|
|
|
|
|
|
|
+ if (_stdOut is not null)
|
|
|
{
|
|
{
|
|
|
- return _result;
|
|
|
|
|
|
|
+ return _stdOut;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (OutputStream is null)
|
|
|
|
|
|
|
+ if (_asyncResult is null)
|
|
|
{
|
|
{
|
|
|
return string.Empty;
|
|
return string.Empty;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- using (var sr = new StreamReader(OutputStream,
|
|
|
|
|
- _encoding,
|
|
|
|
|
- detectEncodingFromByteOrderMarks: true))
|
|
|
|
|
|
|
+ using (var sr = new StreamReader(OutputStream, _encoding))
|
|
|
{
|
|
{
|
|
|
- return _result = sr.ReadToEnd();
|
|
|
|
|
|
|
+ return _stdOut = sr.ReadToEnd();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -135,21 +160,19 @@ namespace Renci.SshNet
|
|
|
{
|
|
{
|
|
|
get
|
|
get
|
|
|
{
|
|
{
|
|
|
- if (_error is not null)
|
|
|
|
|
|
|
+ if (_stdErr is not null)
|
|
|
{
|
|
{
|
|
|
- return _error;
|
|
|
|
|
|
|
+ return _stdErr;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (ExtendedOutputStream is null || !_hasError)
|
|
|
|
|
|
|
+ if (_asyncResult is null || !_hasError)
|
|
|
{
|
|
{
|
|
|
return string.Empty;
|
|
return string.Empty;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- using (var sr = new StreamReader(ExtendedOutputStream,
|
|
|
|
|
- _encoding,
|
|
|
|
|
- detectEncodingFromByteOrderMarks: true))
|
|
|
|
|
|
|
+ using (var sr = new StreamReader(ExtendedOutputStream, _encoding))
|
|
|
{
|
|
{
|
|
|
- return _error = sr.ReadToEnd();
|
|
|
|
|
|
|
+ return _stdErr = sr.ReadToEnd();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -182,8 +205,8 @@ namespace Renci.SshNet
|
|
|
CommandText = commandText;
|
|
CommandText = commandText;
|
|
|
_encoding = encoding;
|
|
_encoding = encoding;
|
|
|
CommandTimeout = Timeout.InfiniteTimeSpan;
|
|
CommandTimeout = Timeout.InfiniteTimeSpan;
|
|
|
- _sessionErrorOccuredWaitHandle = new AutoResetEvent(initialState: false);
|
|
|
|
|
- _commandCancelledWaitHandle = new AutoResetEvent(initialState: false);
|
|
|
|
|
|
|
+ OutputStream = new PipeStream();
|
|
|
|
|
+ ExtendedOutputStream = new PipeStream();
|
|
|
_session.Disconnected += Session_Disconnected;
|
|
_session.Disconnected += Session_Disconnected;
|
|
|
_session.ErrorOccured += Session_ErrorOccured;
|
|
_session.ErrorOccured += Session_ErrorOccured;
|
|
|
}
|
|
}
|
|
@@ -216,7 +239,7 @@ namespace Renci.SshNet
|
|
|
/// <exception cref="ArgumentException">CommandText property is empty.</exception>
|
|
/// <exception cref="ArgumentException">CommandText property is empty.</exception>
|
|
|
/// <exception cref="SshConnectionException">Client is not connected.</exception>
|
|
/// <exception cref="SshConnectionException">Client is not connected.</exception>
|
|
|
/// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
|
|
/// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
|
|
|
- public IAsyncResult BeginExecute(AsyncCallback callback)
|
|
|
|
|
|
|
+ public IAsyncResult BeginExecute(AsyncCallback? callback)
|
|
|
{
|
|
{
|
|
|
return BeginExecute(callback, state: null);
|
|
return BeginExecute(callback, state: null);
|
|
|
}
|
|
}
|
|
@@ -234,47 +257,55 @@ namespace Renci.SshNet
|
|
|
/// <exception cref="ArgumentException">CommandText property is empty.</exception>
|
|
/// <exception cref="ArgumentException">CommandText property is empty.</exception>
|
|
|
/// <exception cref="SshConnectionException">Client is not connected.</exception>
|
|
/// <exception cref="SshConnectionException">Client is not connected.</exception>
|
|
|
/// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
|
|
/// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
|
|
|
-#pragma warning disable CA1859 // Use concrete types when possible for improved performance
|
|
|
|
|
- public IAsyncResult BeginExecute(AsyncCallback callback, object state)
|
|
|
|
|
-#pragma warning restore CA1859 // Use concrete types when possible for improved performance
|
|
|
|
|
|
|
+ public IAsyncResult BeginExecute(AsyncCallback? callback, object? state)
|
|
|
{
|
|
{
|
|
|
- // Prevent from executing BeginExecute before calling EndExecute
|
|
|
|
|
- if (_asyncResult != null && !_asyncResult.EndCalled)
|
|
|
|
|
|
|
+#if NET7_0_OR_GREATER
|
|
|
|
|
+ ObjectDisposedException.ThrowIf(_isDisposed, this);
|
|
|
|
|
+#else
|
|
|
|
|
+ if (_isDisposed)
|
|
|
{
|
|
{
|
|
|
- throw new InvalidOperationException("Asynchronous operation is already in progress.");
|
|
|
|
|
|
|
+ throw new ObjectDisposedException(GetType().FullName);
|
|
|
|
|
+ }
|
|
|
|
|
+#endif
|
|
|
|
|
+
|
|
|
|
|
+ if (_asyncResult is not null)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!_asyncResult.AsyncWaitHandle.WaitOne(0))
|
|
|
|
|
+ {
|
|
|
|
|
+ throw new InvalidOperationException("Asynchronous operation is already in progress.");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ OutputStream.Dispose();
|
|
|
|
|
+ ExtendedOutputStream.Dispose();
|
|
|
|
|
+
|
|
|
|
|
+ // Initialize output streams. We already initialised them for the first
|
|
|
|
|
+ // execution in the constructor (to allow passing them around before execution)
|
|
|
|
|
+ // so we just need to reinitialise them for subsequent executions.
|
|
|
|
|
+ OutputStream = new PipeStream();
|
|
|
|
|
+ ExtendedOutputStream = new PipeStream();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Create new AsyncResult object
|
|
// Create new AsyncResult object
|
|
|
_asyncResult = new CommandAsyncResult
|
|
_asyncResult = new CommandAsyncResult
|
|
|
{
|
|
{
|
|
|
AsyncWaitHandle = new ManualResetEvent(initialState: false),
|
|
AsyncWaitHandle = new ManualResetEvent(initialState: false),
|
|
|
- IsCompleted = false,
|
|
|
|
|
AsyncState = state,
|
|
AsyncState = state,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- if (_channel is not null)
|
|
|
|
|
- {
|
|
|
|
|
- throw new SshException("Invalid operation.");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (string.IsNullOrEmpty(CommandText))
|
|
|
|
|
- {
|
|
|
|
|
- throw new ArgumentException("CommandText property is empty.");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- OutputStream?.Dispose();
|
|
|
|
|
- ExtendedOutputStream?.Dispose();
|
|
|
|
|
-
|
|
|
|
|
- // Initialize output streams
|
|
|
|
|
- OutputStream = new PipeStream();
|
|
|
|
|
- ExtendedOutputStream = new PipeStream();
|
|
|
|
|
-
|
|
|
|
|
|
|
+ _exitStatus = default;
|
|
|
|
|
+ _haveExitStatus = false;
|
|
|
|
|
+ ExitSignal = null;
|
|
|
_result = null;
|
|
_result = null;
|
|
|
- _error = null;
|
|
|
|
|
|
|
+ _stdOut = null;
|
|
|
|
|
+ _stdErr = null;
|
|
|
_hasError = false;
|
|
_hasError = false;
|
|
|
_callback = callback;
|
|
_callback = callback;
|
|
|
|
|
|
|
|
- _channel = CreateChannel();
|
|
|
|
|
|
|
+ _channel = _session.CreateChannelSession();
|
|
|
|
|
+ _channel.DataReceived += Channel_DataReceived;
|
|
|
|
|
+ _channel.ExtendedDataReceived += Channel_ExtendedDataReceived;
|
|
|
|
|
+ _channel.RequestReceived += Channel_RequestReceived;
|
|
|
|
|
+ _channel.Closed += Channel_Closed;
|
|
|
_channel.Open();
|
|
_channel.Open();
|
|
|
|
|
|
|
|
_ = _channel.SendExecRequest(CommandText);
|
|
_ = _channel.SendExecRequest(CommandText);
|
|
@@ -293,8 +324,13 @@ namespace Renci.SshNet
|
|
|
/// </returns>
|
|
/// </returns>
|
|
|
/// <exception cref="SshConnectionException">Client is not connected.</exception>
|
|
/// <exception cref="SshConnectionException">Client is not connected.</exception>
|
|
|
/// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
|
|
/// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
|
|
|
- public IAsyncResult BeginExecute(string commandText, AsyncCallback callback, object state)
|
|
|
|
|
|
|
+ public IAsyncResult BeginExecute(string commandText, AsyncCallback? callback, object? state)
|
|
|
{
|
|
{
|
|
|
|
|
+ if (commandText is null)
|
|
|
|
|
+ {
|
|
|
|
|
+ throw new ArgumentNullException(nameof(commandText));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
CommandText = commandText;
|
|
CommandText = commandText;
|
|
|
|
|
|
|
|
return BeginExecute(callback, state);
|
|
return BeginExecute(callback, state);
|
|
@@ -314,55 +350,88 @@ namespace Renci.SshNet
|
|
|
throw new ArgumentNullException(nameof(asyncResult));
|
|
throw new ArgumentNullException(nameof(asyncResult));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (asyncResult is not CommandAsyncResult commandAsyncResult || _asyncResult != commandAsyncResult)
|
|
|
|
|
|
|
+ if (_asyncResult != asyncResult)
|
|
|
{
|
|
{
|
|
|
- throw new ArgumentException(string.Format("The {0} object was not returned from the corresponding asynchronous method on this class.", nameof(IAsyncResult)));
|
|
|
|
|
|
|
+ throw new ArgumentException("Argument does not correspond to the currently executing command.", nameof(asyncResult));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- lock (_endExecuteLock)
|
|
|
|
|
- {
|
|
|
|
|
- if (commandAsyncResult.EndCalled)
|
|
|
|
|
- {
|
|
|
|
|
- throw new ArgumentException("EndExecute can only be called once for each asynchronous operation.");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- _inputStream?.Close();
|
|
|
|
|
|
|
+ _inputStream?.Dispose();
|
|
|
|
|
|
|
|
- try
|
|
|
|
|
- {
|
|
|
|
|
- // wait for operation to complete (or time out)
|
|
|
|
|
- WaitOnHandle(_asyncResult.AsyncWaitHandle);
|
|
|
|
|
- }
|
|
|
|
|
- finally
|
|
|
|
|
- {
|
|
|
|
|
- UnsubscribeFromEventsAndDisposeChannel(_channel);
|
|
|
|
|
- _channel = null;
|
|
|
|
|
|
|
+ if (!_asyncResult.AsyncWaitHandle.WaitOne(CommandTimeout))
|
|
|
|
|
+ {
|
|
|
|
|
+ // Complete the operation with a TimeoutException (which will be thrown below).
|
|
|
|
|
+ SetAsyncComplete(new SshOperationTimeoutException($"Command '{CommandText}' timed out. ({nameof(CommandTimeout)}: {CommandTimeout})."));
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- OutputStream?.Dispose();
|
|
|
|
|
- ExtendedOutputStream?.Dispose();
|
|
|
|
|
|
|
+ Debug.Assert(_asyncResult.IsCompleted);
|
|
|
|
|
|
|
|
- commandAsyncResult.EndCalled = true;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (_result is Exception exception)
|
|
|
|
|
+ {
|
|
|
|
|
+ ExceptionDispatchInfo.Capture(exception).Throw();
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (!_isCancelled)
|
|
|
|
|
- {
|
|
|
|
|
- return Result;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ Debug.Assert(_result == CompletedResult);
|
|
|
|
|
+ Debug.Assert(!OutputStream.CanWrite, $"{nameof(OutputStream)} should have been disposed (else we will block).");
|
|
|
|
|
|
|
|
- SetAsyncComplete();
|
|
|
|
|
- throw new OperationCanceledException();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return Result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
- /// Cancels command execution in asynchronous scenarios.
|
|
|
|
|
|
|
+ /// Cancels a running command by sending a signal to the remote process.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
/// <param name="forceKill">if true send SIGKILL instead of SIGTERM.</param>
|
|
/// <param name="forceKill">if true send SIGKILL instead of SIGTERM.</param>
|
|
|
- public void CancelAsync(bool forceKill = false)
|
|
|
|
|
|
|
+ /// <param name="millisecondsTimeout">Time to wait for the server to reply.</param>
|
|
|
|
|
+ /// <remarks>
|
|
|
|
|
+ /// <para>
|
|
|
|
|
+ /// This method stops the command running on the server by sending a SIGTERM
|
|
|
|
|
+ /// (or SIGKILL, depending on <paramref name="forceKill"/>) signal to the remote
|
|
|
|
|
+ /// process. When the server implements signals, it will send a response which
|
|
|
|
|
+ /// populates <see cref="ExitSignal"/> with the signal with which the command terminated.
|
|
|
|
|
+ /// </para>
|
|
|
|
|
+ /// <para>
|
|
|
|
|
+ /// When the server does not implement signals, it may send no response. As a fallback,
|
|
|
|
|
+ /// this method waits up to <paramref name="millisecondsTimeout"/> for a response
|
|
|
|
|
+ /// and then completes the <see cref="SshCommand"/> object anyway if there was none.
|
|
|
|
|
+ /// </para>
|
|
|
|
|
+ /// <para>
|
|
|
|
|
+ /// If the command has already finished (with or without cancellation), this method does
|
|
|
|
|
+ /// nothing.
|
|
|
|
|
+ /// </para>
|
|
|
|
|
+ /// </remarks>
|
|
|
|
|
+ /// <exception cref="InvalidOperationException">Command has not been started.</exception>
|
|
|
|
|
+ public void CancelAsync(bool forceKill = false, int millisecondsTimeout = 500)
|
|
|
{
|
|
{
|
|
|
- var signal = forceKill ? "KILL" : "TERM";
|
|
|
|
|
- _ = _channel?.SendExitSignalRequest(signal, coreDumped: false, "Command execution has been cancelled.", "en");
|
|
|
|
|
- _ = _commandCancelledWaitHandle?.Set();
|
|
|
|
|
|
|
+ if (_asyncResult is not { } asyncResult)
|
|
|
|
|
+ {
|
|
|
|
|
+ throw new InvalidOperationException("Command has not been started.");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var exception = new OperationCanceledException($"Command '{CommandText}' was cancelled.");
|
|
|
|
|
+
|
|
|
|
|
+ if (Interlocked.CompareExchange(ref _result, exception, comparand: null) is not null)
|
|
|
|
|
+ {
|
|
|
|
|
+ // Command has already completed.
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Try to send the cancellation signal.
|
|
|
|
|
+ if (_channel?.SendSignalRequest(forceKill ? "KILL" : "TERM") is null)
|
|
|
|
|
+ {
|
|
|
|
|
+ // Command has completed (in the meantime since the last check).
|
|
|
|
|
+ // We won the race above and the command has finished by some other means,
|
|
|
|
|
+ // but will throw the OperationCanceledException.
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Having sent the "signal" message, we expect to receive "exit-signal"
|
|
|
|
|
+ // and then a close message. But since a server may not implement signals,
|
|
|
|
|
+ // we can't guarantee that, so we wait a short time for that to happen and
|
|
|
|
|
+ // if it doesn't, just set the WaitHandle ourselves to unblock EndExecute.
|
|
|
|
|
+
|
|
|
|
|
+ if (!asyncResult.AsyncWaitHandle.WaitOne(millisecondsTimeout))
|
|
|
|
|
+ {
|
|
|
|
|
+ SetAsyncComplete(asyncResult);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
@@ -394,88 +463,73 @@ namespace Renci.SshNet
|
|
|
return Execute();
|
|
return Execute();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private IChannelSession CreateChannel()
|
|
|
|
|
|
|
+ private void Session_Disconnected(object? sender, EventArgs e)
|
|
|
{
|
|
{
|
|
|
- var channel = _session.CreateChannelSession();
|
|
|
|
|
- channel.DataReceived += Channel_DataReceived;
|
|
|
|
|
- channel.ExtendedDataReceived += Channel_ExtendedDataReceived;
|
|
|
|
|
- channel.RequestReceived += Channel_RequestReceived;
|
|
|
|
|
- channel.Closed += Channel_Closed;
|
|
|
|
|
- return channel;
|
|
|
|
|
|
|
+ SetAsyncComplete(new SshConnectionException("An established connection was aborted by the software in your host machine.", DisconnectReason.ConnectionLost));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private void Session_Disconnected(object sender, EventArgs e)
|
|
|
|
|
|
|
+ private void Session_ErrorOccured(object? sender, ExceptionEventArgs 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();
|
|
|
|
|
|
|
+ SetAsyncComplete(e.Exception);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
|
|
|
|
|
|
|
+ private void SetAsyncComplete(object result)
|
|
|
{
|
|
{
|
|
|
- // If objected is disposed or being disposed don't handle this event
|
|
|
|
|
- if (_isDisposed)
|
|
|
|
|
|
|
+ _ = Interlocked.CompareExchange(ref _result, result, comparand: null);
|
|
|
|
|
+
|
|
|
|
|
+ if (_asyncResult is CommandAsyncResult asyncResult)
|
|
|
{
|
|
{
|
|
|
- return;
|
|
|
|
|
|
|
+ SetAsyncComplete(asyncResult);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- _exception = e.Exception;
|
|
|
|
|
-
|
|
|
|
|
- _ = _sessionErrorOccuredWaitHandle.Set();
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private void SetAsyncComplete()
|
|
|
|
|
|
|
+ private void SetAsyncComplete(CommandAsyncResult asyncResult)
|
|
|
{
|
|
{
|
|
|
- OutputStream?.Dispose();
|
|
|
|
|
- ExtendedOutputStream?.Dispose();
|
|
|
|
|
|
|
+ UnsubscribeFromEventsAndDisposeChannel();
|
|
|
|
|
+
|
|
|
|
|
+ OutputStream.Dispose();
|
|
|
|
|
+ ExtendedOutputStream.Dispose();
|
|
|
|
|
|
|
|
- _asyncResult.IsCompleted = true;
|
|
|
|
|
|
|
+ asyncResult.IsCompleted = true;
|
|
|
|
|
|
|
|
- if (_callback is not null && !_isCancelled)
|
|
|
|
|
|
|
+ _ = ((EventWaitHandle)asyncResult.AsyncWaitHandle).Set();
|
|
|
|
|
+
|
|
|
|
|
+ if (Interlocked.Exchange(ref _callback, value: null) is AsyncCallback callback)
|
|
|
{
|
|
{
|
|
|
- // Execute callback on different thread
|
|
|
|
|
- ThreadAbstraction.ExecuteThread(() => _callback(_asyncResult));
|
|
|
|
|
|
|
+ ThreadAbstraction.ExecuteThread(() => callback(asyncResult));
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- _ = ((EventWaitHandle)_asyncResult.AsyncWaitHandle).Set();
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private void Channel_Closed(object sender, ChannelEventArgs e)
|
|
|
|
|
|
|
+ private void Channel_Closed(object? sender, ChannelEventArgs e)
|
|
|
{
|
|
{
|
|
|
- SetAsyncComplete();
|
|
|
|
|
|
|
+ SetAsyncComplete(CompletedResult);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private void Channel_RequestReceived(object sender, ChannelRequestEventArgs e)
|
|
|
|
|
|
|
+ private void Channel_RequestReceived(object? sender, ChannelRequestEventArgs e)
|
|
|
{
|
|
{
|
|
|
if (e.Info is ExitStatusRequestInfo exitStatusInfo)
|
|
if (e.Info is ExitStatusRequestInfo exitStatusInfo)
|
|
|
{
|
|
{
|
|
|
- ExitStatus = (int)exitStatusInfo.ExitStatus;
|
|
|
|
|
|
|
+ _exitStatus = (int)exitStatusInfo.ExitStatus;
|
|
|
|
|
+ _haveExitStatus = true;
|
|
|
|
|
|
|
|
- if (exitStatusInfo.WantReply)
|
|
|
|
|
- {
|
|
|
|
|
- var replyMessage = new ChannelSuccessMessage(_channel.RemoteChannelNumber);
|
|
|
|
|
- _session.SendMessage(replyMessage);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ Debug.Assert(!exitStatusInfo.WantReply, "exit-status is want_reply := false by definition.");
|
|
|
}
|
|
}
|
|
|
- else
|
|
|
|
|
|
|
+ else if (e.Info is ExitSignalRequestInfo exitSignalInfo)
|
|
|
{
|
|
{
|
|
|
- if (e.Info.WantReply)
|
|
|
|
|
- {
|
|
|
|
|
- var replyMessage = new ChannelFailureMessage(_channel.RemoteChannelNumber);
|
|
|
|
|
- _session.SendMessage(replyMessage);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ ExitSignal = exitSignalInfo.SignalName;
|
|
|
|
|
+
|
|
|
|
|
+ Debug.Assert(!exitSignalInfo.WantReply, "exit-signal is want_reply := false by definition.");
|
|
|
|
|
+ }
|
|
|
|
|
+ else if (e.Info.WantReply && _channel?.RemoteChannelNumber is uint remoteChannelNumber)
|
|
|
|
|
+ {
|
|
|
|
|
+ var replyMessage = new ChannelFailureMessage(remoteChannelNumber);
|
|
|
|
|
+ _session.SendMessage(replyMessage);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private void Channel_ExtendedDataReceived(object sender, ChannelExtendedDataEventArgs e)
|
|
|
|
|
|
|
+ private void Channel_ExtendedDataReceived(object? sender, ChannelExtendedDataEventArgs e)
|
|
|
{
|
|
{
|
|
|
- ExtendedOutputStream?.Write(e.Data, 0, e.Data.Length);
|
|
|
|
|
|
|
+ ExtendedOutputStream.Write(e.Data, 0, e.Data.Length);
|
|
|
|
|
|
|
|
if (e.DataTypeCode == 1)
|
|
if (e.DataTypeCode == 1)
|
|
|
{
|
|
{
|
|
@@ -483,64 +537,34 @@ namespace Renci.SshNet
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private void Channel_DataReceived(object sender, ChannelDataEventArgs e)
|
|
|
|
|
|
|
+ private void Channel_DataReceived(object? sender, ChannelDataEventArgs e)
|
|
|
{
|
|
{
|
|
|
- OutputStream?.Write(e.Data, 0, e.Data.Length);
|
|
|
|
|
|
|
+ OutputStream.Write(e.Data, 0, e.Data.Length);
|
|
|
|
|
|
|
|
- if (_asyncResult != null)
|
|
|
|
|
|
|
+ if (_asyncResult is CommandAsyncResult asyncResult)
|
|
|
{
|
|
{
|
|
|
- lock (_asyncResult)
|
|
|
|
|
|
|
+ lock (asyncResult)
|
|
|
{
|
|
{
|
|
|
- _asyncResult.BytesReceived += e.Data.Length;
|
|
|
|
|
|
|
+ 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,
|
|
|
|
|
- _commandCancelledWaitHandle
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- var signaledElement = WaitHandle.WaitAny(waitHandles, CommandTimeout);
|
|
|
|
|
- switch (signaledElement)
|
|
|
|
|
- {
|
|
|
|
|
- case 0:
|
|
|
|
|
- ExceptionDispatchInfo.Capture(_exception).Throw();
|
|
|
|
|
- break;
|
|
|
|
|
- case 1:
|
|
|
|
|
- // Specified waithandle was signaled
|
|
|
|
|
- break;
|
|
|
|
|
- case 2:
|
|
|
|
|
- _isCancelled = true;
|
|
|
|
|
- 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.");
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Unsubscribes the current <see cref="SshCommand"/> from channel events, and disposes
|
|
/// Unsubscribes the current <see cref="SshCommand"/> from channel events, and disposes
|
|
|
- /// the <see cref="IChannel"/>.
|
|
|
|
|
|
|
+ /// the <see cref="_channel"/>.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
- /// <param name="channel">The channel.</param>
|
|
|
|
|
- /// <remarks>
|
|
|
|
|
- /// Does nothing when <paramref name="channel"/> is <see langword="null"/>.
|
|
|
|
|
- /// </remarks>
|
|
|
|
|
- private void UnsubscribeFromEventsAndDisposeChannel(IChannel channel)
|
|
|
|
|
|
|
+ private void UnsubscribeFromEventsAndDisposeChannel()
|
|
|
{
|
|
{
|
|
|
|
|
+ var channel = _channel;
|
|
|
|
|
+
|
|
|
if (channel is null)
|
|
if (channel is null)
|
|
|
{
|
|
{
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ _channel = null;
|
|
|
|
|
+
|
|
|
// unsubscribe from events as we do not want to be signaled should these get fired
|
|
// unsubscribe from events as we do not want to be signaled should these get fired
|
|
|
// during the dispose of the channel
|
|
// during the dispose of the channel
|
|
|
channel.DataReceived -= Channel_DataReceived;
|
|
channel.DataReceived -= Channel_DataReceived;
|
|
@@ -576,66 +600,27 @@ namespace Renci.SshNet
|
|
|
{
|
|
{
|
|
|
// unsubscribe from session events to ensure other objects that we're going to dispose
|
|
// unsubscribe from session events to ensure other objects that we're going to dispose
|
|
|
// are not accessed while disposing
|
|
// are not accessed while disposing
|
|
|
- var session = _session;
|
|
|
|
|
- if (session != null)
|
|
|
|
|
- {
|
|
|
|
|
- session.Disconnected -= Session_Disconnected;
|
|
|
|
|
- session.ErrorOccured -= Session_ErrorOccured;
|
|
|
|
|
- _session = null;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ _session.Disconnected -= Session_Disconnected;
|
|
|
|
|
+ _session.ErrorOccured -= Session_ErrorOccured;
|
|
|
|
|
|
|
|
// unsubscribe from channel events to ensure other objects that we're going to dispose
|
|
// unsubscribe from channel events to ensure other objects that we're going to dispose
|
|
|
// are not accessed while disposing
|
|
// are not accessed while disposing
|
|
|
- var channel = _channel;
|
|
|
|
|
- if (channel != null)
|
|
|
|
|
- {
|
|
|
|
|
- UnsubscribeFromEventsAndDisposeChannel(channel);
|
|
|
|
|
- _channel = null;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ UnsubscribeFromEventsAndDisposeChannel();
|
|
|
|
|
|
|
|
- var inputStream = _inputStream;
|
|
|
|
|
- if (inputStream != null)
|
|
|
|
|
- {
|
|
|
|
|
- inputStream.Dispose();
|
|
|
|
|
- _inputStream = null;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ _inputStream?.Dispose();
|
|
|
|
|
+ _inputStream = null;
|
|
|
|
|
|
|
|
- var outputStream = OutputStream;
|
|
|
|
|
- if (outputStream != null)
|
|
|
|
|
- {
|
|
|
|
|
- outputStream.Dispose();
|
|
|
|
|
- OutputStream = null;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ OutputStream.Dispose();
|
|
|
|
|
+ ExtendedOutputStream.Dispose();
|
|
|
|
|
|
|
|
- var extendedOutputStream = ExtendedOutputStream;
|
|
|
|
|
- if (extendedOutputStream != null)
|
|
|
|
|
|
|
+ if (_asyncResult is not null && _result is null)
|
|
|
{
|
|
{
|
|
|
- extendedOutputStream.Dispose();
|
|
|
|
|
- ExtendedOutputStream = null;
|
|
|
|
|
|
|
+ // In case an operation is still running, try to complete it with an ObjectDisposedException.
|
|
|
|
|
+ SetAsyncComplete(new ObjectDisposedException(GetType().FullName));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- var sessionErrorOccuredWaitHandle = _sessionErrorOccuredWaitHandle;
|
|
|
|
|
- if (sessionErrorOccuredWaitHandle != null)
|
|
|
|
|
- {
|
|
|
|
|
- sessionErrorOccuredWaitHandle.Dispose();
|
|
|
|
|
- _sessionErrorOccuredWaitHandle = null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- _commandCancelledWaitHandle?.Dispose();
|
|
|
|
|
- _commandCancelledWaitHandle = null;
|
|
|
|
|
-
|
|
|
|
|
_isDisposed = true;
|
|
_isDisposed = true;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- /// <summary>
|
|
|
|
|
- /// Finalizes an instance of the <see cref="SshCommand"/> class.
|
|
|
|
|
- /// Releases unmanaged resources and performs other cleanup operations before the
|
|
|
|
|
- /// <see cref="SshCommand"/> is reclaimed by garbage collection.
|
|
|
|
|
- /// </summary>
|
|
|
|
|
- ~SshCommand()
|
|
|
|
|
- {
|
|
|
|
|
- Dispose(disposing: false);
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|