Add SshCommand.ExecuteAsync (#1426)

* Add SshCommand.ExecuteAsync

After the previous change (#1423), this basically entails swapping out the
IAsyncResult for a TaskCompletionSource and hooking up the cancellation/timeout
logic.

As with the prior Begin/End implementation, the initiation of the command
(SendExecRequest) happens synchronously, so there's a bit of room for improvement
there, but otherwise it is the Task-based async that we know and like.

I chose to make it void (Task)- returning instead of string like in the existing
overloads, so that OutputStream is not automatically consumed (and encoded as a
string) when that may not be desired. As in #650, I was initially considering
changing the other overloads to be void-returning as well, but decided that it was
not worth the break since most people will probably want to change over to
ExecuteAsync anyway.

* Update examples

---------

Co-authored-by: Wojciech Nagórski <wojtpl2@gmail.com>
This commit is contained in:
Rob Hague
2024-06-19 12:02:19 +01:00
committed by GitHub
parent 919af754df
commit 3bc568471e
9 changed files with 260 additions and 275 deletions
+4 -3
View File
@@ -113,16 +113,17 @@ using (var client = new SshClient("sftp.foo.com", "guest", "pwd"))
// Make the server echo back the input file with "cat"
using (SshCommand command = client.CreateCommand("cat"))
{
IAsyncResult asyncResult = command.BeginExecute();
Task executeTask = command.ExecuteAsync(CancellationToken.None);
using (Stream inputStream = command.CreateInputStream())
{
inputStream.Write("Hello World!"u8);
}
string result = command.EndExecute(asyncResult);
await executeTask;
Console.WriteLine(result); // "Hello World!"
Console.WriteLine(command.ExitStatus); // 0
Console.WriteLine(command.Result); // "Hello World!"
}
}
```
+8
View File
@@ -180,3 +180,11 @@ dotnet_diagnostic.CA5350.severity = none
# CA5351: Do Not Use Broken Cryptographic Algorithms
# https://learn.microsoft.com/en-ca/dotnet/fundamentals/code-analysis/quality-rules/ca5351
dotnet_diagnostic.CA5351.severity = none
# MA0040: Forward the CancellationToken parameter to methods that take one
# Partial/noisy duplicate of CA2016
dotnet_diagnostic.MA0040.severity = none
# MA0042: Do not use blocking calls in an async method
# duplicate of CA1849
dotnet_diagnostic.MA0042.severity = none
-60
View File
@@ -1,60 +0,0 @@
using System;
using System.Threading;
namespace Renci.SshNet
{
/// <summary>
/// Provides additional information for asynchronous command execution.
/// </summary>
public class CommandAsyncResult : IAsyncResult
{
/// <summary>
/// Initializes a new instance of the <see cref="CommandAsyncResult"/> class.
/// </summary>
internal CommandAsyncResult()
{
}
/// <summary>
/// Gets or sets the bytes received. If SFTP only file bytes are counted.
/// </summary>
/// <value>Total bytes received.</value>
public int BytesReceived { get; set; }
/// <summary>
/// Gets or sets the bytes sent by SFTP.
/// </summary>
/// <value>Total bytes sent.</value>
public int BytesSent { get; set; }
/// <summary>
/// Gets a user-defined object that qualifies or contains information about an asynchronous operation.
/// </summary>
/// <returns>A user-defined object that qualifies or contains information about an asynchronous operation.</returns>
public object AsyncState { get; internal set; }
/// <summary>
/// Gets a <see cref="WaitHandle"/> that is used to wait for an asynchronous operation to complete.
/// </summary>
/// <returns>
/// A <see cref="WaitHandle"/> that is used to wait for an asynchronous operation to complete.
/// </returns>
public WaitHandle AsyncWaitHandle { get; internal set; }
/// <summary>
/// Gets a value indicating whether the asynchronous operation completed synchronously.
/// </summary>
/// <returns>
/// true if the asynchronous operation completed synchronously; otherwise, false.
/// </returns>
public bool CompletedSynchronously { get; internal set; }
/// <summary>
/// Gets a value indicating whether the asynchronous operation has completed.
/// </summary>
/// <returns>
/// true if the operation is complete; otherwise, false.
/// </returns>
public bool IsCompleted { get; internal set; }
}
}
+194 -135
View File
@@ -2,11 +2,10 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Renci.SshNet.Abstractions;
using Renci.SshNet.Channels;
using Renci.SshNet.Common;
using Renci.SshNet.Messages.Connection;
@@ -19,20 +18,13 @@ namespace Renci.SshNet
/// </summary>
public class SshCommand : IDisposable
{
private static readonly object CompletedResult = new();
private readonly ISession _session;
private readonly Encoding _encoding;
/// <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 TaskCompletionSource<object>? _tcs;
private CancellationTokenSource? _cts;
private CancellationTokenRegistration _tokenRegistration;
private string? _stdOut;
private string? _stdErr;
private bool _hasError;
@@ -40,6 +32,17 @@ namespace Renci.SshNet
private ChannelInputStream? _inputStream;
private TimeSpan _commandTimeout;
/// <summary>
/// The token supplied as an argument to <see cref="ExecuteAsync(CancellationToken)"/>.
/// </summary>
private CancellationToken _userToken;
/// <summary>
/// Whether <see cref="CancelAsync(bool, int)"/> has been called
/// (either by a token or manually).
/// </summary>
private bool _cancellationRequested;
private int _exitStatus;
private volatile bool _haveExitStatus; // volatile to prevent re-ordering of reads/writes of _exitStatus.
@@ -113,6 +116,30 @@ namespace Renci.SshNet
/// <returns>
/// The stream that can be used to transfer data to the command's input stream.
/// </returns>
/// <remarks>
/// Callers should ensure that <see cref="Stream.Dispose()"/> is called on the
/// returned instance in order to notify the command that no more data will be sent.
/// Failure to do so may result in the command executing indefinitely.
/// </remarks>
/// <example>
/// This example shows how to stream some data to 'cat' and have the server echo it back.
/// <code>
/// using (SshCommand command = mySshClient.CreateCommand("cat"))
/// {
/// Task executeTask = command.ExecuteAsync(CancellationToken.None);
///
/// using (Stream inputStream = command.CreateInputStream())
/// {
/// inputStream.Write("Hello World!"u8);
/// }
///
/// await executeTask;
///
/// Console.WriteLine(command.ExitStatus); // 0
/// Console.WriteLine(command.Result); // "Hello World!"
/// }
/// </code>
/// </example>
public Stream CreateInputStream()
{
if (_channel == null)
@@ -130,7 +157,7 @@ namespace Renci.SshNet
}
/// <summary>
/// Gets the command execution result.
/// Gets the standard output of the command by reading <see cref="OutputStream"/>.
/// </summary>
public string Result
{
@@ -141,7 +168,7 @@ namespace Renci.SshNet
return _stdOut;
}
if (_asyncResult is null)
if (_tcs is null)
{
return string.Empty;
}
@@ -154,7 +181,8 @@ namespace Renci.SshNet
}
/// <summary>
/// Gets the command execution error.
/// Gets the standard error of the command by reading <see cref="ExtendedOutputStream"/>,
/// when extended data has been sent which has been marked as stderr.
/// </summary>
public string Error
{
@@ -165,7 +193,7 @@ namespace Renci.SshNet
return _stdErr;
}
if (_asyncResult is null || !_hasError)
if (_tcs is null || !_hasError)
{
return string.Empty;
}
@@ -211,6 +239,92 @@ namespace Renci.SshNet
_session.ErrorOccured += Session_ErrorOccured;
}
/// <summary>
/// Executes the command asynchronously.
/// </summary>
/// <param name="cancellationToken">
/// The <see cref="CancellationToken"/>. When triggered, attempts to terminate the
/// remote command by sending a signal.
/// </param>
/// <returns>A <see cref="Task"/> representing the lifetime of the command.</returns>
/// <exception cref="InvalidOperationException">Command is already executing. Thrown synchronously.</exception>
/// <exception cref="ObjectDisposedException">Instance has been disposed. Thrown synchronously.</exception>
/// <exception cref="OperationCanceledException">The <see cref="Task"/> has been cancelled.</exception>
/// <exception cref="SshOperationTimeoutException">The command timed out according to <see cref="CommandTimeout"/>.</exception>
#pragma warning disable CA1849 // Call async methods when in an async method; PipeStream.DisposeAsync would complete synchronously anyway.
public Task ExecuteAsync(CancellationToken cancellationToken = default)
{
#if NET7_0_OR_GREATER
ObjectDisposedException.ThrowIf(_isDisposed, this);
#else
if (_isDisposed)
{
throw new ObjectDisposedException(GetType().FullName);
}
#endif
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
if (_tcs is not null)
{
if (!_tcs.Task.IsCompleted)
{
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();
}
_exitStatus = default;
_haveExitStatus = false;
ExitSignal = null;
_stdOut = null;
_stdErr = null;
_hasError = false;
_tokenRegistration.Dispose();
_tokenRegistration = default;
_cts?.Dispose();
_cts = null;
_cancellationRequested = false;
_tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
_userToken = cancellationToken;
_channel = _session.CreateChannelSession();
_channel.DataReceived += Channel_DataReceived;
_channel.ExtendedDataReceived += Channel_ExtendedDataReceived;
_channel.RequestReceived += Channel_RequestReceived;
_channel.Closed += Channel_Closed;
_channel.Open();
_ = _channel.SendExecRequest(CommandText);
if (CommandTimeout != Timeout.InfiniteTimeSpan)
{
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_cts.CancelAfter(CommandTimeout);
cancellationToken = _cts.Token;
}
if (cancellationToken.CanBeCanceled)
{
_tokenRegistration = cancellationToken.Register(static cmd => ((SshCommand)cmd!).CancelAsync(), this);
}
return _tcs.Task;
}
#pragma warning restore CA1849
/// <summary>
/// Begins an asynchronous command execution.
/// </summary>
@@ -259,58 +373,7 @@ namespace Renci.SshNet
/// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
public IAsyncResult BeginExecute(AsyncCallback? callback, object? state)
{
#if NET7_0_OR_GREATER
ObjectDisposedException.ThrowIf(_isDisposed, this);
#else
if (_isDisposed)
{
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
_asyncResult = new CommandAsyncResult
{
AsyncWaitHandle = new ManualResetEvent(initialState: false),
AsyncState = state,
};
_exitStatus = default;
_haveExitStatus = false;
ExitSignal = null;
_result = null;
_stdOut = null;
_stdErr = null;
_hasError = false;
_callback = callback;
_channel = _session.CreateChannelSession();
_channel.DataReceived += Channel_DataReceived;
_channel.ExtendedDataReceived += Channel_ExtendedDataReceived;
_channel.RequestReceived += Channel_RequestReceived;
_channel.Closed += Channel_Closed;
_channel.Open();
_ = _channel.SendExecRequest(CommandText);
return _asyncResult;
return TaskToAsyncResult.Begin(ExecuteAsync(), callback, state);
}
/// <summary>
@@ -340,38 +403,19 @@ namespace Renci.SshNet
/// 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>
/// <returns><see cref="Result"/>.</returns>
/// <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 <see langword="null"/>.</exception>
public string EndExecute(IAsyncResult asyncResult)
{
if (asyncResult is null)
{
throw new ArgumentNullException(nameof(asyncResult));
}
var executeTask = TaskToAsyncResult.Unwrap(asyncResult);
if (_asyncResult != asyncResult)
if (executeTask != _tcs?.Task)
{
throw new ArgumentException("Argument does not correspond to the currently executing command.", nameof(asyncResult));
}
_inputStream?.Dispose();
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})."));
}
Debug.Assert(_asyncResult.IsCompleted);
if (_result is Exception exception)
{
ExceptionDispatchInfo.Capture(exception).Throw();
}
Debug.Assert(_result == CompletedResult);
Debug.Assert(!OutputStream.CanWrite, $"{nameof(OutputStream)} should have been disposed (else we will block).");
executeTask.GetAwaiter().GetResult();
return Result;
}
@@ -401,50 +445,59 @@ namespace Renci.SshNet
/// <exception cref="InvalidOperationException">Command has not been started.</exception>
public void CancelAsync(bool forceKill = false, int millisecondsTimeout = 500)
{
if (_asyncResult is not { } asyncResult)
if (_tcs is null)
{
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)
if (_tcs.Task.IsCompleted)
{
// Command has already completed.
return;
}
_cancellationRequested = true;
Interlocked.MemoryBarrier(); // ensure fresh read in SetAsyncComplete (possibly unnecessary)
// 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 it doesn't, just complete the task ourselves to unblock waiters.
if (!asyncResult.AsyncWaitHandle.WaitOne(millisecondsTimeout))
try
{
SetAsyncComplete(asyncResult);
if (_tcs.Task.Wait(millisecondsTimeout))
{
return;
}
}
catch (AggregateException)
{
// We expect to be here if the server implements signals.
// But we don't want to propagate the exception on the task from here.
return;
}
SetAsyncComplete();
}
/// <summary>
/// Executes command specified by <see cref="CommandText"/> property.
/// </summary>
/// <returns>
/// Command execution result.
/// </returns>
/// <returns><see cref="Result"/>.</returns>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
/// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
public string Execute()
{
return EndExecute(BeginExecute(callback: null, state: null));
ExecuteAsync().GetAwaiter().GetResult();
return Result;
}
/// <summary>
@@ -465,44 +518,53 @@ namespace Renci.SshNet
private void Session_Disconnected(object? sender, EventArgs e)
{
SetAsyncComplete(new SshConnectionException("An established connection was aborted by the software in your host machine.", DisconnectReason.ConnectionLost));
_ = _tcs?.TrySetException(new SshConnectionException("An established connection was aborted by the software in your host machine.", DisconnectReason.ConnectionLost));
SetAsyncComplete(setResult: false);
}
private void Session_ErrorOccured(object? sender, ExceptionEventArgs e)
{
SetAsyncComplete(e.Exception);
_ = _tcs?.TrySetException(e.Exception);
SetAsyncComplete(setResult: false);
}
private void SetAsyncComplete(object result)
private void SetAsyncComplete(bool setResult = true)
{
_ = Interlocked.CompareExchange(ref _result, result, comparand: null);
Interlocked.MemoryBarrier(); // ensure fresh read of _cancellationRequested (possibly unnecessary)
if (_asyncResult is CommandAsyncResult asyncResult)
if (setResult)
{
SetAsyncComplete(asyncResult);
}
}
Debug.Assert(_tcs is not null, "Should only be completing the task if we've started one.");
if (_userToken.IsCancellationRequested)
{
_ = _tcs.TrySetCanceled(_userToken);
}
else if (_cts?.Token.IsCancellationRequested == true)
{
_ = _tcs.TrySetException(new SshOperationTimeoutException($"Command '{CommandText}' timed out. ({nameof(CommandTimeout)}: {CommandTimeout})."));
}
else if (_cancellationRequested)
{
_ = _tcs.TrySetCanceled();
}
else
{
_ = _tcs.TrySetResult(null!);
}
}
private void SetAsyncComplete(CommandAsyncResult asyncResult)
{
UnsubscribeFromEventsAndDisposeChannel();
OutputStream.Dispose();
ExtendedOutputStream.Dispose();
asyncResult.IsCompleted = true;
_ = ((EventWaitHandle)asyncResult.AsyncWaitHandle).Set();
if (Interlocked.Exchange(ref _callback, value: null) is AsyncCallback callback)
{
ThreadAbstraction.ExecuteThread(() => callback(asyncResult));
}
}
private void Channel_Closed(object? sender, ChannelEventArgs e)
{
SetAsyncComplete(CompletedResult);
SetAsyncComplete();
}
private void Channel_RequestReceived(object? sender, ChannelRequestEventArgs e)
@@ -540,14 +602,6 @@ namespace Renci.SshNet
private void Channel_DataReceived(object? sender, ChannelDataEventArgs e)
{
OutputStream.Write(e.Data, 0, e.Data.Length);
if (_asyncResult is CommandAsyncResult asyncResult)
{
lock (asyncResult)
{
asyncResult.BytesReceived += e.Data.Length;
}
}
}
/// <summary>
@@ -613,10 +667,15 @@ namespace Renci.SshNet
OutputStream.Dispose();
ExtendedOutputStream.Dispose();
if (_asyncResult is not null && _result is null)
_tokenRegistration.Dispose();
_tokenRegistration = default;
_cts?.Dispose();
_cts = null;
if (_tcs is { Task.IsCompleted: false } tcs)
{
// In case an operation is still running, try to complete it with an ObjectDisposedException.
SetAsyncComplete(new ObjectDisposedException(GetType().FullName));
_ = tcs.TrySetException(new ObjectDisposedException(GetType().FullName));
}
_isDisposed = true;
@@ -362,3 +362,6 @@ dotnet_diagnostic.IDE0047.severity = silent
# IDE0032: Use auto-implemented property
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0032
dotnet_diagnostic.IDE0032.severity = silent
# CA1849: Call async methods when in an async method
dotnet_diagnostic.CA1849.severity = silent
@@ -63,7 +63,8 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
cmd.CancelAsync();
Assert.ThrowsException<OperationCanceledException>(() => cmd.EndExecute(asyncResult));
var tce = Assert.ThrowsException<TaskCanceledException>(() => cmd.EndExecute(asyncResult));
Assert.AreEqual(CancellationToken.None, tce.CancellationToken);
Assert.IsTrue(asyncResult.IsCompleted);
Assert.IsTrue(asyncResult.AsyncWaitHandle.WaitOne(0));
Assert.AreEqual(string.Empty, cmd.Result);
@@ -86,7 +87,8 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
cmd.CancelAsync(forceKill: true);
await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => executeTask);
var tce = await Assert.ThrowsExceptionAsync<TaskCanceledException>(() => executeTask);
Assert.AreEqual(CancellationToken.None, tce.CancellationToken);
Assert.IsTrue(asyncResult.IsCompleted);
Assert.IsTrue(asyncResult.AsyncWaitHandle.WaitOne(0));
Assert.AreEqual(string.Empty, cmd.Result);
@@ -116,6 +118,28 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
Assert.IsNull(cmd.ExitSignal);
}
[TestMethod]
[Timeout(5000)]
public async Task Test_ExecuteAsync_CancellationToken()
{
using var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password);
client.Connect();
var testValue = Guid.NewGuid().ToString();
using var cmd = client.CreateCommand($"sleep 15s; echo {testValue}");
using CancellationTokenSource cts = new();
Task executeTask = cmd.ExecuteAsync(cts.Token);
cts.Cancel();
var tce = await Assert.ThrowsExceptionAsync<TaskCanceledException>(() => executeTask);
Assert.AreSame(executeTask, tce.Task);
Assert.AreEqual(cts.Token, tce.CancellationToken);
Assert.AreEqual(string.Empty, cmd.Result);
Assert.AreEqual("TERM", cmd.ExitSignal);
Assert.IsNull(cmd.ExitStatus);
}
[TestMethod]
public void Test_Execute_ExtendedOutputStream()
{
@@ -141,13 +165,29 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
{
using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
#region Example SshCommand CreateCommand Execute CommandTimeout
client.Connect();
using var cmd = client.CreateCommand("sleep 10s");
cmd.CommandTimeout = TimeSpan.FromSeconds(2);
Assert.ThrowsException<SshOperationTimeoutException>(cmd.Execute);
client.Disconnect();
#endregion
}
}
[TestMethod]
public async Task Test_ExecuteAsync_Timeout()
{
using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
client.Connect();
using var cmd = client.CreateCommand("sleep 10s");
cmd.CommandTimeout = TimeSpan.FromSeconds(2);
Task executeTask = cmd.ExecuteAsync();
Assert.IsTrue(((IAsyncResult)executeTask).AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(3)));
await Assert.ThrowsExceptionAsync<SshOperationTimeoutException>(() => executeTask);
client.Disconnect();
}
}
@@ -91,29 +91,6 @@ namespace Renci.SshNet.IntegrationTests
}
}
[TestMethod]
public void Send_InputStream_to_Command_InputStreamNotInUsingBlock_StillWorks()
{
var inputByteArray = Encoding.UTF8.GetBytes("Hello world!");
// Make the server echo back the input file with "cat"
using (var command = _sshClient.CreateCommand("cat"))
{
var asyncResult = command.BeginExecute();
var inputStream = command.CreateInputStream();
for (var i = 0; i < inputByteArray.Length; i++)
{
inputStream.WriteByte(inputByteArray[i]);
}
command.EndExecute(asyncResult);
Assert.AreEqual("Hello world!", command.Result);
Assert.AreEqual(string.Empty, command.Error);
}
}
[TestMethod]
public void CreateInputStream_BeforeBeginExecute_ThrowsInvalidOperationException()
{
+7 -16
View File
@@ -284,7 +284,7 @@ namespace Renci.SshNet.IntegrationTests
}
[TestMethod]
public void Ssh_Command_IntermittentOutput_OutputStream()
public async Task Ssh_Command_IntermittentOutput_OutputStream()
{
const string remoteFile = "/home/sshnet/test.sh";
@@ -325,9 +325,11 @@ namespace Renci.SshNet.IntegrationTests
using (var command = sshClient.CreateCommand(remoteFile))
{
var asyncResult = command.BeginExecute();
await command.ExecuteAsync();
using (var reader = new StreamReader(command.OutputStream, new UTF8Encoding(false), false, 10))
Assert.AreEqual(13, command.ExitStatus);
using (var reader = new StreamReader(command.OutputStream))
{
var lines = new List<string>();
string line = null;
@@ -338,21 +340,10 @@ namespace Renci.SshNet.IntegrationTests
Assert.AreEqual(6, lines.Count, string.Join("\n", lines));
Assert.AreEqual(expectedResult, string.Join("\n", lines));
Assert.AreEqual(13, command.ExitStatus);
}
var actualResult = command.EndExecute(asyncResult);
// command.Result (also returned from EndExecute) consumes OutputStream,
// which we've already read from, so Result will be empty.
// TODO consider the suggested changes in https://github.com/sshnet/SSH.NET/issues/650
//Assert.AreEqual(expectedResult, actualResult);
//Assert.AreEqual(expectedResult, command.Result);
// For now just assert the current behaviour.
Assert.AreEqual(0, actualResult.Length);
Assert.AreEqual(0, command.Result.Length);
// We have already consumed OutputStream ourselves, so we expect Result to be empty.
Assert.AreEqual("", command.Result);
}
}
finally
@@ -1,34 +0,0 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Renci.SshNet.Tests.Common;
namespace Renci.SshNet.Tests.Classes
{
[TestClass()]
public class CommandAsyncResultTest : TestBase
{
[TestMethod()]
public void BytesSentTest()
{
var target = new CommandAsyncResult();
var expected = new Random().Next();
target.BytesSent = expected;
Assert.AreEqual(expected, target.BytesSent);
}
[TestMethod()]
public void BytesReceivedTest()
{
var target = new CommandAsyncResult();
var expected = new Random().Next();
target.BytesReceived = expected;
Assert.AreEqual(expected, target.BytesReceived);
}
}
}