Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

.Net DownloadFileTaskAsync robust WPF code

The WPF code below hangs forever when network connection is lost for 3 or more minutes. When connection is restored it neither throws nor continues downloading nor timeouts. If network connection is lost for a shorter period say half a minute, it throws after connection is restored. How can i make it more robust to survive network outage?

using System;
using System.Net;
using System.Net.NetworkInformation;
using System.Windows;

namespace WebClientAsync
{

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            NetworkChange.NetworkAvailabilityChanged +=
                (sender, e) => Dispatcher.Invoke(delegate()
                    {
                        this.Title = "Network is " + (e.IsAvailable ? " available" : "down");
                    });
        }

        const string SRC = "http://ovh.net/files/10Mio.dat";
        const string TARGET = @"d:\stuff\10Mio.dat";

        private async void btnDownload_Click(object sender, RoutedEventArgs e)
        {
            btnDownload.IsEnabled = false;
            btnDownload.Content = "Downloading " + SRC;
            try {
                using (var wcl = new WebClient())
                {
                    wcl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
                    await wcl.DownloadFileTaskAsync(new Uri(SRC), TARGET);
                    btnDownload.Content = "Downloaded";
                }
            }
            catch (Exception ex)
            {
                btnDownload.Content = ex.Message + Environment.NewLine
                    + ((ex.InnerException != null) ? ex.InnerException.Message : String.Empty);
            }
            btnDownload.IsEnabled = true;
        }
    }
}

UPDATE

Current solution is based on restarting Timer in DownloadProgressChangedEventHandler, so the timer fires only if no DownloadProgressChanged events occur within the timeout. Looks like an ugly hack, still looking for a better solution.

using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace WebClientAsync
{

    public partial class MainWindow : Window
    {

        const string SRC = "http://ovh.net/files/10Mio.dat";
        const string TARGET = @"d:\stuff\10Mio.dat";
        // Time needed to restore network connection
        const int TIMEOUT = 30 * 1000;

        public MainWindow()
        {
            InitializeComponent();
        }

        private async void btnDownload_Click(object sender, RoutedEventArgs e)
        {
            btnDownload.IsEnabled = false;
            btnDownload.Content = "Downloading " + SRC;
            CancellationTokenSource cts = new CancellationTokenSource();
            CancellationToken token = cts.Token;
            Timer timer = new Timer((o) =>
                {
                    // Force async cancellation
                    cts.Cancel();
                }
                , null //state
                , TIMEOUT
                , Timeout.Infinite // once
            );
            DownloadProgressChangedEventHandler handler = (sa, ea) =>
                {
                    // Restart timer
                    if (ea.BytesReceived < ea.TotalBytesToReceive && timer != null)
                    {
                        timer.Change(TIMEOUT, Timeout.Infinite);
                    }

                };
            btnDownload.Content = await DownloadFileTA(token, handler);
            // Note ProgressCallback will fire once again after awaited.
            timer.Dispose();
            btnDownload.IsEnabled = true;
        }

        private async Task<string> DownloadFileTA(CancellationToken token, DownloadProgressChangedEventHandler handler)
        {
            string res = null;
            WebClient wcl = new WebClient();
            wcl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
            wcl.DownloadProgressChanged += handler;
            try
            {
                using (token.Register(() => wcl.CancelAsync()))
                {
                    await wcl.DownloadFileTaskAsync(new Uri(SRC), TARGET);
                }
                res = "Downloaded";
            }
            catch (Exception ex)
            {
                res = ex.Message + Environment.NewLine
                    + ((ex.InnerException != null) ? ex.InnerException.Message : String.Empty);
            }
            wcl.Dispose();
            return res;
        }
    }
}
like image 770
Serg Avatar asked Feb 23 '17 19:02

Serg


2 Answers

You need to implement proper timeout for that download. But you don't need to use timer, just use Task.Delay and Task.WaitAny. For example:

static async Task DownloadFile(string url, string output, TimeSpan timeout) {            
    using (var wcl = new WebClient())
    {
        wcl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;                                                
        var download = wcl.DownloadFileTaskAsync(url, output);
        // await two tasks - download and delay, whichever completes first
        await Task.WhenAny(Task.Delay(timeout), download);
        var exception = download.Exception; // need to observe exception, if any
        bool cancelled = !download.IsCompleted && exception == null;

        // download is not completed yet, nor it is failed - cancel
        if (cancelled) {
            wcl.CancelAsync();
        }

        if (cancelled || exception != null) {
            // delete partially downloaded file if any (note - need to do with retry, might not work with a first try, because CancelAsync is not immediate)
            int fails = 0;
            while (true) {
                try {
                    File.Delete(output);
                    break;
                }
                catch {
                    fails++;
                    if (fails >= 10)
                        break;

                    await Task.Delay(1000);
                }
            }
        }
        if (exception != null) {
            throw new Exception("Failed to download file", exception);
        }
        if (cancelled) {
            throw new Exception($"Failed to download file (timeout reached: {timeout})");
        }
    }
}

Usage:

const string SRC = "http://ovh.net/files/10Mio.dat";
const string TARGET = @"d:\stuff\10Mio.dat";
// Time needed to restore network connection
TimeSpam TIMEOUT = TimeSpan.FromSeconds(30);
DownloadFile(SRC,TARGET, TIMEOUT); // might want to await this to handle exceptions

Update in response to comment. If you want timeout based on received data, not on whole operation time, it's also possible with Task.Delay. For example:

static async Task DownloadFile(string url, string output, TimeSpan timeout)
{
    using (var wcl = new WebClient())
    {
        wcl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
        DateTime? lastReceived = null;
        wcl.DownloadProgressChanged += (o, e) =>
        {
            lastReceived = DateTime.Now;
        };
        var download = wcl.DownloadFileTaskAsync(url, output);
        // await two tasks - download and delay, whichever completes first
        // do that until download fails, completes, or timeout expires
        while (lastReceived == null || DateTime.Now - lastReceived < timeout) {
            await Task.WhenAny(Task.Delay(1000), download); // you can replace 1 second with more reasonable value
            if (download.IsCompleted || download.IsCanceled || download.Exception != null)
                break;
        }
        var exception = download.Exception; // need to observe exception, if any
        bool cancelled = !download.IsCompleted && exception == null;

        // download is not completed yet, nor it is failed - cancel
        if (cancelled)
        {
            wcl.CancelAsync();
        }

        if (cancelled || exception != null)
        {
            // delete partially downloaded file if any (note - need to do with retry, might not work with a first try, because CancelAsync is not immediate)
            int fails = 0;
            while (true)
            {
                try
                {
                    File.Delete(output);
                    break;
                }
                catch
                {
                    fails++;
                    if (fails >= 10)
                        break;

                    await Task.Delay(1000);
                }
            }
        }
        if (exception != null)
        {
            throw new Exception("Failed to download file", exception);
        }
        if (cancelled)
        {
            throw new Exception($"Failed to download file (timeout reached: {timeout})");
        }
    }
}
like image 177
Evk Avatar answered Sep 20 '22 12:09

Evk


Personally, if I were to make a robust download solution, I would add a Network connection monitor because that's what we are actually waiting for. For simplicity, something like this will be enough.

online = true;

NetworkChange.NetworkAvailabilityChanged += NetworkChange_NetworkAvailabilityChanged;
_isNetworkOnline = NetworkInterface.GetIsNetworkAvailable();

void NetworkChange_NetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
{
    online  = e.IsAvailable;
}

Then you can actually check for network availability and wait as appropriate before you attempt to download or progress... I will definitely accept that a simple ping solution seems to work better than this at times based on experience.

Depending on the size of what you're downloading, monitoring the network speed may also help so you can decide how to chunk in case of choppy connections. Take a look at this project for ideas.

like image 27
Chibueze Opata Avatar answered Sep 19 '22 12:09

Chibueze Opata