AUI Framework  develop
Cross-platform base for C++ UI apps
Loading...
Searching...
No Matches
aui::updater

Deliver updates on non-centralized distribution methods. More...

Detailed Description#

Warning
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 priveleges). If that's not your case, you'll need to update your installer configuration 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:

  • Windows - portables only, installers to user's directory only (Inno Setup)
  • Linux - portables only

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 async { /* stub */ }; }
    AFuture<void> downloadUpdateImpl(const APath& unpackedUpdateDir) override { return async { /* stub */ }; }
};
    auto updater = _new<MyUpdater>();
    updater->handleStartup(args);
    // your program routines (i.e., open a window)
    _new<MainWindow>(updater)->show();
    return 0;
}
Represents a value that will be available at some point in the future.
Definition AFuture.h:621
Updater class.
Definition AUpdater.h:30
virtual AFuture< void > downloadUpdateImpl(const APath &unpackedUpdateDir)=0
Performs update delivery to the specified directory.
virtual AFuture< void > checkForUpdatesImpl()=0
Check for updates user's implementation.
#define AUI_ENTRY
Application entry point.
Definition Entry.h:90
#define async
Executes following {} block asynchronously in the global thread pool. Unlike asyncX,...
Definition kAUI.h:329

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#

Status of the AUpdater to observe from outside, i.e., by UI.

status is updated in UI thread only.

status is designed in such a way the user can use their own custom status types or any of predefined ones:

These statuses might be set by AUpdater itself.

Typical observer of status is a UI projection displaying its value. You can even display controls in it:

CustomLayout {} & mUpdater->status.readProjected([&updater = mUpdater](const std::any& status) -> _<AView> {
    if (std::any_cast<AUpdater::StatusIdle>(&status)) {
        return _new<AButton>("Check for updates").connect(&AView::clicked, slot(updater)::checkForUpdates);
    }
    if (std::any_cast<AUpdater::StatusCheckingForUpdates>(&status)) {
        return Label { "Checking for updates..." };
    }
    if (auto downloading = std::any_cast<AUpdater::StatusDownloading>(&status)) {
        return Vertical {
            Label { "Downloading..." },
            _new<AProgressBar>() & downloading->progress,
        };
    }
    if (std::any_cast<AUpdater::StatusWaitingForApplyAndRestart>(&status)) {
        return _new<AButton>("Apply update and restart")
            .connect(&AView::clicked, slot(updater)::applyUpdateAndRestart);
    }
    return nullptr;
}),
emits clicked
Left mouse button clicked.
Definition AView.h:933
An std::weak_ptr with AUI extensions.
Definition SharedPtrTypes.h:179
#define slot(v)
Passes some variable and type of the variable separated by comma. It's convenient to use with the con...
Definition kAUI.h:88

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.

msc_inline_mscgraph_1

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

msc_inline_mscgraph_2

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.

msc_inline_mscgraph_3

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

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 async {
            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 async {
          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;
};
Abstract AUI exception.
Definition AException.h:28
An add-on to AString with functions for working with the path.
Definition APath.h:128
Represents a Unicode character string.
Definition AString.h:38
#define AUI_PP_STRINGIZE(...)
Expands and stringifies the only argument.
Definition APreprocessor.h:28
#define AUI_ASSERTX(condition, what)
Asserts that the passed condition evaluates to true. Adds extra message string.
Definition Assert.h:74
API_AUI_VIEWS ResultButton show(AWindow *parent, const AString &title, const AString &message, Icon icon=Icon::NONE, Button b=Button::OK)
Displays a message box, blocking the caller thread until the user dismisses the message.
@ INFO
Information icon.
Definition AMessageBox.h:63
@ CRITICAL
Critical icon.
Definition AMessageBox.h:73
static Semver fromString(const AString &version)
Parse semver from string.

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.

Classes#

struct  aui::updater::AppropriatePortablePackagePredicate
 Determines whether the passed package name is a portable package that matches current arch and platform. More...
 
class  AUpdater
 Updater class. More...
 
struct  aui::updater::Semver
 Semantic version. More...