From 5be9a0e1df3ad99b0a2f1f22f88b5033362e7701 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:12:48 +0100 Subject: [PATCH] fix desktop bundles (#6773) # Description of Changes Changes - Use 127.0.0.1 instead of localhost for the local backend. The bundled backend starts on a random port and binds the IPv4 wildcard, but the frontend health-checked http://localhost:{port}. On macOS (and some Linux) localhost resolves to IPv6 ::1 first, so the connection is refused and every backend-dependent tool shows "backend offline" even though the backend started fine. Switched getBackendUrl() and the health-check URL to the 127.0.0.1 loopback literal (already in the Tauri HTTP capability allowlist, and what the OAuth loopback server already uses). Client-side tools were unaffected, which matches the reports. - Fail the desktop build when the bundled JRE is older than the app JAR. The app JAR is compiled for Java 25, but the bundle could ship an older runtime/jre (jlink:runtime short-circuits on an existing runtime, and nothing checked its version), producing UnsupportedClassVersionError at launch so the backend never starts. Added a jlink:verify task that reads the jlink release file and fails the build if the bundled JRE major is below REQUIRED_JAVA (25, kept in sync with build.gradle modernJavaVersion). It runs after the runtime is staged - including the short-circuit reuse path that lets a stale JRE slip through. Cross-platform Node script, no new dependencies. --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .taskfiles/desktop.yml | 27 +++++++++++- .../editor/scripts/verify-bundled-jre.mjs | 42 +++++++++++++++++++ .../editor/src-tauri/src/commands/backend.rs | 2 +- .../desktop/services/tauriBackendService.ts | 10 +++-- 4 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 frontend/editor/scripts/verify-bundled-jre.mjs diff --git a/.taskfiles/desktop.yml b/.taskfiles/desktop.yml index 0b37df9f6f..239602beb5 100644 --- a/.taskfiles/desktop.yml +++ b/.taskfiles/desktop.yml @@ -5,6 +5,11 @@ vars: # NoClassDefFoundError: jdk/dynalink/Namespace at runtime in get-info-on-pdf and verify-pdf JLINK_MODULES: "java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported,jdk.dynalink" + # Minimum Java major the bundled JRE must be. Keep in sync with build.gradle + # `modernJavaVersion` - the app JAR is compiled for this, so an older runtime + # fails at launch with UnsupportedClassVersionError. Enforced by jlink:verify. + REQUIRED_JAVA: "25" + # Override via JPDFIUM_PLATFORMS env (csv of platform keys, or 'all'). JPDFIUM_PLATFORMS: sh: | @@ -102,6 +107,20 @@ tasks: jlink: desc: "Build backend JAR and create JLink runtime for Tauri" deps: [jlink:jar, jlink:runtime] + # Runs after the runtime is in place. Lives here (not in jlink:runtime's + # cmds) so it still fires when jlink:runtime short-circuits on its `status:` + # check and reuses an existing runtime/jre - that reuse path is exactly how + # a stale, too-old JRE slips through. + cmds: + - task: jlink:verify + + jlink:verify: + desc: "Fail the build if the bundled JRE is older than the app JAR requires" + dir: editor + env: + REQUIRED_JAVA: "{{.REQUIRED_JAVA}}" + cmds: + - node scripts/verify-bundled-jre.mjs src-tauri/runtime/jre/release jlink:jar: desc: "Build backend JAR for Tauri bundling (host-OS natives only by default)" @@ -127,9 +146,13 @@ tasks: cmds: - rm -rf runtime/jre - mkdir -p runtime + # Pin jlink to JAVA_HOME so the bundled JRE matches the JDK the build + # uses. Bare `jlink` on PATH can resolve to an older system Java (the + # ubuntu runner ships Java 11), producing a runtime jlink:verify rejects. - | - JLINK_COMPRESS="$(jlink --help 2>&1 | grep -q 'zip-\[0-9\]' && echo zip-6 || echo 2)" - jlink \ + JLINK="${JAVA_HOME:+$JAVA_HOME/bin/}jlink" + JLINK_COMPRESS="$("$JLINK" --help 2>&1 | grep -q 'zip-\[0-9\]' && echo zip-6 || echo 2)" + "$JLINK" \ --add-modules {{.JLINK_MODULES}} \ --strip-debug \ --compress="$JLINK_COMPRESS" \ diff --git a/frontend/editor/scripts/verify-bundled-jre.mjs b/frontend/editor/scripts/verify-bundled-jre.mjs new file mode 100644 index 0000000000..b826a0292d --- /dev/null +++ b/frontend/editor/scripts/verify-bundled-jre.mjs @@ -0,0 +1,42 @@ +// Fail the desktop build if the bundled jlink runtime is older than the Java +// version the app JAR is compiled for. A too-old runtime ships happily today +// (the jlink task short-circuits on a stale runtime/jre, and nothing checks the +// version), then dies at launch with UnsupportedClassVersionError -> the +// backend never starts and every tool shows "backend offline". +// +// Reads JAVA_VERSION from the jlink `release` file (always present in a jlink +// output) so it needs no shell and behaves identically on Windows/macOS/Linux. +// +// Required major comes from REQUIRED_JAVA (wired from .taskfiles/desktop.yml, +// which mirrors build.gradle `modernJavaVersion`). Keep them in sync. +import { readFileSync } from "node:fs"; + +const required = Number(process.env.REQUIRED_JAVA ?? "25"); +const releasePath = process.argv[2] ?? "runtime/jre/release"; + +let raw; +try { + raw = readFileSync(releasePath, "utf8"); +} catch (err) { + console.error( + `FATAL: cannot read bundled JRE release file at "${releasePath}": ${err.message}. ` + + `Is the runtime built? Run 'task desktop:jlink'.`, + ); + process.exit(1); +} + +const match = raw.match(/JAVA_VERSION="?(\d+)/); +const major = match ? Number(match[1]) : 0; + +console.log( + `Bundled JRE major: ${major || "unknown"} (required >= ${required})`, +); + +if (!major || major < required) { + console.error( + `FATAL: bundled runtime/jre is Java ${major || "unknown"} but the app JAR requires ` + + `Java ${required}. Run 'task desktop:jlink:clean' and rebuild with JDK ${required} active ` + + `(check 'java -version' / JAVA_HOME).`, + ); + process.exit(1); +} diff --git a/frontend/editor/src-tauri/src/commands/backend.rs b/frontend/editor/src-tauri/src/commands/backend.rs index 8d5a9764f1..00dc1bb486 100644 --- a/frontend/editor/src-tauri/src/commands/backend.rs +++ b/frontend/editor/src-tauri/src/commands/backend.rs @@ -308,7 +308,7 @@ fn monitor_backend_output(mut rx: tauri::async_runtime::Receiver void): () => void { @@ -227,7 +231,7 @@ export class TauriBackendService { }); } - /** Always checks the local bundled backend at localhost:{port}. */ + /** Always checks the local bundled backend at 127.0.0.1:{port}. */ async checkBackendHealth(): Promise { if (!this.backendStarted) { console.debug("[TauriBackendService] Health check: backend not started"); @@ -241,7 +245,7 @@ export class TauriBackendService { return false; } - const configUrl = `http://localhost:${this.backendPort}/api/v1/config/app-config`; + const configUrl = `http://127.0.0.1:${this.backendPort}/api/v1/config/app-config`; console.debug( `[TauriBackendService] Checking local backend health at: ${configUrl}`, );