Tentackle FX RDC Update — Auto-Update for the Rich Desktop Client¶
Overview and Motivation¶
The tentackle-fx-rdc-update module is the JavaFX desktop integration of the application self-update feature.
It is the layer that turns the transport-neutral mechanics of tentackle-update
into an actual user experience: detect that the running client is outdated, ask the user whether to install the new
version, show a download/installation progress bar, swap the image and restart — all without a separate installer
or admin rights.
It does this by extending two extension points of the Rich Desktop Client framework
(tentackle-fx-rdc):
UpdatableDesktopApplication— a drop-in base class for the application's main class that adds single-instance protection and wires in the update-aware login-failure handler.LoginFailedWithUpdateHandler— a specializedLoginFailedHandlerthat interprets a version/handshake failure at login as "the client is too old" and drives the whole download-and-restart flow.
The module deliberately holds only the FX-specific orchestration and its localized messages. The service
contract, the data records (ClientInfo/UpdateInfo), the download/checksum helpers and the server-side service
registration all live in tentackle-update; this module
consumes them.
Design principles¶
- Update-on-login, not background polling. The natural moment to discover an incompatible version is when the
client tries to log in to a server that has since been upgraded. Rather than poll, the module hooks the
login-failure path: a
VersionIncompatibleException(or an SSL handshake error caused by a new server certificate) becomes the trigger to check for an update. - Only self-update when self-update is possible. Everything is gated on
ClientUpdateUtilities.determineInstallationType()returning non-null — i.e., the client really runs from an updatable, writable jlink/jpackage image. Otherwise, the user is told to contact their administrator. - User in control, with an unattended escape hatch. By default, the user is asked before installing, with a
short countdown that auto-confirms — convenient for kiosks/dashboards — but the relevant methods are
protectedso an application can update silently. - Seamless restart. Where a
Cryptoris available, the current credentials are encrypted and passed to the relaunched process via environment variables so the user does not have to log in again after the update.
Key Concepts¶
UpdatableDesktopApplication¶
UpdatableDesktopApplication<C> extends the
RDC DesktopApplication<C> (where C is the main controller type) and adds two things:
- It installs the update-aware login handler. It overrides
createLoginFailedHandler(view, sessionInfo)to return aLoginFailedWithUpdateHandlerinstead of the plain RDC handler. - It enforces a single running instance (
assertNotAlreadyRunning(), called frominitialize()). An in-place update must not race a second instance writing the same image, so when the application runs from an updatable directory, it acquires an exclusive lock on a per-application lockfile (<appname>.lck) and writes its PID into it. If the lock is already held, it reads and reports the offending PID and refuses to start. TheFileChannelis kept open (and referenced) for the whole lifetime of the process so the lock survives. The lockfile lives in the installation root — the jlink image root, or the package root determined bydetermineJPackageRoot()for jpackage, whose launchers do not necessarily start the application in its own installation directory (createLockFilePath(...)).
The check is a no-op for non-updatable installations (normal jar/classpath launches), so the same application class works in development and in a packaged deployment.
The lockfile is never deleted: only the lock counts, and the operating system releases it when the process ends. A lockfile left behind by a crashed instance therefore does no harm — the next start simply locks it again and overwrites the PID.
Applications simply make their main class extend UpdatableDesktopApplication instead of DesktopApplication to
opt into auto-update. The single-instance check can be adapted by overriding:
| Method | Purpose |
|---|---|
assertNotAlreadyRunning() |
Take the lock, or throw a TentackleRuntimeException if another instance holds it. |
createLockFilePath(InstallationType) |
Where the lockfile lives — the jpackage root, or the jlink image root (the working directory). |
getLockFileName() |
The application name reduced to lowercase ASCII letters and digits, plus .lck. |
readPidFromLockFile(Path) |
The PID of the instance holding the lock, unknown if it has not written it yet. |
LoginFailedWithUpdateHandler¶
LoginFailedWithUpdateHandler is the heart
of the module — a LoginFailedHandler whose handle(Exception) decides whether a failed login is actually an
out-of-date client:
- It reacts only to a
VersionIncompatibleExceptionor anSSLHandshakeException(extracted from the exception chain) — the latter because a server upgrade may rotate its TLS certificate. - It proceeds only if
determineInstallationType()reports an updatable image and a remoteUpdateServiceis configured (via the application'supdateServiceproperty). If no service is configured, the update info cannot be retrieved, or the server offers the same version (the handshake failed for another reason), it delegates to the standard handler. - When the image is not updatable, it terminates with a localized "outdated, call your administrator" message. Since an SSL handshake error may as well be an expired or untrusted certificate, an intercepting proxy or a protocol mismatch, the cause is logged and kept as the stop reason, so it is not lost.
- It builds a
ClientInfofrom the application name, version and installation type, callsUpdateService.getUpdateInfo(...)and — if the returned version differs — returns aRunnablethat performs the update.
Its responsibilities, all overridable, are:
| Method | Purpose |
|---|---|
handle(Exception) |
Recognize the outdated-client situation and kick off the flow. |
createUpdateService() |
Look up the remote UpdateService from the updateService application property (null if none). |
createUpdateRunnable(...) |
Decide whether to install — by default asks the user; override for silent updates. |
showUpdateDialog(...) |
Show the "install version X?" notification with Yes/No buttons; closing it without a button means No. |
createCountdownTimeline(...) |
A 3-2-1 countdown on the Yes button that auto-confirms after about four seconds; return null to disable. |
createUpdateDirectory(...) |
Choose where to download/unzip — update below the package root for jpackage, ./update for jlink. |
update(...) |
Download, verify, unzip, launch the update script and exit. |
passSessionInfoViaEnvironment(...) |
Hand the encrypted credentials to the restarted process for auto-login. |
setStopReason(Throwable) |
Record EXIT_VALUE_OUTDATED and the cause as the exit value of the application. |
terminate(...) |
Record the stop reason, show an error/info and close the window, which terminates the application. |
The handler runs in the login service's background thread, and so does the returned Runnable. The dialog is
shown on the FX thread, while the background thread blocks until the user has answered — or the countdown has.
The update directory is wiped and recreated by downloadZip(...), so it never contains leftovers of a previous
attempt.
Exit values¶
When the application terminates because it is outdated and cannot update itself, or because the update failed,
the exit value is LoginFailedWithUpdateHandler.EXIT_VALUE_OUTDATED (97). This differs from the
LoginFailedHandler.EXIT_LOGIN_FAILED (98) of other login failures, so a launcher script can tell them apart.
If the user declines the update, the application just terminates without a stop reason. A successful update
exits with 0.
The update(...) method is where the hand-off happens: it calls
ClientUpdateUtilities.downloadZip(...) (reporting
progress through showApplicationStatus), unzips the archive with FileHelper.unzip(...), makes the
updateExecutor script (update.sh/update.cmd in the archive's bin directory) executable and launches it with
ProcessBuilder. It passes the current PID (so the script can wait for this process to exit before overwriting its
files — required on Windows) and, for jpackage, the path of the launcher to restart. Finally, it
System.exit(0)s so the script can replace the running image.
For the seamless restart, passSessionInfoViaEnvironment(...) adds the username — and the password, if the
credentials are password-based — encrypted with the application's Cryptor to the script's environment. The
variables are named <APP>_USER and <APP>_PASSWORD, where <APP> is the application name reduced to ASCII
letters and digits, in upper case. The restarted client picks them up in
LoginApplication.updateSessionInfoFromEnvironment(...) of
tentackle-fx-rdc. Without a Cryptor, nothing is passed and
the user logs in again.
UpdateFxRdcBundle and localization¶
UpdateFxRdcBundle is the @Bundle-annotated
accessor for the module's localized messages, resolved through the BundleFactory. The bundle ships English
(UpdateFxRdcBundle.properties) and German (UpdateFxRdcBundle_de.properties) variants and supplies the user-
facing strings of the update flow: DOWNLOADING, INSTALLING_{0}, UPDATE_COMPLETE, UPDATE_FAILED,
{0}_{1}_OUTDATED_CALL_ADMIN and {0}_{1}_OUTDATED_INSTALL_{2}. The build verifies bundle completeness for the
en_US and de_DE locales via the bundles goal of the tentackle-check-maven-plugin.
How It Fits Together¶
The client-side half of the end-to-end update flow (the server side is set up via
ServerUpdateUtilities):
- Launch. The application's main class extends
UpdatableDesktopApplication. Oninitialize(), if it runs from an updatable image, it takes the PID lockfile to guarantee it is the only instance. - Login fails with a version mismatch. The user tries to connect to an upgraded server and gets a
VersionIncompatibleException(or a TLS handshake error). The RDC framework routes this toLoginFailedWithUpdateHandler.handle(...). - Check. If the image is updatable and an
updateServiceproperty is configured, the handler asks the remoteUpdateServicefor the matchingUpdateInfo. - Confirm. If a newer version is offered, the user is shown an "install version X?" dialog (with the auto-confirm countdown). Unattended applications can override this to proceed silently.
- Install.
ClientUpdateUtilities.downloadZip(...)downloads and SHA-256-verifies the ZIP into the update directory; the handler unzips it and launches the bundled update script with the current PID (and credentials for auto-login), then exits so the script can overwrite the image and restart it. - Not updatable. If the client is outdated but cannot self-update (e.g., a plain jar launch), it shows the "contact your administrator" message and stops.
Package Layout¶
| Package | Contents |
|---|---|
org.tentackle.fx.rdc.update |
The public API: UpdatableDesktopApplication, LoginFailedWithUpdateHandler and the UpdateFxRdcBundle message accessor. |
org.tentackle.fx.rdc.update.service |
The Hook (ModuleHook) providing resource-bundle/i18n access for the module. |
Module Dependencies¶
tentackle-fx-rdc-update is a thin glue module bridging the desktop client and the update mechanics:
- It requires transitive
tentackle-fx-rdc— the Rich Desktop Client framework whoseDesktopApplicationandLoginFailedHandlerit extends. The dependency is declaredoptionalin the POM to avoid forcing it transitively onto downstream artifacts. - It requires transitive
tentackle-update— theUpdateServicecontract, theClientInfo/UpdateInforecords and theClientUpdateUtilitiesit drives. - It provides a
ModuleHookservice (org.tentackle.fx.rdc.update.service.Hook) for i18n bundle resolution.
Related Documentation¶
- Tentackle Update — the underlying service contract, data records and download/checksum helpers.
- FX RDC — the Rich Desktop Client framework providing
DesktopApplicationand the login-handling extension points. - FX — the JavaFX binding layer beneath the RDC.
- Session —
VersionIncompatibleExceptionandSessionInfo, the credentials carried into the relaunched process.