Некорректная работа 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 шт):
Код хороший, здесь особо предложить нечего. Я бы только сделал 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+