Tentackle Update — The Application Self-Update Service¶
Overview and Motivation¶
The tentackle-update module provides the building blocks for auto-updating a deployed desktop client. A
Tentackle application that is shipped as a per-user jlink
or jpackage image can ask its server
whether a newer version is available, download it, verify it, and replace itself in place — without an installer,
admin rights, or a separate update tool.
The module is intentionally small and split into a server side and a client side that talk to each other over TRIP:
- The server publishes an
UpdateService— a TRIP remote service that, given the calling client'sClientInfo, answers with anUpdateInfodescribing the download URL, checksum, size, and the script to run. - The client uses
ClientUpdateUtilitiesto detect whether it runs from an updatable image, look up the remote service, download and checksum-verify the ZIP, and hand off to an external update script that swaps the files and restarts the application.
The server is the single authority over every decision in an update. It alone determines which image (the
target version) a given client is offered, the URL the image is downloaded from, and the update script that
performs the swap. The client never computes any of this — it merely reports who it is via ClientInfo and then
faithfully executes whatever the server returns. This indirection is deliberate and is what makes the mechanism
scale: because the download URL is dictated per request, the server can point a client at a global CDN, a
regional mirror, or a customer-local cache instead of streaming the (potentially hundreds of megabytes large) image
through the application server itself. For large, worldwide installations whose clients reach the data center only
through SD-WANs or company-managed VPNs — links that are typically bandwidth-constrained and metered — this
is essential: the lightweight UpdateService call still travels over the secured TRIP transport, while the bulk
download is steered onto whatever fast, geographically close, cacheable HTTP(S) endpoint the operator chooses.
The module deliberately contains only the UI-neutral mechanics (service contract, the data records, the
download/verification helpers, and the server-side service registration). The actual user-facing orchestration — the
"a new version is available, install it?" dialog, the unzip, and the process hand-off — lives one layer up in
tentackle-fx-rdc-update, which is where the JavaFX desktop application
plugs everything together.
Design principles¶
- Server-authoritative updates. The server decides everything: the target image/version, the download URL and
the update script to execute. The client contributes only its
ClientInfoand then trusts the returnedUpdateInfoverbatim. Because the URL is chosen by the server per request, the actual bytes can be served from a CDN, a regional mirror, or a customer-local cache — decoupling the (heavy) download from the (light) control channel and keeping bulk traffic off bandwidth-constrained SD-WAN/VPN uplinks. - Remoting over TRIP.
UpdateServiceis a plainRemoteinterface, so the same call works whether the client is connected directly or through a middle-tier server. The reply (UpdateInfo) implementsClientTransportCloseRequestso the transport is closed gracefully after the answer — the client is about to terminate and restart, and this avoids socket-reset noise on the server. - No secrets in the payload.
UpdateInfois not encrypted by itself. To protect it in transit, publish the service over the PSK-encrypted (pre-shared key)tripetransport rather than over TLS: a new server version may come with a new certificate that the outdated client does not trust yet. The download itself is plain HTTP(S) and is protected by a SHA-256 checksum rather than by transport encryption. - Convention-based artifact layout. The server derives the download URLs purely from a naming convention
(
<artifact>-<version>-<platform>-<arch>[-pkg].zipplus a matching.sha256file) under a configurable base URL, so adding a new platform/architecture is a matter of publishing the right files — no server code changes. That base URL is exactly where CDN/mirror steering happens: point it at the edge instead of the data center, and every client is served locally without touching any other code. - In-place, per-user install. Because the images are per-user and writable, the client can overwrite its own files. There is no shared installation and no privilege escalation.
- Single-instance safety. Updating a running image is only safe if exactly one instance owns the directory; the client layer enforces this with a PID lockfile (see How It Fits Together).
Key Concepts¶
The service contract¶
UpdateService is the entire remote API — a single method:
It extends Remote, marking it as a TRIP service that can be
registered on a transport and looked up by clients. The module opens org.tentackle.update to org.tentackle.core
precisely so TRIP can serialize the records that cross the wire.
ClientInfo — what the client tells the server¶
ClientInfo is the request record sent to the server:
| Field | Meaning |
|---|---|
application |
the client application name |
version |
the currently installed client version |
platform |
the operating system (e.g. windows, macosx, linux) |
architecture |
the hardware architecture (e.g. amd64, aarch64) |
installationType |
JLINK or JPACKAGE |
A convenience constructor fills platform and architecture from the running JVM via
StringHelper.getPlatform()/getArchitecture(), so callers normally only pass application, version, and
installation type. getPlatform() keeps only the letters of the lowercased os.name (cut off at the version on
Windows), and getArchitecture() is the lowercased os.arch. These are the same values the
jlink plugin uses for the classifier of the
ZIP files it creates, so the URL convention below matches the published artifacts. The record is annotated @TripReferencable(NEVER)/@TripDeduplicatable(NEVER) — it is a tiny
one-shot value, so TRIP serializes it directly without reference bookkeeping.
UpdateInfo — what the server answers¶
UpdateInfo is the reply record:
| Field | Meaning |
|---|---|
version |
the new application version offered by the server |
urlStr |
the URL of the downloadable ZIP (stored as a string, exposed via getUrl()) |
checksum |
the SHA-256 hash used to verify the download |
size |
the expected ZIP size in bytes (-1 if the server could not determine it), used to drive a progress bar |
updateExecutor |
the file name of the script in the update's bin directory that performs the swap (update.sh / update.cmd) |
Every field here is the server's decision, not the client's. The urlStr in particular need not point at the
application server at all — it can name any HTTP(S) endpoint (CDN, mirror, local cache), so the heavy download is
routed independently of the control channel. The client's only "decision" is whether an update applies at all: it
compares updateInfo.version() against its own version and, if they differ, downloads exactly the URL, verifies
exactly the checksum, and runs exactly the executor the server named. UpdateInfo implements
ClientTransportCloseRequest
and returns true from isCloseAllTransportsRequested() so the transport is torn down cleanly once the answer has
been received.
InstallationType¶
InstallationType distinguishes the two supported per-user
image formats: JLINK (a raw jlink runtime image) and JPACKAGE (a jpackage application bundle). The two differ
in directory layout, which both the URL convention (the server appends -pkg for jpackage) and the file-system
probing on the client (the runtime lives in a different sub-path per OS) have to account for.
Server Side¶
ServerUpdateUtilities — registering the service¶
ServerUpdateUtilities is a @Service singleton
(reachable via getInstance()). Its one job is startUpdateService(...):
ServerUpdateUtilities.getInstance().startUpdateService(
"tripe://0.0.0.0:33001/Update", // TRIP URL the service is registered under
"https://somehost/myapp/downloads", // base URL holding the ZIPs and checksums
UpdateServiceImpl.class); // your AbstractUpdateServiceImpl subclass
It requests (or reuses) the Transport for the given URI,
makes sure it is accepting, instantiates the service implementation and registers it under the URI's path.
The implementation class must provide a public constructor with a single String argument, which receives the
download base URL. Transport failures are wrapped in a TripRuntimeException, while a class that cannot be
instantiated raises a TentackleRuntimeException.
AbstractUpdateServiceImpl — the default implementation¶
AbstractUpdateServiceImpl implements
getUpdateInfo() against the URL naming convention; an application only supplies two pieces of information by
overriding two abstract methods:
public class UpdateServiceImpl extends AbstractUpdateServiceImpl {
public UpdateServiceImpl(String updateURL) {
super(updateURL);
}
@Override public String getArtifactName() { return "myapp-jlink-client"; }
@Override public String getVersion() { return Version.RELEASE; }
}
Given a ClientInfo, the base implementation builds the download artifact name as:
with a sibling .sha256 file. (-pkg is appended for JPACKAGE installations.) A missing trailing slash of the
base URL is added automatically. It then:
- resolves the checksum by downloading the
.sha256file (FileHelper.download) and taking its first whitespace-delimited token, so the output ofsha256sumcan be published as is; - resolves the size by issuing an HTTP
HEADrequest (viajava.net.http.HttpClient, following redirects) and readingcontent-length. Any status other than 2xx fails the request, so that the length of an error page is never taken for the size of the ZIP; - chooses the executor —
update.cmdif the platform containswin,update.shelsewhere; - returns the assembled
UpdateInfo(always reportinggetVersion()as the new version).
If the checksum or the size cannot be determined — typically because the artifact is not published — the call
fails with a TentackleRuntimeException, which TRIP passes on to the client.
The checksum hashes and the download sizes are cached in ConcurrentHashMaps keyed by URL, so repeated client
requests for the same artifact do not re-fetch the metadata. Only a known size is cached, though: a response
without content-length yields -1 and is queried again on the next request. Every URL-building,
executor-naming, and UpdateInfo-construction step is a protected method, so subclasses can customize the
layout (different file extensions, per-customer artifacts, signed executors, …) without rewriting the flow.
Client Side¶
ClientUpdateUtilities¶
ClientUpdateUtilities is a @Service singleton
gathering everything the client needs:
- Detecting an updatable image.
isUpdateableJlinkApplication(),isUpdatableJPackageApplication()and the combineddetermineInstallationType()(jlink first) probe the file system to decide whether the running application sits in a writable jlink/jpackage layout (returningnullif not — meaning "this build cannot self-update"). The jlink check,isUpdatableJlinkDirectory(File), verifies that the image directory and its standard subdirectories (bin,conf,include,legal,lib) exist and are writable; for the application itself, it checks the current working directory, since a jlink image is started from its root. The jpackage check first locates the package root (determineJPackageRoot(), derived from the path of the running executable) and then applies the jlink check to the embedded runtime:
| OS | Package root | Embedded runtime |
|---|---|---|
| Windows | the executable's directory (app/) |
runtime |
| macOS | two levels above Contents/MacOS |
Contents/runtime/Contents/Home |
| Linux | the parent of bin (lib/app/) |
lib/runtime |
- Looking up the service.
getUpdateService(serviceName)requests the TRIP transport for the URL and returns the remoteUpdateServiceproxy. If aCryptoris configured, the service URL may be given in encrypted form, disguised as anhttp:,https:orrmi:URL;Cryptor.deriveURL(...)decrypts it before the transport is requested. This keeps the real address out of plain-text configuration files. - Downloading and verifying.
downloadZip(updateInfo, updateDir, progressConsumer)creates the update directory (removing and recreating it if it already exists, so it is always empty), streams the ZIP into it through aCallbackReadableByteChannel(so progress can be reported), then recomputes the SHA-256 digest and compares it case-insensitively againstupdateInfo.checksum()(sha256sumwrites lowercase,certutiluppercase). A missing checksum or a mismatch fails with aTentackleRuntimeException; only a verified file is returned.
CallbackReadableByteChannel¶
CallbackReadableByteChannel is a thin
ReadableByteChannel wrapper that, after every read returning data, feeds a Consumer<Double> the fraction
bytesRead / expectedSize (0.0–1.0). This is what drives the download progress bar. With a negative (unknown)
expected size, the reported value is negative as well, which a JavaFX progress bar displays as indeterminate.
The byte count is a long, so images larger than 2 GB are fine.
Publishing an Update¶
The module does not create the downloadable artifacts — they come from the build and the operator:
- The ZIP and the update script. The jlink plugin
configured with
<withUpdater>true</withUpdater>adds the platform-specificupdate.sh/update.cmd(from the templatesupdate.ftlorpackage-update.ftl) to the image'sbindirectory and attaches the image as a ZIP with the classifier<platform>-<architecture>(plus-pkgfor the jpackage goal). Deployed to a Maven repository, the ZIP is therefore named<artifactId>-<version>-<platform>-<architecture>[-pkg].zip— exactly the conventionAbstractUpdateServiceImplexpects, withgetArtifactName()returning theartifactIdof the client image module. AnextraClassifierbreaks this convention and requires overridingcreateBaseUrlName(). - The checksum file. The
.sha256file is not generated by the build. Create it next to each ZIP, for example withsha256sum myapp-jlink-client-1.2.0-linux-amd64.zip > myapp-jlink-client-1.2.0-linux-amd64.sha256. Only the first token is read, and the case of the hex digits does not matter. - The location. Publish both files below the server's
updateURL. It may be any HTTP(S) server, CDN or mirror; redirects are followed.
The server caches the checksum and the size per URL. Republishing a different ZIP under the same version therefore requires a server restart — or, better, a new version.
How It Fits Together¶
The end-to-end flow, as wired up by tentackle-fx-rdc-update:
- Server startup. The application server reads its
updateServiceandupdateURLbackend properties and callsServerUpdateUtilities.getInstance().startUpdateService(...), registering itsAbstractUpdateServiceImplsubclass on a TRIP transport. The framework itself does not read these properties; the server generated by the archetype does. - Single-instance guard. On launch, an
UpdatableDesktopApplicationdetects viaClientUpdateUtilities.determineInstallationType()whether it runs from an updatable image and, if so, takes an exclusive PID lockfile — an in-place update must not race a second running instance. - Trigger. The update check is wired into login failure handling
(
LoginFailedWithUpdateHandler). When the client gets aVersionIncompatibleException(or an SSL handshake error caused by a server upgrade) and it runs from an updatable image, it builds aClientInfoand callsUpdateService.getUpdateInfo(...)on the service named by its ownupdateServiceproperty. - Decision. If
updateInfo.version()differs from the running version, the user is asked whether to install (with an optional countdown for unattended/kiosk setups). Applications may override this to update silently. - Download & verify.
ClientUpdateUtilities.downloadZip(...)fetches the ZIP into anupdatedirectory (placed inside the package root for jpackage, or under the working directory for jlink) and verifies the SHA-256 checksum. - Swap & restart. The ZIP is unzipped, and the
updateExecutorscript (update.sh/update.cmd) from itsbindirectory is launched with the current PID (and, for jpackage, the path of the executable to restart). The script waits for this process to exit, overwrites the image files, and relaunches the application. The client thenSystem.exit(0)s. Optionally, the encrypted username/password are handed to the new process via environment variables so the user does not have to log in again.
Package Layout¶
| Package | Contents |
|---|---|
org.tentackle.update |
The public API: the UpdateService contract, the ClientInfo/UpdateInfo records, InstallationType, the server-side ServerUpdateUtilities + AbstractUpdateServiceImpl, the client-side ClientUpdateUtilities and the CallbackReadableByteChannel download helper. |
org.tentackle.update.service |
The Hook (ModuleHook) providing resource-bundle/i18n access for the module. |
Module Dependencies¶
tentackle-update is a thin, high-level module:
- It requires transitive
tentackle-core(and through ittentackle-common) for TRIP remoting, theCryptor,FileHelper/StringHelperand the@Service/ServiceFactorySPI. The dependency is declaredoptionalin the POM to avoid leaking it transitively onto downstream artifacts. - It requires
java.net.httpfor the server's HTTPHEADrequest. The checksum file and the ZIP are downloaded via plainjava.net.URLconnections. - It opens
org.tentackle.updatetoorg.tentackle.coreso TRIP can serializeClientInfo/UpdateInfo. - It provides a
ModuleHookservice (org.tentackle.update.service.Hook), which loads resource bundles within the context of this module.
Related Documentation¶
- TRIP — the remote invocation protocol carrying the update service calls.
- tentackle-fx-rdc-update — the JavaFX desktop layer that orchestrates the update UI, download, and restart.
- Jlink/Jpackage plugin — creates the update scripts and the ZIP files.
- Common —
Cryptor,FileHelper,StringHelperand theModuleHookmechanism. - Services / ServiceFinder — the SPI behind the
ServerUpdateUtilities/ClientUpdateUtilitiessingletons.