Application lifetime & instancing#
This page explains:
- when an AUI application exits;
- how to keep it running without windows (background, tray, daemon);
- what happens when the user launches an application that is already running, and how to make it single instanced.
Lifetime#
AUI_ENTRY is not your main loop. On desktop platforms, AUI runs the main event loop after AUI_ENTRY returns,
while something holds the application:
flowchart TD
A[process start] --> B[AUI_ENTRY]
B --> C{is application held?}
C -- no --> E[cleanup, exit with AUI_ENTRY's code]
C -- yes --> D[main event loop]
D -- last hold released or AApplication::quit --> E
The application is held while:
| Condition | Default |
|---|---|
at least one AWindow is shown (not closed; hidden windows still count) |
yes, controlled by AApplication::inst().quitOnLastWindowClosed |
at least one AApplication::Hold exists |
none |
AApplication::quit(exitCode) stops the loop regardless of holds.
Behaviour by application type#
| Application type | Holds after AUI_ENTRY |
Result | Code changes |
|---|---|---|---|
CLI, aui::core only |
0 | exits right after AUI_ENTRY |
none |
CLI, linked with aui::views, no windows shown |
0 | exits right after AUI_ENTRY |
none |
UI, _new<MyWindow>()->show() |
windows | runs until the last window is closed | none |
| UI with background mode | windows + hold() |
runs until quit() or all holds are released |
opt-in |
| Daemon without windows | hold() |
runs AEventLoop until quit() |
opt-in |
Embedded (SDL, GLFW, custom loop via AGLEmbedContext) |
0 | your loop runs inside AUI_ENTRY, then exits |
none |
| Android, iOS | n/a | the OS controls the lifetime | none |
CLI applications
AUI_ENTRY is a regular entry point for console utilities, including the ones linked with aui::views. Linking
aui::views alone does not make the application stay alive: if you don't show an AWindow and don't call
AApplication::hold(), the process exits right after AUI_ENTRY returns, just like a plain main.
Embedding AUI into another windowing toolkit (SDL, GLFW, etc.)
Such applications run their own loop inside AUI_ENTRY and must call AThread::processMessages() regularly.
AGLEmbedContext is not an AWindow, so it does not hold the application; holds and AApplication::quit() do
not affect your loop. AUI does not install process-wide handlers (like NSApplication delegate) and does not
initialize the native platform abstraction unless you show an AWindow.
Single instance lock works in embedded applications, too: activated is delivered through the AUI message queue,
so it is emitted from your AThread::processMessages() call.
Keeping the application alive without windows#
class Tracker: public AObject {
public:
// the application keeps running while Tracker is alive, even if all windows are closed
_<AApplication::Hold> hold = AApplication::inst().hold();
...
};
Warning
AUI_ENTRY returns before the main loop starts. A Hold stored in a local variable of AUI_ENTRY is released
right away. Store it in a long-living object (i.e., the application state shared with the window).
To stop holding, destroy the Hold (hold.reset()). When the last hold is released, the main loop exits and the
process terminates with the exit code returned from AUI_ENTRY.
Warning
A held application without windows is invisible for the user. Provide a way to bring the window back: single instance activation (see below), a tray icon, or a global shortcut. Otherwise, the user would only be able to kill the process.
Multiple instances#
AUI_ENTRY is called every time the executable is launched, including when the user tries to launch an
application that is already running (clicks the desktop shortcut again, opens it from the start menu, etc.).
By default, each launch is an independent process:
| Platform | Default | Single instance |
|---|---|---|
| Windows | multi instance | AApplication::requestSingleInstanceLock() |
| Linux | multi instance | AApplication::requestSingleInstanceLock() |
| macOS | single instance (Launch Services); open -n makes new ones |
AApplication::requestSingleInstanceLock(); clicking the Dock icon emits activated |
| Android | single instance (OS) | always |
| iOS | single instance (OS) | always |
| Emscripten | each browser tab is an instance | not applicable |
Single instance application#
Call AApplication::requestSingleInstanceLock() at the very beginning of AUI_ENTRY. It returns false if another
instance is already running. In that case, the activation of the current process (arguments, working directory,
activation token) has already been delivered to the running instance, and you should just return:
#include <AUI/Platform/Entry.h>
#include <AUI/Platform/AApplication.h>
AUI_ENTRY {
// anything that may relaunch the executable (i.e., an updater) should be handled before the lock
if (!AApplication::inst().requestSingleInstanceLock()) {
// another instance is running and has been notified
return 0;
}
auto window = _new<MainWindow>();
window->show();
AObject::connect(AApplication::inst().activated, window, [window = window.get()](const AActivation& activation) {
// somebody launched the application again: bring the window to front
window->activate(activation.activationToken);
});
return 0;
}
- the lock key defaults to the application id specified in aui_app (
ID). If the id is not set, the call throws, otherwise unrelated AUI applications would share the same lock. You can also pass a custom key; for example, include a profile name in it to have one instance per profile. activatedis emitted on the main thread. It is not emitted for the first launch; handle that inAUI_ENTRY.AActivation::argscontains the full command line of the launched process, includingargv[0].
sequenceDiagram
actor User
participant S as 2nd instance
participant OS
participant P as 1st instance
User->>S: launch
S->>S: AUI_ENTRY
S->>OS: requestSingleInstanceLock(app_id)
OS-->>S: already owned
S->>OS: grant foreground (AllowSetForegroundWindow / activation token)
S->>P: AActivation { args, workingDir, activationToken }
P-->>S: ack
S-->>S: requestSingleInstanceLock() == false, return 0
P->>P: emit AApplication::activated
P->>P: window->activate(token)
Forwarding is synchronous: when requestSingleInstanceLock() returns false, the primary instance has already
received the message.
The lock is released by the OS if the primary instance crashes, so a stale lock never prevents the application from starting.
Single instance application with a background process#
A typical pattern for trackers, messengers, sync clients, etc.:
- the application works in background (no windows), optionally started by autorun with
--background; - closing the window just hides it and the application keeps working;
- launching the application again shows the existing window instead of starting a new process.
This is how AUIwarden (a screen time tracker) does it. The background
work is kept alive by an explicit AApplication::Hold stored in the application state; the window is created lazily
and merely hidden when the user closes it:
#include <AUI/Platform/Entry.h>
#include <AUI/Platform/AApplication.h>
struct State {
// keeps the application running even when there are no (visible) windows
_<AApplication::Hold> lifetimeHold;
// ... settings, database, timers, etc.
};
static AArc<MainWindow> gMainWindow;
AUI_ENTRY {
// 1. things that may relaunch the executable (i.e., updater) go BEFORE the lock
// 2. lock: if another instance is running, it has been notified and will show its window
if (!AApplication::inst().requestSingleInstanceLock()) {
return 0;
}
// 3. the Hold lives in long-living state, not in a local variable of AUI_ENTRY
auto state = _new<State>();
state->lifetimeHold = AApplication::inst().hold();
// 4. second launch: create the window if it was destroyed/never created, and bring it to front
AObject::connect(AApplication::inst().activated, AObject::GENERIC_OBSERVER, [=](const AActivation& activation) {
if (!gMainWindow) {
gMainWindow = _new<MainWindow>(state);
}
gMainWindow->activate(activation.activationToken);
});
// 5. autorun: stay in background without showing the window
if (args.contains("--startup") && !state->settings.showProgramWindowOnStartup) {
return 0;
}
gMainWindow = _new<MainWindow>(state);
gMainWindow->show();
return 0;
}
The window decides whether closing it exits the application by releasing the hold:
void MainWindow::onCloseButtonClicked() {
hide(); // instead of AWindow::onCloseButtonClicked(), which calls close()
if (!mState->settings.allowBackgroundWork) {
// nothing keeps the application alive anymore: it exits
mState->lifetimeHold.reset();
}
}
void MainWindow::onKeyDown(AInput::Key key) {
AWindow::onKeyDown(key);
if (AInput::isKeyDown(AInput::LCONTROL) && key == AInput::Key::Q) {
// explicit quit regardless of the background mode
close();
mState->lifetimeHold.reset();
}
}
- Make background work optional (a user setting) and always provide an explicit way to quit (a shortcut, a button or
a tray menu entry): releasing the
Hold(orAApplication::quit()) ends the process. - Pass
AObject::GENERIC_OBSERVERas the receiver for theactivatedconnection inAUI_ENTRY: it needs no receiver object, sinceAApplication::inst()outlives everything. - Code that may relaunch the executable (an updater, an elevation helper, etc.) should run before
requestSingleInstanceLock(); otherwise the relaunched process would be treated as a second instance.
Tip
Creating the window lazily in the activated handler frees UI resources while the application is in background.
Hiding the window (as above) is cheaper to restore; destroying it is lighter on memory.
Implementation details#
| Platform | Lock | Activation channel |
|---|---|---|
| Windows | named mutex Local\aui.<key>.<session id> |
named pipe \\.\pipe\aui.<key>.<session id> |
| Linux (preferred) | GApplication (session D-Bus name equal to the key) |
command-line signal of the primary instance |
| Linux (fallback), macOS | flock on $XDG_RUNTIME_DIR/aui-<key>-<uid>.lock ($XDG_RUNTIME_DIR/app/$FLATPAK_ID in Flatpak) |
unix domain socket next to the lock file |
On Linux, the GApplication backend is used when the key is a valid application id (i.e., com.example.app),
libgio-2.0.so.0 can be loaded and the session bus is reachable. Otherwise, AUI silently falls back to flock + unix
socket. The backend can be forced off with AUI_SINGLE_INSTANCE_BACKEND=flock.
- The scope is the current user session: different users (or Windows sessions) have independent instances.
- On Linux,
AActivation::activationTokenisXDG_ACTIVATION_TOKEN(Wayland) orDESKTOP_STARTUP_ID(X11) of the launched process.AWindow::activatepasses it to the window system so the compositor lets the running instance take focus. - On Windows, the launched process calls
AllowSetForegroundWindowbefore forwarding the activation, soAWindow::activateis allowed to bring the window to foreground. - On macOS, the application menu's "Quit" (Cmd+Q) calls
AApplication::quit()and returns control to AUI instead of callingexit(), so cleanup and destructors run normally.
The low-level primitive is available as ASingleInstance if you need a lock outside of AApplication.