Skip to content

Java provisioning (slice 2): install a Temurin JRE when none fits - #39

Merged
CaYatur merged 3 commits into
mainfrom
feat/java-provision-install
Jul 24, 2026
Merged

Java provisioning (slice 2): install a Temurin JRE when none fits#39
CaYatur merged 3 commits into
mainfrom
feat/java-provision-install

Conversation

@CaYatur

@CaYatur CaYatur commented Jul 24, 2026

Copy link
Copy Markdown
Owner

What

Slice 2 completes auto Java provisioning: when no compatible JRE is installed, the args-editor warning grows an "Install Java N" button that downloads a Temurin (Adoptium) JRE into the app's own directory, verifies it, and pins it as that server's Java. So a server can run on a machine that has the wrong Java — or none at all. Opt-in only; nothing installs on its own.

How

Pure (src/shared/javaProvision.ts) — the parts easy to get wrong, kept testable:

  • adoptiumTarget(platform, arch) maps Node's names to Adoptium's (win32→windows, x64→x64, arm64→aarch64), and returns null for anything we can't name — the UI then declines rather than fetch a guess.
  • adoptiumAssetsUrl(major, target) builds the v3 assets endpoint, which returns the download link and the vendor's published SHA256 in one response.
  • pickAdoptiumPackage(assets) throws on an empty or field-missing response — an unverifiable download is worse than none.
  • isZipPackage(name) gates extraction to .zip (Windows) this slice.

Main:

  • core/archive.tsextractZipSafe(): adm-zip behind the same zip-slip guard worlds.ts uses, kept as its own module so this feature reverts without touching worlds.
  • core/javaProvision.tsinstallJava(): fetch assets → verify SHA256 as it streams (net.downloadFile deletes + throws on mismatch) → extract → probe that it actually runs → move into place with an atomic rename staged on the destination filesystem (no cross-device EXDEV, no half-tree if interrupted) → _resetJavaCache(). Installs land in <baseDir>/java/temurin-<major>/, which javaScan already searches, so the new JRE also appears in the picker.
  • IPC java:install + evt:java-install-progress; the handler audits the install (source: panel, action: java.install) exactly like server.create.

UI: the slice-1 needs-install warning becomes an Install button with a live phase/percent line; on success it pins javaPath and rescans. EN/TR keys in lockstep.

Verification

  • typecheck clean, build clean.
  • MSMS_SMOKE_JAVA PASS — new section 7 pins: os/arch mapping (incl. unknown OS/arch → decline), the URL segments, .zip vs .tar.gz, and that an empty or checksum-less assets response throws instead of downloading blind.
  • MSMS_SMOKE (spine) PASS — app still boots with the new IPC/preload/handler.
  • Endpoint shape checked against the live Adoptium v3 assets response before writing the builder.

Disclosed gaps

  • The end-to-end download/extract is not exercised here (network + a ~40MB binary). The pure shaping, the checksum guard, and the zip-slip guard are tested; the fetch→extract→adopt path is inspection-only.
  • Windows-first: only .zip packages install. mac/linux Temurin ships .tar.gz; those decline with unsupported-package (tar extraction is a follow-up). The app's primary target is the Windows portable build.
  • Portability: javaPath stores an absolute <baseDir>/java/... path; if the portable app is later moved, that path breaks and the server falls back to the picker/auto. Acceptable for now.
  • Forge/NeoForge --installServer may want a full JDK; this installs a JRE (enough to run). Out of scope.

Closes #36, closes #37

When no compatible Java is installed, the args editor's warning now carries an
"Install Java N" button that downloads a Temurin (Adoptium) JRE into the app's
own dir and pins it as the server's Java -- so a server can run on a machine
that has the wrong Java, or none.

- shared/javaProvision.ts: pure Adoptium shaping -- adoptiumTarget() maps
  platform/arch (win32->windows, x64->x64, arm64->aarch64, else decline),
  adoptiumAssetsUrl() builds the v3 assets endpoint (link + published SHA256 in
  one response), pickAdoptiumPackage() throws rather than proceed on a
  half-answer, isZipPackage() gates extraction. Plus the JavaInstallProgress type.
- core/archive.ts: extractZipSafe() -- adm-zip behind the same zip-slip guard
  worlds.ts uses, kept standalone so this can be reverted without touching worlds.
- core/javaProvision.ts: installJava() -- fetch assets, verify SHA256 as it
  streams (net.downloadFile), extract, probe, then move into place with an
  atomic rename staged on the destination filesystem so an interrupted install
  never leaves a half-tree for the scanner. _resetJavaCache() after.
- IPC java:install + evt:java-install-progress; register handler audits the
  install (source panel, action java.install) like server.create.
- ArgsEditor: the needs-install warning becomes an Install button with live
  phase/percent; on success it pins javaPath + rescans. EN/TR in lockstep.
- MSMS_SMOKE_JAVA section 7 pins the os/arch mapping, URL segments, and that an
  empty or checksum-less assets response throws instead of downloading blind.

Verified against the live Adoptium v3 assets response shape. The end-to-end
download/extract is not exercised in this env (network + a large binary) -- the
pure shaping and guards are; disclosed in the PR.

Closes #36, closes #37

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 24, 2026 11:20
@CaYatur CaYatur added this to the Java auto-provisioning milestone Jul 24, 2026
@CaYatur CaYatur added enhancement New feature or request area:java Java runtime detection & provisioning labels Jul 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Self-review of slice 2: installJava throws unsupported-platform /
unsupported-package on mac/linux (tar.gz) or an odd arch, but the renderer
showed one generic "check your connection and try again" toast -- misleading,
since retrying never helps. Distinguish it: on an /unsupported/ error, tell the
user auto-install isn't available for their OS and to set a path by hand
(args.javaInstallUnsupported, EN+TR). Also renamed the local handler off the
window.msms.installJava name it shadowed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@CaYatur

CaYatur commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

Self code-review

Reviewed for the two things that matter most in a download-and-run feature: integrity and not leaving a mess on failure.

Integrity path — holds up

  • The download is verified against the vendor's own SHA256 (binary.package.checksum from the assets response), streamed through net.downloadFile, which deletes the file and throws on mismatch. The freshly extracted java is only ever executed (probeJava) after that verification passes — never a probe-before-verify.
  • pickAdoptiumPackage refuses to proceed on an empty or field-missing response rather than fetching a guess. Pinned in smoke (empty → throws; no checksum → throws).

Atomicity — holds up

  • Staging is mkdtemp inside <baseDir>/java, so the final renameSync(home, dest) is same-filesystem (no EXDEV) and atomic. An interrupted install leaves only a .msms-java-* temp dir — which javaScan won't offer (no bin/java at its top level) and which the finally removes. _resetJavaCache() runs only after the move.

Found + fixed on-branch (2b66969)

  • Misleading failure message. installJava throws unsupported-platform / unsupported-package on mac/linux (tar.gz) or an odd arch, but the renderer surfaced one generic "check your connection and try again" — retrying never helps there. Now an /unsupported/ error shows javaInstallUnsupported ("auto-install isn't available for your OS yet — set a path by hand"). EN+TR.
  • Name shadowing. The local click handler was named installJava, shadowing window.msms.installJava. Renamed to runInstall.

Verified

  • typecheck + build clean; MSMS_SMOKE_JAVA PASS (incl. Adoptium shaping section); MSMS_SMOKE (spine) PASS with the new IPC/preload. EN/TR java* key parity checked (11/11).

Disclosed, not fixed (by design)

  • End-to-end fetch/extract is not exercised in CI — network + a ~40 MB binary. Pure shaping + both guards are tested; the fetch→extract→adopt path is inspection-only.
  • Windows-first (.zip only); mac/linux .tar.gz declines cleanly — tar extraction is a follow-up.
  • Reinstalling the same major while a server runs on it would fail at rmSync(dest) on Windows (locked exe). It throws + audits the failure; no corruption. Edge, left as-is.
  • Absolute javaPath breaks if the portable app moves (falls back to picker/auto). Accepted.

Two real issues found and fixed; the integrity + atomicity paths were actually walked, not assumed.

Advisor review caught the important one: runInstall ended with
set('javaPath', ...) which only updates local form state, so the JRE it just
downloaded was NOT saved unless the user then clicked Save -- while the toast
said "now selected for this server". That silently reintroduces the exact
failure this feature exists to fix (the next launch picks the wrong Java again).
Persist it immediately with updateServer(id, { java: { ...server.java, javaPath }})
-- spreading the *saved* server.java so an unrelated unsaved form edit isn't
committed alongside it.

Hardening: stage the download under a fixed name (jre.zip) instead of the
API-supplied package name, so an external string is never a path component.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@CaYatur

CaYatur commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

Follow-up (advisor review)

One more real defect, fixed on-branch (2961fb6) before merge:

  • Install did not persist. runInstall ended with set('javaPath', …) — local form state only. So the downloaded JRE was not saved unless the user then hit Save, while the toast said "now selected for this server." That silently reintroduces the exact bug the feature kills (next launch picks the wrong Java again). Now it persists immediately via updateServer(id, { java: { ...server.java, javaPath } }), spreading the saved server.java so unrelated unsaved form edits aren't committed with it.
  • Hardening: the download stages under a fixed jre.zip name instead of the API-supplied package name — an external string is never used as a path component.

Re-verified: typecheck + build clean, MSMS_SMOKE_JAVA PASS. The quick-fix "Use Java N" and the dropdown stay save-on-Save (lightweight selections); only the heavyweight install self-persists.

@CaYatur
CaYatur merged commit 23dd093 into main Jul 24, 2026
1 check passed
@CaYatur
CaYatur deleted the feat/java-provision-install branch July 24, 2026 11:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:java Java runtime detection & provisioning enhancement New feature or request

Projects

None yet

2 participants