Skip to content

aui::updater#

Deliver updates on non-centralized distribution methods

Detailed Description#

Experimental Feature

This API is experimental. Experimental APIs are likely to contain bugs, might be changed or removed in the future.

This module is purposed for delivering updates to your end users on distribution methods that do not support that by themselves (i.e., occasional Windows installers, portables for Windows and Linux, macOS app bundles downloaded from your website).

aui.updater module expects your program to be installed to user's directory (i.e., updating does not require admin privileges). If that's not your case, you'll need to update your installer configuration (Inno Setup) to install to user's directory (i.e., in AppData).

Note

Check out our App Template ⚡ for a GitHub-hosted app template with auto update implemented.

Supported platforms#

aui::updater supports the following platforms:

On a supported platform, aui::updater checks if the app executable is writable by the current user. If the executable is not writeable, or running on a non-supported platform, AUpdater stubs it's methods (i.e., they do nothing). You can check that the aui::updater functionality is engaged by calling AUpdater::isAvailable().

Updating process requires the initial application running instance to be stopped to replace its files with newer ones. Additionally, the updater process starts the newer version of the application after replacing the files (applying/deploying an update). To minimize downtime for end-users, the replacement should be seamless and quick and thus the deployment process just copies newer files (overwriting old ones), it does not involve network operations.

Getting started#

AUpdater lives inside entrypoint of your application. It needs you to pass program arguments. It might decide to terminate process execution via std::exit.

class MyUpdater : public AUpdater {
protected:
    AFuture<void> checkForUpdatesImpl() override { return AUI_THREADPOOL { /* stub */ }; }
    AFuture<void> downloadUpdateImpl(const APath& unpackedUpdateDir) override { return AUI_THREADPOOL { /* stub */ }; }
};

AUI_ENTRY {
    auto updater = _new<MyUpdater>();
    updater->handleStartup(args);

    // your program routines (i.e., open a window)
    _new<MainWindow>(updater)->show();
    return 0;
}

You can pass updater instance to your window (as shown in the example) and display update information from AUpdater::status and perform the update when requested.

Observing update progress#

See AUpdater::status

Update process#

Checking for updates#

AUpdater expects AUpdater::checkForUpdates() to be called to check for updates. It can be called once per some period of time. It calls user-defined AUpdater::checkForUpdatesImpl() to perform an update checking.

The update steps are reported by changing AUpdater::status property.

sequenceDiagram
 autonumber
 participant a as Your App
 participant u as AUpdater

 a->>u: handleStartup(...)
 u-->>a: status = AUpdater::StatusIdle
 u-->>a: control flow

 Note over a,u: App Normal Lifecycle
 a->>u: checkForUpdates()
 u-->>a: status = AUpdater::StatusCheckingForUpdates
 u-->>a: control flow
 u->>u: checkForUpdatesImpl()
 u-->>a: status = AUpdater::StatusIdle

 Note over a,u: update published
 a->>u: checkForUpdates()
 u-->>a: status = AUpdater::StatusCheckingForUpdates
 u-->>a: control flow
 u->>u: checkForUpdatesImpl()
 Note over u: update was found
 u-->>a: status = AUpdater::StatusIdle

You might want to store update check results (i.e., download url) in your implementation of AUpdater::checkForUpdatesImpl so your AUpdater::downloadUpdateImpl() might reuse this information.

Downloading the update#

When an update is found, your app should call AUpdater::downloadUpdate() to download and unpack the update. It is up to you to decide when to download an update. If you wish, you can call AUpdater::downloadUpdate in AUpdater::checkForUpdatesImpl() to proceed to download process right after update was found (see Updater workflows for more information about update workflow decisions). It calls user-defined AUpdater::downloadUpdateImpl() which might choose to call default AUpdater::downloadAndUnpack(<YOUR DOWNLOAD URL>, unpackedUpdateDir).

sequenceDiagram
   autonumber
   participant a as Your App
   participant u as AUpdater

   a->>u: downloadUpdate()
   u-->>a: status = AUpdater::StatusDownloading
   u->>u: downloadUpdateImpl()
   u-->>a: status = AUpdater::StatusWaitingForApplyAndRestart

   Note over a,u: Your App Prompts User to Update

Applying (deploying) the update#

At this moment, AUpdater waits AUpdater::applyUpdateAndRestart() to be called. When AUpdater::applyUpdateAndRestart() is called (i.e., when user accepted update installation), AUpdater executes the newer copy of your app downloaded before with a special command line argument which is handled by AUpdater::handleStartup() in that executable. The initial app process is finished, closing your app window as well. From now, your app is in "downtime" state, so we need to apply the update and reopen app back again as quickly as possible. This action is required to perform update installation. The copy then replaces old application (where it actually installed) with itself (that is, the downloaded, newer copy). After operation is complete, it passes the control back to the updated application executable. At last, the newly updated application performs a cleanup after update.

sequenceDiagram
   autonumber
   participant a as Your App
   participant u as AUpdater
   u -->> a: applyUpdateAndRestart()
   create participant da as Your App Copy
   u -->> da: Execute with update arg
   u -->> u: exit(0)
   Note over a,u: Process Finished
   create participant du as AUpdater Copy
   da -->> du: handleStartup
   du -->> du: deployUpdate(...)
   du -->> a: Execute
   destroy du
   du -->> da: exit(0)
   destroy da
   da-->a:
   Note over a,u: Process Started
   a -->> u: handleStartup
   u -->> u: cleanup download dir
   a --x u: App Normal Lifecycle

After these operations complete, your app is running in its normal lifecycle.

Typical Implementation#

AUpdater is an abstract class; it needs some functions to be implemented by you.

In this example, let's implement auto update from GitHub release pages.

static constexpr auto LOG_TAG = "MyUpdater";
class MyUpdater: public AUpdater {
public:
    ~MyUpdater() override = default;

protected:
    AFuture<void> checkForUpdatesImpl() override {
        return AUI_THREADPOOL {
            try {
                auto githubLatestRelease = aui::updater::github::latestRelease("aui-framework", "example_app");
                ALogger::info(LOG_TAG) << "Found latest release: " << githubLatestRelease.tag_name;
                auto ourVersion = aui::updater::Semver::fromString(AUI_PP_STRINGIZE(AUI_CMAKE_PROJECT_VERSION));
                auto theirVersion = aui::updater::Semver::fromString(githubLatestRelease.tag_name);

                if (theirVersion <= ourVersion) {
                    getThread()->enqueue([] {
                      AMessageBox::show(
                          nullptr, "No updates found", "You are running the latest version.", AMessageBox::Icon::INFO);
                    });
                    return;
                }
                aui::updater::AppropriatePortablePackagePredicate predicate {};
                auto it = ranges::find_if(
                    githubLatestRelease.assets, predicate, &aui::updater::github::LatestReleaseResponse::Asset::name);
                if (it == ranges::end(githubLatestRelease.assets)) {
                    ALogger::warn(LOG_TAG)
                        << "Newer version was found but a package appropriate for your platform is not available. "
                           "Expected: "
                        << predicate.getQualifierDebug() << ", got: "
                        << (githubLatestRelease.assets |
                            ranges::view::transform(&aui::updater::github::LatestReleaseResponse::Asset::name));
                    return;
                }
                ALogger::info(LOG_TAG) << "To download: " << (mDownloadUrl = it->browser_download_url);

                getThread()->enqueue([this, self = shared_from_this(), version = githubLatestRelease.tag_name] {
                    if (AMessageBox::show(
                            nullptr, "New version found!", "Found version: {}\n\nWould you like to update?"_format(version),
                            AMessageBox::Icon::INFO, AMessageBox::Button::YES_NO) != AMessageBox::ResultButton::YES) {
                        return;
                    }

                    downloadUpdate();
                });

            } catch (const AException& e) {
                ALogger::err(LOG_TAG) << "Can't check for updates: " << e;
                getThread()->enqueue([] {
                    AMessageBox::show(
                        nullptr, "Oops!", "There is an error occurred while checking for updates. Please try again later.",
                        AMessageBox::Icon::CRITICAL);
                });
            }
        };
    }

    AFuture<void> downloadUpdateImpl(const APath& unpackedUpdateDir) override {
        return AUI_THREADPOOL {
          try {
              AUI_ASSERTX(!mDownloadUrl.empty(), "make a successful call to checkForUpdates first");
              downloadAndUnpack(mDownloadUrl, unpackedUpdateDir);
              reportReadyToApplyAndRestart(makeDefaultInstallationCmdline());
          } catch (const AException& e) {
              ALogger::err(LOG_TAG) << "Can't check for updates: " << e;
              getThread()->enqueue([] {
                AMessageBox::show(
                    nullptr, "Oops!", "There is an error occurred while downloading update. Please try again later.",
                    AMessageBox::Icon::CRITICAL);
              });
          }
        };
    }

private:
    AString mDownloadUrl;
};

Updater workflows#

When using AUpdater for your application, you need to consider several factors including usability, user experience, system resources, and particular needs of your project.

Either way, you might want to implement a way to disable auto update feature in your application.

Prompt user on every step#

This approach is implemented in AUI's App Template ⚡.

The updater checks for updater periodically or upon user request and informs the user that an update is available. The user then decides whether to proceed with update or not. If they agree the application will download and install the update.

This way can be considered as better approach because the user may feel they control the situation and the application never does things that user never asked to (trust concerns). On the other hand, such requirement of additional user interaction to can distract them from doing their work, so these interactions should not be annoying.

You should not use AMessageBox (unless user explicitly asked to check for update) as it literally interrupts the user's workflow, opting them to make a decision before they can continue their work. A great example of a bad auto update implementation is qBittorrent client on Windows: hence this application typically launches on OS startup, it checks for updates in background and pops the message box if update was found, even if user is focused on another application or away from keyboard.

Silent download#

This approach is implemented in AUI Telegram Client (AUIgram), as well as in official Qt-based Telegram Desktop client.

The updater silently downloads the update in the background while the user continues working within the application or even other tasks. The update then is applied automatically upon restart. Optionally, the application might show a button/message/notification bubble to restart and apply update.

Despite user trust concerns, this approach allows seamless experience - users don't need to be interrupted during their work. They even might not care about updates.