Full-stack PHP Developer, Sysadmin
10,6
рейтинг
31 марта 2014 в 07:42

Разработка → Автоматическое обновление программ на C#. Часть 2

Несколько дней назад мной была написана статья о реализации автоматического обновления программного обеспечения на языке C#.

Приняв во внимание конструктивную критику комментаторов, было принято решение улучшить тот код, добавив несколько новых возможностей, включая улучшение «старых»:
  • Автоматическая проверка, скачивание и установка обновлений;
  • Предоставление пользователю возможности выбора момента обновления (новое);
  • Улучшен механизм проверки версии файла;
  • Проверка целостности файла обновления (новое)

Дабы не перепечатывать текст прошлой статьи, в этой акцентирую внимание лишь на переработанных частях кода.


Основные замечания


Комментаторами предыдущей статьи были выявлены следующие недочеты кода (указываю только те, на которые опирался):
  • Только лучше сначала скачать новый файл, чтобы не получилась ситуация, когда посередине загрузки, по каким-либо причинам, приложение завершается и пользователь остаётся с .bak файлов и неполным .exe, т.е. без рабочего приложения. (DarkByte);
  • Только не увидел проверки на контрольную сумму. (naum);
  • Показывать пользователю модальное (скорее всего) информационное окно с единственной кнопкой «Ok», не оставляя выбора — не самая хорошая практика. Нужно делать всё тихо и незаметно, либо ненавязчиво предложить обновиться с возможностью сделать это как-нибудь потом, когда пользователю будет удобнее. (iroln);
  • Сравнение версий у вас неправильное. Проблемы будут, например, при обновлении с 9.12.2 до 10.0.0. (eyeless_watcher)
  • А что мешает сравнивать непосредственно типы Version? (teleavtomatika).

Были, конечно, и другие комментарии схожие с этими, но не стал их все приводить и предлагаю приступить к рассмотрению проблем.

Внесение поправок


Так как при скачивании файла может возникнуть какая-либо ошибка, в следствие чего файл будет поврежден, в код были внесены следующие поправки:

public void checkUpdates(){
	try
	{
		if (File.Exists("launcher.update") && new Version(FileVersionInfo.GetVersionInfo("launcher.update").FileVersion) > new Version(Application.ProductVersion))
		{
			Process.Start("updater.exe", "launcher.update \"" + Process.GetCurrentProcess().ProcessName + "\"");
			Process.GetCurrentProcess().CloseMainWindow();
		}
		else
		{
			if (File.Exists("launcher.update")) { File.Delete("launcher.update"); }
			Download();
		}
	}
	catch (Exception)
	{
		if (File.Exists("launcher.update")) { File.Delete("launcher.update"); }
		Download();
	}
}

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

Таким образом, мы убедимся в целостности файла обновления.

private void Download()
{
	try
	{
		XmlDocument doc = new XmlDocument();
		doc.Load(@"http://mysite/version.xml");

		remoteVersion = new Version(doc.GetElementsByTagName("version")[0].InnerText);
		localVersion = new Version(Application.ProductVersion);

		if (localVersion < remoteVersion)
		{
			if (File.Exists("launcher.update")) { File.Delete("launcher.update"); }

			WebClient client = new WebClient();
			client.DownloadProgressChanged += new DownloadProgressChangedEventHandler(ProgressChanged);
			client.DownloadFileCompleted += new AsyncCompletedEventHandler(Completed);
			client.DownloadFileAsync(new Uri(@"http://mysite/launcher.exe"), "launcher.update");
		}
	}
	catch (Exception) { }
}

Далее претерпела изменения функция процесса проверки версии файла, ключевым моментом которого является реализация сравнения версий файла при помощи встроенных средств System.Version, в следствие чего была устранена проблема корректной сверки версий, скажем, «9.12.2» и «10.0.0».
В случае, когда обнаружена более новая версия, действия происходят по следующему сценарию: программа автоматически в фоновом режиме скачивает файл обновления, затем выдает пользователю сообщение о доступности этого обновления и предлагает на выбор 2 варианта развития событий:
  1. Соглашаясь на обновление, программа моментально перезапускается, произведя все необходимые действия;
  2. Отказываясь от обновления программа хранит файл рядом с исполняемым файлом, куда и было скачано. В данном варианте процесс обновления произойдет при следующем запуске программы не выводя никаких уведомлений.


Заключение


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

И отдельное спасибо хотелось бы сказать следующим людям за их конструктивную критику: DarkByte, naum, iroln, eyeless_watcher, teleavtomatika, wire.

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

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

UPD. Убедительная просьба, у кого еще имеются мысли по поводу «кривых рук», «кривого кода» и пр., пишите в комментариях хотя бы что не так. Опираясь на Вашу конструктивную критику я улучшу свою работу, тем самым научившись писать более качественный код.
Заранее благодарен!
Андрей Николаевич @Helldar
карма
13,7
рейтинг 10,6
Full-stack PHP Developer, Sysadmin
Реклама помогает поддерживать и развивать наши сервисы

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

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

Комментарии (28)

  • 0
    Для своих программ использую несколько другой подход — Во первых — сама программа НЕ проверяет обновления, там просто нет такой функции, всё делается внешним апдейтером.
    Во вторых — пользователь должен иметь возможность контролировать и процесс загрузки, и процесс обновления, то есть загружаться только после подтверждения пользователя, так же желательно чтобы была возможность вообще отключить обновления.
    В третьих — если вы хотите реализовать проверку корректности, то не проще в ваш серверный xml добавить CRC код и потом после скачивания просто его проверить?

    PS: А вообще (если не нравиться windows installer и ClickOnce) есть разных библиотеки для обновления. Из первого попавшегося с кодеплекса — autoupdaterdotnet.codeplex.com
    • 0
      Здесь уже зависит от назначения самой программы — где-то уместно будет использовать сторонний апдейтер, а где-то — нет

      так же желательно чтобы была возможность вообще отключить обновления.

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

      По «в-третьих» — CRC-код добавлял, но либо у меня руки кривые, либо одно из двух — суммы не совпадают. Над этим вариантом я еще думаю где ошибка.
      • 0
        Действительно, у меня были кривые руки. В комментарии ниже приведен пример рабочего кода.
  • +1
    Зря вы написали вторую часть статьи. Сейчас вам еще листьев накидают в карму. Самое главное, что вы упустили, читая комментарии к вашей предыдущей статье, так это то, что описанный вами опыт практически невозможно использовать в других решениях. Также совершенно не понятно почему вы решили только часть описанных проблем. И что это за метод проверки целостности файла? (методом чтения заголовков и метаданных!?)
    • –1
      Не ради кармы пишу статьи, а с целью донести информацию до начинающих пользователей, не исключая комментирование более опытных.
      По поводу использования описанного метода в других приложениях, можно сам код апдейтера полностью вынести во внешний файл и, для универсальности, при запуске добавлять параметры где лежат обновления и какой файл заменять.
      Часть проблем описал потому, что именно на них акцентировали внимание комментарии, некоторые же проблемы (вроде предложения BITS, windows installer и ClickOnce) нецелесообразны в моем проекте.
      Да, возможно при создании универсального кода они найдут место, но, ведь, пример-то не на универсальность мной приводился, а на наглядное представление принципа автоматического обновления ПО.
      Метод проверки целостности файла довольно примитивен: берем файл и считываем из него версию, если файл поврежден — это неизбежно вызовет ошибку. Легко и просто, нежели добавлять дополнительный код проверки CRC-суммы. Во всяком случае, в моем проекте нет нужды использовать более сложные методы, а так да, Вы правы, нужно использовать проверку контрольной суммы.
      • +2
        >>Метод проверки целостности файла довольно примитивен: берем файл и считываем из него версию, если файл поврежден — это неизбежно вызовет ошибку. Легко и просто, нежели добавлять дополнительный код проверки CRC-суммы.

        Хочется взять и свернуть вам шею
        • 0
          Вестимо, был не прав? Я угадал?

          Раз так, то и нижеприведенную функцию оцените, если желание будет:

                  private bool checksum(string filename, string summ)
                  {
                      try
                      {
                          using (var md5 = MD5.Create())
                          {
                              using (var stream = File.OpenRead(filename))
                              {
                                  return md5.ComputeHash(stream).ToString() == summ ? true : false;
                              }
                          }
                      }
                      catch (Exception)
                      {
                          return false;
                      }
                  }
          

          В параметре функции передается имя файла, для которого необходимо посчитать CRC-сумму, а во втором контрольная сумма, полученная с сайта (размещена в файле version.xml как дополнительный аттрибут).
          Суть проста — считываем CRC-сумму файла и сравниваем ее с полученной с сайта. Если суммы совпадают, выводится true, если нет — false.
          Вот только проблема в том, что сумма рассчитывается как-то не так — еще ни разу она не совпала. Уже голову сломал в чем проблема…
          На сайте файл хранится под именем launcher.exe, локально — launcher.update.
          Пробовал локально посчитать суммы обоих файлов — они одинаковы. Уже голова не варит что может быть. Может, подопнете в нужном направлении? Буду рад :)
          • 0
            Нашел свою кривость рук!

            Вот так код работает стабильно:

            private bool Checksumm(string filename, string summ)
            {
            	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).Replace("-", String.Empty) == (summ).ToUpper() ? true : false;
            	}
            }
            
          • 0
            Вестимо, был не прав? Я угадал?

            Да. Потому что файл из которого можно прочитать версию не обязательно рабочий. (простейший пример — это заражение вирусом, версия читается, файл запускается, но уже не то :) ).
  • 0
    Навскидку:
    1. Некорректное название. С# используется для написания софта под совершенно разные платформы, от WP7 до винды. Под версией .NET, которая на Windows Phone, ваш код работать не будет.

    2.
    catch (Exception) { }
    


    Не стыдно такое выкладывать?

    3.
    public void checkUpdates(){
    


    private void Download()
    


    У вас какой naming convention вообще? В C# методы начинаются с большой буквы.
    • –3
      Совсем не стыдно.

      При поиске информации очень часто добавляют язык C# в запросе, дабы найти нужное и, поверьте, если ввести в любом поисковике запрос «автоматическое обновление C#», то вряд ли человек будет иметь ввиду Windows Phone.

      Кто сказал, что самостоятельно придуманные методы начинаются с большой буквы? Это все глупый стереотип! Как метод ни назови — главное, чтобы он вызывался. Вот если бы в одном месте было написано «download», а в другом — «Download», тогда да, а в данном случае не пойму почём звон.

      И по Вашему комментарию в целом: Вы сможете лучше? Примеры приведете? Думаю, многие были бы рады взглянуть на мастер-класс от профи…

      P.S.: Если бы данный код был абсолютно совершенен, то вряд ли я стал его выкладывать на всеобщее…
      • 0
        >>Кто сказал, что самостоятельно придуманные методы начинаются с большой буквы? Это все глупый стереотип! Как метод ни назови — главное, чтобы он вызывался. Вот если бы в одном месте было написано «download», а в другом — «Download», тогда да, а в данном случае не пойму почём звон.

        В .Net есть строго оговоренный naming convention. Следует соблюдать его.

        >>И по Вашему комментарию в целом: Вы сможете лучше? Примеры приведете? Думаю, многие были бы рады взглянуть на мастер-класс от профи…

        Сперва добейся, да?
        • 0
          В таком случае не пойму почему обработчик на ту же кнопку выглядит так:
          private void button1_Click(object sender, EventArgs e) { }
          

          С маленькой же буквы начинается…

          Исходя из этого могу предположить, что в документации лишь рекомендуемые понятия, а не принудительно выполняемые.
          • +1
            Ну это исключение из правил, т.к. метод генерируется автоматически. А с маленькой буквы начинается, потому что имя кнопки тоже с маленькой буквы.
            • 0
              В таком разе логично предположить, что и созданные разработчиком функции также могут начинаться с маленькой буквы.
              Я хоть и новичок в этой среде, но уже успел понять, что неважно с какой буквы функция начинается, главное — чтобы она при вызове была 1 в 1 с названием.

              А раз функция, начинающаяся с маленькой буквы, работает, то и ее можно отнести к исключениям?
              • +1
                А раз функция, начинающаяся с маленькой буквы, работает, то и ее можно отнести к исключениям?
                Нет, потому что такая функция нарушает general naming conventions (я пофиксил ссылку, которую вам дали ниже).

                Если хотите,
                private void button1_Click(object sender, EventArgs e) { }
                — это тоже naming convention, только для обработчиков событий. Паттерн легко заметен: %Control%_%Event%. Если %Control% назван с маленькой буквы, фактически имя функции-обработчика тоже будет начинаться с маленькой буквы, и это правильно.
                • 0
                  Я б плюс поставил, да кармы мало.
                  Спасибо за объяснение!
          • 0
            Для дотнета есть официальный naming convention:
            msdn.microsoft.com/en-us/library/ms229045(v=vs.110).aspx

            В дотнете имена методов и свойств должны называться с большой буквы и не иметь подчеркиваний.
            Пока вы пишете код, который никто не будет читать, можете забить на любые требования, но как только этот код ктото захочет посмотреть, он будет долго плеваться. Я, читая чей-то код, из стиля имени идентикатора всегда делаю предположение о том, что это за идентификатор — локальная переменная/параметр, метод, свойство, приватное поле и т.д.
            Несоблюдение правил именования — очень большая проблема, испытано на личном опыте.
            • 0
              Ага! Вот где собака зарыта! Теперь понял для чего такие различия. Спасибо за подробное объяснение!
      • 0
        Также по пункту 2 (try/catch).
        Best practice не применять код вида
        try { //Код } catch(Exception e) { //Код }
        Тем более не делать, как у вас — с пустой обработкой ошибок — это нужно ОЧЕНЬ редко. При вашем коде вообще не понятно выполнился ли код или нет. И если была ошибка, то какая она.

        Исключения стоит ловить при:

        Необходимости логгирования (сюда же отнесу и показ ошибки пользователю, и подготовка-отправка отчёта о падении разработчику).
        Необходимости перебросить исключение в другом месте (например другом потоке).
        Необходимости изменить параметры (к примеру имя файла, при невозможности его открытия).

        Также очень желательно прописывать finally

        Для работы с базой данных/файлами/другими IDisposable вещами лучше использовать конструкцию using. Это тот же try/catch/finally, с корректно написанным блоком finally.

        • 0
          Это в примере код пустой, а так там обработчик стоит, у которого принцип работы довольно прост: передаем 3 переменные, 1-я из которых является текстовой строкой функции, вторая местом вызова (так как в функции может быть несколько обработчиков) и третья — текст ошибки.
          Все это дело сохраняется в файл и по завершении работы программы архивируется. Архив в последствии пользователь может сам передать разработчику, так как функции автоматической отправки я не делал.
  • 0
    В WebClient есть Dispose, не стесняйтесь обернуть его в using.
  • 0
    >>люди добрые из числа минусующих — будьте добры в комментариях пишите почему

    Наверное минусующие по колено в коде :)
    • 0
      Мне тоже так кажется, и вылезают лишь посмеяться над другими да вернуться обратно дебажить свой код. :)
      • 0
        Вы учитесь — это хорошо.
        У вас есть так или иначе работающий обновлятор — это хорошо.
        В качестве учебного пособия по написанию обновляторов — ваш код плох.

        Желаю вам выпустить этот обновлятор в широкие массы и понаступать на различные грабли — встречаясь с реальными ошибками и учась их решать, вы обретёте нужный опыт.
        • 0
          Спасибо на добром слове!
          «Ученье — свет, а неученье — чуть свет и на работу» (с)
  • 0
    Как быть в ситуации: Скачали новую версию, в которой были внесены изменения в функцию автообновления, но оказалось что при внесении изменений были допущены ошибки и функция больше не работает. Можете рассуждать о недостаточном тестировании, но что есть то есть.

    Мне приходят два варианта:
    1) Вынести функцию апдейт в отдельный исполняемый файл, и никогда его не модернизировать, все должно быть просто и надежно как АК.
    2) Добавить спасательную функцию, если текущая версия объявляется неудачной, то запускается дублирующая функция принудительного апдейта или предлагается обновить версию руками.
    • 0
      Озвученная Вами проблема является актуальной, так как не так давно каким-то случайным образом в 3 часа ночи я умудрился добавить символ лишний в путь файла на сайте для скачивания обновлений, в следствие чего, несколько тестеров не смогли обновиться и запрашивали у меня ссылку для ручного обновления.

      И здесь мне пришла мысль о том, что если ПО не может скачать файл исполняемой программы, то она будет выводить уведомление об этом (не каждый раз, а, скажем, раз в 5 дней) и открывать прямую (или не прямую) ссылку в браузере для скачивания данного файла.

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

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