Как я писал модуль обновления на C#

в 18:07, , рубрики: .net, автообновление, модуль обновления, Программирование, метки: , , , ,

Я пишу программы на C# для фирмы, где их использует несколько сотен человек. Время от времени добавляются новые функции и встаёт проблема обновления версий.

Я решил не искать стандартных громоздких решений, а изобрести свой собственный велосипед для автоматического обновления установленных программ.

image

Честно, мне самому не очень нравятся приложения, которые вечно скачивают обновления, но в моём случае проще автоматизировать этот процесс, чем писать должностные инструкции и заставлять коллег скачивать обновления вручную (а потом бегать по всем этажам и делать это самому).

Цели

Прежде чем начать работу над этим проектом, я сформулировал цели, которым должен удовлетворять будущий модуль авто-обновления.

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

И в чём, казалось бы, проблема? Проверил наличие новой версии. Скачал файл. Запустил. Всё!

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

А для выполнения всех необходимых действий в рамках одной программы:

  • скачать новую версию,
  • удалить старую программу,
  • переименовать скачанный файл

– необходимо несколько раз её перезапускать, чтобы последовательно выполнять перечисленные действия.

Блок-схема

Чтобы прояснить, на каком этапе что нужно делать, я составил блок-схему всего процесса.
По собственному опыту знаю, что на составление схемы требуется час, а экономится день.

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

Этапы

На блок-схеме выделены три этапа процесса обновления.

Этап А. Программа запущена в обычном режиме (без ключей).
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)
    {
        progress_download.Value = e.ProgressPercentage;
    }

По завершению скачивания необходимо перейти к этапу «Б» и завершить работу.

    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».

Заключение

Построенный таким образом модуль обновления удовлетворяет всем целям, сформулированным в начале статьи:

  1. Обновление скачивается только при наличии новой версии.
  2. Обновлённая программа автоматически запускается после скачивания.
  3. Предусмотрен механизм сохранения исходного имени файла программы.
  4. Модуль сделан не отдельной программой, а встроен в файл проекта.

Работа модуля продемонстрирована на следующем рисунке.
image

Также могу продемонстрировать работу модуля на собственной программе изучения английских слов на слух. Скачать «старую» версию «Звуковых карточек» можно здесь: http://www.DoubleEnglish.ru/soft/old.ListenCards.exe

Исходный код модуля обновления в тестовом проекте можно скачать здесь:
http://www.fformula.net/docs/updater/updater.zip

Спасибо за внимание.
Волосатов Евгений.

Автор: FFormula

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js