Misiu
Misiu

Reputation: 4919

Convert BeginRead loop into ReadAsync and cancel on demand

I trying to build two applications that will communicate over RDP with each other. I have basic communication, but I would like to optimize code and remove all exceptions.

On server I'm opening FileStream and when I send something from client I'm reading it using BeginRead:

public void Connect()
{
    mHandle = Native.WTSVirtualChannelOpen(IntPtr.Zero, -1, CHANNEL_NAME);
    Task.Factory.StartNew(() =>
    {
        IntPtr tempPointer = IntPtr.Zero;
        uint pointerLen = 0;
        if (Native.WTSVirtualChannelQuery(mHandle, WtsVirtualClass.WTSVirtualFileHandle, out tempPointer, ref pointerLen))
        {
            IntPtr realPointer = Marshal.ReadIntPtr(tempPointer);
            Native.WTSFreeMemory(tempPointer);
            var fileHandle = new SafeFileHandle(realPointer, false);
            fileStream = new FileStream(fileHandle, FileAccess.ReadWrite, 0x640, true);
            var os = new ObjectState();
            os.RDPStream = fileStream;
            fileStream.BeginRead(os.Buffer, 0, 8, CallBack, os);
        }
    });
}

and my Callback:

private void CallBack(IAsyncResult iar)
{
    if (iar.IsCompleted || iar.CompletedSynchronously)
    {
        var os = iar.AsyncState as ObjectState;
        int readed = 0;
        try
        {
            readed = os.RDPStream.EndRead(iar);
        }
        catch (IOException ex)
        {
            MessageBox.Show(ex.Message);
        }

        if (readed != 0)
        {
            switch (os.State)
            {
                case FileDownloadStatus.Length:
                    os.Length = BitConverter.ToInt32(os.Buffer, 0);
                    os.State = FileDownloadStatus.Body;
                    os.RDPStream.BeginRead(os.Buffer, 0, os.Length, CallBack, os);
                    break;
                case FileDownloadStatus.Body:
                    var message = Encoding.UTF8.GetString(os.Buffer, 0, readed);
                    Debug.WriteLine(message);
                    var newos = new ObjectState {RDPStream = os.RDPStream};
                    os.RDPStream.BeginRead(newos.Buffer, 0, 8, CallBack, newos);
                    break;
            }
        }
    }
}

and needed items:

enum FileDownloadStatus
{
    Length,
    Body,
}

class ObjectState
{
    public int Length = 0;
    public FileDownloadStatus State = FileDownloadStatus.Length;
    public byte[] Buffer = new byte[0x640];
    public FileStream RDPStream = null;
}

Unfortunately when I try to close my application I get exception System.OperationCanceledException.

I would like to convert this into ReadAsync because then I could use CancelationToken, but then I must upgrade to .NET 4.5.

I found similar question about CancelationToken, but it's written in F# and I need C#.

So my question is:
How can I stop BeginRead or transform this code into .NET 4.5 and use ReadAsync?

Upvotes: 0

Views: 1809

Answers (1)

Peter Duniho
Peter Duniho

Reputation: 70701

Converting to async/await:

Replace fileStream.BeginRead(os.Buffer, 0, 8, CallBack, os); with:

    Task<int> task = fileStream.ReadAsync(os.Buffer, 0, 8, cancelToken);

    try
    {
        await task;
        await CallBack(os, task);
    }
    catch (IOException ex)
    {
        // TODO: unpack task exception and display correctly
        MessageBox.Show(ex.Message);
    }

Note that I assume you have a new cancelToken variable containing your CancelationToken object. I suppose you might store this in your ObjectState object, so that it's readily available where you'll want to call ReadAsync().

Change the first part of your CallBack() method (before the if (readed != 0) statement) to this:

    private async Task CallBack(ObjectState os, Task<int> task)
    {
        if (task.Status != TaskStatus.Canceled)
        {
            int readed = task.Result;

I.e. replace all of the code before the if (readed != 0) statement with the above, changing the method declaration as above.

Apply similar changes to the code inside the CallBack() method, where you also call BeginRead(). I.e. use ReadAsync, use await with the returned Task<int>, then pass the Task<int> to the callback when the await completes, calling it with await as well. (You should be able to have just the one try/catch block in the Connect() method...exceptions occurring in the continuations should propagate back to that try/catch block).

Note that in addition to the change to the CallBack() method declaration, you will need to change the void Connect() method declaration so that it's async Task Connect(). Note also that this will force callers to themselves become async methods and use await when calling (and so on, up the call chain until you get to a UI event handler, which can be async and use await, while legitimately being a void method), or to deal with the returned Task directly (not recommended, since that generally means negating the benefit of the async/await idiom).

If you keep using the BeginRead() method instead, as far as I can recall the only way to cancel that is to close the FileStream object (which will cause the operation to complete with an ObjectDisposedException).

Finally note: I'm not familiar enough with the RDP I/O to comment on your use of the byte-count result for the async reads. But in other contexts, it would be a really bad idea to assume you have successfully read as many bytes as you originally asked for. Typically, you have to keep looping, reading data until you have as many bytes as you need.

Upvotes: 1

Related Questions