Question

Using Async, Task, CancellationToken, etc. all assume that the Async function/sub you are calling has a loop of things to do, like loading pictures, or getting web content from a list of URLs, and this is fine, as you are interrupting the LIST.

But what they all are missing is a way to cancel (Async/Await/Task, or otherwise) a single call (line of code)... My use case is this:

  1. Go execute a command on an SSH server, wait for the response.

Simple. But if the call to the server fails, or the server takes too long, or whatever, my Console application just sits there. I want it run in Task Scheduler off the OS, and I can create the ErrorCodes/ErrorLevels, but I am hanging on a single line/call, not interrupting a long list in a loop of things to do in the Async Sub/Function.

I'm cool with using a Timer(), but how to I do this in a way that I don't completely cancel the called Sub/Function? Ideally, if I could throw an Exception in the Try() _FROM_ the Timer, I could theoretically accomplish a more useful utility that simply interrupting a loop (I could simply do a while... to check for cancel if that were the case...)

Am I explaining this in a way that is understandable?

TIA for any of your thoughts.

Was it helpful?

Solution

You can do this via Task.WhenAny with Await:

Dim theTask = ExecuteSshCommandAsync() ' Start your operation
Dim timeout = Task.Delay(5000) ' Some timeout

Dim first = Await Task.WhenAny(timeout, theTask)

If (first = theTask) Then
    Dim result = theTask.Result
    ' Use results
Else
    ' You timed out here...
End If

Note that this doesn't cancel or timeout the actual request - it allows you to timeout and cancel how long you'll wait for the request. The only way to cancel a request in flight is if the request actually provides a mechanism for timeouts or cancelation. That being said, this is often "good enough" and gives the proper behavior.

OTHER TIPS

Regarding your specific SSH use case, it still may be possible to cancel a pending SSH request naturally, depending on what method your're using to issue SSH command. For example, SSH.NET library provides SshCommand.BeginExecute/EndExecute/CancelAsync methods, known as Asynchronous Programming Model (APM) pattern. An APM like this can easily be wrapped as an await-able and cancel-able Task, using TaskCompletionSource and CancellationTokenSource.

For example, executing an asynchronous SSH command with SSH.NET may look like this:

Imports System.Threading
Imports System.Threading.Tasks
Imports Renci.SshNet

Module Module1
    Async Function ExecSshCommandAsync(command As Renci.SshNet.SshCommand, ct As CancellationToken) As Task(Of String)
        Dim tcs As TaskCompletionSource(Of String) = New TaskCompletionSource(Of String)

        Dim doCancel As Action = Sub()
                                     command.CancelAsync()
                                     tcs.TrySetCanceled()
                                 End Sub

        Using ct.Register(doCancel)
            Dim asyncCallback As System.AsyncCallback = Sub(iar As IAsyncResult) tcs.TrySetResult(command.EndExecute(iar))
            command.BeginExecute(asyncCallback)
            Await tcs.Task
        End Using

        Return tcs.Task.Result

    End Function


    Sub Main()
        Dim command As Renci.SshNet.SshCommand
        ' Initialze the SSH session etc

        ' signal cancellation in 10 sec
        Dim cts As CancellationTokenSource = New CancellationTokenSource(10000)

        ' Blocking wait for result with 10 sec timeout
        Dim Result = ExecSshCommandAsync(command, cts.Token).Result
        Console.WriteLine(Result) 

        ' If Main was async too, we could await ExecSshCommandAsync here:  
        ' Dim result = Await ExecSshCommandAsync(command, cts.Token) 
    End Sub

End Module

If you're executing SSH commands with a separate console process (like Putty), you still may use almost the same technique. It would asynchronously read and parse the child process's console output, and register a cancellation routine which would kill the process with Process.Kill (or do something more nice like GenerateConsoleCtrlEvent to terminate it, more info).

Furthermore, if you're only interested in the child SSH process's exit code, there's yet another approach. You can turn Process.Handle into an await-able task, which result you can await in a similar way. The cancellation callback (registered via CancellationToken.Register) would kill the process, and make the task cancelled. Or the process may complete naturally. In both cases the task will reach the completed state and the asynchronous wait will be over.

Just keep in mind, calling TaskCompletionSource.TrySetCanceled will lead to an exception being thrown at the point were you await for the TaskCompletionSource.Task.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top