Full-stack PHP Developer, Sysadmin
12,0
рейтинг
28 марта 2014 в 09:46

Разработка → Автоматическое обновление программ на 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!
Андрей Николаевич @Helldar
карма
12,2
рейтинг 12,0
Full-stack PHP Developer, Sysadmin
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (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
                      Да да да)))
    • 0
      Правда, код в данной статье до вчерашней правки. После правки некоторая его часть выглядит так

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