Некорректная работа HttpClient во время скачивания версии приложения из сервера используя Windows 11

Суть задачи заключается в загрузке из интернет-сервера файл инсталлятора приложения используя форму с динамически обновляемым ProgressBar и Label с указанием кол-ва скачанных МБайтов и класса HttpClient для получения непосредственного доступа к интернет-серверу. Так же, при прерывании связи с интернетом - ожидание 60 секунд на переподключение. В случаи потери интернета >= 60 секунд - закрытие формы и прерывание скачивания.

На Windows 10 функционал работает отлично, но на Windows 11 возникают проблемы во время прерывания связи с интернетом, а именно при отключении - формочка перестаёт обрабатывать события взаимодействий (нажатие на Button и т.д.) (при этом GUI работает корректно). На некоторых версиях Windows (Windows 11 version 23h2) - закрывается всё приложение (формочка скачивания вызывается из приложения).

.NET Framework 4.8

StartDownload - метод вызывающийся при запуске скачивания.

private async void StartDownload(string path = "")
{
    if (_cts != null) return;

    _lastSelectedPath = string.IsNullOrEmpty(path) ? SelectFolder() : path;

    if (_lastSelectedPath == null || !CanWriteToFolder(_lastSelectedPath))
    {
        MessageBox.Show(Properties.Resources.AccessViolation);
        Close();
        return;
    }

    FilePath = Path.Combine(_lastSelectedPath, _fileName);
    string url = _plugin.IsTestMode
        ? $"Сервер №1"
        : $"Сервер №2";

    bool isDownloadSuccessful = false;
    using (_cts = new CancellationTokenSource())
    {
        try
        {
            var progress = new Progress<DownloadProgress>(UpdateProgress);
            do
            {
                try
                {
                    await DownloadAndSaveFileAsync(url, FilePath, progress, _cts.Token); // Метод будет описан ниже
                    isDownloadSuccessful = true;
                    break; // Успешная загрузка, выходим из цикла
                }
                catch (IOException ex) // потеря доступа к интернету
                {
                    lbProgress.Text = Properties.Resources.ConnectWithServerLost; // lbProgress - Label на форме отображающий состояние скачивания
                    await WaitForReconnectAsync(_cts.Token); // Запуск 60 секунд для восстановления интернета
                }
                catch (HttpRequestException ex)
                {
                    if (ex.Message.Contains("416")) // 416 (Запрошенный диапазон невыполним = Файл уже полностью загружен)
                    {
                        MessageBox.Show(Properties.Resources.FileAlreadyUploaded);
                        DialogResult = DialogResult.OK;
                        break;
                    }
                    else break; // выходим из цикла, если были другие ошибки типа HttpRequestException
                }
                catch (OperationCanceledException) // Отмена пользователем с помощью кнопки "Cancel" или закрытия формы
                {
                    DialogResult = DialogResult.Cancel;
                    break;
                }
                catch (Exception msg)
                {
                    break;
                }
            }
            while (!_connectionEstablishmentFailed); // Повторяем, пока не исчерпан лимит времени

            
            if (!_connectionEstablishmentFailed && DialogResult != DialogResult.Cancel) isDownloadSuccessful = true;
            else // Соединение не восстановилось
            {
                DialogResult = DialogResult.Cancel;
            }
        }

        finally
        {
            if (isDownloadSuccessful) DialogResult = DialogResult.OK;
            progress.Value = 0;
            Close();
        }
    }
    _cts = null; // Обязательно сбрасываем, чтобы избежать ObjectDisposedException
}

DownloadAndSaveFileAsync - вызывается в процессе выполнения StartDownload для запуска скачивания файла

private async Task DownloadAndSaveFileAsync(string url, string filePath, IProgress<DownloadProgress> progress, CancellationToken token)
{
    const int bufferLength = 8192;
    long downloadedBytes = File.Exists(filePath) ? new FileInfo(filePath).Length : 0;

    using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url))
    {
        if (downloadedBytes > 0) request.Headers.Range = new RangeHeaderValue(downloadedBytes, null);

        using (HttpResponseMessage response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false))
        {
            response.EnsureSuccessStatusCode();

            using (Stream contentStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
            using (FileStream fs = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.None))
            {
                long totalBytes = downloadedBytes + response.Content.Headers.ContentLength ?? 0;
                byte[] buffer = new byte[bufferLength];
                int bytesReceived;

                while ((bytesReceived = await contentStream.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false)) > 0)
                {
                    await fs.WriteAsync(buffer, 0, bytesReceived, token).ConfigureAwait(false);
                    downloadedBytes += bytesReceived;
                    progress?.Report(new DownloadProgress
                    {
                        BytesDownloaded = downloadedBytes,
                        TotalBytes = totalBytes
                    });
                }
            }
        }
    }
}

WaitForReconnectAsync - метод, который запускает процесс 60-ти секундного ожидания для восстановления подключения к интернету

private async Task WaitForReconnectAsync(CancellationToken token)
{
    int elapsedTime = 0;
    while (elapsedTime < maxWaitTime)
    {
        token.ThrowIfCancellationRequested();
        if (PingServer("4.2.2.4"))
        {
            _connectionEstablishmentFailed = false; // Сбрасываем флаг при успешном восстановлении сети Интернет
            return;
        }
        await Task.Delay(checkInterval, token);
        lbProgress.Text = Properties.Resources.ConnectWithServerLost + $" {elapsedTime / 1000} sec.";
        elapsedTime += checkInterval;
    }
    _connectionEstablishmentFailed = true; // Соединение не восстановлено в течении maxWaitTime
}

PingServer - метод отправляющий запрос на указанный адрес

private bool PingServer(string host)
{
    try
    {
        using (var ping = new Ping())
        {
            var reply = ping.Send(host, 1000);
            return reply.Status == IPStatus.Success;
        }
    }
    catch { return false; }
}

Ответы (1 шт):

Автор решения: aepot

Код хороший, здесь особо предложить нечего. Я бы только сделал Ping асинхронным, чтобы он не подвешивал приложение.

private async Task<bool> PingServerAsync(string host)
{
    try
    {
        using (var ping = new Ping())
        {
            var reply = await Task.Run(() => ping.Send(host, 1000));
            return reply.Status == IPStatus.Success;
        }
    }
    catch { return false; }
}

И вот так вызывать

if (await PingServerAsync("4.2.2.4"))
{
    // ...
}

Ну и общий совет - переходите на .NET 8+

→ Ссылка