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

  Доброго часу доби, друзі!
 
У попередніх своїх статтях (раз і два ) я писав про реалізацію функції автоматичного оновлення програми і маючи безліч недоліків, було прийнято рішення щодо її поліпшення, а також з метою зробити код більш «дружелюбним», чи що. Скорочуючи рядка і оптимізуючи формат мені вдалося досягти більш якісної асинхронної завантаження файлів, практично виключає ймовірність підміни файлу оновлення (перевірка контрольної суми), а також додано декілька нових розробок. Там самим робить чергову спробу реабілітуватися.
 
 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 . Застосування поновлення здійсниться автоматично при подальшому запуску програми, тобто, користувачеві не видаватимуться ніякі повідомлення, що, на мій погляд, підвищує «дружелюбність» програми.
 
 

Висновок

Ось, власне, весь код оновлення та враховуючи те, що він розміщений в окремому класі, його з легкістю можна використовувати в будь-якого роду проект, вказавши свої джерела оновлень і кількість і назви файлів.
PS: у класі debug при збереженні задається 3 параметра — по них простіше в коді потрібне місце шукати.
 
Якщо кому треба, файл класу можна скачати ТУТ .
 
 З повагою, Андрій Helldar!
  
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.