Skip to content

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 specialized LoginFailedHandler that 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 protected so an application can update silently.
  • Seamless restart. Where a Cryptor is 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:

  1. It installs the update-aware login handler. It overrides createLoginFailedHandler(view, sessionInfo) to return a LoginFailedWithUpdateHandler instead of the plain RDC handler.
  2. It enforces a single running instance (assertNotAlreadyRunning(), called from initialize()). 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. The FileChannel is 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 by determineJPackageRoot() 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 VersionIncompatibleException or an SSLHandshakeException (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 remote UpdateService is configured (via the application's updateService property). 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 ClientInfo from the application name, version and installation type, calls UpdateService.getUpdateInfo(...) and — if the returned version differs — returns a Runnable that 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):

  1. Launch. The application's main class extends UpdatableDesktopApplication. On initialize(), if it runs from an updatable image, it takes the PID lockfile to guarantee it is the only instance.
  2. 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 to LoginFailedWithUpdateHandler.handle(...).
  3. Check. If the image is updatable and an updateService property is configured, the handler asks the remote UpdateService for the matching UpdateInfo.
  4. 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.
  5. 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.
  6. 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 whose DesktopApplication and LoginFailedHandler it extends. The dependency is declared optional in the POM to avoid forcing it transitively onto downstream artifacts.
  • It requires transitive tentackle-update — the UpdateService contract, the ClientInfo/UpdateInfo records and the ClientUpdateUtilities it drives.
  • It provides a ModuleHook service (org.tentackle.fx.rdc.update.service.Hook) for i18n bundle resolution.
  • Tentackle Update — the underlying service contract, data records and download/checksum helpers.
  • FX RDC — the Rich Desktop Client framework providing DesktopApplication and the login-handling extension points.
  • FX — the JavaFX binding layer beneath the RDC.
  • Session — VersionIncompatibleException and SessionInfo, the credentials carried into the relaunched process.