Различные статьи по Qt и c++

Версионирование и обновление справки в установленном приложении (namespace, virtual folders)

Введение

Приложение обновляется, и вместе с ним должна обновляться справка — но пользователь может пропустить несколько версий, обновляясь сразу с очень старой на текущую, и нужно гарантировать, что после обновления приложения он видит актуальную справку, а не устаревшую версию, оставшуюся от предыдущей установки. В статье разберём стратегии версионирования namespace и практический механизм обновления зарегистрированной справки при запуске новой версии приложения.

Концепция

Поскольку namespace в .qhp/.qch уникально идентифицирует справочник для QHelpEngine, есть два основных подхода к версионированию: включать номер версии прямо в namespace (com.mycompany.myapp.1.0, com.mycompany.myapp.2.0), что делает справку разных версий технически разными, независимыми справочниками, которые могут существовать в коллекции одновременно; либо использовать постоянный namespace без версии (com.mycompany.myapp) и явно заменять зарегистрированный .qch на новый при каждом обновлении приложения, используя unregisterDocumentation() перед повторной registerDocumentation().

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

Пример кода: Версионирование и обновление

#pragma once
#include <QHelpEngine>
#include <QSettings>
#include <QCoreApplication>
#include <QDebug>

class HelpUpdater
{
public:
    static bool ensureUpToDateHelp(QHelpEngine &engine, const QString &qchResourcePath,
                                    const QString &expectedNamespace)
    {
        QSettings settings;
        const QString lastRegisteredVersion = settings.value("help/registeredVersion").toString();
        const QString currentAppVersion = QCoreApplication::applicationVersion();

        const bool alreadyRegistered = engine.registeredDocumentations().contains(expectedNamespace);

        if (alreadyRegistered && lastRegisteredVersion == currentAppVersion) {
            qDebug() << "Справка актуальна (версия" << currentAppVersion << ")";
            return true;
        }

        // Если справка уже зарегистрирована, но устарела — снимаем регистрацию
        // перед добавлением новой версии того же namespace
        if (alreadyRegistered) {
            qDebug() << "Обновление справки с версии" << lastRegisteredVersion
                     << "на" << currentAppVersion;
            if (!engine.unregisterDocumentation(expectedNamespace)) {
                qWarning() << "Не удалось снять регистрацию старой версии справки:"
                          << engine.error();
                return false;
            }
        }

        if (!engine.registerDocumentation(qchResourcePath)) {
            qWarning() << "Не удалось зарегистрировать справку:" << engine.error();
            return false;
        }

        settings.setValue("help/registeredVersion", currentAppVersion);
        qDebug() << "Справка успешно обновлена до версии" << currentAppVersion;
        return true;
    }
};
// Использование при старте приложения
#include "helpupdater.h"
#include <QCoreApplication>

int main(int argc, char *argv[])
{
    QCoreApplication::setApplicationVersion("2.3.0");
    QCoreApplication app(argc, argv);

    QHelpEngine helpEngine(getCollectionFilePath());
    helpEngine.setupData();

    HelpUpdater::ensureUpToDateHelp(helpEngine, ":/docs/myapp.qch",
                                     "com.mycompany.myapp");

    return app.exec();
}

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

ensureUpToDateHelp() хранит версию приложения, для которой справка была последний раз зарегистрирована, в QSettings — при следующем запуске сравнивается текущая версия приложения (QCoreApplication::applicationVersion(), которую важно устанавливать явно при сборке релиза) с сохранённой. Если версии совпадают, повторная регистрация не требуется — это избегает ненужной работы при каждом обычном запуске приложения между обновлениями.

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

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

  • Несоответствие QCoreApplication::applicationVersion() фактической версии .qch-файла, если разработчик обновил содержимое справки, но забыл синхронно поднять версию приложения (или наоборот) — логика обновления в примере опирается именно на версию приложения как индикатор необходимости обновления справки, и рассинхронизация этих двух версионных схем может привести к тому, что обновлённый контент справки не будет переустановлен, хотя файл .qch уже изменился.
  • Параллельный запуск нескольких экземпляров приложения сразу после обновления, каждый из которых пытается одновременно выполнить unregisterDocumentation()/registerDocumentation() для общего файла коллекции — это может привести к гонке при обращении к одному файлу .qhc с диска, особенно если коллекция расположена в общей для всех пользователей системы директории, а не в пользовательском профиле.
  • Откат версии приложения (даунгрейд) без соответствующей логики откатывания справки. Если пользователь по какой-то причине переустанавливает более старую версию приложения после того, как уже использовал более новую, сохранённое в QSettings значение версии «новее», чем фактическая версия выполняемого приложения, и логика lastRegisteredVersion == currentAppVersion определит несовпадение и попытается зарегистрировать .qch, поставляемый именно с этой (более старой) версией инсталлятора — что в большинстве случаев корректно, но стоит явно протестировать сценарий даунгрейда, а не только обновления вперёд.
  • Хранение версии в QSettings без учёта того, что несколько разных профилей пользователей на одной машине могут иметь разные значения настроек — если коллекция справки общая на уровне системы, а версия в QSettings — на уровне пользователя, разные пользователи могут получить противоречивую картину состояния регистрации справки между параллельными профилями.