3

I have access to a Connected Renci.SshNet.SftpClient which I use to get a sequence of the files in the sftp folder. The function used for this is

Renci.SshNet.SftpClient.ListDirectory(string);

Due to the huge amount of files in the directory this takes about 7 seconds. I want to be able to keep my UI responsive using async / await and a cancellationToken.

If Renci.SshNet had a ListDirectoryAsync function that returned a Task, then this would be easy:

async Task<IEnumerable<SftpFiles> GetFiles(SftpClient connectedSftpClient, CancellationToken token)
{
    var listTask connectedSftpClient.ListDirectoryAsync();
    while (!token.IsCancellatinRequested && !listTask.IsCompleted)
    {
        listTask.Wait(TimeSpan.FromSeconds(0.2);
    }
    token.ThrowIfCancellationRequested();
    return await listTask;
}

Alas the SftpClient doesn't have an async function. The following code works, however it doesn't cancel during the download:

public async Task<IEnumerable<SftpFile>> GetFilesAsync(string folderName, CancellationToken token)
{
    token.ThrowIfCancellationRequested();
    return await Task.Run(() => GetFiles(folderName), token);
}

However, the SftpClient does have kind of an async functionality using the functions

public IAsyncResult BeginListDirectory(string path, AsyncCallback asyncCallback, object state, Action<int> listCallback = null);
Public IEnumerable<SftpFile> EndListDirectory(IAsyncResult asyncResult);

In the article Turn IAsyncResult code into the new async and await Pattern is described how to convert an IAsyncResult into an await.

However I don't have a clue what to do with all the parameters in the BeginListdirectory and where to put the EndListDirectory. Anyone able to convert this into a Task on which I can wait with short timeouts to check the cancellation token?

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116

1 Answers1

5

It looks like the SftpClient does not follow the standard APM pattern: the listCallback is an extra parameter for the Begin method. As a result, I'm pretty sure you can't use the standard FromAsync factory method.

You can, however, write your own using TaskCompletionSource<T>. A bit awkward, but doable:

public static Task<IEnumerable<SftpFile>> ListDirectoryAsync(this SftpClient @this, string path)
{
  var tcs = new TaskCompletionSource<IEnumerable<SftpFile>>();
  @this.BeginListDirectory(path, asyncResult =>
  {
    try
    {
      tcs.TrySetResult(@this.EndListDirectory(asyncResult));
    }
    catch (OperationCanceledException)
    {
      tcs.TrySetCanceled();
    }
    catch (Exception ex)
    {
      tcs.TrySetException(ex);
    }
  }, null);
  return tcs.Task;
}

(code written in browser and completely untested :)

I structured it as an extension method, which is the approach I prefer. This way your consuming code can do a very natural connectedSftpClient.ListDirectoryAsync(path) kind of call.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    I see the catch OperationCancelledException block, but how does one incorporate the a CancellationToken when calling this extension method? A CancellationToken isn't passed in. Doesn't one typically call CancellationTokenSource.Cancel() after calling a method that passes in the CancellationTokenSource.Token? – Adam Venezia Jul 02 '15 at 20:19
  • 1
    @AdamVenezia: The `BeginListDirectory` does not take a `CancellationToken`, so `ListDirectoryAsync` shouldn't either. Sometimes libraries like this have another kind of `CancelListDirectory` or just `Cancel` method that can be used to implement cancellation, in which case `EndListDirectory` may throw `OperationCanceledException`. – Stephen Cleary Jul 02 '15 at 20:27