Автоматическое обновление программ на C#

Несколько лет назад, программируя еще на Delphi, лично для себя соорудил некий код автоматического обновления, который в последствии стал незаменимым при разработке любой программы, где есть обновление. В настоящий момент этот код полностью переписан на c# и я хочу с Вами им поделиться.

image

Вначале определим цели этой реализации:

  1. При обнаружении новой версии обновление должно происходить автоматически;
  2. После обновления программа должна автоматически перезапускаться;
  3. После обновления имя программы должно остаться прежним.

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


Этапы


Этап 1: Проверка версии

В силу своей лени искать оптимальный вариант, на сайте было выложено 2 файла:
  • myprogram.exe
  • version.xml

Да, именно XML-формат использую. Забегая вне темы скажу, что в файле version.xml у меня находится список нескольких версий файлов, но мы рассмотрим только одну.

Идем дальше. Структура файла версий выглядит следующим образом:

<version>
     <myprogram>1.0.2.37</myprogram>
</version>

На форму добавлен компонент backgroundWorker (для реализации фоновой загрузки файла) со следующим кодом внутри обработчика DoWork:
try
	{
		double versionRemote = Convert.ToDouble(doc.GetElementsByTagName("myprogram")[0].InnerText.Replace(".", "")),
				thisVersion = Convert.ToDouble(Application.ProductVersion.Replace(".", ""));

		if (thisVersion < versionRemote)
		{
			MessageBox.Show(this, "Обнаружена новая версия (" + doc.GetElementsByTagName("myprogram")[0].InnerText + ")" + Environment.NewLine +
				"Приложение будет автоматически обновлено и перезапущено.", Application.ProductName + " v" + Application.ProductVersion, MessageBoxButtons.OK, MessageBoxIcon.Information);

			var client = new WebClient();
			client.DownloadProgressChanged += new DownloadProgressChangedEventHandler(download_ProgressChanged);
			client.DownloadFileCompleted += new AsyncCompletedEventHandler(download_Completed);
			client.DownloadFileAsync(new Uri(@"http://mysite/myprogram.exe"), "temp_myprogram");
		}
	}
catch (Exception) { }

Что мы видим в коде выше:
Так как версия у нас может иметь большое число, используем тип переменной double. Для сравнения версий мы удаляем все точки и конвертируем версию из строки в число (в примере получится число 10237).
Точно также мы поступим и с версией самого файла, присвоенной переменной thisVersion.

После этого нам нужно сравнить локальную версию с удаленной и если наша версия меньше удаленной, то сперва выводим сообщение, информирующее о дальнейшем обновлении. После этого программа начинает скачивать файл в ту же папку, откуда она запущена. Файлу присваивается имя temp_myprogram.

Для отслеживания статуса загрузки на форму был добавлен компонент progressBar, и в код добавлена функция:

private void download_ProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
	try
		{
			progressBar1.Value = e.ProgressPercentage;
		}
	catch (Exception) { }
}

Функция отображает в прогрессбаре статус загрузки файла. Это нужно лишь для наглядного отображения.
Итак, мы загрузили наш файл и что делать дальше? А дальше вступает в бой функция download_Completed, содержащая код:

private void download_Completed(object sender, AsyncCompletedEventArgs e)
{
	try
	{
		Process.Start("updater.exe", "temp_myprogram myprogram.exe");
		Process.GetCurrentProcess().Kill();
	}
	catch (Exception) { }
}

Здесь все просто: запускаем файл updater.exe с параметрами, о которых расскажу в следующем этапе.
Второй строкой указываем о необходимости принудительного завершения работы приложения.

Этап 2: Обработка обновления

Далее на помощь приходит утилита updater.exe, функциональной особенностью которой является проверка завершения работы основного приложения и обработка обновления.
Ну да не будем вдаваться в текст и сразу перейдем к коду:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.IO;
using System.Threading;

namespace Updater
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
				string process = args[1].Replace(".exe", "");
			
                Console.WriteLine("Terminate process!");
                while (Process.GetProcessesByName(process).Length > 0)
                {
                    Process[] myProcesses2 = Process.GetProcessesByName(process);
                    for (int i = 1; i < myProcesses2.Length; i++) { myProcesses2[i].Kill(); }
					
					Thread.Sleep(300);
                }

				if(File.Exits(args[1])){ File.Delete(args[1]); }
				
				File.Move(args[1], args[0]);
				
                Console.WriteLine("Starting "+args[1]);
                Process.Start(args[1]);
            }
            catch (Exception) { }
        }
    }
}

Так как нам не нужны формы, проект собран как обычное консольное приложение, действия которого довольно просты.
Задаем цикл, который проверяет запущен ли процесс, указанный во 2-ом параметре. Если процесс найден, то ему будет передана команда Kill() для принудительного завершения, после чего выжидаем 300 миллисекунд и повторяем. Цикл будет работать до тех пор, пока процесс не завершится.

Далее удаляем старый файл. Для устранения некоторых ошибок (скорее ошибок в мозгу) добавляем функцию проверки существования файла.
После удаления переименовываем имя файла, заданного в 1-ом параметре на имя, заданное во 2-ом параметре. В нашем случае произойдет переименовывание файла temp_myprogram в myprogram.exe, после чего процесс myprogram.exe будет запущен, а окно данного апдейтера закрыто.
Также хочу сказать, что файл программы «updater» я использую во всех своих проектах, где он требуется, так как у него нет привязки к какому-то конкретному приложению.

И переходим к следующему этапу:

Этап 3: Завершение


И вот мы видим, что обновленный файл версии успешно запустился, а окно «апдейтера» закрылось. Profit!

Статья написана на основании лаунчера для модпака «PROТанки» к игре «World of Tanks» с оригинальными скриншотами приложения. Для тех, кто скажет «нет там этого функционала» сразу скажу, что данный лаунчер находится на бета-тесте и доступен ограниченному количеству лиц.

Если кому будет нужен файл updater.exe, то Вы всегда сможете скачать его актуальную версию ЗДЕСЬ, на моем официальном сайте. В настоящий момент актуальной версией является 1.0.0.2.

И на этой строчке наш код автоматического обновления подходит к концу.

UPD. Мной написана вторая статья, содержащая часть внесенных поправок.
Убедительная просьба, у кого еще имеются мысли по поводу «кривых рук», «кривого кода» и пр., пишите в комментариях хотя бы что не так. Опираясь на Вашу конструктивную критику я улучшу свою работу, тем самым научившись писать более качественный код.
Заранее благодарен!


С уважением, Андрей Helldar!
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 55
  • 0
    Другой подход: myprogram может сама переименовать свой исполняемый файл myprogram.exe в myprogram.bak
    После этого скачать обновление в файл с именем myprogram.exe.
    А дальше апдейтер просто перезапустит myprogram.exe.
    • +4
      Только лучше сначала скачать новый файл, чтобы не получилась ситуация, когда посередине загрузки, по каким-либо причинам, приложение завершается и пользователь остаётся с .bak файлов и неполным .exe, т.е. без рабочего приложения. Это в варианте, когда myprogram обновляет сама себя, без updater.exe.
      • 0
        Конечно. Лучше сперва скачать, но это уже тонкости реализации :)
        Так же возможен немного другой подход:
        1. Приложение скачивает обновление и подменяет свой исполняемый файл. После чего само закрывается.
        2. Резидентная программа раз в (10, 20, 30, 60, как нам удобнее) секунд проверяет наличие запущенной программы. Если не запущено, то просто запускает из заданной папки.

        Получается что после апдейта и выхода, утилита запустит новую версию.
        • 0
          Поэтому для реализации мой метод и использую в приведенном виде.
          Файл myprogram скачивает файл, затем запускает другой, чтобы тот сам все переименовал и вновь запустил основную программу.
          • 0
            А можно вопрос.
            Как я понял из диалога, программа спокойно может заменить свой exe файл сама.
            Почему нельзя сделать так:
            1. Скачать новый файл.bak
            2. Заменить свой экзешник.
            3. Запустить обновлённый файл (при этом запустится вторая копия программы)
            4. Завершиться.

            Если вдруг нельзя допустить запуска 2 копий одновременно (например какие то ресурсы с монопольным доступом), то можно сделать так.
            перед пунктом 3 Создать глобальный mutex «Перезагрузка», а на старте программы проверять его наличие и ждать пока он не исчезнет (породившая его программа, завершилась)
            • 0
              А зачем такие сложности? У меня куда проще — скачал обновление, запустил второй процесс, который заменил файл основной программы, и вновь запустил его, а сам закрылся.
        • +5
          Простите, возможно я чего-то не понимаю, но оба ваших варианта попахивают каким-то адом. Причем, первый ад я узнаю — это Delphi-way. Одна только функция убиения процесса вызывает судорогу.

          Зачем другая программа для перезапуска, если есть механизмы OS, которые позволяют выполнить перезапуск самого себя?

          Почему-бы не реализовывать загрузку средствами OS, используя тот же BITS? Если уж решились на велосипед, пускай он будет стильным.
          • –3
            Спасибо за наводку, изучу материал и приму решение изменять ли мне код и принцип обновления ПО.
            • 0
              А что делать если Windows есть, а BITS нет? И нужна ли лишняя зависимость от BITS вообще? На счет кривости рук автора — согласен, а вот насет BITS — нет.
          • +8
            А что с ClickOnce не так?
            • +2
              Ну, автору хотелось свое и попроще. Только не увидел проверки на контрольную сумму. А если уж хотелось в пару строк и тривиально, можно было обойтись без апдейтера: скачать в %appdata%\%projectname%\updates\application.exe.upd, уйти в рестарт самого себя, при старте чекнуть наличие апдейта по указанному пути, скопировать + переименовать его, запустить с ключом на аннигиляцию родителя и подмену.
              • –1
                Честно сказать, мне мой код проще будет, нежели такой метод.
                Возможно, попробую улучшить его, а пока действую по правилу №1: «Работает — НЕ ТРОГАЙ».
              • –3
                Прочитав статью на MSDN, сделал вывод, что приложение устанавливать надо через установщик Windows в то время как указанный метод сразу заменяет файл безо всякой установки. На мой взгляд, этот метод удобней и быстрей. Хотя кому как.
                • +3
                  Установщик Windows тоже может заменять файл при апгрейде на новую версию. Также он может заменять несколько файлов и делать это внутри транзакции с возможностью отката. И еще много чего полезного. Советую покопать в сторону Windows Installer в целом и WiX Toolset в частности.
                  • 0
                    Windows Installer не пройдет по причине того, что конкретный пример статьи основан на модпаке для игры и является лишь лаунчером, а установка самого модпака осуществляется другими средствами. Вряд ли пользователям будет удобно устанавливать модпак и потом дополнительно еще и лаунчер к нему — это не целесообразно.
                • +1
                  Ох, с ним много что не так. Особенно доставляет, что встроенный метод CheckForDetailedUpdate для проверки новой версии после определённого числа вызовов кидает COMException из-за ошибки в реализации. Чтобы этого избежать нужно самостоятельно выкачивать манифест приложения и сверять версию программы. И таких граблей там на каждом шагу. Однако, предпочитаю доработать напильником, чем изобретать свой велосипед.
                  • 0
                    ClickOnce может прижиться при развертывании в локальной сети. Как средство обновления через Интернет — он бяка.
                  • +1
                    Выбран не самый удачный пример на скриншоте (надеюсь, у вас сделано не так). Показывать пользователю модальное (скорее всего) информационное окно с единственной кнопкой «Ok», не оставляя выбора — не самая хорошая практика. Нужно делать всё тихо и незаметно, либо ненавязчиво предложить обновиться с возможностью сделать это как-нибудь потом, когда пользователю будет удобнее.
                    • 0
                      Не поверишь — было: тихо скачиваем, тихо устанавливаем, громко перезапускаем прогу без каких-либо вопросов.
                      Заказчик попросил это уведомление добавить.
                      А по поводу выбора «обновить сейчас» или «потом» — добавлю такую кнопочку…
                      • 0
                        Можно тихо скачать, а установить при следующем запуске программы. Тогда не будет громкого неожиданного перезапуска, приводящего пользователя в недоумение.

                        Хотя чаще всего эту функцию реализуют так, как, например, сделано в Google Chrome или Firefox.
                        • 0
                          Только подумал про Google Chrome. Хотя у них немного другой алгоритм реализован — в папку скачивается программа, а «стартер» (ака лаунчер браузера) запускает имя клиента которого является самым новым. Можно, конечно, старые файлы удалять, но вот зачем?
                          В плане браузерной ОС это хорошо, а в моем случае не комильфо.
                    • +2
                      Сравнение версий у вас неправильное. Проблемы будут, например, при обновлении с 9.12.2 до 10.0.0. Нужно честно нарезать на части по точкам и сравнивать массивы целых чисел.
                      • +1
                        А что мешает сравнивать непосредственно типы Version? Текущую получить очень просто:
                        var myVersion = System.Reflection.Assembly.GetEntryAssembly().GetName().Version;

                        А новую создать:
                        var xmlVersionText = "9.12.2"; // for test var version = new Version(xmlVersionText);
                        • +1
                          Незнание мной стандартной библиотеки .NET, очевидно :) Здорово, что такая штука там есть, естественно, лучше использовать встроенные средства, чем городить свои.
                          • 0
                            Вчера переписал часть кода по проверке версий с использованием System.Version…
                            Первое что бросилось в глаза — функция работает и также соглашусь со словами:
                            Незнание мной стандартной библиотеки .NET, очевидно :)
                      • +2
                        Какой ужасть.
                        По соглашениям винды у программы не должно быть прав на запись в свою же папку. Если это понадобилось, то нужно проверять, можем ли мы это сделать и если нет, то затребовать админских привилегий. Также невозможно заменить что-то кроме исполняемого файла, отсутствует возможность миграции настроек из старой версии в новую, неправильно сравниваются версии (замечание выше), присутствует «бег» (когда первая программа ещё не успела завершиться, но вспомогательная уже начала пытаться заменить её), нельзя отказаться от обновления и продолжить работу. Это всё так, на быстрый взгляд.
                        Да и вообще стиль кодирования — 3-й курс, не больше.
                        • 0
                          Самоучка я) Не в курсе как в ВУЗах преподают. Лично мне хватает, а по замечаниям учту. Спасибо!

                          По поводу «бега» — вспомогательная сначала проверяет завершился ли процесс, если нет — сама его принудительно выгружает и как только процесс закроется, после этого начинает обработку обновлений.
                        • 0
                          Зачем конвертить в Double, когда есть нормальный тип System.Version, который поддерживает сравнение?

                                      System.Version thisVersion = new System.Version(Application.ProductVersion);
                                      System.Version remoteVersion = new System.Version("1.0.2.37");
                                      if (remoteVersion > thisVersion)
                                      { 
                                          //обновляемся
                                      }
                          
                          • 0
                            ваш Double неправильно сработает на версиях «1.2» и «1.1.100», например.
                            • –1
                              Это верно, хотя в моем случае программы имеют версию, состоящую из 4-х цифр: 0.0.0.0
                            • 0
                              Как говорится, век живи — век учись :)
                              Я новичок в данном языке (всего около года пишу), хотя уровня моих знаний вполне хватает для написания стабильных приложений, а улучшать не всегда есть время. Это жаль.
                              За подсказку спасибо! Сегодня потестирую.
                            • 0
                              Все интересно и просто. Сам использовал и пользуюсь подобным способом для простых программ (исполняемый файл и пара конфигов).
                              Но в случае использования более сложных программ (наборы библиотек, как своих так и сторонних и т.д.), нужно очень хорошо следить за целостностью файлов, об этом писалось выше, и такой способ неприемлем, увы.
                              • 0
                                АДЪ!!!
                                Так нельзя!
                                                    Process[] myProcesses2 = Process.GetProcessesByName(process);
                                                    for (int i = 1; i < myProcesses2.Length; i++) { myProcesses2[i].Kill(); }
                                


                                Мало того, что вы таким способом можете не убить процесс (закрытие главного окна может не привести к завершению процесса), так ещё и повиснуть этот код может (если, наример, MainWindow будет переоткрываться после закрытия, или, например при попытке закрытия будет выдаваться MessageBox с предложением сохранить результаты работы).
                                • –1
                                  Чтобы окно выдавало сообщение с просьбой сохранить файлы/завершить работу, передается функция
                                  myProcesses[i].CloseMainWindow();
                                  

                                  А если именно завершить процесс, то:
                                  myProcesses[0].Kill();
                                  

                                  И, как показала практика запуска приложения на нескольких машинах, эта функция работает.
                                  Правда, 1 из 10 раз приложение действительно зависало, предлагая отправить отчет разработчику ОС…
                                  • 0
                                    >> Правда, 1 из 10 раз приложение действительно зависало, предлагая отправить отчет разработчику ОС…
                                    Ничего страшного. Мы все так пишем. Главное чтобы не чаще 3-4 раз из 10.
                                • –1
                                  >Так как версия у нас может иметь большое число, используем тип переменной double. Для сравнения версий мы удаляем все точки и конвертируем версию из строки в число (в примере получится число 10237).

                                  Лучше сразу использовать тогда Int64. Те же 8 байт, что и в double, но уже без плавающей точки.
                                  • +1
                                    System.Version. Если хочется написать велосипед, то нужно строки с версиями разбить по '.' на четыре токена и сравнивать каждую пару. Но никак не Int64 и тем более double.
                                  • +2
                                    А как Вы обновляете updater.exe?
                                    • 0
                                      Основная программа при запуске проверяет версию updater.exe на сайте и, если найдет, скачивает. Уже после этого проверяет обновления себя самой.

                                      Чуть не забыл: также программа обновляет (а если файлов нет — скачивает) дополнительные библиотеки, необходимые для работы ПО.
                                      • 0
                                        Можно updater.exe запаковать в ресурсы программы. При обновлении качается обновленная версия программы, распаковывается updater.exe, дальше всё, как у автора. При запуске программы, если имеется файл updater.exe, то он удаляется.
                                        Мне такой способ больше нравится, так как нет лишних исполняемых файлов для пользователя и есть возможность обновления (хотя и не очень оптимальная) updater.exe.
                                        • 0
                                          Можно, но не особо нужно. В любом случае пользователь не сможет сам воспользоваться updater.exe, так как он работает только с определенным набором параметров (указаны в статье). Так что в конкретно моем случае размещение файла updater.exe вне ресурсов основной программы является оптимальным решение.
                                      • 0
                                        Хм. использую штатные средства MS VS для публикации проекта.
                                        Там прямо в настройках можно указать, куда лезть за обновлениями, и как регулярно их проверять.
                                        • 0
                                          Это полезно при самостоятельном проекте.
                                          Если приложение используется как вторичное, то само устанавливаться оно не будет, вот и используются вышеописанные методы.
                                          В любом случае, любому новичку, я так думаю, данный пример пригодится.
                                        • 0
                                          А теперь вы попробуйте обновить программу в папке Program Files в windows vista и старше и слегка удивитесь
                                          • 0
                                            Мсье, Вы сильно удивитесь…
                                            Windows 7 x64. Скопировал файл в папку «c:\Program Files\myProgram\» — запустил.
                                            Прога скачала все необходимые библиотеки с сайта, перезапустилась, нашла обновление — скачала, предложила обновиться — согласился. Обновилась, перезапустилась…
                                            … ни единой ошибки! :)
                                            • 0
                                              А запуск от имени администратора что ли? Просто из-за виртуализации program files мы получали кучу проблем если запускать просто от пользователя
                                              • 0
                                                Просто от пользователя не пробовал. Я админ в системе и являюсь одной учетной записью на компе (из числа вручную созданных) при установке ОС.
                                                Попробую сегодня запустить под обычным юзером на другой машине, сообщу о результатах.
                                                • 0
                                                  Просто при попытке записи файлов в папке программы реально эти файлы создавались в папке Program data, и если потом запуститься от администратора или от другого пользователя их не было видно.
                                                  • 0
                                                    Просто от пользователя не пробовал. Я админ в системе
                                                    Windows 7 x64
                                                    UAC, видимо, отключили?
                                                    • 0
                                                      Совершенно верно, сразу же после установки ОС.
                                                      • 0
                                                        Ну что ж, вы сами себе злобный Буратино ;)
                                                        Позвольте только поинтересоваться, а зачем?
                                                        • 0
                                                          Позвольте узнать у Вас, зачем он нужен сисадмину со стажем? :)
                                                          • 0
                                                            такой первоапрельский комментарий…
                                            • 0
                                              Правда, код в данной статье до вчерашней правки. После правки некоторая его часть выглядит так

                                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.