Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 17.06.2014 | нет комментариев
Доброго времени суток, друзья!

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

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, присваивая им имена переменных (task3task4task5).
Для тех, кто не в теме, класс 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 параметра — по ним проще в коде необходимое место искать.

Если кому нужно, файл класса дозволено скачать ТУТ.

С уважением, Андрей Helldar!

 

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB