В этой статье рассматривается два наиболее простых способа реализации автообновления приложения .Net. Первый способ — с помощью стандартной технологии Microsoft — Click Once, второй — с помощью опенсорного компонента NET Application Updater Component.
Обновление с помощью ClickOnce
Процесс использования ClickOnce описан в этой статье. Однако, установка с помощью этой технология делает неправильный мед» не позволяет установить программу для всех пользователей, поэтому переходим к следующему пункту.
Сразу оговорюсь, что в итоге в Windows 7 хранение в папке ProgramData требует администраторских прав и в итоге, описанное здесь решение можно рассматривать только в качестве собственной реализации автообновления, исходный код которого всегда можно изменить для своих нужд.
Обновление с помощью NET Application Updater Component
Компонент явно сырой, но с определенными улучшениями и исправлениями, его можно использовать.
1. Создаем новый проект Windows Forms, например с именем MySimpleSample
Уроки C# – Как обновлять свою программу на C#
2. Добавляем проекты AppStart и AppUpdater. Добавляем Reference на на проект AppUpdater. В AppUpdater есть ссылка на класс AppStartConfig, лежащий в проекте AppStart, поэтому проекты AppStart и AppUpdater лучше положить на одном уровне файловой структуры.
3. Добавляем на форму компонент AppUpdater ( Со окна Toolbox-AppUpdater Components). Тут были некоторые проблемы, скорее всего это связано с обновлением проекта до Visual Studio 2010 . UpdateLog хочет писать в файл AppUpdate.log, но текущим каталогом, возвращаемым функцией GetLogFilePath является путь к Visual Studio, куда, естественно запись запрещена. Можно поправить выбор каталога на что-нибудь такое :
DirectoryInfo DI = new DirectoryInfo(Assembly.GetCallingAssembly().Location);
В классе AppDownloader нас будет ожидать еще один сюрприз — захардкоженное имя конфига «AppStart.config». Явная недоделка — исправляем хотя бы на константу для централизации.
4. Приложением для запуска будет AppStart.exe, которое будет читать конфигурацию из файла AppStart.config. Предполагается, что версии приложении складываются в папки, соответствующие версии программы, например SampleApp_ClientSetup1.0.0.0
Момент с созданием конфига абсолютно непродуман, равно как и то, что автообновление будет требовать администраторских прав случае, если записывать в папку, находящуюся в Program Files. Но об этом позже.
1.0.0.0 SimpleSample.exe
5. Запускаем TestApp. И тут самое интересное — из Visual Studio все работает, а если просто запустить TestApp.exe, то получаем исключение: Configuration file AppStart.config does not have root tag. Разбираться почему работает из студии, тут небольшая дискуссия, смысл же простой, наш собственный конфиг считывается .NET как системный конфиг приложения.
Самый простой вариант — как здесь, изменить расширение файла конфигурации.
6. Теперь к самому процессу обновления. На выбор предлагаются 2 метода (поле ChangeDetectionMode):
Как сделать обновление программы (Легко и Просто) на C#
- DirectFileCheck — сверка версий файлов напрямую. В этом случае в UpdateUrl указывается папка с файлами программы. Изначально рассчитано на использование HTTP-сервера(IIS) или Web Dav папками, но я добавил возможность использования обычной файловой системы — для того, чтобы можно было выкладывать обновления на сетевой ресурс (расшаренную папку). Обновление запустится, если найдутся файлы с более новой датой.
- ServerManifestCheck — производится проверка текущей сборки и версии, указанной в конфигурационном файле — доступной для обновления.
7. Что-то нужно сделать с путями — используется ворох функций определения каталога, привязанных к программе, а также не пока непонятен процесс развертывания приложения. Я перенес функцию LoadConfig, проверяющую несколько путей для конфигурационного файла в класс AppStartConfig, так как в AppDownloader почему-то использовалась своя версия определения пути, что не может быть хорошо.
Хочется, чтобы обновляемые файлы хранились в каталоге пользователя, например, как это делает Google Chrome — в этом случае и возможно «тихое» обновление без прав администратора и запроса UAC. Для того, чтобы хранить в папке пользователя + именем компании + именем продукта можно использовать примерно такой код:
Так как будем хранить данные приложения в с именем продукта и внутри каталога Environment.SpecialFolder.ApplicationData ( В Windows 7 это c:UsersUsernameAppDataRoaming), в InnoSetup этому пути будет соответствовать константа
- Используем Inno Setup
- При установке мы кладем AutoStart.exe c конфигурационным файлом в пользовательский каталог , например c:Users%USERNAME%AppDataRoamingMyCompanyMySimpleSample (autoupdatable). В папку с постоянным именем (например, BaseInstall) копируем текущую версию приложения — чтобы не возиться с настройк
- Генерируем конфиг из InnoSetup, либо просто копируем начальный конфиг, содержащий путь BaseInstall. Код генерации из InnoSetup (Весь скрипт инсталляции можно скачать отсюда)
; Additions #define BaseCatalog «BaseInstall» #define ConfigName «AppStart.conf» #define MainExeName «MySimpleApp.exe» [Code] procedure AfterMyProgInstall(S: String); var AppPath: string; ConfigPath: string; BaseInstallPath: string; begin AppPath := Format(‘%s%s%s’, [ExpandConstant(»), ExpandConstant(»), ExpandConstant(»)] ); ConfigPath :=Format(‘%s%s’, [AppPath , ExpandConstant(»)]); BaseInstallPath :=Format(‘%s%s’, [AppPath , ExpandConstant(»)]); SaveStringToFile(ConfigPath,» + #13#10,false); SaveStringToFile(ConfigPath,» ,true); SaveStringToFile(ConfigPath, ExpandConstant(») ,true); SaveStringToFile(ConfigPath,» + #13#10 ,true); SaveStringToFile(ConfigPath,» ,true); SaveStringToFile(ConfigPath,ExpandConstant(») ,true); SaveStringToFile(ConfigPath,»+ #13#10 ,true); SaveStringToFile(ConfigPath,» + #13#10,true); end;
8. Похоже, что режим проверки наличия новой версии напрямую по дате не протестирован, список файлов берется из так называемого файла манифеста (класс AppManifest), однако, при обновлении записывается манифест со списком старых файлов. Так как хранить его нужно только для возобновления загрузки, то строку Manifest = AppManifest.Load(AppManifestPath); можно вообще заменить на Manifest = new AppManifest(AppManifestPath);
Для того, чтобы хранить одну версию программу первым в голову приходить использование пути ProgramData, однако запись по этому пути требует администраторских прав и ведет к чудесным вещам — в виде виртуализации пути для программы, работающей с правами обычного пользователя, заставляющим разбираться с Junction Points. Можно сделать вывод, что это является плохой практикой.
Полностью исходный код (со скриптом InnoSetup) можно скачать отсюда. Для тестирования нужно будет только настроить путь для обновлений на свой и попробовать обновить дату в каком-нибудь из файлов. На данный момент происходит только проверка загруженных сборок, если нужна проверка обновлений не для исполняемых файлов, придется добавить такую функциональность.
- StackOverflow: Auto-update library for .NET?
Источник: yahnev.ru
Как я писал модуль обновления на C#
Я пишу программы на C# для фирмы, где их использует несколько сотен человек. Время от времени добавляются новые функции и встаёт проблема обновления версий.
Я решил не искать стандартных громоздких решений, а изобрести свой собственный велосипед для автоматического обновления установленных программ.
Честно, мне самому не очень нравятся приложения, которые вечно скачивают обновления, но в моём случае проще автоматизировать этот процесс, чем писать должностные инструкции и заставлять коллег скачивать обновления вручную (а потом бегать по всем этажам и делать это самому).
Цели
Прежде чем начать работу над этим проектом, я сформулировал цели, которым должен удовлетворять будущий модуль авто-обновления.
- Обновление должно происходить автоматически при наличии новой версии.
- После обновления программа должна автоматически перезапуститься.
- После обновления имя программы должно сохраниться прежним.
- Модуль должен встраиваться в ехе-файл проекта.
Задача становится немного запутанной из-за того, что запущенный ехе-файл не может сам себя удалять или переименовывать, это должен делать другой процесс при закрытой программе.
- скачать новую версию,
- удалить старую программу,
- переименовать скачанный файл
Блок-схема
Чтобы прояснить, на каком этапе что нужно делать, я составил блок-схему всего процесса.
По собственному опыту знаю, что на составление схемы требуется час, а экономится день.
Схемы я всегда рисую от руки, перерисовывая их по несколько раз, каждый раз добавляются новые детали, да и сам процесс мне очень нравится.
Этапы
На блок-схеме выделены три этапа процесса обновления.
Этап А. Программа запущена в обычном режиме (без ключей).
get up_version
Считываем и проверяем номер версии на сервере.
my_version == up_version?
Если серверная версия совпадает с нашей – пропускаем модуль обновления.
download new.name.exe
Закачиваем новую программу в файл new.name.exe.
% % %% % %
Ожидаем окончание процесса загрузки.
start new.name.exe /u
После окончания загрузки запускаем скачанный файл.
Закрываем программу, чтобы потом её удалить.
Этап Б. Программа запущенна с ключом /u.
del name.exe
Удаляем программу name.exe.
copy new.name.exe name.exe
Копируем new.name.exe в name.exe.
start name.exe /d
Запускаем name.exe с ключом /d.
Закрываем программу, чтобы потом её удалить.
Этап Ц. Программа запущенна с ключом /d:
del new.name.exe
Удаляем временную копию программы new.name.exe
Запускаем основную программу.
Теперь переходим к практической части, как я это всё реализовал в классе на C#.
Основные поля данных
Начнём обзор модуля со знакомства с полями данных, то есть с рабочими константами и переменными.
// Текущая версия проекта, доступная для всего проекта public static string my_version = «1.23»; // Ссылки на txt-файл версии, на exe-файл программы и на сайт private string url_version = «http://localhost/version.txt»; private string url_program = «http://localhost/program.exe»; private string url_foruser = «http://localhost/index.php»; private string my_filename; // Имя файла запущенной программы private string up_filename; // Имя временного файла для загрузки обновления private bool is_download; // Признак, что началось скачивание обновления private bool is_skipped; // Признак, что обновление не требуется или закончено
Запуск!
Работа модуля начинается с его запуска. В каком месте программы это сделать лучше всего? Я перепробовал разные варианты, и самым удачным мне показался вариант его запуска из файла Program.cs
static void Main() < Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); // Инициализация и запуск модуля обновления FormUpdate up = new FormUpdate(); if (up.download()) // Если началось скачивание Application.Run(up); // … ожидаем его окончания if (up.skipped()) // Обновление не требуется или закончено Application.Run(new Form1()); // … запускаем основную программу >
Вся логика модуля обновления выполняется в конструкторе, без визуального отображения. Форма отображается только в процессе скачивания файла.
Метод download() информирует о том, что на этапе «А» началось асинхронное скачивание новой версии программы, в связи с чем нужно отобразить визуальную форму, на которой размещён ProgressBar с текстовым полем, и ждать завершения процесса. Остальные этапы обновления выполняются «молча» и отображение формы пропускается.
Метод skipped() информирует о том, что обновление пропущено (или закончено), программа может быть запущенна в обычном режиме. В ином случае программа должна закрыться для перехода к следующему этапу.
Конструктор
Напишем конструктор класса, который по аргументам командной строки установит, на каком этапе обновления мы находимся, и вызовет соответствующие методы класса.
private FormUpdater() < // Получаем имя запущенной программы (без полного пути) my_filename = get_exec_filename (); // Формируем имя временного файла up_filename = «new.» + my_filename; // Получаем аргументы командной строки string [] keys = Environment.GetCommandLineArgs(); if (keys.Length < 3) // Этап А. Аргументов нет – проверим версию на сервере do_check_update (); else < if (keys[1] == «/u») // Этап Б. Запущена новая версия из временного файла do_copy_downloaded_program (keys [2]); if (keys[1] == «/d») // Этап Ц. Осталось удалить временный файл. do_delete_old_program (keys [2]); >>
Несколько слов о вспомогательном методе get_exec_filename(). В C# можно получить имя запущенного файла только с полным путём. Для изъятия чистого имени файла я написал свой метод, который разбивает путь на части по символу «» и возвращает последнюю его часть – искомое имя файла.
private string get_exec_filename() < string fullname = Application.ExecutablePath; // Например: D:WorkProjectsName.exe string[] split = < «\» >; string[] parts = fullname.Split(split, StringSplitOptions.None); // Получим массив из 4 элементов: D: , Work , Projects , Name.exe if (parts.Length > 0) return parts[parts.Length — 1]; // Последний элемент = искомое имя файла return «»; >
Остальные методы, которые вызываются из конструктора, будут рассмотрены на соответствующих этапах.
Этап «А»
Метод do_check_update() проверяет наличие обновления на сервере и, в зависимости от результата, либо запускает процесс обновления, либо разрешает запуск основной программы.
private void do_check_update() < // получаем номер версии программы на сервере string up_version = get_server_version(); if (my_version == up_version) // Если обновление не нужно < is_download = false; // Пропускаем скачивание is_skipped = true; // Пропускаем модуль обновления >else do_download_update (); // Запускаем скачивание новой версии >
Метод get_server_version() использует стандартный метод класса WebClient для считывания номера версии.
Если номер версии не считывается, логично предположить, что обновление тоже не удастся скачать, поэтому будем считать, что обновления нет.
private string get_server_version() < try < WebClient webClient = new WebClient(); return webClient.DownloadString(url_version).Trim(); >catch < // Если номер версии не можем получить, return my_version; // то программу даже и не будем пытаться. >>
Метод do_download_update() отображает экранную форму и запускает асинхронную загрузку обновлённого файла программы.
private void do_download_update () < InitializeComponent(); // Инициализация формы label_status.Text = «Скачивается файл: » + url_program; download_file (); // Начинаем скачивание is_download = true; // Будем ждать завершение процесса is_skipped = false; // Основную программу не нужно запускать >
Метод download_file() запускает асинхронное скачивание и подключает два события:
для отображения прогресса и для завершения этапа загрузки файла.
private void download_file () < try < WebClient webClient = new WebClient(); // Создаём обработчики событий продвижения прогресса и его окончания webClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler(ProgressChanged); webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(Completed); // Начинаем скачивание webClient.DownloadFileAsync(new Uri(url_program), up_filename); >catch (Exception ex) < // В случае ошибки выводим сообщение и предлагаем скачать вручную error(ex.Message + » » + filename); >>
В случае ошибки вызывается метод error(), который отображает текст ошибки и предлагает скачать файл самостоятельно, в этом случае будет запущен браузер и открыта соответствующая страница. Текст этого метода, думаю, можно опустить.
Из этапа «А» осталось реализовать обработку двух событий:
изменение прогресса скачивания и обработку его окончания.
Метод изменения прогресса написан тривиально.
private void ProgressChanged(object sender, DownloadProgressChangedEventArgs e)
По завершению скачивания необходимо перейти к этапу «Б» и завершить работу.
private void Completed(object sender, AsyncCompletedEventArgs e) < run_program(up_filename, «/u «» + my_filename + «»»); this.Close (); >
Обратите внимание, что кроме ключа /u в программу передаётся исходное имя программы, чтобы вновь запущенная программа знала, как переименовывать файл, согласно 3-ей цели: после обновления имя программы должно сохраниться прежним.
Имя файла в параметре командной строки необходимо заключать в кавычки на случай наличия в нём пробелов.
Поля метода is_download, is_skipped в этом методе устанавливать не нужно, так как этап их проверки в файле Program.cs был пройден сразу после запуска скачивания.
Метод для запуска программы может выглядеть следующим образом.
private void run_program(string filename, string keys) < try < // Использование системных методов для запуска программы System.Diagnostics.Process proc = new System.Diagnostics.Process(); proc.StartInfo.WorkingDirectory = Application.StartupPath; proc.StartInfo.FileName = filename; proc.StartInfo.Arguments = keys; // Аргументы командной строки proc.Start(); // Запускаем! >catch (Exception ex) < error(ex.Message + » » + filename); >>
Итак, с этапом «А» мы разобрались.
Если что-то показалось запутанным, рекомендую ещё раз просмотреть блок-схему и сопоставить методы модуля с элементами на блок-схеме.
Этап «Б»
Переходим к этапу «Б», он будет значительно проще. Из конструктора вызывается метод do_copy_downloaded_program(string filename), который копирует загруженную версию программы на место старой.
void do_copy_downloaded_program(string filename) < try_to_delete_file(filename); // Удаляем файл со старой версией программы try < // Копируем скачанный файл в оригинальное имя файла File.Copy(my_filename, filename); // Запускаем этап «Ц» run_program(filename, «/d «» + my_filename + «»»); is_download = false; // Форма не отображается is_skipped = false; // Обновление ещё не закончено >catch (Exception ex) < error(ex.Message + » » + filename); >>
Несколько слов о методе try_to_delete_file(string filename). Может оказаться так, что мы пытаемся удалить файл, который ещё заблокирован не до конца завершённым процессом предыдущей программы. Этот метод пытается удалять файл несколько раз подряд в течении нескольких секунд с небольшими задержками.
private void try_to_delete_file(string filename) < int loop = 10; // Количество попыток while (—loop >0 File.Exists(filename)) try < File.Delete(filename); >catch < Thread.Sleep(200); // Небольшая задержка >>
Этап «Ц»
Остался последний, самый короткий этап «Ц», который удаляет «мусор» и запускает основную программу. Для этой цели из конструктора вызывается метод do_delete_old_program(string filename).
void do_delete_old_program(string filename) < try_to_delete_file(filename); is_download = false; // Форма не отображается is_skipped = true; // Обновление отработало, запускайте! >
Если версия программы на сервере не будет совпадать с записанной версией в текстовом файле, то при каждом запуске будет повторно загружаться «новая» версия. После этого программа всё-таки будет запущена невзирая на различие версий: по блок-схеме на этапе «Ц» версия программы уже не проверяется. Так и должно быть! Дополнительная проверка на этом этапе рискует зациклить процесс скачивания навечно…
Кстати, эту «фишку» можно использовать для запуска программы без проверки наличия обновлений, достаточно её запускать с ключом «/u».
Заключение
Построенный таким образом модуль обновления удовлетворяет всем целям, сформулированным в начале статьи:
- Обновление скачивается только при наличии новой версии.
- Обновлённая программа автоматически запускается после скачивания.
- Предусмотрен механизм сохранения исходного имени файла программы.
- Модуль сделан не отдельной программой, а встроен в файл проекта.
Работа модуля продемонстрирована на следующем рисунке.
Также могу продемонстрировать работу модуля на собственной программе изучения английских слов на слух. Скачать «старую» версию «Звуковых карточек» можно здесь: http://www.DoubleEnglish.ru/soft/old.ListenCards.exe
Исходный код модуля обновления в тестовом проекте можно скачать здесь:
http://www.fformula.net/docs/updater/updater.zip
Спасибо за внимание.
Волосатов Евгений.
Источник: h.amazingsoftworks.com
Часть II. Как создать автоматическое обновление для настольного приложения (GitHub Actions)
У вас может быть другой способ опубликовать ваши пакеты, но для этого руководства я, например, буду использовать выпуск GitHub.
Во-первых, вам нужно создать папку с именем .github/workflows в корневой папке вашего проекта, затем файл с любым именем с расширением yml в workflows
Папка проекта будет выглядеть примерно так:
YAML-файл
В этом разделе я буду объяснять синтаксис один за другим
- имя — GitHub отобразит имя на странице действий вашего репозитория.
- on — имя события GitHub, запускающего рабочий процесс. Это может быть массив, строка и т. д., но мы будем запускать рабочий процесс при добавлении любых новых тегов.
- по умолчанию —настройка по умолчанию для jobs.run . Мы устанавливаем его с помощью bash по умолчанию
- задания —запуск рабочего процесса состоит из одного или нескольких заданий. По умолчанию задания выполняются параллельно.
- jobs.‹job_id› —каждая вакансия должна иметь идентификатор, чтобы связать ее с вакансией. Ключ job_id представляет собой строку, а его значение представляет собой карту данных конфигурации задания. Здесь мы назвали его release
- jobs.‹job_id›.strategy.matrix —Матрица имеет ключи и значения. Матрица повторно использует конфигурацию задания и создает задание для каждой настроенной вами матрицы, и вы сможете использовать его из matrix контекста. В этом примере мы хотим использовать последнюю версию macos, поэтому используем [macos-latest] . Если вы хотите использовать Ubuntu и Windows, вы можете написать что-то вроде этого:
jobs: release: strategy: matrix: os: [ macos-latest, windows-latest, ubuntu-latest ]
- jobs.‹job_id›.strategy.fail-fast —если установлено значение true , GitHub отменяет все выполняемые задания, если какое-либо задание matrix не выполняется.
- jobs.‹job_id›.runs-on —тип машины, на которой выполняется задание. Машина может быть запущена как на GitHub, так и на собственном сервере. Мы используем $> , это как i в for loop
- jobs.‹job_id›.if —вы можете использовать условное выражение if , чтобы запретить запуск шага, если условие не выполнено. Посмотреть еще
- jobs.‹job_id›.steps.name —имя текущего потока
- jobs.‹job_id›.steps.uses —повторно используемый код действия из GitHub.
- jobs.‹job_id›.steps.run — здесь можно написать одно или несколько действий командной строки.
- jobs.‹job_id›.steps.with —это пара ключ-значение, передающая параметр в action
Шаги
- Checkout — это действие извлекает ваш репозиторий под $GITHUB_WORKSPACE , чтобы ваш рабочий процесс мог получить к нему доступ.
- Настройка Java — мы будем использовать JDK 15 для сборки
- Проверить оболочку Gradle. Это действие проверяет контрольные суммы JAR-файлов Gradle Wrapper, присутствующих в исходном дереве, и завершается ошибкой, если обнаружены неизвестные JAR-файлы Gradle Wrapper.
- Копировать gradle.properties CI — скопировать предопределенные gradle.properties в каталог ~/.gradle .
- Оформить кэш сборки Gradle. Это действие позволяет кэшировать зависимости и выходные данные сборки, чтобы сократить время выполнения рабочего процесса.
- Build Release App — сборка текущего дистрибутива ОС.
- Архивировать артефакты — мы сохраняем созданные артефакты на GitHub и используем их на следующем этапе.
- Release — выпустить артефакты.
Бонус
использованная литература
- https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
- https://docs.github.com/en/actions/reference/authentication-in-a-workflow
- https://github.com/theapache64/stackzy (Вдохновение)
Источник: design-hero.ru