DevOps

Механизм автообновления через QtIFW maintenance tool

Введение

Установив приложение один раз, пользователь должен иметь возможность получать обновления без необходимости вручную скачивать и повторно запускать полный установщик каждый раз. QtIFW решает эту задачу через «maintenance tool» — отдельный исполняемый файл, устанавливаемый вместе с самим приложением (упоминался в статье 210 через параметр MaintenanceToolName), который умеет проверять наличие новых версий компонентов на сервере и устанавливать только изменившиеся файлы, а не весь дистрибутив целиком. В статье разберём настройку этого механизма.

Концепция

Для работы автообновления необходим онлайн-репозиторий — структура файлов на веб-сервере (или просто в файловой системе, доступной по сети), сгенерированная через инструмент repogen на основе того же набора пакетов, что используется для офлайн-установщика. Maintenance tool, запущенный пользователем (либо явно через меню, либо автоматически по расписанию, если приложение само инициирует такую проверку), обращается к URL этого репозитория, сравнивает версии установленных компонентов с версиями, доступными в репозитории, и предлагает пользователю установить найденные обновления.

Пример кода: Механизм автообновления через

# Генерация онлайн-репозитория из тех же пакетов, что использовались для офлайн-установщика
repogen -p packages --update-new-components my_online_repository

# Структура результата:
# my_online_repository/
# ├── Updates.xml          — манифест доступных компонентов и версий
# └── com.mycompany.myapp/
#     └── 1.1.0meta.7z      — метаданные и файлы версии 1.1.0

# Этот репозиторий нужно разместить на веб-сервере, например через простой rsync
rsync -avz my_online_repository/ user@updates.mycompany.com:/var/www/myapp-updates/
<!-- config/config.xml — добавление ссылки на онлайн-репозиторий для проверки обновлений -->
<?xml version="1.0" encoding="UTF-8"?>
<Installer>
    <Name>MyApp</Name>
    <Version>1.0.0</Version>
    <MaintenanceToolName>MyAppUpdater</MaintenanceToolName>
    <RemoteRepositories>
        <RemoteRepository>
            <Url>https://updates.mycompany.com/myapp-updates</Url>
            <Enabled>1</Enabled>
        </RemoteRepository>
    </RemoteRepositories>
</Installer>
// Программная инициация проверки обновлений из самого приложения
#include <QProcess>
#include <QCoreApplication>
#include <QDir>

void checkForUpdatesInBackground()
{
    const QString maintenanceToolPath = QCoreApplication::applicationDirPath()
                                       + "/MyAppUpdater";

    QProcess *updateCheckProcess = new QProcess();
    updateCheckProcess->start(maintenanceToolPath, {"--checkupdates"});

    QObject::connect(updateCheckProcess, &QProcess::finished,
        [updateCheckProcess](int exitCode, QProcess::ExitStatus) {
        const QString output = updateCheckProcess->readAllStandardOutput();
        if (output.contains("<update")) {
            qDebug() << "Доступно обновление приложения";
            // Здесь можно показать пользователю уведомление и предложить
            // запустить полный maintenance tool для установки
        }
        updateCheckProcess->deleteLater();
    });
}

Пояснения к коду

repogen --update-new-components генерирует структуру онлайн-репозитория, центральным элементом которой является Updates.xml — манифест, описывающий все доступные компоненты и их версии; именно этот файл maintenance tool скачивает первым при проверке обновлений, чтобы сравнить с локально установленными версиями без необходимости скачивать сразу весь объём данных каждого компонента. Размещение сгенерированного репозитория на веб-сервере через простой rsync показывает, что технически репозиторий — это просто статические файлы, не требующие никакой специальной серверной логики, кроме обычной отдачи файлов по HTTP/HTTPS.

checkForUpdatesInBackground() демонстрирует, как само приложение (а не только пользователь, вручную запускающий maintenance tool) может программно инициировать фоновую проверку через флаг --checkupdates, который не выполняет реальную установку, а только проверяет и возвращает информацию о доступности новых версий — это позволяет приложению самостоятельно решить, когда и как уведомить пользователя о доступном обновлении, не вынуждая его помнить о необходимости периодически запускать maintenance tool вручную.

Подводные камни

  • Несовпадение версии компонента в локально установленном package.xml и в Updates.xml онлайн-репозитория при некорректном процессе релиза (например, repogen запущен на устаревшем наборе пакетов) — приведёт к тому, что maintenance tool не обнаружит реально существующее обновление, либо, что хуже, попытается «обновить» до версии, формально не новее текущей, что может смутить пользователя сообщением об отсутствии изменений при реально установленной более старой функциональности.
  • HTTPS для онлайн-репозитория — не опция, а необходимость для безопасности процесса автообновления. Если репозиторий обновлений доступен только по обычному HTTP, злоумышленник, способный выполнить атаку «человек посередине» на сетевом уровне, теоретически может подменить содержимое обновления на вредоносное — несмотря на то что сами файлы данных внутри репозитория QtIFW поддерживают отдельную возможность подписи, транспортный уровень в виде HTTPS остаётся базовым, необходимым уровнем защиты.
  • Парсинг вывода --checkupdates через простую проверку наличия подстроки (output.contains("<update") в примере) — хрупкий подход, чувствительный к изменению формата вывода между версиями самого QtIFW; для более надёжной интеграции стоит либо использовать более структурированный разбор XML-вывода через QXmlStreamReader, либо проверить, предоставляет ли конкретная используемая версия QtIFW более удобный, машиночитаемый формат вывода для программной интеграции (например, через код возврата процесса вместо разбора текста).
  • Частые автоматические фоновые проверки обновлений, инициируемые приложением при каждом запуске без какого-либо троттлинга, создают ненужную постоянную нагрузку на серверную инфраструктуру обновлений при большом количестве одновременных пользователей приложения — стоит реализовать разумный интервал между проверками (например, не чаще раза в день), кэшируя результат последней успешной проверки локально, а не выполняя сетевой запрос при абсолютно каждом запуске приложения.