Skip to content

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's ClientInfo, answers with an UpdateInfo describing the download URL, checksum, size, and the script to run.
  • The client uses ClientUpdateUtilities to 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 ClientInfo and then trusts the returned UpdateInfo verbatim. 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. UpdateService is a plain Remote interface, so the same call works whether the client is connected directly or through a middle-tier server. The reply (UpdateInfo) implements ClientTransportCloseRequest so 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. UpdateInfo is 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].zip plus a matching .sha256 file) 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:

UpdateInfo getUpdateInfo(ClientInfo info);

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:

<updateURL>/<artifactName>-<version>-<platform>-<architecture>[-pkg].zip

with a sibling .sha256 file. (-pkg is appended for JPACKAGE installations.) It then:

  1. resolves the checksum by downloading the .sha256 file (FileHelper.download) and taking its first token;
  2. resolves the size by issuing an HTTP HEAD request (via java.net.http.HttpClient) and reading content-length;
  3. chooses the executorupdate.cmd on Windows, update.sh elsewhere;
  4. returns the assembled UpdateInfo (always reporting getVersion() 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 combined determineInstallationType() probe the file system to decide whether the running application sits in a writable jlink/jpackage layout (returning null if 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 remote UpdateService proxy. If a Cryptor is configured, the service URL is first run through deriveURL(...) so a fake (honeypot) http:/https:/rmi form 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 a CallbackReadableByteChannel (so progress can be reported), then recomputes the SHA-256 digest, and compares it against updateInfo.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:

  1. Server startup. The application server reads its updateService and updateURL properties and calls ServerUpdateUtilities.getInstance().startUpdateService(...), registering its AbstractUpdateServiceImpl subclass on a TRIP transport.
  2. Single-instance guard. On launch, an UpdatableDesktopApplication detects via ClientUpdateUtilities.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.
  3. Trigger. The update check is wired into login failure handling (LoginFailedWithUpdateHandler). When the client gets a VersionIncompatibleException (or an SSL handshake error caused by a server upgrade) and it runs from an updatable image, it builds a ClientInfo and calls UpdateService.getUpdateInfo(...).
  4. 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.
  5. Download & verify. ClientUpdateUtilities.downloadZip(...) fetches the ZIP into an update directory (placed inside the package root for jpackage, or under the working directory for jlink) and verifies the SHA-256 checksum.
  6. Swap & restart. The ZIP is unzipped, and the updateExecutor script (update.sh/update.cmd) from its bin directory 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 then System.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 it tentackle-common) for TRIP remoting, the Cryptor, FileHelper/StringHelper and the @Service/ServiceFactory SPI. The dependency is declared optional in the POM to avoid leaking it transitively onto downstream artifacts.
  • It requires java.net.http for the HTTP HEAD/download calls.
  • It opens org.tentackle.update to org.tentackle.core so TRIP can serialize ClientInfo/UpdateInfo.
  • It provides a ModuleHook service (org.tentackle.update.service.Hook) for i18n bundle resolution.
  • 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.
  • CommonCryptor, FileHelper, StringHelper and the ModuleHook mechanism.
  • Services / ServiceFinder — the SPI behind the ServerUpdateUtilities/ClientUpdateUtilities singletons.