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 transport-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; the recommendation is to fetch updates over the "tripe" (PKI, not TLS, since the server's certificate might have changed) transport instead of encrypting the record by hand. 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. win, mac, 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. 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 to run to perform 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/Update:33001", // 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 (via its String-argument constructor,
passing the download base URL) and registers it under the URI path. Failures are wrapped in a
TripRuntimeException.
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.) It then:
- resolves the checksum by downloading the
.sha256file (FileHelper.download) and taking its first token; - resolves the size by issuing an HTTP
HEADrequest (viajava.net.http.HttpClient) and readingcontent-length; - chooses the executor —
update.cmdon Windows,update.shelsewhere; - returns the assembled
UpdateInfo(always reportinggetVersion()as the new version).
Both the checksum hashes and download sizes are cached in ConcurrentHashMaps keyed by URL, so repeated client
requests for the same artifact do not re-fetch the metadata. 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()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 verifies the standard sub-directories (bin,conf,include,legal,lib) exist and are writable; the jpackage check first locates the package root (determineJPackageRoot(), OS-specific) and then checks the embedded runtime. - Looking up the service.
getUpdateService(serviceName)requests the TRIP transport for the URL and returns the remoteUpdateServiceproxy. If aCryptoris configured, the service URL is first run throughderiveURL(...)so a fake (honeypot)http:/https:/rmiform can be rewritten to the secured transport. - Downloading and verifying.
downloadZip(updateInfo, updateDir, progressConsumer)(re)creates an empty update directory, streams the ZIP into it through aCallbackReadableByteChannel(so progress can be reported), then recomputes the SHA-256 digest, and compares it againstupdateInfo.checksum(), throwing if they differ. Only a verified file is returned.
CallbackReadableByteChannel¶
CallbackReadableByteChannel is a thin
ReadableByteChannel wrapper that, on every read, feeds a Consumer<Double> the fraction bytesRead /
expectedSize (0.0–1.0). This is what drives the download progress bar; with a negative expected size, it simply
reports indefinite progress.
How It Fits Together¶
The end-to-end flow, as wired up by tentackle-fx-rdc-update:
- Server startup. The application server reads its
updateServiceandupdateURLproperties and callsServerUpdateUtilities.getInstance().startUpdateService(...), registering itsAbstractUpdateServiceImplsubclass on a TRIP transport. - 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(...). - 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 HTTPHEAD/download calls. - It opens
org.tentackle.updatetoorg.tentackle.coreso TRIP can serializeClientInfo/UpdateInfo. - It provides a
ModuleHookservice (org.tentackle.update.service.Hook) for i18n bundle resolution.
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.
- Common —
Cryptor,FileHelper,StringHelperand theModuleHookmechanism. - Services / ServiceFinder — the SPI behind the
ServerUpdateUtilities/ClientUpdateUtilitiessingletons.