Pull to refresh

Асинхронное обновление программы на C#

Reading time 10 min
Views 20K
Доброго времени суток, друзья!

В предыдущих своих статьях (раз и два) я писал о реализации функции автоматического обновления программы и имея множество недочетов, было принято решение по ее улучшению, а также с целью сделать код более «дружелюбным», что ли. Сокращая строки и оптимизируя формат мне удалось достичь более качественной асинхронной загрузки файлов, практически исключающей вероятность подмены файла обновления (проверка контрольной суммы), а также добавлено несколько новых разработок. Там самым предпринимаю очередную попытку реабилитироваться.

image

В работе моя программа использует следующие файлы, находящиеся в той же папке, что и исполняемый файл:
  • Ionic.Zip.dll — реализация архивирования файлов дебага;
  • LanguagePack.dll — собственная библиотека, содержащая перевод названия элементов формы на нужный язык;
  • Newtonsoft.Json.dll — JSON-библиотека;
  • ProcessesLibrary.dll — своя библиотека, содержащая список процессов;
  • restart.exe — утилита перезапуска основного приложения;
  • updater.exe — утилита обновления основного приложения
  • settings.xml — файл настроек.

В предыдущих версиях кода каждый файл скачивался отдельно, что доставляло массу неудобств, начиная со времени ожидания скачивания. Также отсутствовала функция проверки контрольной суммы, что не очень хорошо сказывалось на безопасности их использования.
Что же такого изменилось в коде, что я решил написать третью статью о все той же истории?

Сперва претерпел изменения файл version.xml, расположенный на сервере:

<?xml version="1.0" encoding="utf-8"?>
<version>
	<myprogram checksumm="05b2b2eb79c4f11834b25095acc047f9">1.0.7.88</myprogram>
	<updater checksumm="aaef7c8a1f9437138acfc80fb2c4354b">1.0.0.7</updater>
	<restart checksumm="d3904a3fe5ff2ab3a0f246bdde293345">1.0.1.9</restart>
	<processesLibrary checksumm="2b999c9eb771374c87490f5dee5da9ec">1.0.1.10</processesLibrary>
	<languagePack checksumm="d5724f066cea211eb5f0efb6365ba0c9">1.0.0.4</languagePack>
	<Newtonsoft.Json checksumm="5619e5d4b93e544c7b9174c062f6c40b">6.0.1.17001</Newtonsoft.Json>
	<Ionic.Zip checksumm="6ded8fcbf5f1d9e422b327ca51625e24">1.9.1.8</Ionic.Zip>
</version>


Изменения


Как Вы успели заметить, по сравнению с предыдущим ее вариантом, добавился аттрибут checksumm, содержащий как раз MD5-сумму конкретного файла.

При использовании кода, за ненадобностью, убраны компоненты класса backgroundWorker в пользу Task, и в определение класса были добавлены следующие строки:

debug debug = new debug();
private string url = @"http://mysite/";
private ProgressBar downloadPercent = null;

Класс debug производит запись возникающих ошибок в файл для лучшего логирования. Но в этой статье мы не будем о нем говорить.
Строковый параметр url задает путь к папке на сайте, содержащей все наши файлы. До этого данный путь у каждого файла был прописан — а зря.
Компонент downloadPercent из класса ProgressBar используется для отображения процентов загрузки обновления основного файла программы.

Далее функция запуска процесса обновления Check() была приведена к виду:

public void Check(bool launcher = false, ProgressBar report = null)
{
	try
	{
		XmlDocument doc = new XmlDocument();
		doc.Load(url + "version.xml");

		if (!File.Exists("settings.xml"))
		{
			using (var client = new WebClient())
			Task.Factory.StartNew(() => client.DownloadFile(new Uri(url + "settings.xml"), "settings.xml")).Wait();
		}

		// Если файлы имеют нулевой размер, то удаляем их
		if (File.Exists("settings.xml") && new FileInfo("settings.xml").Length == 0) { File.Delete("settings.xml"); }
		if (File.Exists("Ionic.Zip.dll") && new FileInfo("Ionic.Zip.dll").Length == 0) { File.Delete("Ionic.Zip.dll"); }
		if (File.Exists("restart.exe") && new FileInfo("restart.exe").Length == 0) { File.Delete("restart.exe"); }
		if (File.Exists("updater.exe") && new FileInfo("updater.exe").Length == 0) { File.Delete("updater.exe"); }
		if (File.Exists("Newtonsoft.Json.dll") && new FileInfo("Newtonsoft.Json.dll").Length == 0) { File.Delete("Newtonsoft.Json.dll"); }
		if (File.Exists("ProcessesLibrary.dll") && new FileInfo("ProcessesLibrary.dll").Length == 0) { File.Delete("ProcessesLibrary.dll"); }
		if (File.Exists("LanguagePack.dll") && new FileInfo("LanguagePack.dll").Length == 0) { File.Delete("LanguagePack.dll"); }
		if (File.Exists("launcher.update") && new FileInfo("launcher.update").Length == 0) { File.Delete("launcher.update"); }

		if (!launcher)
		{
			var task1 = Task.Factory.StartNew(() => DownloadFile("Ionic.Zip.dll", doc.GetElementsByTagName("Ionic.Zip")[0].InnerText, doc.GetElementsByTagName("Ionic.Zip")[0].Attributes["checksumm"].InnerText));
			var task2 = Task.Factory.StartNew(() => DownloadFile("restart.exe", doc.GetElementsByTagName("restart")[0].InnerText, doc.GetElementsByTagName("restart")[0].Attributes["checksumm"].InnerText));
			var task6 = Task.Factory.StartNew(() => DownloadFile("LanguagePack.dll", doc.GetElementsByTagName("languagePack")[0].InnerText, doc.GetElementsByTagName("languagePack")[0].Attributes["checksumm"].InnerText));

			Task.WaitAll(task1, task2, task6);
		}

Теперь обо все по подробнее.
В самом начале мы проверяем существует ли файл настроек программы (settings.xml) и если он осутствует — скачиваем его
Далее (иногда случалось), если файлы имеют нулевую длину, то мы также их удаляем. Зачем нам нерабочие файлы. Верно, ведь?
Уже после этого идет проверка был ли задан параметр launcher при инициализации функции. Он нужен для определения последовательности выполнения кода, а также для оптимизации решения, так как только 3 файла из вышеприведенного списка являются обязательными при инициализации формы главного окна. Если параметр launcher равен false, то мы скачиваем основные файлы (Ionic.Zip.dll, LanguagePack.dll, restart.exe), после чего инициализируем код основной программы.

Для проверки обновлений основной программы и вспомогательных файлов, в коде главной формы в обработчике public Form1() после вызова функции InitializeComponent(); добавляем вызов нашего класса обновления. Да класса, так как весь его код размещен отдельно (для удобства).

update_launcher update = new update_launcher();
update.Check(true, pbDownload);

В вызове update.Check(true, progressBar1); мы в качестве первого параметра мы указываем, что сейчас будет производиться проверка обновлений вспомогательных файлов и обновлений главного файла приложения. В качестве второго указываем на progressBar, отвечающий за отображения процентов загрузки основного файла.

Так как мы указали параметр launcher = true, то программа выполнит следующий код из функции Check() (продолжение кода, указанного выше):

		else
		{
			try
			{
				var task3 = Task.Factory.StartNew(() => DownloadFile("updater.exe", doc.GetElementsByTagName("updater")[0].InnerText, doc.GetElementsByTagName("updater")[0].Attributes["checksumm"].InnerText));
				var task4 = Task.Factory.StartNew(() => DownloadFile("Newtonsoft.Json.dll", doc.GetElementsByTagName("Newtonsoft.Json")[0].InnerText, doc.GetElementsByTagName("Newtonsoft.Json")[0].Attributes["checksumm"].InnerText));
				var task5 = Task.Factory.StartNew(() => DownloadFile("ProcessesLibrary.dll", doc.GetElementsByTagName("processesLibrary")[0].InnerText, doc.GetElementsByTagName("processesLibrary")[0].Attributes["checksumm"].InnerText));

				Task.WaitAll(task3, task4, task5);
                        
				if (File.Exists("launcher.update") && new Version(FileVersionInfo.GetVersionInfo("launcher.update").FileVersion) > new Version(Application.ProductVersion))
				{
					Process.Start("updater.exe", "launcher.update \"" + Application.ProductName + ".exe\"");
					Process.GetCurrentProcess().CloseMainWindow();
				}
				else if (new Version(Application.ProductVersion) < new Version(doc.GetElementsByTagName("version")[0].InnerText))
				{
					if (report != null)
					{
						downloadPercent = report;
						downloadPercent.Value = 0;
					}

					Task.Factory.StartNew(() => DownloadFile("launcher.exe", doc.GetElementsByTagName("version")[0].InnerText, doc.GetElementsByTagName("version")[0].Attributes["checksumm"].InnerText, "launcher.update", true)).Wait();
				}
				else if (File.Exists("launcher.update")) { File.Delete("launcher.update"); }
			}
			catch (Exception ex1)
			{
				debug.Save("public void Check(bool launcher = false)", "launcher.update", ex1.Message);
			}
		}
	}
	catch (Exception ex)
	{
		debug.Save("public void Check(bool launcher = false)", "", ex.Message);
	}
}

Что мы имеем здесь. Не забыв добавить using System.Threading.Tasks;, мы инициализируем объект Task, присваивая им имена переменных (task3, task4, task5).
Для тех, кто не в теме, класс Task представляет собой обертку над потоками для выполнения асинхронных операций, предоставляя разработчику возможность забыть о том, как создать поток, запустить его и уничтожить по окончании.
В общем, в нашем случае в качестве параметра мы задаем функцию DownloadFile();, передав ей необходимые параметры, а именно:

private void DownloadFile(string filename, string xmlVersion, string xmlChecksumm, string localFile = null, bool showStatus = false)

где:

  • filename — имя файла, расположенного на сервере;
  • xmlVersion — версия файла на сервере (читается из version.xml);
  • xmlChecksumm — строка, содержащая контрольную сумму файла (читается из version.xml);
  • localFile — необязательный параметр, нужен если файл, сохраняемый локально, отличается по имени от расположенного на сайте (в нашем случае используется только при загрузке обновлений главного файла приложения);
  • showStatus — необязательный параметр, используемый для определения будет ли отображаться статус загрузки файла в progressBar. Используется только совместно с параметром localFile.

Функция Task.Factory.StartNew(); позволяет асинхронно запускать какие-либо процессы на выполнение. Для того, чтобы определить момент скачивания всех файлов, была задействована функция Task.WaitAll(task3, task4, task5);, ожидающая завершения выполнения кода во всех указанных элементах.
Итак, после скачивания дополнительных файлов, можно перейти на проверку обновлений основного, а так как обновления уже могут быть скачаны, то вначале проверяем существование и версию локального файла обновлений, если он существует.

if (File.Exists("launcher.update") && new Version(FileVersionInfo.GetVersionInfo("launcher.update").FileVersion) > new Version(Application.ProductVersion))

Почему здесь нет функции проверки контрольной суммы, расскажу чуть позже, а пока вернемся к этой.
Если файл обновления («launcher.update») существует и его версия более свежая, то запускаем дополнительную утилиту updater.exe, передав в параметре имена файлов (см код выше).

Далее, если файл обновления отсутствует или его версия не свежая, то задействуем следующую проверку соответствий, где мы проверяем версию ПО с версией на сайте:

if (new Version(Application.ProductVersion) < new Version(doc.GetElementsByTagName("version")[0].InnerText))

Если найдена более свежая версия, то переходим к ее скачиванию. И вновь об этом позже.
Третье же условие действует тогда, когда первые два не выполнены, а именно, если файл присутствует и имеет более старую версию, то мы его просто удаляем.

Вызов функции debug.Save(); сохраняет информацию об обработчике ошибок в файл, чтобы потом можно было прочитать. К обновлениям ПО данный код особого значения не имеет, а размещен чтобы люди не спрашивали «почему у тебя числится catch(Exception) {}, это же не комильфо». Вот так вот.
Идем дальше.

Скачивание


За скачивание файлов отвечает приватная функция DownloadFile();, имеющая набор параметров, описанных выше, а ее кот код Вы можете лицезреть ниже:

private void DownloadFile(string filename, string xmlVersion, string xmlChecksumm, string localFile = null, bool showStatus = false)
{
	localFile = localFile != null ? localFile : filename;

	if (File.Exists(localFile) && new FileInfo(localFile).Length == 0) { File.Delete(localFile); }

	try
	{
		if ((File.Exists(localFile) && new Version(FileVersionInfo.GetVersionInfo(localFile).FileVersion) < new Version(xmlVersion)) || !File.Exists(localFile))
		{
			using (var client = new WebClient())
			{
				try
				{
					if (showStatus && downloadPercent != null) { client.DownloadProgressChanged += new DownloadProgressChangedEventHandler(ProgressChanged); }
					client.DownloadFileAsync(new Uri(url + filename), localFile);

					if (!Checksumm(localFile, xmlChecksumm) && File.Exists(localFile)) { File.Delete(localFile); }
				}
				catch (Exception ex)
				{
					debug.Save("private void DownloadFile(string filename, string xmlVersion, string xmlChecksumm)", "Filename: " + filename + Environment.NewLine + "Localname: " + (localFile != null ? localFile : "null") + Environment.NewLine + "URL: " + url, ex.Message);
				}
			}
		}
	}
	catch (Exception ex1)
	{
		debug.Save("private void DownloadFile(string filename, string xmlVersion, string xmlChecksumm)", "Filename: " + filename + Environment.NewLine + "Localname: " + (localFile != null ? localFile : "null") + Environment.NewLine + "URL: " + url, ex1.Message);
	}
}

В самом начале выполняется проверка переданного значения в параметр localFile и если параметр равняется null, то присваиваем ей значение параметра filename. После этого идет проверка файла на длину размер и если он равен нулю, то его удалим.
Далее начинается ключевая часть функции — проверяем существование файла и актуальность его версии и если файл отсутствует либо найдена новая версия, то переходим к скачиванию, иначе, соответственно, пропускаем.
Непосредственно перед скачиванием проверяем параметр showStatus, который необходим нам для включения/отключения отображения статуса загрузки. Я рассмотрю пример, когда статус нужен. Так вот, если параметр showStatus не равен null И задан параметр downloadPercent, то объекту client класса WebClient() подключаем функцию ProgressChanged(); для отслеживания статуса загрузки.
Далее идет сам процесс асинхронного скачивания файла DownloadFileAsync(). Файл скачали, что дальше?
А дальше мы проверяем контрольную сумму скачанного файла со значением на сайте в файле version.xml через функцию Checksumm() в параметрах которой передается имя локального файла и строка, содержащая md5-кэш с сайта.

private bool Checksumm(string filename, string summ)
{
	try
	{
		if (File.Exists(filename) && summ != null && new FileInfo(filename).Length > 0)
			using (FileStream fs = File.OpenRead(filename))
			{
				System.Security.Cryptography.MD5 md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
				byte[] fileData = new byte[fs.Length];
				fs.Read(fileData, 0, (int)fs.Length);
				byte[] checkSumm = md5.ComputeHash(fileData);
				return BitConverter.ToString(checkSumm) == summ.ToUpper() ? true : false;
			}
			else
				return false;
	}
	catch (Exception ex)
	{
		debug.Save("private bool Checksumm(string filename, string summ)", "Filename: " + filename, ex.Message);
		return false;
	}
}

Если контрольная сумма локального файла совпадает со значением на сайте, функция возвращает true, иначе по действию логики.
А что же там в DownloadFile()? Если контрольная сумма верна, то завершаем работу функции, если нет — удаляем файл.

Что еще не указал? Эм… Ах да! Функцию обработки статуса загрузки:

Private void ProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
	downloadPercent.Value = e.ProgressPercentage;
}

И, чуть не забыл, в указанном коде скачивание обновления основной программы осуществляется в файл launcher.update. Применение обновления осуществится автоматически при последующем запуске программы, то есть, пользователю не будут выдаваться никакие уведомления, что, на мой взгляд, повышает «дружелюбность» программы.

Заключение


Вот, собственно, весь код обновления и учитывая то, что он размещен в отдельном классе, его с легкостью можно использовать в любого рода проекте, указав свои источники обновлений и количество и названия файлов.
P.S.: в классе debug при сохранении задается 3 параметра — по ним проще в коде нужное место искать.

Если кому надо, с ноября 2017 года репозиторий выложен ЗДЕСЬ.

С уважением, Андрей Helldar!
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
-1
Comments 77
Comments Comments 77

Articles