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:
- Windows - portables (AUI_PORTABLE_ZIP, AUI_PORTABLE_TGZ) 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 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.
Related Pages#
-
Updater class.
-
Semantic version.
-
aui::updater::AppropriatePortablePackagePredicate
Determines whether the passed package name is a portable package that matches current arch and platform.