-
-
Notifications
You must be signed in to change notification settings - Fork 906
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/executeasync #937
base: develop
Are you sure you want to change the base?
Feature/executeasync #937
Conversation
src/Renci.SshNet/SshCommand.cs
Outdated
/// that is used to schedule the task that executes the end method.</param> | ||
/// <returns>Returns <see cref="Task{Int32}"/> as command execution status result.</returns> | ||
public Task<int> ExecuteAsync( | ||
string commandText, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should have an overload that provides the commandText. commandText is already provided in constructor and allowing it to be changed here again adds to confusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can certainly remove that, but I was just mirroring the regular Execute() API functions and there is an overload for that already?
SSH.NET/src/Renci.SshNet/SshCommand.cs
Line 357 in bc99ada
public string Execute(string commandText) |
src/Renci.SshNet/SshCommand.cs
Outdated
/// <returns>Returns <see cref="Task{Int32}"/> as command execution status result.</returns> | ||
public Task<int> ExecuteAsync( | ||
CancellationToken cancellationToken = default(CancellationToken), | ||
TaskFactory<int> factory = null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Too much flexibility here, can we just provide a simple '''Task ExecuteAsync(CancellationToken cancellationToken)```? I would also specifically not provide a default for cancellationToken, making the caller aware that this task might never complete and they should provide some means of timeout.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, those were options in the TAP wrapping code I found, but can certainly get removed.
src/Renci.SshNet/SshCommand.cs
Outdated
TaskCreationOptions creationOptions = default(TaskCreationOptions), | ||
TaskScheduler scheduler = null) | ||
{ | ||
return AsyncExtensions.FromAsync( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really don't like using these extensions... we can use builtin Task<int>.Factory.FromAsync()
. The only "advantage" these extensions have is that on the surface they seem to support cancellation, but this is not actually true, the channel will not be closed in this case. I'll try to provide an example that closes the channel.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is how I propose to implement it:
public async Task<int> ExecuteAsync(CancellationToken cancellationToken)
{
var ctr = cancellationToken.Register((c) => ((IChannelSession)c).Dispose(), _channel, false);
try
{
return await Task<int>.Factory.FromAsync(BeginExecute(), EndExecuteWithStatus).ConfigureAwait(false);
}
catch (Exception) when (cancellationToken.IsCancellationRequested) // ToDo: Define a more specific exception
{
throw new OperationCanceledException("Command execution has been cancelled.", cancellationToken);
}
finally
{
ctr.Dispose();
}
}
No extensions required and it should handle cancellation correctly, Please note that this has not been tested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the point of the extension method is that TaskFactory.FromAsync() by default does not support cancellation tokens at all, so if you use one say for a timeout, it won't do anything. The code I am using does support that. I tried to implement closing of the session by setting the wait handle, but we could register the channel to be closed when it gets canceled also. I can play around with this some more. But your code as it stands I don't think will do anything useful because the cancellation token is not actually used anywhere?
I know I tested timeout with the code as is and it works (I wrote a unit test for it), but it's possible the channel is not getting closed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok yeah, the above code did not work. I have re-written it using a modified version of the extension which works correctly. To make it work you still need to set up a second task for the cancelation operation so you can determine if the task got cancelled, but this new version uses a callback to cancel the operation (closes the channel) and it works. Since we are actually closing the channel and terminating the original operation. I have verified the channel is being properly closed with this iteration.
I did leave the code as an extension as it would be useful to do use the same for other operations? But if you want I can simply remove the extension and implement the same code in the function itself? Either way the approach taken to set up a separate cancellation task so you know if the cancellation is needed is what works. Otherwise the original task just blocks until it's fully complete and the cancellation token does nothing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you are missing the point about cancellation: cancellation in dotnet is cooperative. If the underlying code doesn't observe it, the task will not get canceled. This is what I was proposing to cancel the channel in case of cancellation. There might be better options in SSH protocol, but this could do for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, but cancelling the channel itself did not seem to early out of the await, which I think is the primary reason for the code I found and used. It creates a separate task that will bail when the cancellation token fires, and you can then dispose of the channel and return from the await. When I tried just closing the channel when the cancelation token fired, the await did not return until the /bin/sleep 5 completed. The way it is written now it works. The awaited task returns as soon as the cancelation token fires, and the channel also gets properly closed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but it is very "cludgy". If disposing the channel doesn't terminate the wait in EndExecute we have a problem that we need to solve first. I wouldn't want to layer patch-fixes on top to hide this problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough. I don't know enough about async programming to debug why that is not happening unfortunately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok I debugged it again. Part of the problem when I was debugging it last time was that the registration on the cancelation token was done before the command was started, so since I have such a short timeout in my test (100ms) by the time I debug that code it runs immediately, but there is no channel to close. So I changed it slightly to this:
var asyncResult = BeginExecute();
var ctr = cancellationToken.Register(_ => _channel?.Dispose(), false);
try
{
return await Task<int>.Factory.FromAsync(asyncResult, EndExecuteWithStatus).ConfigureAwait(false);
}
catch (Exception) when (cancellationToken.IsCancellationRequested) // ToDo: Define a more specific exception
{
throw new OperationCanceledException("Command execution has been cancelled.", cancellationToken);
}
finally
{
ctr.Dispose();
}
Now when it fires it has a channel to dispose and I can see that it is terminating the command and the await does actually finish and return as soon as the cancellation occurs, so that part was incorrect on my part. But the real problem is there is no cancellation exception happening so the await just returns and we never get into the catch block. In the test, it comes out and I get a blank string for the result, so it fails the test as I never got a cancellation exception.
With the way I have it structured in this PR, the channel is dispose and closed, the original await does finish but because we also run waited on another task to capture just the cancellation we now get the correct exception so the caller knows the command was cancelled.
I am not sure how else you would get the original await to throw the correct exception without doing that, since TaskFactory.FromAsync does not natively support a cancellation token.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other way this could potentially work is if there was a way to flag the running task that it was cancelled, and then you wake it up by setting the wait handle it's waiting on. It sees that it has been cancelled and then throws the appropriate exception that we want to indicate it was cancelled?
9e88692
to
df827b5
Compare
The unit test I have in there for the cancellation assumes a Unix on the other end so it can call /bin/sleep to cause a long running command so we can test cancellation with a shorter timeout. I am not sure how to make that work otherwise, but it does test the functionality. So it could be left or perhaps set as ignored or something depending on how you normally run the tests? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please clean up the PR:
- Remove .idea/* files
- Remove AsyncExtensions.cs as it is not needed any more
src/Renci.SshNet/SshClient.cs
Outdated
/// <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand RunCommand Result" language="C#" title="Running simple command" /> | ||
/// <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand RunCommand Parallel" language="C#" title="Run many commands in parallel" /> | ||
/// <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand RunCommand Result" language="C#" title="Running simple command async" /> | ||
/// <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand RunCommand Parallel" language="C#" title="Run many commands in parallel async" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the sync version, why are you changing the title of examples to "async"?
60ab385
to
bda1b8b
Compare
Flattened all the code and force pushed it up now so it's clean. |
using (var client = new SshClient(host, username, password)) | ||
{ | ||
#region Example SshCommand RunCommand Result Async | ||
await client.ConnectAsync(default); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please provide a cancellationToken, something like
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok done.
src/Renci.SshNet/SshClient.cs
Outdated
/// <exception cref="InvalidOperationException">Asynchronous operation is already in progress.</exception> | ||
/// <exception cref="SshConnectionException">Client is not connected.</exception> | ||
/// <exception cref="ArgumentNullException"><paramref name="commandText"/> is <c>null</c>.</exception> | ||
public async Task<SshCommand> RunCommandAsync(string commandText, CancellationToken cancellationToken = default(CancellationToken)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I propose to not provide a default cancellationToken. I real-life networking nothing really works all the time, and users are not aware that by not providing the CancellationToken, they are willing to wait forever. And waiting forever is almost never what they really wanted. And if somebody really wants to wait forever, he/she can still explicitly specify the default...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. I already removed it on sshCommand.Execute but forgot for this one.
All good points. I will get it adjusted this weekend. |
bda1b8b
to
1e0571e
Compare
Ok cleaned up and force pushed. The one issue is that now that Result is obsolete, I had to disable the complaint about using it in EndExecute as that is still written to return the result string. It gets it compiling but not sure it's the best approach? |
…shCommand.Execute.
1e0571e
to
a06a310
Compare
@@ -315,7 +320,9 @@ public string EndExecute(IAsyncResult asyncResult) | |||
|
|||
commandAsyncResult.EndCalled = true; | |||
|
|||
#pragma warning disable CS0618 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how to best handle this, but since this is obsolete, we have to stop the compiler from giving a warning here as the project will not compile otherwise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could have private GetResult() that would be called from here and from Result.
#region Example SshClient(host, username) Connect | ||
using (var client = new SshClient(host, username, password)) | ||
{ | ||
var cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When CancellationTokenSource is used with timeout, it should be disposed, otherwise Timers will pile up. SOmething like:
using (var cts = new CancellationTokenSource(timeout))
{
await SomethingAsync(cts.Token);
}
|
||
#endregion | ||
|
||
Assert.Inconclusive(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there something to discuss here? Console.WriteLine and Inconclusive...
/// <exception cref="InvalidOperationException">Asynchronous operation is already in progress.</exception> | ||
/// <exception cref="SshConnectionException">Client is not connected.</exception> | ||
/// <exception cref="ArgumentNullException"><paramref name="commandText"/> is <c>null</c>.</exception> | ||
public async Task<SshCommand> RunCommandAsync(string commandText, CancellationToken cancellationToken) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The more I think about it I don't feel we should have such simple helpers... yes, this is aligned with the sync version, but... something to discuss with Gert, I think.
@@ -315,7 +320,9 @@ public string EndExecute(IAsyncResult asyncResult) | |||
|
|||
commandAsyncResult.EndCalled = true; | |||
|
|||
#pragma warning disable CS0618 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could have private GetResult() that would be called from here and from Result.
No description provided.