Compare commits
46
Commits
ci/faster-builds
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f183b4a5dc | ||
|
|
8eec436e23 | ||
|
|
df319745fc | ||
|
|
d02fb95bd4 | ||
|
|
8d1c01aed6 | ||
|
|
4ee5c0984f | ||
|
|
bcefa87e27 | ||
|
|
26187e6f61 | ||
|
|
eb7fb914cb | ||
|
|
adbdc85735 | ||
|
|
8dd4ef09aa | ||
|
|
5ca7bcaad0 | ||
|
|
040dceb9ed | ||
|
|
c905b49102 | ||
|
|
23ded32db1 | ||
|
|
9e66c9dbc7 | ||
|
|
07fffc4b01 | ||
|
|
141df8c7b8 | ||
|
|
f5d739711d | ||
|
|
a19679fd00 | ||
|
|
87b5efa64b | ||
|
|
8aaaadc2b0 | ||
|
|
c4240a24bf | ||
|
|
e81e974af7 | ||
|
|
551bd4cef2 | ||
|
|
95111d2f84 | ||
|
|
327c0ea940 | ||
|
|
aace7932e0 | ||
|
|
93bb547a9e | ||
|
|
0f29fafc0a | ||
|
|
7f3b7cfe8a | ||
|
|
9de2d8eeef | ||
|
|
6434573105 | ||
|
|
3cbab39d16 | ||
|
|
0d58eac860 | ||
|
|
82b0459f4d | ||
|
|
b8305f85b0 | ||
|
|
b7e9d7fda4 | ||
|
|
a4e6ab3673 | ||
|
|
096146449d | ||
|
|
320c5644e9 | ||
|
|
5f61a0d05c | ||
|
|
bdacd65473 | ||
|
|
f176fbd1fd | ||
|
|
9d43a6fc0f | ||
|
|
23811dad7f |
No files matched your search
@@ -21,63 +21,93 @@ jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
# Same pattern as TDLM ForestryMC/IndustrialCraft2 setup-build: cache the JDK toolchain.
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v6
|
||||
with:
|
||||
distribution: 'liberica'
|
||||
java-version: '17'
|
||||
java-package: 'jdk+fx'
|
||||
cache-jdk: true
|
||||
|
||||
# Nest mirrors github.com/gradle/actions as actions/gradle-actions (also as gradle/actions).
|
||||
# Match ForestryMC/IC2: on Gitea use cache-provider external (no setup-gradle Actions cache).
|
||||
# Do NOT actions/cache the whole ~/.gradle/caches — that hit ~4GB here and OOM'd the
|
||||
# Windows Docker runner on restore; siblings never cache that tree.
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
uses: actions/gradle-actions/setup-gradle@v6
|
||||
with:
|
||||
cache-cleanup: never
|
||||
# Only main writes the dependency cache; pull requests read it.
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
# The repository's wrapper downloads Gradle itself, so cache that download too.
|
||||
- name: Cache Gradle distribution
|
||||
cache-provider: ${{ github.server_url == 'https://github.com' && 'basic' || 'external' }}
|
||||
|
||||
# Wrapper only (small). Dependency jars re-download when cold — same as org mod CI on Gitea.
|
||||
- name: Cache Gradle wrapper
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
.gradle/wrapper-neo
|
||||
key: gradle-dist-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/wrapper/GradleWrapperNeo.java') }}
|
||||
- name: Install Xvfb
|
||||
key: gradle-wrapper-${{ runner.os }}-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/wrapper/GradleWrapperNeo.java') }}
|
||||
|
||||
# Xvfb alone is not enough: JavaFX Glass needs GTK/X11 libs or Platform.startup fails with
|
||||
# GtkPlatformFactory "Internal Error" and :HMCL:test can hang (looks like a checkstyle stall).
|
||||
- name: Cache apt packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/apt-cache
|
||||
key: apt-javafx-xvfb-${{ runner.os }}-${{ runner.arch }}-1
|
||||
|
||||
- name: Install Xvfb and JavaFX GTK deps
|
||||
run: |
|
||||
# Installing is the slowest setup step, so skip it when the runner already has xvfb, and
|
||||
# only refresh the package index when a plain install fails.
|
||||
command -v xvfb-run >/dev/null || sudo apt-get install -y --no-install-recommends xvfb \
|
||||
|| { sudo apt-get update && sudo apt-get install -y --no-install-recommends xvfb; }
|
||||
pkgs='xvfb libgtk-3-0 libxtst6 libxxf86vm1 libgl1 libxi6 libx11-6 libxext6 libxrender1'
|
||||
sudo mkdir -p /var/cache/apt/archives/partial
|
||||
if [ -d "${HOME}/apt-cache" ] && [ -n "$(ls -A "${HOME}/apt-cache" 2>/dev/null)" ]; then
|
||||
sudo cp -a "${HOME}/apt-cache/." /var/cache/apt/archives/
|
||||
fi
|
||||
# Prefer cached debs; refresh the index only when install cannot resolve packages.
|
||||
sudo apt-get install -y --no-install-recommends $pkgs \
|
||||
|| { sudo apt-get update && sudo apt-get install -y --no-install-recommends $pkgs; }
|
||||
mkdir -p "${HOME}/apt-cache"
|
||||
cp -a /var/cache/apt/archives/*.deb "${HOME}/apt-cache/" 2>/dev/null || true
|
||||
|
||||
# One Gradle run builds, tests and runs the code and translation checks, so the
|
||||
# project is configured and compiled once instead of in two workflows.
|
||||
- name: Build and check with Gradle
|
||||
run: xvfb-run --auto-servernum ./gradlew build checkstyle checkTranslations --no-daemon --parallel
|
||||
env:
|
||||
JAVA_TOOL_OPTIONS: -Djava.awt.headless=true
|
||||
JAVA_TOOL_OPTIONS: -Djava.awt.headless=true -Dprism.order=sw
|
||||
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
|
||||
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
|
||||
|
||||
- name: Get short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload JAR
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-jar
|
||||
path: HMCL/build/libs/HMCL-*.jar
|
||||
archive: false
|
||||
|
||||
- name: Upload EXE
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-exe
|
||||
path: HMCL/build/libs/HMCL-*.exe
|
||||
archive: false
|
||||
|
||||
- name: Upload SH
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-sh
|
||||
path: HMCL/build/libs/HMCL-*.sh
|
||||
archive: false
|
||||
|
||||
- name: Upload DEB
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
name: Publish TDLM launcher
|
||||
|
||||
# Builds HMCL.exe / HMCL.jar and publishes them for TDLMPack / players:
|
||||
# 1. Gitea Release assets (versioned download page)
|
||||
# 2. Gitea generic package tdlm-hmcl/latest (stable curl URLs)
|
||||
#
|
||||
# Mirrors TDLM/TDLMPack's pack.yml publish style (nest API host; Cloudflare
|
||||
# in front of git.potatolab.uk blocks Python urllib for large uploads).
|
||||
#
|
||||
# Auth (same name as TDLMPack pack.yml):
|
||||
# secrets.TDLM_PACK_TOKEN — Gitea API token with package + release write.
|
||||
# Prefer an *org* secret (like MAVEN_USER/MAVEN_PASSWORD) so HMCL inherits it;
|
||||
# today it exists only as a TDLMPack *repo* secret unless promoted.
|
||||
# Falls back to github.token when TDLM_PACK_TOKEN is unset (same-repo release;
|
||||
# org generic packages may still need the pack token).
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: "Release tag (also used as the release title). Example: 2026.11.08 or v3.dev-abc1234"
|
||||
required: true
|
||||
type: string
|
||||
# Gitea records a push run whenever this file changes; without a push job the run
|
||||
# fails with 0 jobs. Keep a no-op so the check is green; real publish stays dispatch-only.
|
||||
push:
|
||||
paths:
|
||||
- '.github/workflows/publish-tdlm.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
noop:
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "publish-tdlm is workflow_dispatch-only; push no-op ok"
|
||||
|
||||
publish:
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
# Same caching pattern as gradle.yml / TDLM ForestryMC+IC2 setup-build.
|
||||
- name: Set up JDK 17 + JavaFX
|
||||
uses: actions/setup-java@v6
|
||||
with:
|
||||
distribution: 'liberica'
|
||||
java-version: '17'
|
||||
java-package: 'jdk+fx'
|
||||
cache-jdk: true
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: actions/gradle-actions/setup-gradle@v6
|
||||
with:
|
||||
cache-cleanup: never
|
||||
cache-provider: ${{ github.server_url == 'https://github.com' && 'basic' || 'external' }}
|
||||
|
||||
# Wrapper only — do not cache ~/.gradle/caches (org siblings don't; ~4GB killed Docker).
|
||||
- name: Cache Gradle wrapper
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
.gradle/wrapper-neo
|
||||
key: gradle-wrapper-${{ runner.os }}-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/wrapper/GradleWrapperNeo.java') }}
|
||||
|
||||
- name: Cache apt packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/apt-cache
|
||||
key: apt-javafx-xvfb-${{ runner.os }}-${{ runner.arch }}-1
|
||||
|
||||
- name: Install Xvfb and JavaFX GTK deps
|
||||
run: |
|
||||
pkgs='xvfb libgtk-3-0 libxtst6 libxxf86vm1 libgl1 libxi6 libx11-6 libxext6 libxrender1'
|
||||
sudo mkdir -p /var/cache/apt/archives/partial
|
||||
if [ -d "${HOME}/apt-cache" ] && [ -n "$(ls -A "${HOME}/apt-cache" 2>/dev/null)" ]; then
|
||||
sudo cp -a "${HOME}/apt-cache/." /var/cache/apt/archives/
|
||||
fi
|
||||
sudo apt-get install -y --no-install-recommends $pkgs \
|
||||
|| { sudo apt-get update && sudo apt-get install -y --no-install-recommends $pkgs; }
|
||||
mkdir -p "${HOME}/apt-cache"
|
||||
cp -a /var/cache/apt/archives/*.deb "${HOME}/apt-cache/" 2>/dev/null || true
|
||||
|
||||
|
||||
- name: Build executables
|
||||
run: xvfb-run --auto-servernum ./gradlew :HMCL:makeExecutables --no-daemon --parallel
|
||||
env:
|
||||
JAVA_TOOL_OPTIONS: -Djava.awt.headless=true -Dprism.order=sw
|
||||
VERSION_TYPE: unofficial
|
||||
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
|
||||
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
|
||||
|
||||
- name: Stage stable launcher names
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p build/tdlm-hmcl
|
||||
jar="$(ls -1 HMCL/build/libs/HMCL-*.jar | grep -v '\.sha' | head -n1)"
|
||||
exe="$(ls -1 HMCL/build/libs/HMCL-*.exe | grep -v '\.sha' | head -n1)"
|
||||
test -n "$jar" && test -n "$exe"
|
||||
cp -f "$jar" build/tdlm-hmcl/HMCL.jar
|
||||
cp -f "$exe" build/tdlm-hmcl/HMCL.exe
|
||||
# Prefer Gradle-produced checksums when present; otherwise compute.
|
||||
if [ -f "${jar}.sha256" ]; then cp -f "${jar}.sha256" build/tdlm-hmcl/HMCL.jar.sha256
|
||||
else sha256sum build/tdlm-hmcl/HMCL.jar | awk '{print $1}' > build/tdlm-hmcl/HMCL.jar.sha256
|
||||
fi
|
||||
if [ -f "${exe}.sha256" ]; then cp -f "${exe}.sha256" build/tdlm-hmcl/HMCL.exe.sha256
|
||||
else sha256sum build/tdlm-hmcl/HMCL.exe | awk '{print $1}' > build/tdlm-hmcl/HMCL.exe.sha256
|
||||
fi
|
||||
echo "STAGED_JAR=$jar" >> "$GITHUB_ENV"
|
||||
echo "STAGED_EXE=$exe" >> "$GITHUB_ENV"
|
||||
ls -la build/tdlm-hmcl
|
||||
|
||||
# Stable player/CI channel: overwritten in place under tdlm-hmcl/latest.
|
||||
- name: Publish generic package tdlm-hmcl/latest
|
||||
env:
|
||||
# Same secret name as TDLM/TDLMPack pack.yml (org or repo). Not MAVEN_*.
|
||||
TDLM_PACK_TOKEN: ${{ secrets.TDLM_PACK_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GITEA_PACKAGE_ROOT: https://git.nest.potatolab.uk:8443/api/packages/TDLM/generic/tdlm-hmcl/latest
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import os, pathlib, urllib.error, urllib.parse, urllib.request
|
||||
token = (os.environ.get("TDLM_PACK_TOKEN") or os.environ.get("GITHUB_TOKEN") or "").strip()
|
||||
token_src = "TDLM_PACK_TOKEN" if os.environ.get("TDLM_PACK_TOKEN", "").strip() else "github.token"
|
||||
if not token:
|
||||
raise SystemExit(
|
||||
"Need secrets.TDLM_PACK_TOKEN (promote TDLMPack's repo secret to org, like MAVEN_*) "
|
||||
"or a usable github.token to publish tdlm-hmcl")
|
||||
print("auth=" + token_src)
|
||||
upload_root = os.environ["GITEA_PACKAGE_ROOT"].rstrip("/")
|
||||
root = pathlib.Path("build/tdlm-hmcl")
|
||||
|
||||
def put(name: str, data: bytes):
|
||||
url = upload_root + "/" + urllib.parse.quote(name, safe="")
|
||||
# Gitea refuses to overwrite a generic package file (HTTP 409), so the previous copy of this one
|
||||
# file is deleted right before it is uploaded again (keeps the gap without a file as short as possible).
|
||||
delete = urllib.request.Request(url, method="DELETE")
|
||||
delete.add_header("Authorization", "token " + token)
|
||||
delete.add_header("User-Agent", "tdlm-hmcl-publish")
|
||||
try:
|
||||
with urllib.request.urlopen(delete, timeout=300) as response:
|
||||
response.read()
|
||||
except urllib.error.HTTPError as exc:
|
||||
if exc.code != 404:
|
||||
raise SystemExit("HTTP %s deleting the old %s: %s" % (exc.code, name, exc.read()[:300]))
|
||||
request = urllib.request.Request(url, data=data, method="PUT")
|
||||
request.add_header("Authorization", "token " + token)
|
||||
request.add_header("Content-Type", "application/octet-stream")
|
||||
request.add_header("User-Agent", "tdlm-hmcl-publish")
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=1800) as response:
|
||||
response.read()
|
||||
except urllib.error.HTTPError as exc:
|
||||
raise SystemExit("HTTP %s uploading %s: %s" % (exc.code, name, exc.read()[:500]))
|
||||
|
||||
for path in sorted(root.iterdir()):
|
||||
if path.is_file():
|
||||
put(path.name, path.read_bytes())
|
||||
print("PACKAGE " + path.name)
|
||||
print("generic=" + upload_root)
|
||||
PY
|
||||
|
||||
# Gitea release (versioned). Nest API avoids Cloudflare blocking large urllib uploads.
|
||||
- name: Publish Gitea release
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
TDLM_PACK_TOKEN: ${{ secrets.TDLM_PACK_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GITEA_API_URL: https://git.nest.potatolab.uk:8443/api/v1
|
||||
GITEA_PACKAGE_ROOT: https://git.nest.potatolab.uk:8443/api/packages/TDLM/generic/tdlm-hmcl/latest
|
||||
TARGET_COMMIT: ${{ github.sha }}
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import json, os, urllib.error, urllib.parse, urllib.request
|
||||
tag = os.environ["RELEASE_TAG"].strip()
|
||||
token = (os.environ.get("TDLM_PACK_TOKEN") or os.environ.get("GITHUB_TOKEN") or "").strip()
|
||||
token_src = "TDLM_PACK_TOKEN" if os.environ.get("TDLM_PACK_TOKEN", "").strip() else "github.token"
|
||||
api = os.environ["GITEA_API_URL"].rstrip("/") + "/repos/TDLM/HMCL"
|
||||
package_root = os.environ["GITEA_PACKAGE_ROOT"].rstrip("/")
|
||||
target = os.environ.get("TARGET_COMMIT", "main").strip() or "main"
|
||||
if not token:
|
||||
raise SystemExit(
|
||||
"Need secrets.TDLM_PACK_TOKEN (org-preferred, same as TDLMPack) or github.token")
|
||||
print("auth=" + token_src)
|
||||
if not tag:
|
||||
raise SystemExit("release_tag is empty")
|
||||
|
||||
def call(method, path, data=None, content_type="application/json", timeout=90):
|
||||
request = urllib.request.Request(api + path, data=data, method=method)
|
||||
request.add_header("Authorization", "token " + token)
|
||||
request.add_header("Accept", "application/json")
|
||||
request.add_header("User-Agent", "tdlm-hmcl-release")
|
||||
if data is not None and content_type:
|
||||
request.add_header("Content-Type", content_type)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||
body = response.read()
|
||||
return json.loads(body) if body else {}
|
||||
except urllib.error.HTTPError as exc:
|
||||
if exc.code == 404:
|
||||
return None
|
||||
detail = exc.read()[:500]
|
||||
raise SystemExit("HTTP %s for %s: %s" % (exc.code, path, detail))
|
||||
|
||||
body = (
|
||||
"TDLM/HMCL launcher binaries for players and TDLMPack.\n\n"
|
||||
"Stable generic (overwrite in place):\n"
|
||||
"- %s/HMCL.exe\n"
|
||||
"- %s/HMCL.jar\n\n"
|
||||
"TDLMPack does not Maven-resolve this launcher; point the pack README / install docs at these URLs "
|
||||
"instead of hand-copying exe/jar.\n"
|
||||
"Client pack fileApi default: http://159.75.5.225:19527/tdlm-client\n"
|
||||
"(Pack must publish/mirror the same layout; see docs/TDLM_CONSUME.md).\n"
|
||||
) % (package_root, package_root)
|
||||
|
||||
quoted = urllib.parse.quote(tag, safe="")
|
||||
release = call("GET", "/releases/tags/" + quoted)
|
||||
if release and not release.get("draft"):
|
||||
raise SystemExit("Release tag already exists: " + tag)
|
||||
if not release:
|
||||
release = call("POST", "/releases", json.dumps({
|
||||
"tag_name": tag,
|
||||
"name": tag,
|
||||
"target_commitish": target,
|
||||
"draft": True,
|
||||
"prerelease": False,
|
||||
"body": body,
|
||||
}).encode("utf-8"))
|
||||
release_id = release["id"]
|
||||
|
||||
staged = [
|
||||
("HMCL.exe", "build/tdlm-hmcl/HMCL.exe"),
|
||||
("HMCL.jar", "build/tdlm-hmcl/HMCL.jar"),
|
||||
("HMCL.exe.sha256", "build/tdlm-hmcl/HMCL.exe.sha256"),
|
||||
("HMCL.jar.sha256", "build/tdlm-hmcl/HMCL.jar.sha256"),
|
||||
]
|
||||
for name, path in staged:
|
||||
if not os.path.exists(path):
|
||||
raise SystemExit("missing " + path)
|
||||
for old in call("GET", "/releases/%s/assets" % release_id) or []:
|
||||
if old.get("name") == name:
|
||||
call("DELETE", "/releases/%s/assets/%s" % (release_id, old["id"]))
|
||||
with open(path, "rb") as asset:
|
||||
call(
|
||||
"POST",
|
||||
"/releases/%s/assets?name=%s" % (release_id, urllib.parse.quote(name)),
|
||||
asset.read(),
|
||||
"application/octet-stream",
|
||||
1800,
|
||||
)
|
||||
print("RELEASE asset " + name)
|
||||
call("PATCH", "/releases/%s" % release_id, json.dumps({"draft": False}).encode("utf-8"))
|
||||
print(release.get("html_url", "Release created"))
|
||||
PY
|
||||
@@ -77,3 +77,6 @@ language-subtag-registry
|
||||
!/.gemini/config.yaml
|
||||
.codex
|
||||
/.gradle-*/
|
||||
|
||||
# Managed Java downloaded by the launcher when it is run from the repository root
|
||||
/java/
|
||||
@@ -141,18 +141,46 @@ public final class Metadata {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Default client Server Modpack fileApi: self-hosted static mirror on the TDLM VPS (nginx, port 19527).
|
||||
public static final String DEFAULT_CLIENT_FILE_API =
|
||||
"http://159.75.5.225:19527/tdlm-client";
|
||||
|
||||
/// Stable client update root (`server-manifest.json` and `overrides/` live under this URL).
|
||||
///
|
||||
/// Override with system property `hmcl.client.fileApi` or jar attribute `hmcl.client.fileApi`.
|
||||
public static String getClientFileApi() {
|
||||
String fromProperty = System.getProperty("hmcl.client.fileApi", "").trim();
|
||||
if (StringUtils.isNotBlank(fromProperty)) {
|
||||
return StringUtils.removeSuffix(fromProperty, "/");
|
||||
}
|
||||
String fromJar = JarUtils.getAttribute("hmcl.client.fileApi", null);
|
||||
if (StringUtils.isNotBlank(fromJar)) {
|
||||
return StringUtils.removeSuffix(fromJar.trim(), "/");
|
||||
}
|
||||
return DEFAULT_CLIENT_FILE_API;
|
||||
}
|
||||
|
||||
/// Absolute URL of the client `server-manifest.json` used for first install and differential updates.
|
||||
public static String getClientManifestUrl() {
|
||||
return getClientFileApi() + "/server-manifest.json";
|
||||
}
|
||||
|
||||
/// Fallback join address when the installed modpack manifest has no `serverAddress` / routes.
|
||||
public static final String DEFAULT_SERVER_ADDRESS = "mc.potatolab.uk";
|
||||
|
||||
/// Server address used when the installed modpack manifest does not provide one.
|
||||
///
|
||||
/// Can be set at build or run time with the `hmcl.server.address` system property; empty by default.
|
||||
/// Override at build or run time with `-Dhmcl.server.address=…`; defaults to [#DEFAULT_SERVER_ADDRESS].
|
||||
public static String getDefaultServerAddress() {
|
||||
return System.getProperty("hmcl.server.address", "").trim();
|
||||
return System.getProperty("hmcl.server.address", DEFAULT_SERVER_ADDRESS).trim();
|
||||
}
|
||||
|
||||
/// The server name shown in the title bar.
|
||||
///
|
||||
/// Can be set with the `hmcl.server.name` system property; empty by default.
|
||||
/// Can be set with the `hmcl.server.name` system property; defaults to `TDLM`.
|
||||
public static String getServerName() {
|
||||
return System.getProperty("hmcl.server.name", "").trim();
|
||||
String name = System.getProperty("hmcl.server.name", "TDLM").trim();
|
||||
return name.isEmpty() ? "TDLM" : name;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
package org.jackhuang.hmcl.download;
|
||||
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.cleanroom.CleanroomRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
|
||||
@@ -132,6 +133,13 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
||||
public HMCLDownloadProvider(Supplier<@Unmodifiable List<String>> mirrorRoots) {
|
||||
this.mirrorRoots = Objects.requireNonNull(mirrorRoots);
|
||||
this.rules = List.of(
|
||||
// The self-hosted client mirror carries the pack's full dependency set. BMCLAPI's
|
||||
// redirect CDNs (sakura.sld.tw, cernet) and the Mojang/Forge origins are TLS-blocked
|
||||
// on many mainland networks, so the VPS must be tried before any of them.
|
||||
new SimpleRule("https://piston-meta.mojang.com", Metadata.getClientFileApi() + "/piston-meta", "https://bmclapi2.bangbang93.com"),
|
||||
new SimpleRule("https://libraries.minecraft.net", Metadata.getClientFileApi() + "/libraries", "https://bmclapi2.bangbang93.com/libraries"),
|
||||
new SimpleRule("https://maven.minecraftforge.net", Metadata.getClientFileApi() + "/forge-maven", "https://bmclapi2.bangbang93.com/maven"),
|
||||
new SimpleRule("https://launcher.mojang.com", Metadata.getClientFileApi() + "/launcher-mojang", "https://bmclapi2.bangbang93.com"),
|
||||
new MultiRootRule("https://bmclapi2.bangbang93.com", Function.identity()),
|
||||
new MultiRootRule("https://bmclapi.bangbang93.com", Function.identity()),
|
||||
new MultiRootRule("https://launchermeta.mojang.com", Function.identity()),
|
||||
@@ -183,6 +191,11 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
||||
new SimpleRule("https://hmcl.glavo.site/metadata/fmllibs", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/fmllibs"),
|
||||
new SimpleRule("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"),
|
||||
|
||||
// files.prismlauncher.org is unreachable from mainland China (TLS handshake reset) and BMCLAPI
|
||||
// does not mirror its repackaged log4j-fixed artifacts. Serve them from the self-hosted client
|
||||
// mirror (alongside the fileApi root); Prism stays as the fallback for reachable networks.
|
||||
new SimpleRule("https://files.prismlauncher.org/maven", Metadata.getClientFileApi() + "/maven"),
|
||||
|
||||
// https://github.com/mcmod-info-mirror/mcim-rust-api
|
||||
new SimpleRule("https://api.modrinth.com", null, "https://mod.mcimirror.top/modrinth"),
|
||||
new SimpleRule("https://cdn.modrinth.com", null, "https://mod.mcimirror.top"),
|
||||
@@ -295,11 +308,18 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
||||
@Override
|
||||
public DownloadCandidates getAssetObjectCandidates(AssetObject assetObject) {
|
||||
String location = assetObject.getLocation();
|
||||
return getCandidates(
|
||||
fileSource,
|
||||
WebURL.parse("https://resources.download.minecraft.net/" + location),
|
||||
mirrorUrls("/assets/" + location)
|
||||
);
|
||||
WebURL officialUrl = WebURL.parse("https://resources.download.minecraft.net/" + location);
|
||||
List<WebURL> mirrors = mirrorUrls("/assets/" + location);
|
||||
if (preferMirror(fileSource)) {
|
||||
// Keep the VPS strictly first: spreadPrimary would otherwise rotate BMCLAPI ahead of
|
||||
// it, and BMCLAPI's asset CDN is unreachable on blocked networks (per-file timeouts).
|
||||
var candidates = new ArrayList<DownloadCandidate>(mirrors.size() + 2);
|
||||
candidates.add(DownloadCandidate.of(WebURL.parse(Metadata.getClientFileApi() + "/assets/" + location)));
|
||||
candidates.addAll(spreadPrimary(mirrors.stream().map(DownloadCandidate::of).toList(), officialUrl.href()));
|
||||
candidates.add(DownloadCandidate.of(officialUrl));
|
||||
return DownloadCandidates.of(List.copyOf(candidates));
|
||||
}
|
||||
return getCandidates(fileSource, officialUrl, mirrors);
|
||||
}
|
||||
|
||||
@NotNullByDefault
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.jackhuang.hmcl.setting.JavaVersionType;
|
||||
import org.jackhuang.hmcl.setting.LauncherVisibility;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.instances.Instances;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
@@ -255,32 +256,12 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && setting.getInheritable(GameSettings::highPerformanceProperty)) {
|
||||
@Nullable String javaPathGpuReg = FileUtils.getAbsolutePath(context.javaExecutable);
|
||||
try {
|
||||
WinReg reg = WinReg.INSTANCE;
|
||||
if (reg != null) {
|
||||
Object current = reg.queryValue(
|
||||
WinReg.HKEY.HKEY_CURRENT_USER,
|
||||
"Software\\Microsoft\\DirectX\\UserGpuPreferences",
|
||||
javaPathGpuReg
|
||||
);
|
||||
if (current == null) {
|
||||
boolean result = reg.setValue(
|
||||
WinReg.HKEY.HKEY_CURRENT_USER,
|
||||
"Software\\Microsoft\\DirectX\\UserGpuPreferences",
|
||||
javaPathGpuReg,
|
||||
"GpuPreference=2;"
|
||||
);
|
||||
if (result)
|
||||
LOG.info("Successfully applied high performance GPU preference for java: " + javaPathGpuReg);
|
||||
else
|
||||
LOG.warning("Failed to apply high performance GPU preference for java: " + javaPathGpuReg);
|
||||
} else {
|
||||
LOG.info("GPU preference for %s already exists: %s".formatted(javaPathGpuReg, current));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to apply high performance GPU preference", e);
|
||||
// Register both java.exe and javaw.exe: the game may be started through either one.
|
||||
applyHighPerformanceGpuPreference(context.javaExecutable);
|
||||
Path sibling = context.javaExecutable.resolveSibling(
|
||||
FileUtils.getName(context.javaExecutable).equalsIgnoreCase("javaw.exe") ? "java.exe" : "javaw.exe");
|
||||
if (Files.isRegularFile(sibling)) {
|
||||
applyHighPerformanceGpuPreference(sibling);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1048,11 +1029,44 @@ public final class LauncherHelper {
|
||||
runLater(() -> new GameCrashWindow(process, exitType, gameInstance, launchOptions, logs).show());
|
||||
}
|
||||
|
||||
if (exitType == ExitType.NORMAL) {
|
||||
// The game asked to be restarted (hot update): offer to update and relaunch.
|
||||
String restartReason = RestartRequest.consume(gameInstance.getRunDirectory());
|
||||
if (restartReason != null) {
|
||||
LOG.info("Game requested a restart, reason: " + restartReason);
|
||||
runLater(() -> Instances.offerRestart(gameInstance, restartReason));
|
||||
}
|
||||
}
|
||||
|
||||
checkExit();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Asks Windows to run `executable` on the high-performance GPU (`GpuPreference=2`) unless the user
|
||||
/// already has a preference stored for it.
|
||||
private static void applyHighPerformanceGpuPreference(Path executable) {
|
||||
String path = FileUtils.getAbsolutePath(executable);
|
||||
try {
|
||||
WinReg reg = WinReg.INSTANCE;
|
||||
if (reg == null) {
|
||||
return;
|
||||
}
|
||||
Object current = reg.queryValue(WinReg.HKEY.HKEY_CURRENT_USER,
|
||||
"Software\\Microsoft\\DirectX\\UserGpuPreferences", path);
|
||||
if (current != null) {
|
||||
LOG.info("GPU preference for %s already exists: %s".formatted(path, current));
|
||||
} else if (reg.setValue(WinReg.HKEY.HKEY_CURRENT_USER,
|
||||
"Software\\Microsoft\\DirectX\\UserGpuPreferences", path, "GpuPreference=2;")) {
|
||||
LOG.info("Successfully applied high performance GPU preference for java: " + path);
|
||||
} else {
|
||||
LOG.warning("Failed to apply high performance GPU preference for java: " + path);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to apply high performance GPU preference", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Queue<WeakReference<ManagedProcess>> PROCESSES = new ConcurrentLinkedQueue<>();
|
||||
|
||||
public static int countMangedProcesses() {
|
||||
|
||||
@@ -19,7 +19,6 @@ import org.jackhuang.hmcl.modpack.server.ServerModpackRemoteInstallTask;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.PortablePath;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
||||
@@ -44,6 +43,7 @@ import java.util.Set;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// Utilities for reading, installing, and applying modpack-specific game settings.
|
||||
@NotNullByDefault
|
||||
@@ -314,18 +314,13 @@ public final class ModpackHelper {
|
||||
setting.minMemoryProperty().setValue(c.getMinMemory());
|
||||
}
|
||||
|
||||
// MultiMC packs are untrusted input: never auto-apply PreLaunch/Wrapper/JVM overrides.
|
||||
if (c.isOverrideCommands()) {
|
||||
setting.getOverrideProperties().addAll(List.of(
|
||||
GameSettings.PROPERTY_COMMAND_WRAPPER,
|
||||
GameSettings.PROPERTY_PRE_LAUNCH_COMMAND
|
||||
));
|
||||
setting.commandWrapperProperty().setValue(Objects.requireNonNullElse(c.getWrapperCommand(), ""));
|
||||
setting.preLaunchCommandProperty().setValue(Objects.requireNonNullElse(c.getPreLaunchCommand(), ""));
|
||||
LOG.warning("Ignoring MultiMC OverrideCommands (PreLaunch/Wrapper) from modpack for security");
|
||||
}
|
||||
|
||||
if (c.isOverrideJavaArgs()) {
|
||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_JVM_OPTIONS);
|
||||
setting.jvmOptionsProperty().setValue(Objects.requireNonNullElse(c.getJvmArgs(), ""));
|
||||
LOG.warning("Ignoring MultiMC OverrideJavaArgs from modpack for security");
|
||||
}
|
||||
|
||||
if (c.isOverrideConsole()) {
|
||||
@@ -348,18 +343,13 @@ public final class ModpackHelper {
|
||||
}
|
||||
|
||||
private static void applyCommandAndJvmSettings(MultiMCInstanceConfiguration c, GameSettings.Instance setting) {
|
||||
// Same policy as toGameSettings: do not import executable commands / raw JVM args from packs.
|
||||
if (c.isOverrideCommands()) {
|
||||
setting.getOverrideProperties().addAll(List.of(
|
||||
GameSettings.PROPERTY_COMMAND_WRAPPER,
|
||||
GameSettings.PROPERTY_PRE_LAUNCH_COMMAND
|
||||
));
|
||||
setting.commandWrapperProperty().setValue(Lang.nonNull(c.getWrapperCommand(), ""));
|
||||
setting.preLaunchCommandProperty().setValue(Lang.nonNull(c.getPreLaunchCommand(), ""));
|
||||
LOG.warning("Ignoring MultiMC OverrideCommands (PreLaunch/Wrapper) from modpack update for security");
|
||||
}
|
||||
|
||||
if (c.isOverrideJavaArgs()) {
|
||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_JVM_OPTIONS);
|
||||
setting.jvmOptionsProperty().setValue(Lang.nonNull(c.getJvmArgs(), ""));
|
||||
LOG.warning("Ignoring MultiMC OverrideJavaArgs from modpack update for security");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/// The "please restart me" handshake between the game and the launcher.
|
||||
///
|
||||
/// When the game wants to be restarted (for example after a hot update), it writes
|
||||
/// [#FILE_NAME] into its run directory as `{"reason":"update"}` and exits normally. After the
|
||||
/// process ends the launcher calls [#consume(Path)], deletes the file and offers to update and relaunch.
|
||||
@NotNullByDefault
|
||||
public final class RestartRequest {
|
||||
private RestartRequest() {
|
||||
}
|
||||
|
||||
/// The marker file name inside the game's run directory.
|
||||
public static final String FILE_NAME = "tdlm-restart-request.json";
|
||||
|
||||
/// Reads and deletes the restart request in `runDirectory`.
|
||||
///
|
||||
/// A malformed file still counts as a request with reason `"unknown"`, because the file's presence is
|
||||
/// the signal. The file is always removed so a request cannot trigger twice.
|
||||
///
|
||||
/// @param runDirectory the game's run directory
|
||||
/// @return the reason, or `null` if no request was pending
|
||||
public static @Nullable String consume(Path runDirectory) {
|
||||
Path file = runDirectory.resolve(FILE_NAME);
|
||||
if (!Files.isRegularFile(file)) {
|
||||
return null;
|
||||
}
|
||||
String reason = "unknown";
|
||||
try {
|
||||
JsonObject object = JsonUtils.GSON.fromJson(Files.readString(file, StandardCharsets.UTF_8), JsonObject.class);
|
||||
if (object != null && object.has("reason") && object.get("reason").isJsonPrimitive()) {
|
||||
reason = object.get("reason").getAsString();
|
||||
}
|
||||
} catch (IOException | RuntimeException ignored) {
|
||||
// The presence of the file is the signal; keep the default reason.
|
||||
}
|
||||
try {
|
||||
Files.deleteIfExists(file);
|
||||
} catch (IOException ignored) {
|
||||
// Nothing sensible to do; the worst case is one more prompt on the next exit.
|
||||
}
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
@@ -645,9 +645,12 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
public static final String PROPERTY_HIGH_PERFORMANCE = "highPerformance";
|
||||
|
||||
/// Whether to launch games with high-performance GPU.
|
||||
///
|
||||
/// On by default: on a laptop with an integrated and a discrete GPU, Windows otherwise picks the
|
||||
/// integrated one for java. Machines with a single GPU are unaffected by the preference.
|
||||
@SerializedName(PROPERTY_HIGH_PERFORMANCE)
|
||||
private final InheritableProperty<Boolean> highPerformance =
|
||||
newInheritableProperty(PROPERTY_HIGH_PERFORMANCE, false);
|
||||
newInheritableProperty(PROPERTY_HIGH_PERFORMANCE, true);
|
||||
|
||||
/// Returns the high performance GPU preference property.
|
||||
public InheritableProperty<Boolean> highPerformanceProperty() {
|
||||
|
||||
@@ -354,36 +354,32 @@ public final class Controllers {
|
||||
);
|
||||
}
|
||||
|
||||
/// Installs the package found by [Metadata#findBundledModpackFile()] asynchronously, if present.
|
||||
///
|
||||
/// Shows progress without a cancel action and attempts to delete the package after success.
|
||||
/// Failures are logged; a retained package may be retried at the next startup.
|
||||
/// Installs a launcher-bundled modpack when present. First-run remote install is deferred to the
|
||||
/// home Start button ([Instances#ensureClientAndJoin]) so players never enter the install wizard.
|
||||
private static void tryInstallBundledModpack(HMCLGameRepository repository) {
|
||||
@Nullable Path modpackFile = Metadata.findBundledModpackFile();
|
||||
if (modpackFile == null) {
|
||||
return;
|
||||
if (modpackFile != null) {
|
||||
LOG.info("Found bundled modpack at " + modpackFile + "; starting automatic install");
|
||||
|
||||
Controllers.taskDialog(
|
||||
Task.composeAsync(Schedulers.io(), () -> {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(modpackFile);
|
||||
return ModpackHelper.getInstallTask(
|
||||
repository, modpackFile, new GameInstanceID(modpack.getName()), modpack, null);
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (ignored, exception) -> {
|
||||
if (exception != null) {
|
||||
LOG.warning("Failed to install bundled modpack", exception);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Files.deleteIfExists(modpackFile);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete bundled modpack: " + modpackFile, e);
|
||||
}
|
||||
}), i18n("modpack.installing"), TaskCancellationAction.NO_CANCEL
|
||||
);
|
||||
}
|
||||
|
||||
LOG.info("Found bundled modpack at " + modpackFile + "; starting automatic install");
|
||||
|
||||
Controllers.taskDialog(
|
||||
Task.composeAsync(Schedulers.io(), () -> {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(modpackFile);
|
||||
return ModpackHelper.getInstallTask(
|
||||
repository, modpackFile, new GameInstanceID(modpack.getName()), modpack, null);
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (ignored, exception) -> {
|
||||
if (exception != null) {
|
||||
LOG.warning("Failed to install bundled modpack", exception);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Files.deleteIfExists(modpackFile);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete bundled modpack: " + modpackFile, e);
|
||||
}
|
||||
}), i18n("modpack.installing"), TaskCancellationAction.NO_CANCEL
|
||||
);
|
||||
}
|
||||
|
||||
/// Shows the content in the main-window dialog stack without waiting for it to close.
|
||||
|
||||
@@ -602,6 +602,10 @@ public final class FXUtils {
|
||||
return;
|
||||
|
||||
String url = NetworkUtils.encodeLocation(link);
|
||||
if (!isAllowedOpenLink(url)) {
|
||||
LOG.warning("Refusing to open link with disallowed scheme: " + link);
|
||||
return;
|
||||
}
|
||||
thread(() -> {
|
||||
try {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
@@ -635,6 +639,23 @@ public final class FXUtils {
|
||||
});
|
||||
}
|
||||
|
||||
/// Only {@code http}/{@code https}/{@code mailto} may be handed to the OS URL handler.
|
||||
static boolean isAllowedOpenLink(String link) {
|
||||
try {
|
||||
URI uri = new URI(link);
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null) {
|
||||
return false;
|
||||
}
|
||||
return switch (scheme.toLowerCase(java.util.Locale.ROOT)) {
|
||||
case "http", "https", "mailto" -> true;
|
||||
default -> false;
|
||||
};
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> void bind(JFXTextField textField, Property<T> property, StringConverter<T> converter) {
|
||||
TextFieldBinding<T> binding = new TextFieldBinding<>(textField, property, converter);
|
||||
binding.updateTextField();
|
||||
|
||||
+20
-3
@@ -28,25 +28,40 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
private final HMCLGameRepository repository;
|
||||
private final Path file;
|
||||
private final String updateVersion;
|
||||
private final ServerModpackManifest remoteManifest;
|
||||
private String iconUrl;
|
||||
private boolean hasSource;
|
||||
|
||||
public ModpackInstallWizardProvider(HMCLGameRepository repository) {
|
||||
this(repository, null, null);
|
||||
this(repository, null, null, null);
|
||||
}
|
||||
|
||||
public ModpackInstallWizardProvider(HMCLGameRepository repository, Path modpackFile) {
|
||||
this(repository, modpackFile, null);
|
||||
this(repository, modpackFile, null, null);
|
||||
}
|
||||
|
||||
public ModpackInstallWizardProvider(HMCLGameRepository repository, GameInstanceID updateInstanceId) {
|
||||
this(repository, null, updateInstanceId);
|
||||
this(repository, null, updateInstanceId, null);
|
||||
}
|
||||
|
||||
/// Starts the wizard with a remote Server Modpack manifest (TDLM fileApi install).
|
||||
public ModpackInstallWizardProvider(HMCLGameRepository repository, ServerModpackManifest remoteManifest) {
|
||||
this(repository, null, null, remoteManifest);
|
||||
}
|
||||
|
||||
public ModpackInstallWizardProvider(HMCLGameRepository repository, Path modpackFile, GameInstanceID updateInstanceId) {
|
||||
this(repository, modpackFile, updateInstanceId, null);
|
||||
}
|
||||
|
||||
private ModpackInstallWizardProvider(
|
||||
HMCLGameRepository repository,
|
||||
Path modpackFile,
|
||||
GameInstanceID updateInstanceId,
|
||||
ServerModpackManifest remoteManifest) {
|
||||
this.repository = repository;
|
||||
this.file = modpackFile;
|
||||
this.updateVersion = updateInstanceId != null ? updateInstanceId.toString() : null;
|
||||
this.remoteManifest = remoteManifest;
|
||||
}
|
||||
|
||||
public void setIconUrl(String iconUrl) {
|
||||
@@ -57,6 +72,8 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
public void start(SettingsMap settings) {
|
||||
if (file != null)
|
||||
settings.put(LocalModpackPage.MODPACK_FILE, file);
|
||||
if (remoteManifest != null)
|
||||
settings.put(RemoteModpackPage.MODPACK_SERVER_MANIFEST, remoteManifest);
|
||||
if (updateVersion != null)
|
||||
settings.put(LocalModpackPage.MODPACK_NAME, updateVersion);
|
||||
if (StringUtils.isNotBlank(iconUrl))
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import javafx.beans.property.*;
|
||||
import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.TaskManager;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/// Observable progress of the home button's "install / update the client, then join" workflow.
|
||||
///
|
||||
/// The model is only a view of the task executor started by [Instances#ensureClientAndJoin(ClientSyncProgress)]:
|
||||
/// it never changes what is downloaded. All properties must be read and written on the JavaFX thread.
|
||||
/// Overall percentage is estimated from finished file downloads against the total announced by the sync task, and stays indeterminate until that total is known.
|
||||
@NotNullByDefault
|
||||
public final class ClientSyncProgress {
|
||||
|
||||
/// Lifecycle of the workflow.
|
||||
public enum State {
|
||||
/// Nothing is running; the home card shows its normal facts and button.
|
||||
IDLE,
|
||||
/// A task is running; the home card shows the progress state.
|
||||
RUNNING,
|
||||
/// The last run failed; the home card shows [#errorProperty()] and a retry button.
|
||||
FAILED
|
||||
}
|
||||
|
||||
/// Index of the step that checks the remote manifest.
|
||||
public static final int STEP_CHECK = 0;
|
||||
/// Index of the step that prepares Java.
|
||||
public static final int STEP_JAVA = 1;
|
||||
/// Index of the step that synchronises the modpack.
|
||||
public static final int STEP_SYNC = 2;
|
||||
/// Index of the final step that joins the server.
|
||||
public static final int STEP_JOIN = 3;
|
||||
|
||||
private final ObjectProperty<State> state = new SimpleObjectProperty<>(this, "state", State.IDLE);
|
||||
/// The first step that is not yet finished; equals 4 once every step is done.
|
||||
private final IntegerProperty step = new SimpleIntegerProperty(this, "step", STEP_CHECK);
|
||||
/// Fraction in `[0, 1]`, or `-1` while unknown.
|
||||
private final DoubleProperty progress = new SimpleDoubleProperty(this, "progress", -1);
|
||||
private final StringProperty detail = new SimpleStringProperty(this, "detail", "");
|
||||
private final StringProperty error = new SimpleStringProperty(this, "error", "");
|
||||
|
||||
private @Nullable TaskExecutor executor;
|
||||
private long downloadedBytes;
|
||||
/// Total number of downloads announced by tasks so far.
|
||||
private int expectedFiles;
|
||||
/// Number of finished file downloads.
|
||||
private int finishedFiles;
|
||||
/// Tasks whose `total` has already been added to [#expectedFiles].
|
||||
private final java.util.Set<Task<?>> announcedTasks = java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>());
|
||||
private boolean cancelled;
|
||||
|
||||
/// Keeps the weakly registered speed listener alive.
|
||||
@SuppressWarnings({"unused", "FieldCanBeLocal"})
|
||||
private final Consumer<FetchTask.SpeedEvent> speedHandler;
|
||||
|
||||
/// Creates an idle progress model.
|
||||
public ClientSyncProgress() {
|
||||
speedHandler = FetchTask.SPEED_EVENT.registerWeak(event -> FXUtils.runInFX(() -> {
|
||||
if (state.get() != State.RUNNING || event.getSpeed() <= 0)
|
||||
return;
|
||||
downloadedBytes += event.getSpeed();
|
||||
detail.set(String.format("%.0f MB", downloadedBytes / 1048576.0) + " · " + I18n.formatSpeed(event.getSpeed()));
|
||||
}));
|
||||
}
|
||||
|
||||
/// The workflow state.
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return state;
|
||||
}
|
||||
|
||||
/// The first unfinished step index (0–3), or 4 when everything is done.
|
||||
public ReadOnlyIntegerProperty stepProperty() {
|
||||
return step;
|
||||
}
|
||||
|
||||
/// Overall fraction in `[0, 1]`, or `-1` when it cannot be estimated yet.
|
||||
public ReadOnlyDoubleProperty progressProperty() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
/// Transfer details such as downloaded size and speed; empty when unknown.
|
||||
public ReadOnlyStringProperty detailProperty() {
|
||||
return detail;
|
||||
}
|
||||
|
||||
/// The failure message of the last run; empty unless the state is [State#FAILED].
|
||||
public ReadOnlyStringProperty errorProperty() {
|
||||
return error;
|
||||
}
|
||||
|
||||
/// Starts a new run: resets counters and shows the progress state at the first step.
|
||||
void begin() {
|
||||
executor = null;
|
||||
cancelled = false;
|
||||
downloadedBytes = 0;
|
||||
expectedFiles = 0;
|
||||
finishedFiles = 0;
|
||||
announcedTasks.clear();
|
||||
progress.set(-1);
|
||||
detail.set("");
|
||||
error.set("");
|
||||
step.set(STEP_CHECK);
|
||||
state.set(State.RUNNING);
|
||||
}
|
||||
|
||||
/// Moves the indicator forward to `newStep`; it never moves backwards.
|
||||
void advanceTo(int newStep) {
|
||||
if (newStep > step.get())
|
||||
step.set(newStep);
|
||||
}
|
||||
|
||||
/// Returns to the idle state, for example after the game was launched or the run was cancelled.
|
||||
void finish() {
|
||||
executor = null;
|
||||
state.set(State.IDLE);
|
||||
}
|
||||
|
||||
/// Shows `message` in the card together with the retry button.
|
||||
void fail(String message) {
|
||||
executor = null;
|
||||
error.set(message);
|
||||
state.set(State.FAILED);
|
||||
}
|
||||
|
||||
/// Dismisses a failure and returns to the idle state.
|
||||
public void dismiss() {
|
||||
if (state.get() == State.FAILED)
|
||||
state.set(State.IDLE);
|
||||
}
|
||||
|
||||
/// Cancels the running task, if any, and returns to the idle state.
|
||||
public void cancel() {
|
||||
TaskExecutor current = executor;
|
||||
cancelled = true;
|
||||
if (current != null)
|
||||
current.cancel();
|
||||
finish();
|
||||
}
|
||||
|
||||
/// Returns whether the last run was cancelled by the user.
|
||||
boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
/// Recomputes the fraction from finished downloads; stays indeterminate until a total is known.
|
||||
private void refreshProgress(TaskExecutor current) {
|
||||
if (executor != current || expectedFiles <= 0)
|
||||
return;
|
||||
double value = Math.min(0.99, (double) finishedFiles / expectedFiles);
|
||||
if (value > progress.get())
|
||||
progress.set(value);
|
||||
}
|
||||
|
||||
/// Submits `task` to the serial task queue and mirrors its task count into the progress.
|
||||
///
|
||||
/// @param task the task to run
|
||||
void submit(Task<?> task) {
|
||||
TaskExecutor taskExecutor = task.executor();
|
||||
executor = taskExecutor;
|
||||
taskExecutor.addTaskListener(new TaskListener() {
|
||||
/// Tasks announce how many downloads they will start through the `total` property
|
||||
/// (set once the file list is known), so the denominator never grows with discovered tasks.
|
||||
@Override
|
||||
public void onPropertiesUpdate(Task<?> updated) {
|
||||
Object total = updated.getProperties().get("total");
|
||||
if (total instanceof Integer count && count > 0) {
|
||||
FXUtils.runInFX(() -> {
|
||||
if (announcedTasks.add(updated))
|
||||
expectedFiles += count;
|
||||
refreshProgress(taskExecutor);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinished(Task<?> finishedTask) {
|
||||
if (finishedTask instanceof FileDownloadTask) {
|
||||
FXUtils.runInFX(() -> {
|
||||
finishedFiles++;
|
||||
refreshProgress(taskExecutor);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
TaskManager.getInstance().submit(taskExecutor, null);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,10 @@ import org.jackhuang.hmcl.download.game.GameDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.modpack.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackManifest;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -22,21 +26,26 @@ import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.download.LocalModpackPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.modpack.Modpack;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -45,36 +54,244 @@ public final class Instances {
|
||||
private Instances() {
|
||||
}
|
||||
|
||||
/// Installs or updates the server client from the launcher-bundled modpack when present.
|
||||
/// Returns whether the installed server modpack is behind the remote `server-manifest.json`.
|
||||
///
|
||||
/// Falls back to the local/remote modpack import wizard when no bundled package is found.
|
||||
public static void installServerModpack() {
|
||||
/// Compares the local embedded manifest version (and file path/hash set) with
|
||||
/// `fileApi/server-manifest.json`. Network or parse failures return `false` so the home button
|
||||
/// stays on "join".
|
||||
public static boolean isClientUpdateAvailable(@Nullable HMCLGameInstance instance) {
|
||||
if (instance == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Path configurationFile = instance.getModpackConfigurationFile();
|
||||
if (Files.notExists(configurationFile)) {
|
||||
return false;
|
||||
}
|
||||
ModpackConfiguration<ServerModpackManifest> configuration = JsonUtils.fromJsonFile(
|
||||
configurationFile, ModpackConfiguration.typeOf(ServerModpackManifest.class));
|
||||
ServerModpackManifest local = configuration.getManifest();
|
||||
if (local == null || StringUtils.isBlank(local.getFileApi())) {
|
||||
return false;
|
||||
}
|
||||
String manifestUrl = StringUtils.removeSuffix(local.getFileApi().trim(), "/") + "/server-manifest.json";
|
||||
ServerModpackManifest remote = JsonUtils.fromNonNullJson(
|
||||
NetworkUtils.doGet(manifestUrl), ServerModpackManifest.class);
|
||||
if (!Objects.equals(local.getVersion(), remote.getVersion())) {
|
||||
return true;
|
||||
}
|
||||
return !fileHashes(local).equals(fileHashes(remote));
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to check TDLM client update", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, String> fileHashes(ServerModpackManifest manifest) {
|
||||
return manifest.getFiles().stream().collect(Collectors.toMap(
|
||||
ModpackConfiguration.FileInformation::getPath,
|
||||
ModpackConfiguration.FileInformation::getHash,
|
||||
(left, right) -> right));
|
||||
}
|
||||
|
||||
/// Installs or updates the TDLM client from fileApi / bundled zip without opening the install UI,
|
||||
/// then joins the configured server when ready.
|
||||
///
|
||||
/// Prefer this for the home Start / Join button. Existing server-modpack instances get differential
|
||||
/// updates during launch via [org.jackhuang.hmcl.modpack.server.ServerModpackCompletionTask].
|
||||
public static void ensureClientAndJoin() {
|
||||
ensureClientAndJoin(null);
|
||||
}
|
||||
|
||||
/// Same as [#ensureClientAndJoin()], but reports task progress to `progress` instead of opening a
|
||||
/// task dialog, so the home card can show it in place. Errors are shown through `progress` too.
|
||||
///
|
||||
/// @param progress the progress model to drive, or `null` to use the task dialog
|
||||
public static void ensureClientAndJoin(@Nullable ClientSyncProgress progress) {
|
||||
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
|
||||
if (!repository.isLoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Path bundled = Metadata.findBundledModpackFile();
|
||||
if (bundled != null) {
|
||||
Controllers.getDecorator().startWizard(
|
||||
new ModpackInstallWizardProvider(repository, bundled),
|
||||
i18n("install.modpack"));
|
||||
// Already installed: join immediately. Launch runs ServerModpackCompletionTask for updates.
|
||||
if (repository.getSelectedInstance() != null && Metadata.findBundledModpackFile() == null) {
|
||||
joinServer(repository.getSelectedInstance());
|
||||
return;
|
||||
}
|
||||
|
||||
importModpack();
|
||||
Path bundled = Metadata.findBundledModpackFile();
|
||||
if (bundled != null) {
|
||||
installBundledThenJoin(repository, bundled, progress);
|
||||
return;
|
||||
}
|
||||
|
||||
if (progress != null) {
|
||||
progress.begin();
|
||||
}
|
||||
Task<?> task = new GetTask(WebURL.parse(Metadata.getClientManifestUrl()))
|
||||
.thenComposeAsync(Schedulers.javafx(), result -> {
|
||||
ServerModpackManifest manifest = JsonUtils.fromMaybeMalformedJson(
|
||||
result, ServerModpackManifest.class);
|
||||
if (manifest == null) {
|
||||
throw new IOException(i18n("modpack.type.server.malformed"));
|
||||
}
|
||||
if (progress != null) {
|
||||
// Java is prepared by the launch step; mark it as passed once the manifest is known.
|
||||
progress.advanceTo(ClientSyncProgress.STEP_SYNC);
|
||||
}
|
||||
return ensureRemoteClientInstalled(repository, manifest);
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (ignored, exception) -> {
|
||||
if (exception != null) {
|
||||
LOG.warning("Failed to install or update TDLM client", exception);
|
||||
reportFailure(progress, exception);
|
||||
return;
|
||||
}
|
||||
if (progress != null) {
|
||||
progress.advanceTo(ClientSyncProgress.STEP_JOIN);
|
||||
}
|
||||
joinServer(GameDirectoryManager.getSelectedRepository().getSelectedInstance());
|
||||
if (progress != null) {
|
||||
progress.advanceTo(ClientSyncProgress.STEP_JOIN + 1);
|
||||
progress.finish();
|
||||
}
|
||||
});
|
||||
runSyncTask(task, progress, TaskCancellationAction.NORMAL);
|
||||
}
|
||||
|
||||
public static void importModpack() {
|
||||
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
|
||||
if (repository.isLoaded()) {
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(repository), i18n("install.modpack"));
|
||||
/// Runs `task` in the progress card when `progress` is set, otherwise in the task dialog.
|
||||
private static void runSyncTask(Task<?> task, @Nullable ClientSyncProgress progress, TaskCancellationAction cancellation) {
|
||||
if (progress != null) {
|
||||
progress.submit(task);
|
||||
} else {
|
||||
Controllers.taskDialog(task, i18n("modpack.installing"), cancellation);
|
||||
}
|
||||
}
|
||||
|
||||
/// @deprecated use [#installServerModpack()]
|
||||
/// Shows a failure in the progress card, or in an error dialog when there is no card.
|
||||
/// A user-requested cancellation is not reported as an error.
|
||||
private static void reportFailure(@Nullable ClientSyncProgress progress, Throwable exception) {
|
||||
if (progress != null) {
|
||||
if (progress.isCancelled()) {
|
||||
progress.finish();
|
||||
} else {
|
||||
progress.fail(StringUtils.getStackTrace(exception));
|
||||
}
|
||||
return;
|
||||
}
|
||||
Controllers.dialog(
|
||||
StringUtils.getStackTrace(exception),
|
||||
i18n("install.failed"),
|
||||
MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
|
||||
/// Silent first-install / update from the org client manifest — no wizard UI.
|
||||
public static void installServerModpack() {
|
||||
ensureClientAndJoin();
|
||||
}
|
||||
|
||||
/// Install wizard is disabled for the dedicated server-client launcher.
|
||||
public static void importModpack() {
|
||||
LOG.info("Ignoring importModpack: install UI is disabled; use Start to install/update");
|
||||
}
|
||||
|
||||
/// @deprecated use [#ensureClientAndJoin()]
|
||||
public static void addNewGame() {
|
||||
installServerModpack();
|
||||
ensureClientAndJoin();
|
||||
}
|
||||
|
||||
/// Installs a local modpack zip without the wizard, then selects the new instance.
|
||||
public static void installLocalModpackSilently(HMCLGameRepository repository, Path file) {
|
||||
if (!repository.isLoaded()) {
|
||||
return;
|
||||
}
|
||||
Controllers.taskDialog(
|
||||
Task.composeAsync(Schedulers.io(), () -> {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(file);
|
||||
String name = LocalModpackPage.processModpackName(modpack.getName());
|
||||
if (StringUtils.isBlank(name)) {
|
||||
name = "TDLM";
|
||||
}
|
||||
GameInstanceID instanceId = new GameInstanceID(name);
|
||||
if (repository.hasInstance(instanceId)) {
|
||||
instanceId = new GameInstanceID(name + "-" + System.currentTimeMillis());
|
||||
}
|
||||
GameInstanceID id = instanceId;
|
||||
return ModpackHelper.getInstallTask(repository, file, id, modpack, null)
|
||||
.thenRunAsync(Schedulers.javafx(), () ->
|
||||
repository.setSelectedInstance(repository.getInstance(id)));
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (ignored, exception) -> {
|
||||
if (exception != null) {
|
||||
LOG.warning("Failed to install local modpack", exception);
|
||||
Controllers.dialog(
|
||||
StringUtils.getStackTrace(exception),
|
||||
i18n("install.failed"),
|
||||
MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}),
|
||||
i18n("modpack.installing"),
|
||||
TaskCancellationAction.NORMAL);
|
||||
}
|
||||
|
||||
private static void installBundledThenJoin(HMCLGameRepository repository, Path bundled, @Nullable ClientSyncProgress progress) {
|
||||
if (progress != null) {
|
||||
progress.begin();
|
||||
progress.advanceTo(ClientSyncProgress.STEP_SYNC);
|
||||
}
|
||||
Task<?> task = Task.composeAsync(Schedulers.io(), () -> {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(bundled);
|
||||
String name = LocalModpackPage.processModpackName(modpack.getName());
|
||||
if (StringUtils.isBlank(name)) {
|
||||
name = "TDLM";
|
||||
}
|
||||
GameInstanceID instanceId = new GameInstanceID(name);
|
||||
return ModpackHelper.getInstallTask(repository, bundled, instanceId, modpack, null)
|
||||
.thenRunAsync(Schedulers.javafx(), () -> {
|
||||
repository.setSelectedInstance(repository.getInstance(instanceId));
|
||||
try {
|
||||
Files.deleteIfExists(bundled);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete bundled modpack: " + bundled, e);
|
||||
}
|
||||
});
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (ignored, exception) -> {
|
||||
if (exception != null) {
|
||||
LOG.warning("Failed to install bundled modpack", exception);
|
||||
reportFailure(progress, exception);
|
||||
return;
|
||||
}
|
||||
joinServer(GameDirectoryManager.getSelectedRepository().getSelectedInstance());
|
||||
if (progress != null) {
|
||||
progress.advanceTo(ClientSyncProgress.STEP_JOIN + 1);
|
||||
progress.finish();
|
||||
}
|
||||
});
|
||||
runSyncTask(task, progress, TaskCancellationAction.NO_CANCEL);
|
||||
}
|
||||
|
||||
/// Installs when missing; selects an existing instance otherwise.
|
||||
///
|
||||
/// Differential updates for an installed server modpack run during launch via
|
||||
/// [org.jackhuang.hmcl.modpack.server.ServerModpackCompletionTask].
|
||||
private static Task<?> ensureRemoteClientInstalled(
|
||||
HMCLGameRepository repository, ServerModpackManifest manifest) throws IOException {
|
||||
Modpack modpack = manifest.toModpack();
|
||||
String name = LocalModpackPage.processModpackName(manifest.getName());
|
||||
if (StringUtils.isBlank(name)) {
|
||||
name = "TDLM";
|
||||
}
|
||||
GameInstanceID instanceId = new GameInstanceID(name);
|
||||
|
||||
if (repository.hasInstance(instanceId)) {
|
||||
repository.setSelectedInstance(repository.getInstance(instanceId));
|
||||
return Task.completed(null);
|
||||
}
|
||||
|
||||
return ModpackHelper.getInstallTask(repository, manifest, instanceId, modpack)
|
||||
.thenRunAsync(Schedulers.javafx(), () ->
|
||||
repository.setSelectedInstance(repository.getInstance(instanceId)));
|
||||
}
|
||||
|
||||
public static void deleteInstance(HMCLGameInstance gameInstance) {
|
||||
@@ -227,8 +444,25 @@ public final class Instances {
|
||||
.addQuestion(new PromptDialogPane.Builder.BooleanQuestion(i18n("instance.manage.duplicate.duplicate_save"), false)));
|
||||
}
|
||||
|
||||
/// Asks whether to update and relaunch after the game requested a restart (see [RestartRequest]).
|
||||
///
|
||||
/// Yes runs the Start flow, which syncs the client pack and joins the server again; No leaves the
|
||||
/// launcher as it is. The request file is already deleted, so the prompt shows only once.
|
||||
///
|
||||
/// @param gameInstance the instance that just exited
|
||||
/// @param reason the reason from the request file, for example `update`
|
||||
public static void offerRestart(HMCLGameInstance gameInstance, String reason) {
|
||||
Controllers.confirm(
|
||||
i18n("home.restart." + ("update".equals(reason) ? "update" : "generic")),
|
||||
i18n("home.restart.title"),
|
||||
() -> updateInstance(gameInstance),
|
||||
null);
|
||||
}
|
||||
|
||||
/// Updates via fileApi through the Start flow — no install wizard.
|
||||
public static void updateInstance(HMCLGameInstance gameInstance) {
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(gameInstance.getRepository(), gameInstance.getId()));
|
||||
gameInstance.getRepository().setSelectedInstance(gameInstance);
|
||||
ensureClientAndJoin();
|
||||
}
|
||||
|
||||
/// Queues an asset check with a fresh asset index for the supplied instance.
|
||||
@@ -308,14 +542,7 @@ public final class Instances {
|
||||
@SafeVarargs
|
||||
public static void launch(@Nullable HMCLGameInstance gameInstance, Consumer<LauncherHelper>... injecters) {
|
||||
if (gameInstance == null) {
|
||||
JFXButton installModpack = new JFXButton(i18n("install.modpack"));
|
||||
installModpack.getStyleClass().add("dialog-accept");
|
||||
installModpack.setOnAction(e -> installServerModpack());
|
||||
|
||||
Controllers.confirmAction(i18n("instance.empty.launch"), i18n("launch.failed"),
|
||||
MessageDialogPane.MessageType.ERROR,
|
||||
installModpack,
|
||||
null);
|
||||
ensureClientAndJoin();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -382,11 +609,10 @@ public final class Instances {
|
||||
Controllers.navigate(Controllers.getSettingsPage());
|
||||
}
|
||||
|
||||
/// Instance management UI is disabled for the dedicated server-client launcher.
|
||||
public static void modifyGameSettings(HMCLGameInstance gameInstance) {
|
||||
Controllers.getGameInstancePage().setInstance(gameInstance.getId(), gameInstance.getRepository());
|
||||
Controllers.getGameInstancePage().showInstanceSettings();
|
||||
// VersionPage.loadVersion will be invoked after navigation
|
||||
Controllers.navigate(Controllers.getGameInstancePage());
|
||||
LOG.info("Ignoring modifyGameSettings for " + gameInstance.getId()
|
||||
+ ": instance management UI is disabled");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,11 +3,18 @@
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.instances.Instances;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
@@ -20,7 +27,6 @@ import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
import org.jackhuang.hmcl.util.platform.hardware.CentralProcessor;
|
||||
import org.jackhuang.hmcl.util.platform.hardware.DeviceInfo;
|
||||
import org.jackhuang.hmcl.util.platform.hardware.GraphicsCard;
|
||||
import org.jackhuang.hmcl.util.platform.hardware.PageFileConfiguration;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
@@ -34,16 +40,22 @@ import static org.jackhuang.hmcl.util.i18n.I18n.getTranslator;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// Displays launcher, system, device, runtime, and project information.
|
||||
/// Displays a compact about page: identity, update check, licenses, source link and diagnostics.
|
||||
///
|
||||
/// System, device and runtime details are not listed; they are collected by "Copy diagnostics".
|
||||
@NotNullByDefault
|
||||
public final class AboutPage extends SpinnerPane {
|
||||
|
||||
/// The upstream project that this launcher is based on, and the source of its GPL-licensed code.
|
||||
private static final String SOURCE_URL = "https://github.com/HMCL-dev/HMCL";
|
||||
|
||||
/// Keeps theme listeners alive while the page is visible.
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
/// Builds the page from cached hardware information and bundled project lists.
|
||||
/// Builds the page from bundled project lists.
|
||||
public AboutPage() {
|
||||
VBox content = new VBox();
|
||||
VBox content = new VBox(13);
|
||||
content.setPadding(new Insets(21));
|
||||
content.getStyleClass().add("spinner-pane-content");
|
||||
ScrollPane scrollPane = new ScrollPane(content);
|
||||
scrollPane.setFitToWidth(true);
|
||||
@@ -56,196 +68,112 @@ public final class AboutPage extends SpinnerPane {
|
||||
launcher.setLargeTitle(true);
|
||||
launcher.setLeading(FXUtils.newBuiltinImage("/assets/img/icon.png"));
|
||||
launcher.setTitle(Metadata.FULL_NAME);
|
||||
launcher.setSubtitle(Metadata.VERSION);
|
||||
launcher.setSubtitle(Metadata.VERSION + " · " + i18n("about.based_on"));
|
||||
|
||||
JFXButton checkUpdate = FXUtils.newBorderButton(i18n("about.check_update"));
|
||||
checkUpdate.setOnAction(e -> checkClientUpdate(checkUpdate));
|
||||
launcher.setRight(checkUpdate);
|
||||
|
||||
about.getContent().setAll(launcher);
|
||||
}
|
||||
|
||||
ComponentList systemInfo = new ComponentList();
|
||||
{
|
||||
var system = new LinePane();
|
||||
system.setLargeTitle(true);
|
||||
system.setLeading(SVG.LUCIDE_APP_WINDOW.createIcon(32));
|
||||
system.setTitle(i18n("about.system.os"));
|
||||
system.setSubtitle((OperatingSystem.OS_RELEASE_PRETTY_NAME == null
|
||||
? "%s %s".formatted(OperatingSystem.SYSTEM_NAME, OperatingSystem.SYSTEM_VERSION.getVersion())
|
||||
: "%s (%s %s)".formatted(OperatingSystem.OS_RELEASE_PRETTY_NAME, OperatingSystem.SYSTEM_NAME, OperatingSystem.SYSTEM_VERSION.getVersion())));
|
||||
systemInfo.getContent().add(system);
|
||||
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && SystemInfo.getDesktopEnvironment() != null) {
|
||||
var desktop = new LinePane();
|
||||
desktop.setLargeTitle(true);
|
||||
desktop.setLeading(SVG.LUCIDE_PANELS_TOP_LEFT.createIcon(32));
|
||||
desktop.setTitle(i18n("about.system.desktop"));
|
||||
desktop.setSubtitle(SystemInfo.getDesktopEnvironment());
|
||||
systemInfo.getContent().add(desktop);
|
||||
}
|
||||
}
|
||||
|
||||
ComponentList deviceInfo = new ComponentList();
|
||||
{
|
||||
@Nullable DeviceInfo deviceIdentity = SystemInfo.getDeviceInfo();
|
||||
if (deviceIdentity != null
|
||||
&& !"Virtual Machine".equals(deviceIdentity.name())
|
||||
&& OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS
|
||||
&& (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS || !Architecture.SYSTEM_ARCH.isX86())) {
|
||||
var device = new LinePane();
|
||||
device.setLargeTitle(true);
|
||||
device.setLeading(SVG.HOST.createIcon(32));
|
||||
device.setTitle(i18n("about.device.model"));
|
||||
device.setSubtitle(getTranslator().formatDeviceName(deviceIdentity));
|
||||
deviceInfo.getContent().add(device);
|
||||
}
|
||||
|
||||
@Unmodifiable List<CentralProcessor> processors = SystemInfo.getCentralProcessors();
|
||||
for (int i = 0; i < processors.size(); i++) {
|
||||
String title = processors.size() == 1
|
||||
? i18n("about.device.cpu")
|
||||
: i18n("about.device.cpu.n", i + 1);
|
||||
CentralProcessor processor = processors.get(i);
|
||||
String name = processor.getName();
|
||||
@Nullable CentralProcessor.Cores cores = processor.getCores();
|
||||
String subtitle;
|
||||
if (cores == null)
|
||||
subtitle = i18n("about.device.cpu.name", name);
|
||||
else if (cores.efficiency() > 0) {
|
||||
subtitle = cores.logical() == cores.physical()
|
||||
? i18n("about.device.cpu.name.cores.hybrid", name, cores.performance(), cores.efficiency())
|
||||
: i18n("about.device.cpu.name.cores.hybrid.threads", name, cores.performance(), cores.efficiency(), cores.logical());
|
||||
} else {
|
||||
subtitle = cores.logical() == cores.physical()
|
||||
? i18n("about.device.cpu.name.cores", name, cores.physical())
|
||||
: i18n("about.device.cpu.name.cores.threads", name, cores.physical(), cores.logical());
|
||||
}
|
||||
|
||||
var pane = new LinePane();
|
||||
pane.setLargeTitle(true);
|
||||
pane.setTitle(title);
|
||||
pane.setSubtitle(subtitle);
|
||||
pane.setLeading(SVG.LUCIDE_CPU.createIcon(32));
|
||||
pane.getTags().add(new Tag(Architecture.SYSTEM_ARCH.getDisplayName()));
|
||||
deviceInfo.getContent().add(pane);
|
||||
}
|
||||
|
||||
@Unmodifiable List<GraphicsCard> visibleGraphicsCards = SystemInfo.getGraphicsCards().stream()
|
||||
.filter(card -> card.getType() != GraphicsCard.Type.DISPLAY_ONLY && !"Unknown".equals(card.getName()))
|
||||
.sorted(Comparator.comparing(GraphicsCard::getType))
|
||||
.toList();
|
||||
|
||||
for (int i = 0; i < visibleGraphicsCards.size(); i++) {
|
||||
GraphicsCard graphicsCard = visibleGraphicsCards.get(i);
|
||||
|
||||
String title = visibleGraphicsCards.size() == 1
|
||||
? i18n("about.device.gpu")
|
||||
: i18n("about.device.gpu.n", i + 1);
|
||||
|
||||
var button = new LinePane();
|
||||
button.setLargeTitle(true);
|
||||
button.setLeading(SVG.LUCIDE_GPU.createIcon(32));
|
||||
button.setTitle(title);
|
||||
|
||||
StringBuilder subtitle = new StringBuilder(graphicsCard.getName());
|
||||
|
||||
if (graphicsCard.getDriver() != null || graphicsCard.getDriverVersion() != null) {
|
||||
subtitle.append(" (");
|
||||
if (graphicsCard.getDriver() != null) {
|
||||
subtitle.append(graphicsCard.getDriver());
|
||||
if (graphicsCard.getDriverVersion() != null) {
|
||||
subtitle.append(' ');
|
||||
}
|
||||
}
|
||||
if (graphicsCard.getDriverVersion() != null) {
|
||||
subtitle.append(graphicsCard.getDriverVersion());
|
||||
}
|
||||
subtitle.append(')');
|
||||
}
|
||||
|
||||
button.setSubtitle(subtitle.toString());
|
||||
|
||||
switch (graphicsCard.getType()) {
|
||||
case INTEGRATED -> button.getTags().add(new Tag(i18n("about.device.gpu.type.integrated")));
|
||||
case DISCRETE -> button.getTags().add(new Tag(i18n("about.device.gpu.type.discrete")));
|
||||
case VIRTUAL -> button.getTags().add(new Tag(i18n("about.device.gpu.type.virtual")));
|
||||
}
|
||||
|
||||
deviceInfo.getContent().add(button);
|
||||
}
|
||||
|
||||
if (SystemInfo.getTotalMemorySize() > 0) {
|
||||
String subtitle = i18n("about.device.memory.system",
|
||||
"%.1f GiB".formatted(DataSizeUnit.GIGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize())));
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
long pageFileSize = SystemInfo.getTotalPageFileSize();
|
||||
if (pageFileSize < 0) {
|
||||
subtitle = i18n("about.device.memory.page_files.unknown", subtitle);
|
||||
} else {
|
||||
String pageFileCapacity = "%.1f GiB".formatted(DataSizeUnit.GIGABYTES.convertFromBytes(pageFileSize));
|
||||
PageFileConfiguration configuration = SystemInfo.getPageFileConfiguration();
|
||||
subtitle = switch (configuration.mode()) {
|
||||
case MANUAL -> i18n("about.device.memory.page_files.maximum",
|
||||
subtitle,
|
||||
pageFileCapacity,
|
||||
"%.1f GiB".formatted(DataSizeUnit.GIGABYTES.convertFromBytes(configuration.maximumSize())));
|
||||
case SYSTEM_MANAGED, MIXED, UNKNOWN -> i18n("about.device.memory.page_files", subtitle, pageFileCapacity);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var memory = new LinePane();
|
||||
memory.setLargeTitle(true);
|
||||
memory.setLeading(SVG.LUCIDE_MEMORY_STICK.createIcon(32));
|
||||
memory.setTitle(i18n("about.device.memory"));
|
||||
memory.setSubtitle(subtitle);
|
||||
deviceInfo.getContent().add(memory);
|
||||
}
|
||||
}
|
||||
|
||||
ComponentList runtimeInfo = new ComponentList();
|
||||
{
|
||||
var java = new LinePane();
|
||||
java.setLargeTitle(true);
|
||||
java.setLeading(SVG.LOCAL_CAFE.createIcon(32));
|
||||
java.setTitle("Java");
|
||||
java.setSubtitle("%s, %s, %s".formatted(
|
||||
Runtime.version().toString(),
|
||||
System.getProperty("java.vm.name"),
|
||||
System.getProperty("java.vendor")));
|
||||
runtimeInfo.getContent().add(java);
|
||||
|
||||
var javaFX = new LinePane();
|
||||
javaFX.setLargeTitle(true);
|
||||
javaFX.setLeading(SVG.LOCAL_CAFE.createIcon(32));
|
||||
javaFX.setTitle("JavaFX");
|
||||
javaFX.setSubtitle(System.getProperty("javafx.runtime.version"));
|
||||
runtimeInfo.getContent().add(javaFX);
|
||||
}
|
||||
|
||||
ComponentList deps = loadIconedTwoLineList("/assets/about/deps.json");
|
||||
|
||||
// The HMCL GPLv3 notice and the third-party dependency list must stay reachable.
|
||||
ComponentList legal = new ComponentList();
|
||||
{
|
||||
var copyright = new LinePane();
|
||||
copyright.setLargeTitle(true);
|
||||
copyright.setTitle(i18n("about.copyright"));
|
||||
copyright.setSubtitle(i18n("about.copyright.statement"));
|
||||
var license = new LinePane();
|
||||
license.setLargeTitle(true);
|
||||
license.setTitle(i18n("about.open_source"));
|
||||
license.setSubtitle(i18n("about.open_source.statement") + "\n" + i18n("about.copyright.statement"));
|
||||
legal.getContent().add(license);
|
||||
|
||||
legal.getContent().setAll(copyright);
|
||||
var source = new LineButton();
|
||||
source.setLargeTitle(true);
|
||||
source.setTitle(i18n("about.source_code"));
|
||||
source.setSubtitle(SOURCE_URL);
|
||||
source.setTrailingIcon(SVG.OPEN_IN_NEW);
|
||||
source.setOnAction(e -> FXUtils.openLink(SOURCE_URL));
|
||||
legal.getContent().add(source);
|
||||
|
||||
ComponentList deps = loadIconedTwoLineList("/assets/about/deps.json");
|
||||
var depsSublist = new ComponentSublist(() -> List.of(deps));
|
||||
depsSublist.setTitle(i18n("about.dependency"));
|
||||
legal.getContent().add(depsSublist);
|
||||
}
|
||||
|
||||
content.getChildren().setAll(
|
||||
ComponentList.createComponentListTitle(i18n("about")),
|
||||
about,
|
||||
ComponentList.createComponentListTitle(i18n("about.system")),
|
||||
systemInfo,
|
||||
ComponentList.createComponentListTitle(i18n("about.device")),
|
||||
deviceInfo,
|
||||
ComponentList.createComponentListTitle(i18n("about.runtime")),
|
||||
runtimeInfo,
|
||||
ComponentList.createComponentListTitle(i18n("about.dependency")),
|
||||
deps,
|
||||
ComponentList.createComponentListTitle(i18n("about.legal")),
|
||||
legal
|
||||
);
|
||||
ComponentList support = new ComponentList();
|
||||
{
|
||||
var diagnostics = new LineButton();
|
||||
diagnostics.setLargeTitle(true);
|
||||
diagnostics.setTitle(i18n("about.copy_diagnostics"));
|
||||
diagnostics.setTrailingIcon(SVG.CONTENT_COPY);
|
||||
diagnostics.setOnAction(e -> FXUtils.copyText(buildDiagnostics()));
|
||||
support.getContent().add(diagnostics);
|
||||
}
|
||||
|
||||
content.getChildren().setAll(about, legal, support);
|
||||
}
|
||||
|
||||
/// Checks whether the installed client pack is behind the remote manifest and tells the user.
|
||||
///
|
||||
/// @param button the button to disable while the check runs
|
||||
private static void checkClientUpdate(JFXButton button) {
|
||||
HMCLGameInstance instance = GameDirectoryManager.getSelectedRepository().getSelectedInstance();
|
||||
if (instance == null) {
|
||||
Controllers.showToast(i18n("about.check_update.not_installed"));
|
||||
return;
|
||||
}
|
||||
button.setDisable(true);
|
||||
Schedulers.io().execute(() -> {
|
||||
boolean available = Instances.isClientUpdateAvailable(instance);
|
||||
FXUtils.runInFX(() -> {
|
||||
button.setDisable(false);
|
||||
Controllers.showToast(i18n(available ? "about.check_update.available" : "about.check_update.latest"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Collects launcher, system, device and runtime information as plain text for bug reports.
|
||||
private static String buildDiagnostics() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(Metadata.FULL_NAME).append(' ').append(Metadata.VERSION).append('\n');
|
||||
sb.append("OS: ").append(OperatingSystem.OS_RELEASE_PRETTY_NAME == null
|
||||
? "%s %s".formatted(OperatingSystem.SYSTEM_NAME, OperatingSystem.SYSTEM_VERSION.getVersion())
|
||||
: "%s (%s %s)".formatted(OperatingSystem.OS_RELEASE_PRETTY_NAME, OperatingSystem.SYSTEM_NAME, OperatingSystem.SYSTEM_VERSION.getVersion()))
|
||||
.append(" (").append(Architecture.SYSTEM_ARCH.getDisplayName()).append(")\n");
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && SystemInfo.getDesktopEnvironment() != null)
|
||||
sb.append("Desktop: ").append(SystemInfo.getDesktopEnvironment()).append('\n');
|
||||
|
||||
@Nullable DeviceInfo deviceIdentity = SystemInfo.getDeviceInfo();
|
||||
if (deviceIdentity != null && !"Virtual Machine".equals(deviceIdentity.name()))
|
||||
sb.append("Device: ").append(getTranslator().formatDeviceName(deviceIdentity)).append('\n');
|
||||
|
||||
for (CentralProcessor processor : SystemInfo.getCentralProcessors())
|
||||
sb.append("CPU: ").append(processor.getName()).append('\n');
|
||||
|
||||
@Unmodifiable List<GraphicsCard> cards = SystemInfo.getGraphicsCards().stream()
|
||||
.filter(card -> card.getType() != GraphicsCard.Type.DISPLAY_ONLY && !"Unknown".equals(card.getName()))
|
||||
.sorted(Comparator.comparing(GraphicsCard::getType))
|
||||
.toList();
|
||||
for (GraphicsCard card : cards) {
|
||||
sb.append("GPU: ").append(card.getName());
|
||||
if (card.getDriver() != null || card.getDriverVersion() != null) {
|
||||
sb.append(" (");
|
||||
if (card.getDriver() != null)
|
||||
sb.append(card.getDriver());
|
||||
if (card.getDriverVersion() != null)
|
||||
sb.append(card.getDriver() != null ? " " : "").append(card.getDriverVersion());
|
||||
sb.append(')');
|
||||
}
|
||||
sb.append('\n');
|
||||
}
|
||||
|
||||
if (SystemInfo.getTotalMemorySize() > 0)
|
||||
sb.append("Memory: %.1f GiB%n".formatted(DataSizeUnit.GIGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize())));
|
||||
|
||||
sb.append("Java: %s, %s, %s%n".formatted(
|
||||
Runtime.version(), System.getProperty("java.vm.name"), System.getProperty("java.vendor")));
|
||||
sb.append("JavaFX: ").append(System.getProperty("javafx.runtime.version")).append('\n');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static Image loadImage(String url) {
|
||||
|
||||
@@ -4,60 +4,21 @@ package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class LauncherSettingsPage extends DecoratorAnimatedPage implements DecoratorPage, PageAware {
|
||||
/// The launcher settings destination: a single [SettingsPage] without sidebar tabs.
|
||||
@NotNullByDefault
|
||||
public class LauncherSettingsPage extends StackPane implements DecoratorPage {
|
||||
/// The page state holding the "Settings" title.
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("settings")));
|
||||
/// Manages tab selection and content transitions independently of the sidebar.
|
||||
private final TabController tabController;
|
||||
/// The tab whose content manages installed Java runtimes.
|
||||
private final TabItem<JavaManagementPage> javaManagementTab;
|
||||
/// The tab for general launcher settings.
|
||||
private final TabItem<SettingsPage> settingsTab;
|
||||
/// The tab for download settings.
|
||||
private final TabItem<DownloadSettingsPage> downloadTab;
|
||||
/// The tab displaying launcher information.
|
||||
private final TabItem<AboutPage> aboutTab;
|
||||
private final TransitionPane transitionPane = new TransitionPane();
|
||||
|
||||
/// Creates the settings tabs and fills the shared navigation sidebar.
|
||||
/// Creates the page with the single settings view.
|
||||
public LauncherSettingsPage() {
|
||||
javaManagementTab = new TabItem<>(JavaManagementPage::new);
|
||||
settingsTab = new TabItem<>(SettingsPage::new);
|
||||
downloadTab = new TabItem<>(DownloadSettingsPage::new);
|
||||
aboutTab = new TabItem<>(AboutPage::new);
|
||||
tabController = new TabController(transitionPane,
|
||||
javaManagementTab,
|
||||
settingsTab,
|
||||
downloadTab,
|
||||
aboutTab);
|
||||
|
||||
tabController.selectTab(settingsTab);
|
||||
|
||||
getSidebar()
|
||||
.addNavigationDrawerTab(tabController, javaManagementTab, i18n("java.management"), SVG.LOCAL_CAFE, SVG.LOCAL_CAFE_FILL)
|
||||
.addNavigationDrawerTab(tabController, settingsTab, i18n("settings.launcher.general"), SVG.TUNE)
|
||||
.addNavigationDrawerTab(tabController, downloadTab, i18n("download"), SVG.DOWNLOAD)
|
||||
.addNavigationDrawerTab(tabController, aboutTab, i18n("about"), SVG.INFO, SVG.INFO_FILL);
|
||||
|
||||
setContent(transitionPane);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageShown() {
|
||||
tabController.onPageShown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageHidden() {
|
||||
tabController.onPageHidden();
|
||||
getChildren().setAll(new SettingsPage());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -13,6 +14,7 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressBar;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
@@ -26,6 +28,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedMenuItem;
|
||||
import org.jackhuang.hmcl.ui.construct.PopupMenu;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.instances.ClientSyncProgress;
|
||||
import org.jackhuang.hmcl.ui.instances.Instances;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.ServerAddress;
|
||||
@@ -58,6 +61,12 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
BindingMapping.of(GameDirectoryManager.selectedRepositoryProperty())
|
||||
.flatMap(HMCLGameRepository::snapshotProperty);
|
||||
|
||||
/// Bumped when the home join button starts a new remote-update check, so stale results are ignored.
|
||||
private int updateCheckGeneration;
|
||||
|
||||
/// Progress of the install / update workflow, shown in place of the home card while it runs.
|
||||
private final ClientSyncProgress syncProgress = new ClientSyncProgress();
|
||||
|
||||
{
|
||||
HBox titleNode = new HBox(8);
|
||||
titleNode.setPadding(new Insets(0, 0, 0, 2));
|
||||
@@ -133,26 +142,40 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
card.getStyleClass().add("home-card");
|
||||
card.setAlignment(Pos.CENTER_LEFT);
|
||||
card.setMaxHeight(Region.USE_PREF_SIZE);
|
||||
StackPane.setAlignment(card, Pos.BOTTOM_CENTER);
|
||||
// Server announcement (the server's MOTD), shown above the card only when there is one.
|
||||
Label motdLabel = new Label();
|
||||
motdLabel.getStyleClass().add("home-subtitle");
|
||||
motdLabel.setWrapText(true);
|
||||
motdLabel.setMaxHeight(Region.USE_PREF_SIZE);
|
||||
Label motdTitle = factLabel(i18n("home.motd"));
|
||||
VBox motdBox = new VBox(3, motdTitle, motdLabel);
|
||||
motdBox.setPadding(new Insets(0, 0, 14, 4));
|
||||
motdBox.setVisible(false);
|
||||
motdBox.setManaged(false);
|
||||
VBox bottom = new VBox(motdBox, card);
|
||||
bottom.setMaxHeight(Region.USE_PREF_SIZE);
|
||||
StackPane.setAlignment(bottom, Pos.BOTTOM_CENTER);
|
||||
|
||||
// Start always installs/updates from fileApi then joins — never opens install or instance UI.
|
||||
FXUtils.setOnActionWithCooldown(joinButton, MainPage.this::launch);
|
||||
|
||||
FXUtils.onChangeAndOperate(currentGameProperty(), currentGame -> {
|
||||
refreshJoinButton(joinButton, currentGame);
|
||||
if (currentGame == null) {
|
||||
joinButton.setText(i18n("instance.launch.empty"));
|
||||
FXUtils.setOnActionWithCooldown(joinButton, MainPage.this::launchNoGame);
|
||||
packValue.setText(i18n("home.not_installed"));
|
||||
routeFact.setVisible(false);
|
||||
routeFact.setManaged(false);
|
||||
serverFact.setVisible(false);
|
||||
serverFact.setManaged(false);
|
||||
routeFact.setOnMouseClicked(null);
|
||||
routeFact.setCursor(Cursor.DEFAULT);
|
||||
} else {
|
||||
joinButton.setText(i18n("server.join"));
|
||||
FXUtils.setOnActionWithCooldown(joinButton, MainPage.this::launch);
|
||||
packValue.setText(currentGame.getId().toString());
|
||||
ServerEndpoint.@Nullable Route route = ServerEndpoint.selected(currentGame.getModpackConfigurationFile());
|
||||
routeFact.setVisible(route != null);
|
||||
routeFact.setManaged(route != null);
|
||||
routeValue.setText(route != null ? route.name() : "");
|
||||
refreshServerStatus(route, serverFact, serverValue);
|
||||
refreshServerStatus(route, serverFact, serverValue, motdBox, motdLabel);
|
||||
|
||||
// With more than one route (for example direct and CDN), the route can be switched.
|
||||
List<ServerEndpoint.Route> routes = ServerEndpoint.routes(currentGame.getModpackConfigurationFile());
|
||||
@@ -169,7 +192,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
items.add(new IconedMenuItem(null, candidate.name(), () -> {
|
||||
ServerEndpoint.select(currentGame.getModpackConfigurationFile(), candidate);
|
||||
routeValue.setText(candidate.name() + " ▾");
|
||||
refreshServerStatus(candidate, serverFact, serverValue);
|
||||
refreshServerStatus(candidate, serverFact, serverValue, motdBox, motdLabel);
|
||||
}, popup));
|
||||
}
|
||||
list.getContent().setAll(items);
|
||||
@@ -178,32 +201,160 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
}
|
||||
});
|
||||
|
||||
getChildren().addAll(art, hero, card);
|
||||
// While the client is installed or updated the card turns into a progress view in place.
|
||||
VBox progressCard = createProgressCard();
|
||||
StackPane.setAlignment(progressCard, Pos.BOTTOM_CENTER);
|
||||
progressCard.setMaxHeight(Region.USE_PREF_SIZE);
|
||||
progressCard.visibleProperty().bind(syncProgress.stateProperty().isNotEqualTo(ClientSyncProgress.State.IDLE));
|
||||
progressCard.managedProperty().bind(progressCard.visibleProperty());
|
||||
// The whole bottom block (announcement + card) gives way to the progress card.
|
||||
bottom.visibleProperty().bind(progressCard.visibleProperty().not());
|
||||
bottom.managedProperty().bind(bottom.visibleProperty());
|
||||
|
||||
getChildren().addAll(art, hero, bottom, progressCard);
|
||||
}
|
||||
|
||||
/// Builds the progress state of the home card: title with percentage, a thin progress bar,
|
||||
/// transfer details, a cancel / retry button and the four-step indicator.
|
||||
private VBox createProgressCard() {
|
||||
Label title = new Label();
|
||||
title.getStyleClass().add("home-progress-title");
|
||||
ProgressBar bar = new ProgressBar();
|
||||
bar.getStyleClass().add("home-progress-bar");
|
||||
bar.setMaxWidth(Double.MAX_VALUE);
|
||||
bar.progressProperty().bind(syncProgress.progressProperty());
|
||||
Label detail = new Label();
|
||||
detail.getStyleClass().add("home-fact-label");
|
||||
detail.textProperty().bind(syncProgress.detailProperty());
|
||||
Label error = new Label();
|
||||
error.getStyleClass().add("home-progress-error");
|
||||
error.setWrapText(true);
|
||||
error.setMaxHeight(55);
|
||||
error.textProperty().bind(syncProgress.errorProperty());
|
||||
|
||||
VBox status = new VBox(8, title, bar, detail, error);
|
||||
status.setAlignment(Pos.CENTER_LEFT);
|
||||
HBox.setHgrow(status, Priority.ALWAYS);
|
||||
|
||||
JFXButton action = new JFXButton();
|
||||
action.getStyleClass().add("home-progress-button");
|
||||
action.setOnAction(e -> {
|
||||
if (syncProgress.stateProperty().get() == ClientSyncProgress.State.FAILED) {
|
||||
Instances.ensureClientAndJoin(syncProgress);
|
||||
} else {
|
||||
syncProgress.cancel();
|
||||
}
|
||||
});
|
||||
// A failed run can also be dismissed to get back to the normal card.
|
||||
JFXButton dismiss = new JFXButton(i18n("button.cancel"));
|
||||
dismiss.getStyleClass().add("home-progress-button");
|
||||
dismiss.setOnAction(e -> syncProgress.dismiss());
|
||||
dismiss.visibleProperty().bind(syncProgress.stateProperty().isEqualTo(ClientSyncProgress.State.FAILED));
|
||||
dismiss.managedProperty().bind(dismiss.visibleProperty());
|
||||
|
||||
HBox top = new HBox(21, status, new HBox(8, dismiss, action));
|
||||
top.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
String[] stepKeys = {"home.progress.step.check", "home.progress.step.java", "home.progress.step.sync", "home.progress.step.join"};
|
||||
HBox steps = new HBox(21);
|
||||
Label[] stepLabels = new Label[stepKeys.length];
|
||||
for (int i = 0; i < stepKeys.length; i++) {
|
||||
stepLabels[i] = new Label();
|
||||
stepLabels[i].getStyleClass().add("home-progress-step");
|
||||
steps.getChildren().add(stepLabels[i]);
|
||||
}
|
||||
|
||||
Runnable refresh = () -> {
|
||||
ClientSyncProgress.State state = syncProgress.stateProperty().get();
|
||||
boolean failed = state == ClientSyncProgress.State.FAILED;
|
||||
int current = syncProgress.stepProperty().get();
|
||||
for (int i = 0; i < stepKeys.length; i++) {
|
||||
boolean done = i < current;
|
||||
boolean active = i == current && !failed;
|
||||
stepLabels[i].setText((done ? "✓ " : active ? "● " : "○ ") + i18n(stepKeys[i]));
|
||||
stepLabels[i].pseudoClassStateChanged(PseudoClass.getPseudoClass("done"), done);
|
||||
stepLabels[i].pseudoClassStateChanged(PseudoClass.getPseudoClass("active"), active);
|
||||
}
|
||||
int percent = (int) Math.round(Math.max(0, syncProgress.progressProperty().get()) * 100);
|
||||
if (failed) {
|
||||
title.setText(i18n("home.progress.failed"));
|
||||
action.setText(i18n("home.progress.retry"));
|
||||
} else {
|
||||
title.setText(current <= ClientSyncProgress.STEP_JAVA
|
||||
? i18n("home.progress.checking")
|
||||
: i18n("home.progress.syncing", percent));
|
||||
action.setText(i18n("button.cancel"));
|
||||
}
|
||||
error.setVisible(failed);
|
||||
error.setManaged(failed);
|
||||
detail.setVisible(!failed);
|
||||
detail.setManaged(!failed);
|
||||
};
|
||||
syncProgress.stateProperty().addListener(o -> refresh.run());
|
||||
syncProgress.stepProperty().addListener(o -> refresh.run());
|
||||
syncProgress.progressProperty().addListener(o -> refresh.run());
|
||||
refresh.run();
|
||||
|
||||
VBox box = new VBox(21, top, steps);
|
||||
box.getStyleClass().add("home-card");
|
||||
return box;
|
||||
}
|
||||
|
||||
/// Sets the home button to install / update / join based on the selected instance and remote manifest.
|
||||
private void refreshJoinButton(JFXButton joinButton, @Nullable HMCLGameInstance currentGame) {
|
||||
int generation = ++updateCheckGeneration;
|
||||
if (currentGame == null) {
|
||||
joinButton.setText(i18n("instance.launch.empty"));
|
||||
return;
|
||||
}
|
||||
joinButton.setText(i18n("server.join"));
|
||||
Schedulers.io().execute(() -> {
|
||||
boolean updateAvailable = Instances.isClientUpdateAvailable(currentGame);
|
||||
FXUtils.runInFX(() -> {
|
||||
if (generation != updateCheckGeneration || getCurrentGame() != currentGame) {
|
||||
return;
|
||||
}
|
||||
joinButton.setText(i18n(updateAvailable ? "server.update" : "server.join"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Pings the server behind `route` in the background and shows the result in `serverValue`.
|
||||
///
|
||||
/// WebSocket routes cannot be pinged with plain TCP, so the status is hidden for them.
|
||||
private static void refreshServerStatus(ServerEndpoint.@Nullable Route route, Node serverFact, Label serverValue) {
|
||||
private static void refreshServerStatus(ServerEndpoint.@Nullable Route route, Node serverFact, Label serverValue,
|
||||
Node motdBox, Label motdLabel) {
|
||||
String address = route == null ? "" : route.address().trim();
|
||||
boolean pingable = !address.isEmpty() && !address.toLowerCase(java.util.Locale.ROOT).startsWith("ws");
|
||||
serverFact.setVisible(pingable);
|
||||
serverFact.setManaged(pingable);
|
||||
motdBox.setVisible(false);
|
||||
motdBox.setManaged(false);
|
||||
if (!pingable) {
|
||||
return;
|
||||
}
|
||||
serverValue.setText("…");
|
||||
Schedulers.io().execute(() -> {
|
||||
String text;
|
||||
String motd = "";
|
||||
try {
|
||||
ServerAddress parsed = ServerAddress.parse(address);
|
||||
ServerStatusPing.Status status = ServerStatusPing.ping(parsed.host(), parsed.port() >= 0 ? parsed.port() : 25565, 3000);
|
||||
text = status.online() + " / " + status.max() + " · " + status.latencyMillis() + " ms";
|
||||
motd = status.motd();
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
text = i18n("home.server.unreachable");
|
||||
}
|
||||
String result = text;
|
||||
FXUtils.runInFX(() -> serverValue.setText(result));
|
||||
String announcement = motd;
|
||||
FXUtils.runInFX(() -> {
|
||||
serverValue.setText(result);
|
||||
// Unreachable or empty MOTD: stay silent, the announcement area stays hidden.
|
||||
boolean show = !announcement.isEmpty();
|
||||
motdLabel.setText(announcement);
|
||||
motdBox.setVisible(show);
|
||||
motdBox.setManaged(show);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -214,14 +365,9 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
return label;
|
||||
}
|
||||
|
||||
/// Installs or updates the client from fileApi when needed, then launches into the server.
|
||||
private void launch() {
|
||||
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
|
||||
Instances.joinServer(repository.getSelectedInstance());
|
||||
}
|
||||
|
||||
/// Opens the server modpack install/update flow when no instance is selected.
|
||||
private void launchNoGame() {
|
||||
Instances.installServerModpack();
|
||||
Instances.ensureClientAndJoin(syncProgress);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,7 +4,6 @@ package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
@@ -17,8 +16,6 @@ import org.jackhuang.hmcl.ui.account.AccountListPopupMenu;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.instances.GameAdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.instances.Instances;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -43,20 +40,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
accountListItem.setOnAction(e -> AccountListPopupMenu.show(accountListItem, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, 0, -accountListItem.getHeight()));
|
||||
accountListItem.accountProperty().bind(Accounts.selectedAccountProperty());
|
||||
|
||||
// second item in left sidebar
|
||||
GameAdvancedListItem gameInstanceItem = new GameAdvancedListItem();
|
||||
gameInstanceItem.setOnAction(e -> {
|
||||
@Nullable HMCLGameInstance instance = GameDirectoryManager.getSelectedRepository().getSelectedInstance();
|
||||
if (instance == null) {
|
||||
Instances.installServerModpack();
|
||||
} else {
|
||||
Instances.modifyGameSettings(instance);
|
||||
}
|
||||
});
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
gameInstanceItem.setPrepareAction(Controllers::prepareGameInstancePage);
|
||||
}
|
||||
|
||||
// Instance management entry removed: Start on the home page installs/updates and joins.
|
||||
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
|
||||
launcherSettingsItem.setLeftIcon(SVG.SETTINGS);
|
||||
launcherSettingsItem.setTitle(i18n("settings"));
|
||||
@@ -68,9 +52,8 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
launcherSettingsItem.setPrepareAction(Controllers::prepareSettingsPage);
|
||||
}
|
||||
|
||||
// The sidebar holds the modpack and settings entries; the account sits at the bottom-left.
|
||||
// Sidebar: settings only; account sits at the bottom-left.
|
||||
getSidebar()
|
||||
.add(gameInstanceItem)
|
||||
.add(launcherSettingsItem);
|
||||
setSidebarBottom(accountListItem);
|
||||
|
||||
@@ -92,9 +75,9 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
modpacks -> {
|
||||
Path file = modpacks.get(0);
|
||||
if (ModpackHelper.isFileModpackByExtension(file)) {
|
||||
Controllers.getDecorator().startWizard(
|
||||
new ModpackInstallWizardProvider(GameDirectoryManager.getSelectedRepository(), file),
|
||||
i18n("install.modpack"));
|
||||
// Silent install — no modpack wizard UI.
|
||||
Instances.installLocalModpackSilently(
|
||||
GameDirectoryManager.getSelectedRepository(), file);
|
||||
} else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) {
|
||||
Instances.installFromJson(GameDirectoryManager.getSelectedRepository(), file);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXSlider;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
@@ -11,6 +17,14 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.setting.DownloadSource;
|
||||
import org.jackhuang.hmcl.setting.GameSettings;
|
||||
import org.jackhuang.hmcl.setting.SettingsManager;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.DataSizeUnit;
|
||||
import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -33,6 +47,7 @@ import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
@@ -43,90 +58,177 @@ import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// The single launcher settings page: general, game and help cards.
|
||||
///
|
||||
/// Java management and the about page are reachable from the game and help cards and open as sub pages.
|
||||
public final class SettingsPage extends ScrollPane {
|
||||
|
||||
/// Smallest selectable maximum game memory in MiB.
|
||||
private static final int MIN_MEMORY_MIB = 1024;
|
||||
/// Step of the memory slider in MiB.
|
||||
private static final int MEMORY_STEP_MIB = 256;
|
||||
|
||||
/// Builds the three setting cards.
|
||||
public SettingsPage() {
|
||||
this.setFitToWidth(true);
|
||||
|
||||
VBox rootPane = new VBox(10);
|
||||
rootPane.setPadding(new Insets(10));
|
||||
VBox rootPane = new VBox(13);
|
||||
rootPane.setPadding(new Insets(21));
|
||||
this.setContent(rootPane);
|
||||
FXUtils.smoothScrolling(this);
|
||||
|
||||
{
|
||||
ComponentList languagePaneList = new ComponentList();
|
||||
rootPane.getChildren().addAll(
|
||||
ComponentList.createComponentListTitle(i18n("settings.launcher.general")), createGeneralCard(),
|
||||
ComponentList.createComponentListTitle(i18n("settings.group.game")), createGameCard(),
|
||||
ComponentList.createComponentListTitle(i18n("settings.group.help")), createHelpCard());
|
||||
}
|
||||
|
||||
{
|
||||
var chooseLanguagePane = new LineSelectButton<SupportedLocale>();
|
||||
chooseLanguagePane.setTitle(i18n("settings.launcher.language"));
|
||||
chooseLanguagePane.setSubtitle(i18n("settings.take_effect_after_restart"));
|
||||
/// Creates the card with the language and download source choices.
|
||||
private static ComponentList createGeneralCard() {
|
||||
ComponentList list = new ComponentList();
|
||||
|
||||
SupportedLocale currentLocale = I18n.getLocale();
|
||||
chooseLanguagePane.setNullSafeConverter(locale -> {
|
||||
if (locale.isDefault())
|
||||
return locale.getDisplayName(currentLocale);
|
||||
else if (locale.isSameLanguage(currentLocale))
|
||||
return locale.getDisplayName(locale);
|
||||
else
|
||||
return locale.getDisplayName(currentLocale) + " - " + locale.getDisplayName(locale);
|
||||
});
|
||||
chooseLanguagePane.setItems(SupportedLocale.getSupportedLocales());
|
||||
chooseLanguagePane.valueProperty().bindBidirectional(settings().languageProperty());
|
||||
var chooseLanguagePane = new LineSelectButton<SupportedLocale>();
|
||||
chooseLanguagePane.setTitle(i18n("settings.launcher.language"));
|
||||
chooseLanguagePane.setSubtitle(i18n("settings.take_effect_after_restart"));
|
||||
|
||||
languagePaneList.getContent().add(chooseLanguagePane);
|
||||
SupportedLocale currentLocale = I18n.getLocale();
|
||||
chooseLanguagePane.setNullSafeConverter(locale -> {
|
||||
if (locale.isDefault())
|
||||
return locale.getDisplayName(currentLocale);
|
||||
else if (locale.isSameLanguage(currentLocale))
|
||||
return locale.getDisplayName(locale);
|
||||
else
|
||||
return locale.getDisplayName(currentLocale) + " - " + locale.getDisplayName(locale);
|
||||
});
|
||||
chooseLanguagePane.setItems(SupportedLocale.getSupportedLocales());
|
||||
chooseLanguagePane.valueProperty().bindBidirectional(settings().languageProperty());
|
||||
list.getContent().add(chooseLanguagePane);
|
||||
|
||||
}
|
||||
// One choice drives both the file download source and the version list source.
|
||||
var downloadSourcePane = new LineSelectButton<DownloadSource>();
|
||||
downloadSourcePane.setTitle(i18n("settings.launcher.download_source"));
|
||||
downloadSourcePane.setNullSafeConverter(source -> i18n("settings.download_source." + source.name().toLowerCase(Locale.ROOT)));
|
||||
downloadSourcePane.setItems(List.of(DownloadSource.values()));
|
||||
downloadSourcePane.valueProperty().bindBidirectional(settings().fileDownloadSourceProperty());
|
||||
downloadSourcePane.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null)
|
||||
settings().versionListSourceProperty().set(newValue);
|
||||
});
|
||||
list.getContent().add(downloadSourcePane);
|
||||
|
||||
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.language")), languagePaneList);
|
||||
return list;
|
||||
}
|
||||
|
||||
/// Creates the card with the maximum memory slider and the Java summary row.
|
||||
private static ComponentList createGameCard() {
|
||||
ComponentList list = new ComponentList();
|
||||
GameSettings.Preset preset = SettingsManager.getDefaultGameSettingsPresetOrCreate();
|
||||
|
||||
int totalMiB = Math.max(MIN_MEMORY_MIB, (int) DataSizeUnit.MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize()));
|
||||
JFXSlider slider = new JFXSlider(MIN_MEMORY_MIB, totalMiB, MIN_MEMORY_MIB);
|
||||
slider.setBlockIncrement(MEMORY_STEP_MIB);
|
||||
slider.setPrefWidth(280);
|
||||
slider.setValueFactory(s -> Bindings.createStringBinding(
|
||||
() -> "%.1f GB".formatted(s.getValue() / 1024), s.valueProperty()));
|
||||
@Nullable Integer current = preset.maxMemoryProperty().getValue();
|
||||
slider.setValue(current != null && current > 0 ? Math.min(current, totalMiB) : Math.min(4096, totalMiB));
|
||||
// Moving the slider switches off automatic allocation and stores the chosen limit.
|
||||
slider.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
int mib = (int) (Math.round(newValue.doubleValue() / MEMORY_STEP_MIB) * MEMORY_STEP_MIB);
|
||||
preset.autoMemoryProperty().setValue(false);
|
||||
preset.maxMemoryProperty().setValue(Math.max(MIN_MEMORY_MIB, mib));
|
||||
});
|
||||
|
||||
BorderPane memoryPane = new BorderPane();
|
||||
Label memoryLabel = new Label(i18n("settings.memory"));
|
||||
BorderPane.setAlignment(memoryLabel, Pos.CENTER_LEFT);
|
||||
memoryPane.setLeft(memoryLabel);
|
||||
memoryPane.setRight(slider);
|
||||
list.getContent().add(memoryPane);
|
||||
|
||||
BorderPane javaPane = new BorderPane();
|
||||
Label javaLabel = new Label(i18n("settings.java.summary"));
|
||||
BorderPane.setAlignment(javaLabel, Pos.CENTER_LEFT);
|
||||
javaPane.setLeft(javaLabel);
|
||||
JFXButton advanced = FXUtils.newBorderButton(i18n("settings.java.advanced"));
|
||||
advanced.setOnAction(e -> Controllers.navigate(new SubPage(i18n("java.management"), new JavaManagementPage())));
|
||||
javaPane.setRight(advanced);
|
||||
list.getContent().add(javaPane);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// Creates the card with the log actions and the entry to the about page.
|
||||
private ComponentList createHelpCard() {
|
||||
ComponentList list = new ComponentList();
|
||||
|
||||
BorderPane debugPane = new BorderPane();
|
||||
|
||||
Label left = new Label(i18n("settings.launcher.debug"));
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
debugPane.setLeft(left);
|
||||
|
||||
JFXButton openLogFolderButton = new JFXButton(i18n("settings.launcher.launcher_log.reveal"));
|
||||
openLogFolderButton.setOnAction(e -> openLogFolder());
|
||||
openLogFolderButton.getStyleClass().add("jfx-button-border");
|
||||
if (LOG.getLogFile() == null)
|
||||
openLogFolderButton.setDisable(true);
|
||||
|
||||
SpinnerPane exportLogPane = new SpinnerPane();
|
||||
|
||||
JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export"));
|
||||
exportLogPane.setContent(logButton);
|
||||
logButton.setOnAction(e -> {
|
||||
exportLogPane.showSpinner();
|
||||
onExportLogs().whenCompleteAsync((result, exception) -> {
|
||||
exportLogPane.hideSpinner();
|
||||
if (exception == null) {
|
||||
Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", result));
|
||||
FXUtils.showFileInExplorer(result);
|
||||
} else {
|
||||
LOG.warning("Failed to export logs", exception);
|
||||
Controllers.dialog(
|
||||
i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception),
|
||||
null,
|
||||
MessageType.ERROR
|
||||
);
|
||||
}
|
||||
}, Schedulers.javafx());
|
||||
});
|
||||
|
||||
HBox buttonBox = new HBox();
|
||||
buttonBox.setSpacing(10);
|
||||
buttonBox.getChildren().addAll(openLogFolderButton, exportLogPane);
|
||||
BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT);
|
||||
debugPane.setRight(buttonBox);
|
||||
list.getContent().add(debugPane);
|
||||
|
||||
var about = new LineButton();
|
||||
about.setTitle(i18n("settings.help.about"));
|
||||
about.setTrailingIcon(SVG.ARROW_FORWARD);
|
||||
about.setOnAction(e -> Controllers.navigate(new SubPage(i18n("about"), new AboutPage())));
|
||||
list.getContent().add(about);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// A titled page that hosts a settings sub page (Java management, about) inside the main navigator.
|
||||
private static final class SubPage extends StackPane implements DecoratorPage {
|
||||
/// The page state, holding the title shown in the window header.
|
||||
private final ReadOnlyObjectWrapper<State> state;
|
||||
|
||||
/// Creates the page.
|
||||
///
|
||||
/// @param title the title shown in the header
|
||||
/// @param content the page content
|
||||
SubPage(String title, Node content) {
|
||||
this.state = new ReadOnlyObjectWrapper<>(State.fromTitle(title));
|
||||
getChildren().setAll(content);
|
||||
}
|
||||
|
||||
{
|
||||
ComponentList miscPaneList = new ComponentList();
|
||||
|
||||
{
|
||||
BorderPane debugPane = new BorderPane();
|
||||
|
||||
Label left = new Label(i18n("settings.launcher.debug"));
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
debugPane.setLeft(left);
|
||||
|
||||
JFXButton openLogFolderButton = new JFXButton(i18n("settings.launcher.launcher_log.reveal"));
|
||||
openLogFolderButton.setOnAction(e -> openLogFolder());
|
||||
openLogFolderButton.getStyleClass().add("jfx-button-border");
|
||||
if (LOG.getLogFile() == null)
|
||||
openLogFolderButton.setDisable(true);
|
||||
|
||||
SpinnerPane exportLogPane = new SpinnerPane();
|
||||
|
||||
JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export"));
|
||||
exportLogPane.setContent(logButton);
|
||||
logButton.setOnAction(e -> {
|
||||
exportLogPane.showSpinner();
|
||||
onExportLogs().whenCompleteAsync((result, exception) -> {
|
||||
exportLogPane.hideSpinner();
|
||||
if (exception == null) {
|
||||
Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", result));
|
||||
FXUtils.showFileInExplorer(result);
|
||||
} else {
|
||||
LOG.warning("Failed to export logs", exception);
|
||||
Controllers.dialog(
|
||||
i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception),
|
||||
null,
|
||||
MessageType.ERROR
|
||||
);
|
||||
}
|
||||
}, Schedulers.javafx());
|
||||
});
|
||||
|
||||
HBox buttonBox = new HBox();
|
||||
buttonBox.setSpacing(10);
|
||||
buttonBox.getChildren().addAll(openLogFolderButton, exportLogPane);
|
||||
BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT);
|
||||
debugPane.setRight(buttonBox);
|
||||
|
||||
miscPaneList.getContent().add(debugPane);
|
||||
}
|
||||
|
||||
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.misc")), miscPaneList);
|
||||
@Override
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return state.getReadOnlyProperty();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2237,6 +2237,65 @@
|
||||
-fx-text-fill: -monet-on-primary;
|
||||
}
|
||||
|
||||
.home-progress-title {
|
||||
-fx-font-family: "Noto Sans SC";
|
||||
-fx-font-size: 16px;
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
}
|
||||
|
||||
.home-progress-bar {
|
||||
-fx-pref-height: 8px;
|
||||
-fx-min-height: 8px;
|
||||
-fx-max-height: 8px;
|
||||
}
|
||||
|
||||
.home-progress-bar > .track {
|
||||
-fx-background-color: -monet-outline-variant;
|
||||
-fx-background-radius: 8px;
|
||||
-fx-background-insets: 0;
|
||||
}
|
||||
|
||||
.home-progress-bar > .bar {
|
||||
-fx-background-color: -monet-primary;
|
||||
-fx-background-radius: 8px;
|
||||
-fx-background-insets: 0;
|
||||
-fx-padding: 0;
|
||||
}
|
||||
|
||||
.home-progress-error {
|
||||
-fx-font-size: 12px;
|
||||
-fx-text-fill: -monet-error;
|
||||
}
|
||||
|
||||
.home-progress-button {
|
||||
-fx-font-family: "Noto Sans SC";
|
||||
-fx-background-color: transparent;
|
||||
-fx-border-color: -monet-primary;
|
||||
-fx-border-radius: 13px;
|
||||
-fx-background-radius: 13px;
|
||||
-fx-text-fill: -monet-primary;
|
||||
-fx-padding: 8px 21px;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.home-progress-button .label {
|
||||
-fx-text-fill: -monet-primary;
|
||||
}
|
||||
|
||||
.home-progress-step {
|
||||
-fx-font-family: "Noto Sans SC";
|
||||
-fx-font-size: 12px;
|
||||
-fx-text-fill: -monet-on-surface-variant;
|
||||
}
|
||||
|
||||
.home-progress-step:active {
|
||||
-fx-text-fill: -monet-primary;
|
||||
}
|
||||
|
||||
.home-progress-step:done {
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
}
|
||||
|
||||
.home-art-line {
|
||||
-fx-stroke: -monet-outline-variant;
|
||||
-fx-stroke-width: 1.5px;
|
||||
|
||||
@@ -808,9 +808,34 @@ first_run.microsoft=Sign in with Microsoft
|
||||
first_run.invalid=Use 1 to 16 letters, digits or underscores.
|
||||
home.server=Server
|
||||
home.server.unreachable=Unreachable
|
||||
home.progress.step.check=Check updates
|
||||
home.progress.step.java=Prepare Java
|
||||
home.progress.step.sync=Sync modpack
|
||||
home.progress.step.join=Join server
|
||||
home.progress.checking=Checking for updates…
|
||||
home.progress.syncing=Syncing modpack · %d%%
|
||||
home.progress.failed=Sync failed
|
||||
home.progress.retry=Retry
|
||||
settings.group.game=Game
|
||||
settings.group.help=Help
|
||||
settings.java.summary=Java · managed automatically
|
||||
settings.java.advanced=Advanced…
|
||||
settings.help.about=About
|
||||
about.based_on=Based on HMCL
|
||||
about.check_update=Check for updates
|
||||
about.check_update.latest=The modpack is up to date.
|
||||
about.check_update.available=An update is available. Press "Update" on the home page.
|
||||
about.check_update.not_installed=The modpack is not installed yet.
|
||||
about.source_code=Source code
|
||||
about.copy_diagnostics=Copy diagnostics
|
||||
home.motd=Server announcement
|
||||
home.restart.title=Restart game
|
||||
home.restart.update=The game closed to apply an update. Update the client and start it again now?
|
||||
home.restart.generic=The game asked to be restarted. Update the client and start it again now?
|
||||
server.join=Join Server
|
||||
server.update=Update
|
||||
instance.launch_and_enter_world=Play World
|
||||
instance.launch.empty=Start Game
|
||||
instance.launch.empty=Join Server
|
||||
instance.launch.empty.installing=Installing Game
|
||||
instance.launch.empty.tooltip=Install and launch the bundled server modpack
|
||||
instance.launch.test=Test Launch
|
||||
@@ -1559,6 +1584,9 @@ settings.launcher.download.threads=Download Threads
|
||||
settings.launcher.download.threads.auto=Automatically Determine
|
||||
settings.launcher.download.threads.custom=Custom
|
||||
settings.launcher.download_source=Download Source
|
||||
settings.download_source.default=Automatic
|
||||
settings.download_source.official=Official
|
||||
settings.download_source.mirror=Mirror
|
||||
settings.launcher.download_source.auto=Automatically Choose Download Sources
|
||||
settings.launcher.default_addon_source=Default Addon Source
|
||||
settings.launcher.misc=Misc
|
||||
|
||||
@@ -628,7 +628,12 @@ first_run.microsoft=登正版
|
||||
first_run.invalid=一至十六字,限英文數字底綫。
|
||||
home.server=服
|
||||
home.server.unreachable=不通
|
||||
home.motd=服公告
|
||||
home.restart.title=重啟遊戲
|
||||
home.restart.update=遊戲爲更新而止。今更新並重啟乎?
|
||||
home.restart.generic=遊戲請重啟。今更新並重啟乎?
|
||||
server.join=入服
|
||||
server.update=更新
|
||||
# instance.launch_and_enter_world=
|
||||
instance.launch.empty=啟戲
|
||||
instance.launch.empty.installing=裝戲
|
||||
|
||||
@@ -631,9 +631,34 @@ first_run.microsoft=登录正版账号
|
||||
first_run.invalid=只能用 1 到 16 个英文字母、数字或下划线。
|
||||
home.server=服务器
|
||||
home.server.unreachable=连接不上
|
||||
home.progress.step.check=检查更新
|
||||
home.progress.step.java=准备 Java
|
||||
home.progress.step.sync=同步整合包
|
||||
home.progress.step.join=进入服务器
|
||||
home.progress.checking=正在检查更新…
|
||||
home.progress.syncing=正在同步整合包 · %d%%
|
||||
home.progress.failed=同步失败
|
||||
home.progress.retry=重试
|
||||
settings.group.game=游戏
|
||||
settings.group.help=帮助
|
||||
settings.java.summary=Java · 自动管理
|
||||
settings.java.advanced=高级…
|
||||
settings.help.about=关于
|
||||
about.based_on=基于 HMCL
|
||||
about.check_update=检查更新
|
||||
about.check_update.latest=整合包已是最新。
|
||||
about.check_update.available=有可用更新,请在首页点击“更新”。
|
||||
about.check_update.not_installed=尚未安装整合包。
|
||||
about.source_code=源代码
|
||||
about.copy_diagnostics=复制诊断信息
|
||||
home.motd=服务器公告
|
||||
home.restart.title=重启游戏
|
||||
home.restart.update=游戏为应用更新已退出。现在更新客户端并重新启动游戏吗?
|
||||
home.restart.generic=游戏请求重启。现在更新客户端并重新启动吗?
|
||||
server.join=进入服务器
|
||||
server.update=更新
|
||||
instance.launch_and_enter_world=进入世界
|
||||
instance.launch.empty=开始游戏
|
||||
instance.launch.empty=进入服务器
|
||||
instance.launch.empty.installing=安装游戏
|
||||
instance.launch.empty.tooltip=安装并启动服务器整合包
|
||||
instance.launch.test=测试游戏
|
||||
@@ -1376,6 +1401,9 @@ settings.launcher.download.threads=下载线程数
|
||||
settings.launcher.download.threads.auto=自动选择线程数
|
||||
settings.launcher.download.threads.custom=自定义线程数
|
||||
settings.launcher.download_source=下载源
|
||||
settings.download_source.default=自动
|
||||
settings.download_source.official=官方源
|
||||
settings.download_source.mirror=镜像源
|
||||
settings.launcher.download_source.auto=自动选择下载源
|
||||
settings.launcher.default_addon_source=附加内容默认下载源
|
||||
settings.launcher.misc=杂项
|
||||
|
||||
@@ -625,9 +625,34 @@ first_run.microsoft=登入正版帳號
|
||||
first_run.invalid=只能使用 1 到 16 個英文字母、數字或底線。
|
||||
home.server=伺服器
|
||||
home.server.unreachable=無法連線
|
||||
home.progress.step.check=檢查更新
|
||||
home.progress.step.java=準備 Java
|
||||
home.progress.step.sync=同步整合包
|
||||
home.progress.step.join=進入伺服器
|
||||
home.progress.checking=正在檢查更新…
|
||||
home.progress.syncing=正在同步整合包 · %d%%
|
||||
home.progress.failed=同步失敗
|
||||
home.progress.retry=重試
|
||||
settings.group.game=遊戲
|
||||
settings.group.help=說明
|
||||
settings.java.summary=Java · 自動管理
|
||||
settings.java.advanced=進階…
|
||||
settings.help.about=關於
|
||||
about.based_on=基於 HMCL
|
||||
about.check_update=檢查更新
|
||||
about.check_update.latest=整合包已是最新。
|
||||
about.check_update.available=有可用更新,請在首頁點擊「更新」。
|
||||
about.check_update.not_installed=尚未安裝整合包。
|
||||
about.source_code=原始碼
|
||||
about.copy_diagnostics=複製診斷資訊
|
||||
home.motd=伺服器公告
|
||||
home.restart.title=重啟遊戲
|
||||
home.restart.update=遊戲為套用更新已結束。現在更新客戶端並重新啟動遊戲嗎?
|
||||
home.restart.generic=遊戲要求重啟。現在更新客戶端並重新啟動嗎?
|
||||
server.join=進入伺服器
|
||||
server.update=更新
|
||||
instance.launch_and_enter_world=進入世界
|
||||
instance.launch.empty=開始遊戲
|
||||
instance.launch.empty=進入伺服器
|
||||
instance.launch.empty.installing=安裝遊戲
|
||||
instance.launch.empty.tooltip=安裝並啟動伺服器整合包
|
||||
instance.launch.test=測試遊戲
|
||||
@@ -1370,6 +1395,9 @@ settings.launcher.download.threads=下載執行緒數
|
||||
settings.launcher.download.threads.auto=自動選取執行緒數
|
||||
settings.launcher.download.threads.custom=自訂執行緒數
|
||||
settings.launcher.download_source=下載來源
|
||||
settings.download_source.default=自動
|
||||
settings.download_source.official=官方來源
|
||||
settings.download_source.mirror=鏡像來源
|
||||
settings.launcher.download_source.auto=自動選取下載來源
|
||||
settings.launcher.default_addon_source=附加內容預設下載來源
|
||||
settings.launcher.misc=雜項
|
||||
|
||||
@@ -10,6 +10,9 @@ import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/// Owns the shared JavaFX toolkit for the lifetime of the test process.
|
||||
@NotNullByDefault
|
||||
@@ -27,11 +30,36 @@ public final class JavaFXLauncher {
|
||||
System.setProperty("hmcl.scroll.speed", "1");
|
||||
try {
|
||||
initializeAnimationPolicy();
|
||||
Platform.startup(() -> {
|
||||
});
|
||||
started = true;
|
||||
} catch (IllegalStateException e) {
|
||||
started = true;
|
||||
// Platform.startup can hang when GTK/Glass is broken (minimal CI images with only xvfb).
|
||||
// Bound class-init so :HMCL:test cannot stall the whole Gradle build.
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
AtomicReference<Throwable> error = new AtomicReference<>();
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
Platform.startup(() -> {
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
error.set(e);
|
||||
} finally {
|
||||
done.countDown();
|
||||
}
|
||||
}, "hmcl-javafx-startup");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
if (!done.await(45, TimeUnit.SECONDS)) {
|
||||
System.err.println("JavaFX Platform.startup timed out; JavaFX tests will be skipped");
|
||||
} else {
|
||||
Throwable e = error.get();
|
||||
if (e == null) {
|
||||
started = true;
|
||||
} else if (e instanceof IllegalStateException) {
|
||||
started = true;
|
||||
} else {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/// Tests [RestartRequest].
|
||||
public final class RestartRequestTest {
|
||||
@Test
|
||||
void noFileMeansNoRequest(@TempDir Path dir) {
|
||||
assertNull(RestartRequest.consume(dir));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readsReasonAndDeletesFile(@TempDir Path dir) throws Exception {
|
||||
Path file = dir.resolve(RestartRequest.FILE_NAME);
|
||||
Files.writeString(file, "{\"reason\":\"update\"}");
|
||||
assertEquals("update", RestartRequest.consume(dir));
|
||||
assertFalse(Files.exists(file));
|
||||
assertNull(RestartRequest.consume(dir));
|
||||
}
|
||||
|
||||
@Test
|
||||
void malformedFileStillCounts(@TempDir Path dir) throws Exception {
|
||||
Path file = dir.resolve(RestartRequest.FILE_NAME);
|
||||
Files.writeString(file, "not json");
|
||||
assertEquals("unknown", RestartRequest.consume(dir));
|
||||
assertFalse(Files.exists(file));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
@@ -19,13 +20,15 @@ public final class ServerEndpointTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void noConfigurationMeansNoRoute(@TempDir Path dir) {
|
||||
assertTrue(ServerEndpoint.routes(dir.resolve("modpack.json")).isEmpty());
|
||||
assertNull(ServerEndpoint.selected(dir.resolve("modpack.json")));
|
||||
void fallsBackToBakedDefaultAddress(@TempDir Path dir) {
|
||||
var routes = ServerEndpoint.routes(dir.resolve("modpack.json"));
|
||||
assertEquals(1, routes.size());
|
||||
assertEquals(Metadata.DEFAULT_SERVER_ADDRESS, routes.get(0).address());
|
||||
assertEquals(routes.get(0), ServerEndpoint.selected(dir.resolve("modpack.json")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackToDefaultAddress(@TempDir Path dir) {
|
||||
void propertyOverridesBakedDefaultAddress(@TempDir Path dir) {
|
||||
System.setProperty("hmcl.server.address", " play.example.net:25565 ");
|
||||
var routes = ServerEndpoint.routes(dir.resolve("modpack.json"));
|
||||
assertEquals(1, routes.size());
|
||||
|
||||
@@ -42,6 +42,11 @@ public final class DesktopUtils {
|
||||
}
|
||||
|
||||
public static void openLink(String link) {
|
||||
if (!isAllowedOpenLink(link)) {
|
||||
System.err.println("Refusing to open link with disallowed scheme: " + link);
|
||||
return;
|
||||
}
|
||||
|
||||
String osName = System.getProperty("os.name").toLowerCase(Locale.ROOT);
|
||||
|
||||
Throwable e1 = null;
|
||||
@@ -79,6 +84,21 @@ public final class DesktopUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/// Only {@code http}/{@code https}/{@code mailto} may be handed to the OS URL handler.
|
||||
static boolean isAllowedOpenLink(String link) {
|
||||
try {
|
||||
URI uri = new URI(link);
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null) {
|
||||
return false;
|
||||
}
|
||||
String lower = scheme.toLowerCase(Locale.ROOT);
|
||||
return "http".equals(lower) || "https".equals(lower) || "mailto".equals(lower);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private DesktopUtils() {
|
||||
}
|
||||
}
|
||||
+11
-3
@@ -45,10 +45,18 @@ public class AuthlibInjectorServer implements Observable {
|
||||
String ali = conn.getHeaderField("x-authlib-injector-api-location");
|
||||
if (ali != null) {
|
||||
WebURL absoluteAli = WebURL.parse(ali, WebURL.of(conn.getURL()));
|
||||
// Only follow same-host HTTPS redirects; open redirects to other hosts are rejected.
|
||||
if (!urlEqualsIgnoreSlash(webURL.toString(), absoluteAli.toString())) {
|
||||
conn.disconnect();
|
||||
url = absoluteAli.toString();
|
||||
conn = NetworkUtils.resolveConnection(NetworkUtils.createHttpConnection(absoluteAli));
|
||||
if (!"https".equalsIgnoreCase(absoluteAli.getScheme())
|
||||
|| absoluteAli.getHost() == null
|
||||
|| webURL.getHost() == null
|
||||
|| !absoluteAli.getHost().equalsIgnoreCase(webURL.getHost())) {
|
||||
LOG.warning("Ignoring x-authlib-injector-api-location redirect outside original HTTPS host: " + absoluteAli);
|
||||
} else {
|
||||
conn.disconnect();
|
||||
url = absoluteAli.toString();
|
||||
conn = NetworkUtils.resolveConnection(NetworkUtils.createHttpConnection(absoluteAli));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
@@ -25,6 +25,7 @@ import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -37,6 +38,8 @@ public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Re
|
||||
private final Path tempDir;
|
||||
private final Task<MojangJavaRemoteFiles> javaDownloadsTask;
|
||||
private final List<Task<?>> dependencies = new ArrayList<>();
|
||||
private final AtomicInteger completedFiles = new AtomicInteger();
|
||||
private volatile int totalFiles;
|
||||
|
||||
private volatile MojangJavaDownloads.JavaDownload download;
|
||||
|
||||
@@ -147,6 +150,21 @@ public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Re
|
||||
Files.createSymbolicLink(dest, Paths.get(link.getTarget()));
|
||||
}
|
||||
}
|
||||
|
||||
// Report per-file progress so the bar advances through the download, LZMA decompress and
|
||||
// cache-copy phases; otherwise it stays indeterminate while files appear on disk and players
|
||||
// assume the install is frozen. The callbacks run after each file completes and never touch
|
||||
// the download/decompress logic above.
|
||||
totalFiles = dependencies.size();
|
||||
if (totalFiles > 0) {
|
||||
List<Task<?>> tracked = new ArrayList<>(totalFiles);
|
||||
for (Task<?> dependency : dependencies) {
|
||||
tracked.add(dependency.thenRunAsync(() -> updateProgress(completedFiles.incrementAndGet(), totalFiles)));
|
||||
}
|
||||
dependencies.clear();
|
||||
dependencies.addAll(tracked);
|
||||
updateProgress(0, totalFiles);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -327,6 +327,10 @@ public class LaunchOptions implements Serializable {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Path getGameDir() {
|
||||
return options.gameDir;
|
||||
}
|
||||
|
||||
public Builder setJava(JavaRuntime java) {
|
||||
options.java = java;
|
||||
return this;
|
||||
|
||||
@@ -799,6 +799,10 @@ public class DefaultLauncher extends Launcher {
|
||||
env.put("__NV_PRIME_RENDER_OFFLOAD", "1");
|
||||
env.put("__GLX_VENDOR_LIBRARY_NAME", "nvidia");
|
||||
env.put("__VK_LAYER_NV_optimus", "NVIDIA_only");
|
||||
} else if (HardwareVendor.AMD.equals(discreteGraphicsCard.getVendor())
|
||||
|| HardwareVendor.NVIDIA.equals(discreteGraphicsCard.getVendor())) {
|
||||
// Mesa PRIME offload (open source AMD / nouveau drivers)
|
||||
env.put("DRI_PRIME", "1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.JvmArgumentFilter;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
@@ -282,7 +283,7 @@ public final class MultiMCInstancePatch {
|
||||
if (minecraftArguments == null & patch.getMinecraftArguments() != null) {
|
||||
minecraftArguments = StringUtils.tokenize(patch.getMinecraftArguments());
|
||||
}
|
||||
for (String jvmArg : patch.getJvmArgs()) {
|
||||
for (String jvmArg : JvmArgumentFilter.filter(patch.getJvmArgs())) {
|
||||
jvmArguments.add(new StringArgument(jvmArg));
|
||||
}
|
||||
mainClass = Lang.requireNonNullElse(mainClass, patch.getMainClass());
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.modpack.server;
|
||||
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCComponents;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCInstancePatch;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCManifest;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.*;
|
||||
|
||||
/// Applies the default-on lwjgl3ify MultiMC companion under {@link Lwjgl3ifySupport#ROOT}.
|
||||
///
|
||||
/// Replaces the plain Forge install produced by {@link org.jackhuang.hmcl.download.GameBuilder} with the
|
||||
/// resolved MultiMC artifact (libraries, JVM args, bootstrap main class) so Java 25 launches work
|
||||
/// without a player toggle.
|
||||
@NotNullByDefault
|
||||
public final class Lwjgl3ifyApplyTask extends Task<Void> {
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final DefaultGameRepository repository;
|
||||
private final GameInstanceID instanceId;
|
||||
private final @Nullable DefaultGameInstance updateTarget;
|
||||
private final List<Task<?>> dependents = new ArrayList<>();
|
||||
private final List<Task<?>> dependencies = new ArrayList<>();
|
||||
private @Nullable MultiMCInstancePatch.ResolvedInstance artifact;
|
||||
|
||||
public Lwjgl3ifyApplyTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
GameInstanceID instanceId,
|
||||
@Nullable DefaultGameInstance updateTarget) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.repository = dependencyManager.getGameRepository();
|
||||
this.instanceId = instanceId;
|
||||
this.updateTarget = updateTarget;
|
||||
setStage("hmcl.modpack");
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
|
||||
Path instanceRoot = repository.getLayout().getInstanceRoot(instanceId);
|
||||
if (!Lwjgl3ifySupport.isPresent(instanceRoot)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
dependents.add(new AssemblePatchesTask(instanceRoot));
|
||||
} catch (IOException e) {
|
||||
dependents.add(Task.failed(e));
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs after Server Modpack file sync when the companion is present; otherwise a completed no-op.
|
||||
public static Task<?> afterCompletion(DefaultDependencyManager dependencyManager, DefaultGameInstance instance) {
|
||||
if (!Lwjgl3ifySupport.isPresent(instance.getInstanceRoot())) {
|
||||
return Task.completed(null);
|
||||
}
|
||||
return new Lwjgl3ifyApplyTask(dependencyManager, instance.getId(), instance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task<?>> getDependents() {
|
||||
return dependents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task<?>> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
Path instanceRoot = repository.getLayout().getInstanceRoot(instanceId);
|
||||
if (!Lwjgl3ifySupport.isPresent(instanceRoot)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@Nullable AssemblePatchesTask assemble = null;
|
||||
for (Task<?> dependent : dependents) {
|
||||
if (dependent instanceof AssemblePatchesTask patchesTask) {
|
||||
assemble = patchesTask;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Objects.requireNonNull(assemble, "lwjgl3ify patch assemble task");
|
||||
artifact = MultiMCInstancePatch.resolveArtifact(assemble.getResult(), instanceId);
|
||||
|
||||
Path embeddedLibraries = Lwjgl3ifySupport.root(instanceRoot).resolve("libraries");
|
||||
if (Files.isDirectory(embeddedLibraries)) {
|
||||
FileUtils.copyDirectory(embeddedLibraries, instanceRoot.resolve("libraries"));
|
||||
}
|
||||
|
||||
for (Library library : artifact.getManifest().getLibraries()) {
|
||||
if ("local".equals(library.hint())) {
|
||||
Path from = repository.getLayout().getLibraryFile(artifact.getManifest().id(), library);
|
||||
Path target = repository.getLayout().getLibraryFile(artifact.getManifest().id(), library.withoutCommunityFields());
|
||||
if (Files.exists(from)) {
|
||||
Files.createDirectories(target.getParent());
|
||||
Files.copy(from, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try (InputStream input = Objects.requireNonNull(
|
||||
Lwjgl3ifyApplyTask.class.getResourceAsStream("/assets/game/HMCLMultiMCBootstrap-1.0.jar"),
|
||||
"Bundled HMCLMultiMCBootstrap is missing.")) {
|
||||
Path libraryPath = repository.getLayout().getLibraryFile(artifact.getManifest().id(), MultiMCInstancePatch.BOOTSTRAP_LIBRARY);
|
||||
Files.createDirectories(libraryPath.getParent());
|
||||
Files.copy(input, libraryPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
GameInstanceManifest instanceManifest = artifact.getManifest();
|
||||
repository.save(instanceManifest);
|
||||
|
||||
dependencies.add(new GameAssetDownloadTask(dependencyManager, instanceManifest, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true));
|
||||
dependencies.add(new GameLibrariesTask(
|
||||
dependencyManager,
|
||||
instanceManifest.withLibraries(Lang.merge(instanceManifest.getLibraries(), artifact.getMavenOnlyFiles())),
|
||||
true));
|
||||
|
||||
Path instanceJar = getPrimaryJarFile();
|
||||
dependencies.add(new GameDownloadTask(dependencyManager, instanceManifest)
|
||||
.thenAcceptAsync(cachedJar -> FileUtils.copyFile(cachedJar, instanceJar))
|
||||
.withStage("hmcl.modpack.download"));
|
||||
}
|
||||
|
||||
private Path getPrimaryJarFile() {
|
||||
if (updateTarget == null) {
|
||||
return repository.getLayout().getInstanceJarFile(instanceId);
|
||||
}
|
||||
Path manifestFile = updateTarget.getManifestFile();
|
||||
return manifestFile.resolveSibling(FileUtils.getNameWithoutExtension(manifestFile) + ".jar");
|
||||
}
|
||||
|
||||
private static final class AssemblePatchesTask extends Task<List<MultiMCInstancePatch>> {
|
||||
private final Path companionRoot;
|
||||
private final String mcVersion;
|
||||
private final List<Task<MultiMCInstancePatch>> patches = new ArrayList<>();
|
||||
|
||||
AssemblePatchesTask(Path instanceRoot) throws IOException {
|
||||
this.companionRoot = Lwjgl3ifySupport.root(instanceRoot);
|
||||
MultiMCManifest mmcPack = Lwjgl3ifySupport.readMmcPack(instanceRoot);
|
||||
List<MultiMCManifest.MultiMCManifestComponent> components = Objects.requireNonNull(
|
||||
mmcPack.getComponents(), "components");
|
||||
|
||||
String resolvedMc = null;
|
||||
for (MultiMCManifest.MultiMCManifestComponent component : components) {
|
||||
if (MultiMCComponents.getComponent(component.getUid()) == GameComponentType.GAME) {
|
||||
resolvedMc = component.getVersion();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (resolvedMc == null) {
|
||||
throw new IOException("lwjgl3ify mmc-pack.json has no Minecraft component");
|
||||
}
|
||||
this.mcVersion = resolvedMc;
|
||||
|
||||
for (MultiMCManifest.MultiMCManifestComponent component : components) {
|
||||
String componentID = Objects.requireNonNull(component.getUid(), "Component ID");
|
||||
Path patchPath = companionRoot.resolve("patches").resolve(componentID + ".json");
|
||||
if (Files.isRegularFile(patchPath)) {
|
||||
MultiMCInstancePatch patch = MultiMCInstancePatch.read(componentID, Files.readString(patchPath));
|
||||
patches.add(Task.supplyAsync(() -> patch));
|
||||
} else {
|
||||
patches.add(new GetTask(MultiMCComponents.getMetaURL(componentID, component.getVersion(), mcVersion))
|
||||
.thenApplyAsync(s -> MultiMCInstancePatch.read(componentID, s)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends Task<?>> getDependents() {
|
||||
return patches;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
Map<String, MultiMCInstancePatch> existed = new LinkedHashMap<>();
|
||||
for (Task<MultiMCInstancePatch> patch : patches) {
|
||||
MultiMCInstancePatch result = patch.getResult();
|
||||
existed.put(result.getID(), result);
|
||||
}
|
||||
|
||||
checking:
|
||||
while (true) {
|
||||
for (MultiMCInstancePatch patch : existed.values()) {
|
||||
for (MultiMCManifest.MultiMCManifestCachedRequires require : patch.getRequires()) {
|
||||
String componentID = require.getID();
|
||||
if (!existed.containsKey(componentID)) {
|
||||
Task<MultiMCInstancePatch> task = new GetTask(MultiMCComponents.getMetaURL(
|
||||
componentID,
|
||||
Lang.requireNonNullElse(require.getEqualsVersion(), require.getSuggests()),
|
||||
mcVersion
|
||||
)).thenApplyAsync(s -> MultiMCInstancePatch.read(componentID, s));
|
||||
task.run();
|
||||
MultiMCInstancePatch result = Objects.requireNonNull(task.getResult());
|
||||
existed.put(result.getID(), result);
|
||||
continue checking;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
setResult(new ArrayList<>(existed.values()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.modpack.server;
|
||||
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCInstancePatch;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCManifest;
|
||||
import org.jackhuang.hmcl.util.JvmArgumentFilter;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// Default-on lwjgl3ify companion shipped under {@code tdlm/lwjgl3ify/} in the TDLM Server Modpack.
|
||||
@NotNullByDefault
|
||||
public final class Lwjgl3ifySupport {
|
||||
/// Instance-root-relative directory written by the TDLM pack builder.
|
||||
public static final String ROOT = "tdlm/lwjgl3ify";
|
||||
|
||||
private Lwjgl3ifySupport() {
|
||||
}
|
||||
|
||||
/// Returns whether the instance carries an enabled lwjgl3ify MultiMC companion.
|
||||
public static boolean isPresent(Path instanceRoot) {
|
||||
Path root = root(instanceRoot);
|
||||
return Files.isRegularFile(root.resolve("enabled")) && Files.isRegularFile(root.resolve("mmc-pack.json"));
|
||||
}
|
||||
|
||||
/// Returns the companion root under the instance.
|
||||
public static Path root(Path instanceRoot) {
|
||||
return instanceRoot.resolve(ROOT);
|
||||
}
|
||||
|
||||
/// Injects {@code +jvmArgs} from every local patch when the companion is present.
|
||||
///
|
||||
/// Version-json application via {@link Lwjgl3ifyApplyTask} is the primary path; this covers launches
|
||||
/// where the resolved manifest has not yet absorbed those args.
|
||||
public static void injectLaunchOptions(Path instanceRoot, LaunchOptions.Builder builder) {
|
||||
if (!isPresent(instanceRoot)) {
|
||||
return;
|
||||
}
|
||||
Path patches = root(instanceRoot).resolve("patches");
|
||||
if (!Files.isDirectory(patches)) {
|
||||
return;
|
||||
}
|
||||
List<String> args = new ArrayList<>();
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(patches, "*.json")) {
|
||||
for (Path patchFile : stream) {
|
||||
try {
|
||||
MultiMCInstancePatch patch = MultiMCInstancePatch.read(
|
||||
patchFile.getFileName().toString(), Files.readString(patchFile));
|
||||
args.addAll(JvmArgumentFilter.filter(patch.getJvmArgs()));
|
||||
} catch (IOException | RuntimeException e) {
|
||||
LOG.warning("Failed to read lwjgl3ify patch " + patchFile, e);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to list lwjgl3ify patches under " + patches, e);
|
||||
return;
|
||||
}
|
||||
for (String arg : args) {
|
||||
if (!builder.getJavaArguments().contains(arg)) {
|
||||
builder.getJavaArguments().add(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses {@code mmc-pack.json} from the companion root.
|
||||
public static MultiMCManifest readMmcPack(Path instanceRoot) throws IOException {
|
||||
Path mmcPack = root(instanceRoot).resolve("mmc-pack.json");
|
||||
return JsonUtils.fromNonNullJson(Files.readString(mmcPack), MultiMCManifest.class);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -213,6 +213,6 @@ public class ServerModpackCompletionTask extends Task<Void> {
|
||||
public void postExecute() throws Exception {
|
||||
if (manifest == null || StringUtils.isBlank(manifest.getManifest().getFileApi())) return;
|
||||
Files.createDirectories(configurationFile.getParent());
|
||||
JsonUtils.writeToJsonFile(configurationFile, new ModpackConfiguration<>(remoteManifest, this.manifest.getType(), this.manifest.getName(), this.manifest.getVersion(), remoteManifest.getFiles()));
|
||||
JsonUtils.writeToJsonFile(configurationFile, new ModpackConfiguration<>(remoteManifest, this.manifest.getType(), this.manifest.getName(), remoteManifest.getVersion(), remoteManifest.getFiles()));
|
||||
}
|
||||
}
|
||||
+6
@@ -32,6 +32,7 @@ public class ServerModpackLocalInstallTask extends Task<Void> {
|
||||
/// Existing instance selecting update mode, or `null` for a new installation.
|
||||
private final @Nullable DefaultGameInstance updateTarget;
|
||||
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final DefaultGameRepository repository;
|
||||
private final List<Task<?>> dependencies = new ArrayList<>();
|
||||
private final List<Task<?>> dependents = new ArrayList<>(4);
|
||||
@@ -97,6 +98,7 @@ public class ServerModpackLocalInstallTask extends Task<Void> {
|
||||
this.manifest = manifest;
|
||||
this.instanceId = instanceId;
|
||||
this.updateTarget = updateTarget;
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.repository = dependencyManager.getGameRepository();
|
||||
Path run = repository.getLayout().getInstanceRoot(instanceId);
|
||||
|
||||
@@ -148,5 +150,9 @@ public class ServerModpackLocalInstallTask extends Task<Void> {
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
Path instanceRoot = repository.getLayout().getInstanceRoot(instanceId);
|
||||
if (Lwjgl3ifySupport.isPresent(instanceRoot)) {
|
||||
dependencies.add(new Lwjgl3ifyApplyTask(dependencyManager, instanceId, updateTarget));
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -6,6 +6,7 @@ import com.google.gson.JsonParseException;
|
||||
import kala.compress.archivers.zip.ZipArchiveReader;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.game.DefaultGameInstance;
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
import org.jackhuang.hmcl.modpack.MismatchedModpackTypeException;
|
||||
import org.jackhuang.hmcl.modpack.Modpack;
|
||||
import org.jackhuang.hmcl.modpack.ModpackProvider;
|
||||
@@ -29,7 +30,8 @@ public final class ServerModpackProvider implements ModpackProvider {
|
||||
|
||||
@Override
|
||||
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, DefaultGameInstance instance) {
|
||||
return new ServerModpackCompletionTask(dependencyManager, instance);
|
||||
return new ServerModpackCompletionTask(dependencyManager, instance)
|
||||
.thenComposeAsync(ignored -> Lwjgl3ifyApplyTask.afterCompletion(dependencyManager, instance));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,4 +53,12 @@ public final class ServerModpackProvider implements ModpackProvider {
|
||||
ServerModpackManifest manifest = JsonUtils.fromNonNullJson(json, ServerModpackManifest.class);
|
||||
return manifest.toModpack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectLaunchOptions(String modpackConfigurationJson, LaunchOptions.Builder builder) {
|
||||
Path gameDir = builder.getGameDir();
|
||||
if (gameDir != null) {
|
||||
Lwjgl3ifySupport.injectLaunchOptions(gameDir, builder);
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -126,10 +126,12 @@ public class ServerModpackRemoteInstallTask extends Task<Void> {
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
DefaultGameInstance instance = repository.getInstance(instanceId);
|
||||
dependencies.add(new ServerModpackCompletionTask(
|
||||
dependency,
|
||||
repository.getInstance(instanceId),
|
||||
new ModpackConfiguration<>(manifest, MODPACK_TYPE, manifest.getName(), manifest.getVersion(), Collections.emptyList())));
|
||||
instance,
|
||||
new ModpackConfiguration<>(manifest, MODPACK_TYPE, manifest.getName(), manifest.getVersion(), Collections.emptyList()))
|
||||
.thenComposeAsync(ignored -> Lwjgl3ifyApplyTask.afterCompletion(dependency, instance)));
|
||||
}
|
||||
|
||||
public static final String MODPACK_TYPE = "Server";
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* Filters JVM arguments coming from untrusted modpack / MultiMC patch input.
|
||||
*
|
||||
* <p>Denies agent injection and {@code -XX:On*Error} command hooks while allowing ordinary
|
||||
* memory / GC / system-property args used by companions such as lwjgl3ify.
|
||||
*/
|
||||
public final class JvmArgumentFilter {
|
||||
private JvmArgumentFilter() {
|
||||
}
|
||||
|
||||
/// Returns whether a single JVM argument is safe to accept from modpack / patch metadata.
|
||||
public static boolean isAllowed(@Nullable String arg) {
|
||||
if (arg == null) {
|
||||
return false;
|
||||
}
|
||||
String trimmed = arg.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String lower = trimmed.toLowerCase(Locale.ROOT);
|
||||
if (lower.startsWith("-javaagent")
|
||||
|| lower.startsWith("-agentpath")
|
||||
|| lower.startsWith("-agentlib")
|
||||
|| lower.startsWith("-xbootclasspath")
|
||||
|| lower.startsWith("-xx:onoutofmemoryerror")
|
||||
|| lower.startsWith("-xx:onerror")
|
||||
|| lower.startsWith("-xx:exitonoutofmemoryerror")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Copies {@code args}, dropping disallowed entries and logging each skip.
|
||||
public static List<String> filter(Iterable<String> args) {
|
||||
List<String> out = new ArrayList<>();
|
||||
for (String arg : args) {
|
||||
if (isAllowed(arg)) {
|
||||
out.add(arg);
|
||||
} else {
|
||||
LOG.warning("Ignoring disallowed JVM argument from modpack/patch: " + arg);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,12 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
@@ -28,7 +30,40 @@ public final class ServerStatusPing {
|
||||
/// @param online the number of players online
|
||||
/// @param max the player limit
|
||||
/// @param latencyMillis the round-trip time of the ping packet in milliseconds
|
||||
public record Status(int online, int max, long latencyMillis) {
|
||||
/// @param motd the server's message of the day as plain text without colour codes; empty if absent
|
||||
public record Status(int online, int max, long latencyMillis, String motd) {
|
||||
/// Creates a status without a message of the day.
|
||||
public Status(int online, int max, long latencyMillis) {
|
||||
this(online, max, latencyMillis, "");
|
||||
}
|
||||
}
|
||||
|
||||
/// Flattens the `description` field of a status response (a plain string or a chat component with
|
||||
/// `text` and `extra`) into plain text, dropping `§x` colour and format codes.
|
||||
///
|
||||
/// @param description the `description` element, may be `null`
|
||||
/// @return the plain text, trimmed; empty when there is nothing to show
|
||||
public static String motdToPlainText(@Nullable JsonElement description) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
appendComponent(description, builder);
|
||||
return builder.toString().replaceAll("\u00a7.", "").strip();
|
||||
}
|
||||
|
||||
private static void appendComponent(@Nullable JsonElement element, StringBuilder out) {
|
||||
if (element == null || element.isJsonNull()) {
|
||||
return;
|
||||
}
|
||||
if (element.isJsonPrimitive()) {
|
||||
out.append(element.getAsString());
|
||||
} else if (element.isJsonArray()) {
|
||||
for (JsonElement item : element.getAsJsonArray()) {
|
||||
appendComponent(item, out);
|
||||
}
|
||||
} else if (element.isJsonObject()) {
|
||||
JsonObject object = element.getAsJsonObject();
|
||||
appendComponent(object.get("text"), out);
|
||||
appendComponent(object.get("extra"), out);
|
||||
}
|
||||
}
|
||||
|
||||
/// The protocol number sent in the handshake; Minecraft 1.7.10 uses 5 and servers answer any value.
|
||||
@@ -73,9 +108,11 @@ public final class ServerStatusPing {
|
||||
in.readFully(json);
|
||||
int online;
|
||||
int max;
|
||||
String motd;
|
||||
try {
|
||||
JsonObject players = JsonUtils.GSON.fromJson(new String(json, StandardCharsets.UTF_8), JsonObject.class)
|
||||
.getAsJsonObject("players");
|
||||
JsonObject response = JsonUtils.GSON.fromJson(new String(json, StandardCharsets.UTF_8), JsonObject.class);
|
||||
motd = motdToPlainText(response.get("description"));
|
||||
JsonObject players = response.getAsJsonObject("players");
|
||||
online = players.get("online").getAsInt();
|
||||
max = players.get("max").getAsInt();
|
||||
} catch (JsonParseException | NullPointerException | ClassCastException | IllegalStateException e) {
|
||||
@@ -94,7 +131,7 @@ public final class ServerStatusPing {
|
||||
throw new IOException("Unexpected pong packet");
|
||||
}
|
||||
in.readLong();
|
||||
return new Status(online, max, (System.nanoTime() - start) / 1_000_000);
|
||||
return new Status(online, max, (System.nanoTime() - start) / 1_000_000, motd);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -306,6 +306,7 @@ public final class FileUtils {
|
||||
}
|
||||
|
||||
public static void copyDirectory(Path src, Path dest, Predicate<String> filePredicate) throws IOException {
|
||||
Path destRoot = dest.toAbsolutePath().normalize();
|
||||
Files.walkFileTree(src, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
@@ -313,7 +314,11 @@ public final class FileUtils {
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
}
|
||||
|
||||
Path destFile = dest.resolve(src.relativize(file).toString());
|
||||
Path destFile = destRoot.resolve(src.relativize(file).toString()).toAbsolutePath().normalize();
|
||||
if (!destFile.startsWith(destRoot)) {
|
||||
throw new IOException("Refusing to copy outside destination directory: " + file);
|
||||
}
|
||||
Files.createDirectories(destFile.getParent());
|
||||
Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
@@ -324,7 +329,10 @@ public final class FileUtils {
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
}
|
||||
|
||||
Path destDir = dest.resolve(src.relativize(dir).toString());
|
||||
Path destDir = destRoot.resolve(src.relativize(dir).toString()).toAbsolutePath().normalize();
|
||||
if (!destDir.startsWith(destRoot)) {
|
||||
throw new IOException("Refusing to create directory outside destination: " + dir);
|
||||
}
|
||||
Files.createDirectories(destDir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
@@ -134,9 +135,10 @@ public abstract class HttpRequest {
|
||||
con = resolveConnection(con);
|
||||
checkResponseCode(con);
|
||||
|
||||
return IOUtils.readFullyAsString("gzip".equals(con.getContentEncoding())
|
||||
InputStream body = "gzip".equals(con.getContentEncoding())
|
||||
? IOUtils.wrapFromGZip(con.getInputStream())
|
||||
: con.getInputStream());
|
||||
: con.getInputStream();
|
||||
return IOUtils.readFullyAsString(body, UTF_8, IOUtils.DEFAULT_MAX_HTTP_BODY_BYTES);
|
||||
}, retryTimes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,13 @@ package org.jackhuang.hmcl.util.io;
|
||||
import kala.encdet.EncodingDetector;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.FileChannel;
|
||||
@@ -29,6 +35,9 @@ public final class IOUtils {
|
||||
|
||||
public static final int DEFAULT_BUFFER_SIZE = 32 * 1024;
|
||||
|
||||
/// Soft cap for HTTP / metadata bodies read into memory (authlib, API JSON, etc.).
|
||||
public static final long DEFAULT_MAX_HTTP_BODY_BYTES = 16L * 1024L * 1024L;
|
||||
|
||||
public static BufferedReader newBufferedReaderMaybeNativeEncoding(Path file) throws IOException {
|
||||
if (NATIVE_CHARSET == UTF_8)
|
||||
return Files.newBufferedReader(file);
|
||||
@@ -68,8 +77,29 @@ public final class IOUtils {
|
||||
}
|
||||
|
||||
public static byte[] readFully(InputStream stream) throws IOException {
|
||||
return readFully(stream, -1L);
|
||||
}
|
||||
|
||||
/// Reads the stream fully, optionally refusing bodies larger than {@code maxBytes}.
|
||||
///
|
||||
/// @param maxBytes maximum accepted size, or {@code <= 0} for no limit
|
||||
public static byte[] readFully(InputStream stream, long maxBytes) throws IOException {
|
||||
try (stream) {
|
||||
return stream.readAllBytes();
|
||||
if (maxBytes <= 0) {
|
||||
return stream.readAllBytes();
|
||||
}
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(Math.min(DEFAULT_BUFFER_SIZE, (int) Math.min(maxBytes, Integer.MAX_VALUE)));
|
||||
byte[] buf = new byte[DEFAULT_BUFFER_SIZE];
|
||||
long total = 0;
|
||||
int n;
|
||||
while ((n = stream.read(buf)) >= 0) {
|
||||
total += n;
|
||||
if (total > maxBytes) {
|
||||
throw new IOException("Response body exceeds limit of " + maxBytes + " bytes");
|
||||
}
|
||||
out.write(buf, 0, n);
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +111,10 @@ public final class IOUtils {
|
||||
return new String(readFully(stream), charset);
|
||||
}
|
||||
|
||||
public static String readFullyAsString(InputStream stream, Charset charset, long maxBytes) throws IOException {
|
||||
return new String(readFully(stream, maxBytes), charset);
|
||||
}
|
||||
|
||||
public static void skipNBytes(InputStream input, long n) throws IOException {
|
||||
while (n > 0) {
|
||||
long ns = input.skip(n);
|
||||
|
||||
@@ -437,13 +437,13 @@ public final class NetworkUtils {
|
||||
Charset charset = getCharsetFromContentType(con.getHeaderField("Content-Type"));
|
||||
|
||||
try (InputStream stdout = con.getInputStream()) {
|
||||
return IOUtils.readFullyAsString(contentEncoding.wrap(stdout), charset);
|
||||
return IOUtils.readFullyAsString(contentEncoding.wrap(stdout), charset, IOUtils.DEFAULT_MAX_HTTP_BODY_BYTES);
|
||||
} catch (IOException e) {
|
||||
if (con instanceof HttpURLConnection) {
|
||||
try (InputStream stderr = ((HttpURLConnection) con).getErrorStream()) {
|
||||
if (stderr == null)
|
||||
throw e;
|
||||
return IOUtils.readFullyAsString(contentEncoding.wrap(stderr), charset);
|
||||
return IOUtils.readFullyAsString(contentEncoding.wrap(stderr), charset, IOUtils.DEFAULT_MAX_HTTP_BODY_BYTES);
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public final class JavaFXLauncher {
|
||||
|
||||
private JavaFXLauncher() {
|
||||
@@ -10,13 +14,37 @@ public final class JavaFXLauncher {
|
||||
private static boolean started = false;
|
||||
|
||||
static {
|
||||
// init JavaFX Toolkit
|
||||
// init JavaFX Toolkit — never block class init forever if GTK/Glass hangs under CI.
|
||||
try {
|
||||
javafx.application.Platform.startup(() -> {
|
||||
});
|
||||
started = true;
|
||||
} catch (IllegalStateException e) {
|
||||
started = true;
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
AtomicReference<Throwable> error = new AtomicReference<>();
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
javafx.application.Platform.startup(() -> {
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
error.set(e);
|
||||
} finally {
|
||||
done.countDown();
|
||||
}
|
||||
}, "hmcl-javafx-startup");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
if (!done.await(45, TimeUnit.SECONDS)) {
|
||||
System.err.println("JavaFX Platform.startup timed out; JavaFX tests will be skipped");
|
||||
} else {
|
||||
Throwable e = error.get();
|
||||
if (e == null) {
|
||||
started = true;
|
||||
} else if (e instanceof IllegalStateException) {
|
||||
// Toolkit already running in this JVM.
|
||||
started = true;
|
||||
} else {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class JvmArgumentFilterTest {
|
||||
@Test
|
||||
public void allowsOrdinaryArgs() {
|
||||
assertTrue(JvmArgumentFilter.isAllowed("-Xmx2G"));
|
||||
assertTrue(JvmArgumentFilter.isAllowed("-Djava.library.path=/tmp"));
|
||||
assertTrue(JvmArgumentFilter.isAllowed("--add-opens=java.base/java.lang=ALL-UNNAMED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsAgentsAndOnErrorHooks() {
|
||||
assertFalse(JvmArgumentFilter.isAllowed("-javaagent:/tmp/evil.jar"));
|
||||
assertFalse(JvmArgumentFilter.isAllowed("-agentpath:/tmp/evil.so"));
|
||||
assertFalse(JvmArgumentFilter.isAllowed("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"));
|
||||
assertFalse(JvmArgumentFilter.isAllowed("-Xbootclasspath/a:/tmp/evil.jar"));
|
||||
assertFalse(JvmArgumentFilter.isAllowed("-XX:OnOutOfMemoryError=curl evil.example"));
|
||||
assertFalse(JvmArgumentFilter.isAllowed("-XX:OnError=id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterDropsDisallowed() {
|
||||
assertEquals(List.of("-Xmx1G"),
|
||||
JvmArgumentFilter.filter(List.of("-Xmx1G", "-javaagent:x.jar")));
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ public final class ServerStatusPingTest {
|
||||
var status = ServerStatusPing.ping("127.0.0.1", server.getLocalPort(), 3000);
|
||||
assertEquals(12, status.online());
|
||||
assertEquals(50, status.max());
|
||||
assertEquals("TDLM 公告: 今晚维护", status.motd());
|
||||
assertTrue(status.latencyMillis() >= 0);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +46,7 @@ public final class ServerStatusPingTest {
|
||||
skipPacket(in); // handshake
|
||||
skipPacket(in); // status request
|
||||
|
||||
byte[] json = "{\"players\":{\"online\":12,\"max\":50}}".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] json = "{\"description\":{\"text\":\"§aTDLM \",\"extra\":[{\"text\":\"公告: 今晚维护\"}]},\"players\":{\"online\":12,\"max\":50}}".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] body = concat(new byte[]{0x00}, varInt(json.length), json);
|
||||
out.write(varInt(body.length));
|
||||
out.write(body);
|
||||
@@ -88,4 +89,11 @@ public final class ServerStatusPingTest {
|
||||
for (byte[] part : parts) out.writeBytes(part);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
@Test
|
||||
void flattensPlainStringMotd() {
|
||||
var json = com.google.gson.JsonParser.parseString("\"§l欢迎§r来玩\"");
|
||||
assertEquals("欢迎来玩", ServerStatusPing.motdToPlainText(json));
|
||||
assertEquals("", ServerStatusPing.motdToPlainText(null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.util.io;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class CopyDirectoryTraversalTest {
|
||||
@TempDir
|
||||
Path temp;
|
||||
|
||||
@Test
|
||||
public void copiesWithinDestination() throws IOException {
|
||||
Path src = temp.resolve("src");
|
||||
Path dest = temp.resolve("dest");
|
||||
Files.createDirectories(src.resolve("a"));
|
||||
Files.writeString(src.resolve("a/b.txt"), "hi");
|
||||
FileUtils.copyDirectory(src, dest);
|
||||
assertEquals("hi", Files.readString(dest.resolve("a/b.txt")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void destinationRootGuardMatchesUnzipperStyle() {
|
||||
Path destRoot = temp.resolve("dest").toAbsolutePath().normalize();
|
||||
Path escape = destRoot.resolve("sub/../../outside.txt").toAbsolutePath().normalize();
|
||||
assertFalse(escape.startsWith(destRoot), "normalize + startsWith must reject escapes");
|
||||
Path inside = destRoot.resolve("sub/file.txt").toAbsolutePath().normalize();
|
||||
assertTrue(inside.startsWith(destRoot));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.util.io;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class IOUtilsReadLimitTest {
|
||||
@Test
|
||||
public void rejectsOversizedBody() {
|
||||
byte[] data = new byte[100];
|
||||
assertThrows(IOException.class, () -> IOUtils.readFully(new ByteArrayInputStream(data), 50));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptsWithinLimit() throws IOException {
|
||||
byte[] data = "hello".getBytes(StandardCharsets.UTF_8);
|
||||
assertArrayEquals(data, IOUtils.readFully(new ByteArrayInputStream(data), 16));
|
||||
assertEquals("hello", IOUtils.readFullyAsString(new ByteArrayInputStream(data), StandardCharsets.UTF_8, 16));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
# TDLMPack consume path for TDLM/HMCL
|
||||
|
||||
TDLMPack does **not** pull this launcher via Maven, a version pin, or a pack-build download step.
|
||||
The pack build publishes the HMCL Server Modpack client (`server-manifest.json` + `overrides/`)
|
||||
under the `tdlm-client/latest` layout. Players install the launcher separately.
|
||||
|
||||
## After this repo’s publish workflow
|
||||
|
||||
Run **Actions → Publish TDLM launcher → Run workflow** with a new `release_tag`.
|
||||
|
||||
### Secrets (sibling pattern)
|
||||
|
||||
| Name | Where it lives today | Used by |
|
||||
|---|---|---|
|
||||
| `MAVEN_USER` / `MAVEN_PASSWORD` | **Org** secrets | Stratum / mod Maven publish — **not** used for launcher binaries |
|
||||
| `TDLM_PACK_TOKEN` | **TDLMPack repo** secret only (not org) | TDLMPack `pack.yml` Gitea package + release uploads |
|
||||
| `github.token` | Actions runtime | HMCL publish fallback when `TDLM_PACK_TOKEN` is unset |
|
||||
|
||||
HMCL `publish-tdlm.yml` uses the **same name** as TDLMPack: `secrets.TDLM_PACK_TOKEN`, then falls back to `github.token`.
|
||||
Org Maven secrets cannot publish Gitea generic packages / releases.
|
||||
|
||||
**Recommended:** promote `TDLM_PACK_TOKEN` to an **org** secret (like `MAVEN_*`) so HMCL inherits it with no new repo secret. Until then, either copy it onto `TDLM/HMCL` or rely on `github.token` (same-repo release usually works; org `tdlm-hmcl` package write may still need the pack token).
|
||||
|
||||
### 1. Stable generic URLs (preferred for README / curl / CI)
|
||||
|
||||
Overwrite-in-place, same host style as the client fileApi:
|
||||
|
||||
```text
|
||||
https://git.nest.potatolab.uk:8443/api/packages/TDLM/generic/tdlm-hmcl/latest/HMCL.exe
|
||||
https://git.nest.potatolab.uk:8443/api/packages/TDLM/generic/tdlm-hmcl/latest/HMCL.jar
|
||||
```
|
||||
|
||||
Checksums (when published): `HMCL.exe.sha256`, `HMCL.jar.sha256` next to those names.
|
||||
|
||||
### 2. Gitea Release (versioned page)
|
||||
|
||||
```text
|
||||
https://git.potatolab.uk/TDLM/HMCL/releases/latest
|
||||
```
|
||||
|
||||
Assets on each tag: `HMCL.exe`, `HMCL.jar`, plus `.sha256` files.
|
||||
(Use the nest API host for scripted uploads; public download links on `git.potatolab.uk` are fine for browsers.)
|
||||
|
||||
### 3. What TDLMPack should change
|
||||
|
||||
In `TDLM/TDLMPack` README §启动, replace the repo-only “安装 TDLM/HMCL” step with one of the URLs above.
|
||||
**Do not** add HMCL as a Gradle/Maven dependency of the pack — pack CI stays pack-only.
|
||||
|
||||
### 4. Client pack fileApi (self-hosted VPS)
|
||||
|
||||
HMCL default (`Metadata.DEFAULT_CLIENT_FILE_API`):
|
||||
|
||||
```text
|
||||
http://159.75.5.225:19527/tdlm-client
|
||||
```
|
||||
|
||||
First install / updates fetch `{fileApi}/server-manifest.json` (and `overrides/**` under that root).
|
||||
Override at runtime: `-Dhmcl.client.fileApi=…` or jar attribute `hmcl.client.fileApi`.
|
||||
|
||||
The fileApi is a read-only nginx static mirror on the TDLM VPS (`159.75.5.225`, port `19527`,
|
||||
root `/var/www/tdlm-dl`). Players read it anonymously; integrity is verified by the SHA-1 hashes
|
||||
in `server-manifest.json`.
|
||||
|
||||
**TDLMPack must use the same URL** so `server-manifest.json` embeds it:
|
||||
|
||||
```text
|
||||
TDLM_CLIENT_FILE_API=http://159.75.5.225:19527/tdlm-client
|
||||
```
|
||||
|
||||
Set that as the Gitea Actions **variable** on `TDLM/TDLMPack` (pack.yml passes `vars.TDLM_CLIENT_FILE_API`;
|
||||
`PackBuild.DEFAULT_FILE_API` is the build-time fallback).
|
||||
|
||||
**Publish path (upload origin split from public fileApi):** Pack CI no longer PUTs to the read URL.
|
||||
The `pack.yml` "Publish the client Server Modpack to fileApi" step rsyncs `build/pack/client/overrides/`
|
||||
then `server-manifest.json` (manifest last) over SSH to `[email protected]:/var/www/tdlm-dl/tdlm-client`.
|
||||
Auth is the repo secret `TDLM_VPS_SSH_KEY`, an ed25519 deploy key confined on the VPS by
|
||||
`command="/usr/bin/rrsync /var/www/tdlm-dl/tdlm-client"` (rsync-only, no shell, no forwarding).
|
||||
The public port 19527 serves GET/HEAD only — there is no public write surface.
|
||||
File diff suppressed because it is too large.
Load diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-123
@@ -1,123 +0,0 @@
|
||||
################################################################################
|
||||
# JAXP Strict Configuration Template
|
||||
#
|
||||
# This file, jaxp-strict.properties.template, provides a template for creating
|
||||
# custom configuration files. The settings in this file are more restrictive than
|
||||
# those in the default configuration, jaxp.properties. In particular:
|
||||
# - JDKCatalog Resolve is on "strict" setting
|
||||
# - Extension Functions are disabled
|
||||
# - JAXP Limits are set to smaller numbers
|
||||
#
|
||||
# To create a configuration file, copy the template to a new file with
|
||||
# the .properties extension, that is:
|
||||
#
|
||||
# cp $JAVA_HOME/conf/jaxp-strict.properties.template /path/to/jaxp-strict.properties
|
||||
#
|
||||
# The configuration file can then be set up using the system property
|
||||
# java.xml.config.file to override the default configuration jaxp.properties
|
||||
# and used to assess the impact of a stricter configuration, for example:
|
||||
#
|
||||
# java -Djava.xml.config.file=/path/to/jaxp-strict.properties
|
||||
#
|
||||
# The system property java.xml.config.file is defined in the java.xml module
|
||||
# description.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
#
|
||||
# ---- Implementation Specific Properties ----
|
||||
# For a complete list of properties, refer to the Implementation Specific Properties
|
||||
# table in the java.xml/module-summary.
|
||||
#
|
||||
# Extension Functions:
|
||||
#
|
||||
# This property determines whether XSLT and XPath extension functions are allowed.
|
||||
# The value type is boolean and the default value is true (allowing
|
||||
# extension functions). The following entry overrides the default value and
|
||||
# disallows extension functions:
|
||||
#
|
||||
jdk.xml.enableExtensionFunctions=false
|
||||
#
|
||||
#
|
||||
# Overriding the default parser:
|
||||
#
|
||||
# This property allows a third party implementation to override the default
|
||||
# parser provided by the JDK. The value type is boolean and the default value is
|
||||
# false, disallowing overriding the default parser. The setting below reflects
|
||||
# the default property setting:
|
||||
#
|
||||
jdk.xml.overrideDefaultParser=false
|
||||
#
|
||||
# Implementation Specific Properties - jdkcatalog.resolve
|
||||
#
|
||||
# This property instructs the JDK default CatalogResolver to act in accordance with
|
||||
# the setting when unable to resolve an external reference with the built-in Catalog.
|
||||
# The options are:
|
||||
# continue -- indicates that the processing should continue
|
||||
# ignore -- indicates that the reference is skipped
|
||||
# strict -- indicates that the resolver should throw a CatalogException
|
||||
#
|
||||
# The following setting causes the default CatalogResolver to throw a CatalogException
|
||||
# when external references are not resolved by a user-defined resolver or catalog,
|
||||
# or the built-in Catalog:
|
||||
jdk.xml.jdkcatalog.resolve=strict
|
||||
#
|
||||
# Implementation Specific Properties - DTD
|
||||
#
|
||||
# This property instructs the parsers to deny, ignore or allow DTD processing.
|
||||
# The following setting causes the parser to reject DTDs by throwing an exception.
|
||||
# jdk.xml.dtd.support=deny
|
||||
#
|
||||
# The following setting permits the processor to continue processing DTDs. Note
|
||||
# that while DTDs are allowed in this configuration, external references are
|
||||
# restricted, and limits on DTD entities are tightened:
|
||||
jdk.xml.dtd.support=allow
|
||||
#
|
||||
# Implementation Specific Properties - Limits
|
||||
#
|
||||
# Limits have a value type Integer. The values must be positive integers. Zero
|
||||
# means no limit.
|
||||
#
|
||||
# Limits the number of entity expansions
|
||||
jdk.xml.entityExpansionLimit=2500
|
||||
#
|
||||
# Limits the total size of all entities that include general and parameter entities.
|
||||
# The size is calculated as an aggregation of all entities.
|
||||
jdk.xml.totalEntitySizeLimit=100000
|
||||
#
|
||||
# Limits the maximum size of any general entities.
|
||||
jdk.xml.maxGeneralEntitySizeLimit=100000
|
||||
#
|
||||
# Limits the maximum size of any parameter entities, including the result of
|
||||
# nesting multiple parameter entities.
|
||||
jdk.xml.maxParameterEntitySizeLimit=15000
|
||||
#
|
||||
# Limits the total number of nodes in all entity references.
|
||||
jdk.xml.entityReplacementLimit=100000
|
||||
#
|
||||
# Limits the number of attributes an element can have. The default value is 200.
|
||||
jdk.xml.elementAttributeLimit=200
|
||||
#
|
||||
# Limits the number of content model nodes that may be created when building a
|
||||
# grammar for a W3C XML Schema that contains maxOccurs attributes with values
|
||||
# other than "unbounded". The default value is 5000.
|
||||
jdk.xml.maxOccurLimit=5000
|
||||
#
|
||||
# Limits the maximum element depth. The default value is 100.
|
||||
jdk.xml.maxElementDepth=100
|
||||
#
|
||||
# Limits the maximum size of XML names, including element name, attribute name
|
||||
# and namespace prefix and URI. The default value is 1000.
|
||||
jdk.xml.maxXMLNameLimit=1000
|
||||
#
|
||||
#
|
||||
# XPath Limits
|
||||
#
|
||||
# Limits the number of groups an XPath expression can contain. The default value is 10.
|
||||
jdk.xml.xpathExprGrpLimit=10
|
||||
#
|
||||
# Limits the number of operators an XPath expression can contain. The default value is 100.
|
||||
jdk.xml.xpathExprOpLimit=100
|
||||
#
|
||||
# Limits the total number of XPath operators in an XSL Stylesheet. The default value is 10000.
|
||||
jdk.xml.xpathTotalOpLimit=10000
|
||||
-200
@@ -1,200 +0,0 @@
|
||||
################################################################################
|
||||
# JAXP Configuration File
|
||||
#
|
||||
# jaxp.properties (this file) is the default configuration file for JAXP, the API
|
||||
# defined in the java.xml module. It is in java.util.Properties format and typically
|
||||
# located in the {java.home}/conf directory. It may contain key/value pairs for
|
||||
# specifying the implementation classes of JAXP factories and/or properties
|
||||
# that have corresponding system properties.
|
||||
#
|
||||
# A user-specified configuration file can be set up using the system property
|
||||
# java.xml.config.file to override any or all of the entries in jaxp.properties.
|
||||
# The following statement provides myConfigurationFile as a custom configuration
|
||||
# file:
|
||||
# java -Djava.xml.config.file=myConfigurationFile
|
||||
################################################################################
|
||||
|
||||
# ---- JAXP Default Configuration ----
|
||||
#
|
||||
# The JAXP default configuration (jaxp.properties) contains entries for the
|
||||
# Factory Lookup Mechanism and properties with corresponding system properties.
|
||||
# The values are generally set to the default values of the properties.
|
||||
#
|
||||
#
|
||||
# JAXP Lookup Mechanism:
|
||||
#
|
||||
# The JAXP configuration file ranks 2nd to the System Property in the precedent
|
||||
# order of the JAXP Lookup Mechanism. When the System Property is not specified,
|
||||
# a JAXP factory reads the configuration file in order to locate an implementation
|
||||
# class. If found, the class specified will be used as the factory implementation
|
||||
# class.
|
||||
#
|
||||
# The format of an entry is key=value where the key is the fully qualified name
|
||||
# of the factory and value that of the implementation class. The following entry
|
||||
# sets a DocumentBuilderFactory implementation class:
|
||||
#
|
||||
# javax.xml.parsers.DocumentBuilderFactory=com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl
|
||||
#
|
||||
#
|
||||
# Java SE and JDK Implementation Specific Properties:
|
||||
#
|
||||
# The JAXP configuration file ranks above the default settings in the Property
|
||||
# Precedence in that its entries will override the default values of the corresponding
|
||||
# properties.
|
||||
#
|
||||
# All properties that have System Properties defined in Java SE or supported
|
||||
# by the JDK Implementation can be placed in the configuration file to override
|
||||
# the default property values. The format is:
|
||||
# system-property-name=value
|
||||
#
|
||||
# For example, the RESOLVE property in CatalogFeatures has an associated system
|
||||
# property called javax.xml.catalog.resolve. An entry for the RESOLVE property in the
|
||||
# configuration file therefore uses javax.xml.catalog.resolve as the key, that
|
||||
# is:
|
||||
# javax.xml.catalog.resolve=strict
|
||||
#
|
||||
#
|
||||
# Extension Functions:
|
||||
#
|
||||
# This property determines whether XSLT and XPath extension functions are allowed.
|
||||
# The value type is boolean and the default value is false (disallowing
|
||||
# extension functions).
|
||||
#
|
||||
jdk.xml.enableExtensionFunctions=false
|
||||
#
|
||||
#
|
||||
# Overriding the default parser:
|
||||
#
|
||||
# This property allows a third party implementation to override the default
|
||||
# parser provided by the JDK. The value type is boolean and the default value is
|
||||
# false, disallowing overriding the default parser. The setting below reflects
|
||||
# the default property setting:
|
||||
#
|
||||
jdk.xml.overrideDefaultParser=false
|
||||
#
|
||||
#
|
||||
# External Access Properties:
|
||||
#
|
||||
# The External Access Properties are defined in javax.xml.XMLConstants. Their
|
||||
# system properties are javax.xml.accessExternalDTD, javax.xml.accessExternalSchema,
|
||||
# and javax.xml.accessExternalStylesheet. The values are a list of protocols separated
|
||||
# by comma, plus empty string ("") to represent no protocol allowed and the key
|
||||
# word "all" for all access. The default is "all", allowing all external resources
|
||||
# to be fetched. The followings are example of external access settings:
|
||||
#
|
||||
# allow local (file) DTDs to be retrieved
|
||||
# javax.xml.accessExternalDTD=file
|
||||
#
|
||||
# allow local (file) and remote (http) external schemas
|
||||
# javax.xml.accessExternalSchema=file, http
|
||||
#
|
||||
# reject any external stylesheets
|
||||
# javax.xml.accessExternalStylesheet=""
|
||||
#
|
||||
# allow all external stylesheets
|
||||
# javax.xml.accessExternalStylesheet="all"
|
||||
#
|
||||
#
|
||||
# Catalog Properties:
|
||||
#
|
||||
# The Catalog API defines four features: FILES, PREFER, DEFER and RESOLVE.
|
||||
# Except PREFER, all other properties can be placed in the configuration file
|
||||
# using the system properties defined for them.
|
||||
#
|
||||
# FILES: A semicolon-delimited list of URIs to locate the catalog files. The URIs
|
||||
# must be absolute and have a URL protocol handler for the URI scheme. The following
|
||||
# is an example of setting up a catalog file:
|
||||
#
|
||||
# javax.xml.catalog.files = file:///users/auser/catalog/catalog.xml
|
||||
#
|
||||
# DEFER: Indicates that the alternative catalogs including those specified in
|
||||
# delegate entries or nextCatalog are not read until they are needed. The value
|
||||
# is a boolean and the default value is true.
|
||||
#
|
||||
# javax.xml.catalog.defer=true
|
||||
#
|
||||
# RESOLVE: Determines the action if there is no matching entry found after all of
|
||||
# the specified catalogs are exhausted. The values are key words: strict, continue,
|
||||
# and ignore. The default is strict. The following setting reflects the default
|
||||
# setting.
|
||||
#
|
||||
# javax.xml.catalog.resolve=strict
|
||||
#
|
||||
#
|
||||
# useCatalog:
|
||||
# This property instructs XML processors to use XML Catalogs to resolve entity
|
||||
# references. The value is a boolean and the default value is true.
|
||||
#
|
||||
# javax.xml.useCatalog=true
|
||||
#
|
||||
# Implementation Specific Properties - jdkcatalog.resolve
|
||||
#
|
||||
# This property instructs the JDK default CatalogResolver to act in accordance with
|
||||
# the setting when unable to resolve an external reference with the built-in Catalog.
|
||||
# The options are:
|
||||
# continue -- indicates that the processing should continue
|
||||
# ignore -- indicates that the reference is skipped
|
||||
# strict -- indicates that the resolver should throw a CatalogException
|
||||
#
|
||||
# The following setting allows the resolution to continue in cases where
|
||||
# external references are not resolved by a user-defined resolver or catalog if
|
||||
# any, and the built-in Catalog:
|
||||
jdk.xml.jdkcatalog.resolve=continue
|
||||
#
|
||||
# Implementation Specific Properties - DTD
|
||||
#
|
||||
# This property instructs the parsers to deny, ignore or allow DTD processing.
|
||||
# The following setting causes the parser to reject DTDs by throwing an exception.
|
||||
# jdk.xml.dtd.support=deny
|
||||
#
|
||||
# The following setting permits the processor to continue processing DTDs
|
||||
jdk.xml.dtd.support=allow
|
||||
#
|
||||
# Implementation Specific Properties - Limits
|
||||
#
|
||||
# Limits have a value type Integer. The values must be positive integers. Zero
|
||||
# means no limit.
|
||||
#
|
||||
# Limits the number of entity expansions. The default value is 2500
|
||||
jdk.xml.entityExpansionLimit=2500
|
||||
#
|
||||
# Limits the total size of all entities that include general and parameter entities.
|
||||
# The size is calculated as an aggregation of all entities. The default value is 100000.
|
||||
jdk.xml.totalEntitySizeLimit=100000
|
||||
#
|
||||
# Limits the maximum size of any general entities. The default value is 100000.
|
||||
jdk.xml.maxGeneralEntitySizeLimit=100000
|
||||
#
|
||||
# Limits the maximum size of any parameter entities, including the result of
|
||||
# nesting multiple parameter entities. The default value is 15000.
|
||||
jdk.xml.maxParameterEntitySizeLimit=15000
|
||||
#
|
||||
# Limits the total number of nodes in all entity references. The default value is 100000.
|
||||
jdk.xml.entityReplacementLimit=100000
|
||||
#
|
||||
# Limits the number of attributes an element can have. The default value is 200.
|
||||
jdk.xml.elementAttributeLimit=200
|
||||
#
|
||||
# Limits the number of content model nodes that may be created when building a
|
||||
# grammar for a W3C XML Schema that contains maxOccurs attributes with values
|
||||
# other than "unbounded". The default value is 5000.
|
||||
jdk.xml.maxOccurLimit=5000
|
||||
#
|
||||
# Limits the maximum element depth. The default value is 100.
|
||||
jdk.xml.maxElementDepth=100
|
||||
#
|
||||
# Limits the maximum size of XML names, including element name, attribute name
|
||||
# and namespace prefix and URI. The default value is 1000.
|
||||
jdk.xml.maxXMLNameLimit=1000
|
||||
#
|
||||
#
|
||||
# XPath Limits
|
||||
#
|
||||
# Limits the number of groups an XPath expression can contain. The default value is 10.
|
||||
jdk.xml.xpathExprGrpLimit=10
|
||||
#
|
||||
# Limits the number of operators an XPath expression can contain. The default value is 100.
|
||||
jdk.xml.xpathExprOpLimit=100
|
||||
#
|
||||
# Limits the total number of XPath operators in an XSL Stylesheet. The default value is 10000.
|
||||
jdk.xml.xpathTotalOpLimit=10000
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
############################################################
|
||||
# Default Logging Configuration File
|
||||
#
|
||||
# You can use a different file by specifying a filename
|
||||
# with the java.util.logging.config.file system property.
|
||||
# For example, java -Djava.util.logging.config.file=myfile
|
||||
############################################################
|
||||
|
||||
############################################################
|
||||
# Global properties
|
||||
############################################################
|
||||
|
||||
# "handlers" specifies a comma-separated list of log Handler
|
||||
# classes. These handlers will be installed during VM startup.
|
||||
# Note that these classes must be on the system classpath.
|
||||
# By default we only configure a ConsoleHandler, which will only
|
||||
# show messages at the INFO and above levels.
|
||||
handlers= java.util.logging.ConsoleHandler
|
||||
|
||||
# To also add the FileHandler, use the following line instead.
|
||||
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler
|
||||
|
||||
# Default global logging level.
|
||||
# This specifies which kinds of events are logged across
|
||||
# all loggers. For any given facility this global level
|
||||
# can be overridden by a facility-specific level
|
||||
# Note that the ConsoleHandler also has a separate level
|
||||
# setting to limit messages printed to the console.
|
||||
.level= INFO
|
||||
|
||||
############################################################
|
||||
# Handler specific properties.
|
||||
# Describes specific configuration info for Handlers.
|
||||
############################################################
|
||||
|
||||
# default file output is in user's home directory.
|
||||
java.util.logging.FileHandler.pattern = %h/java%u.log
|
||||
java.util.logging.FileHandler.limit = 50000
|
||||
java.util.logging.FileHandler.count = 1
|
||||
# Default number of locks FileHandler can obtain synchronously.
|
||||
# This specifies maximum number of attempts to obtain lock file by FileHandler
|
||||
# implemented by incrementing the unique field %u as per FileHandler API documentation.
|
||||
java.util.logging.FileHandler.maxLocks = 100
|
||||
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
|
||||
|
||||
# Limit the messages that are printed on the console to INFO and above.
|
||||
java.util.logging.ConsoleHandler.level = INFO
|
||||
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
|
||||
|
||||
# Example to customize the SimpleFormatter output format
|
||||
# to print one-line log message like this:
|
||||
# <level>: <log message> [<date/time>]
|
||||
#
|
||||
# java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
|
||||
|
||||
############################################################
|
||||
# Facility-specific properties.
|
||||
# Provides extra control for each logger.
|
||||
############################################################
|
||||
|
||||
# For example, set the com.xyz.foo logger to only log SEVERE
|
||||
# messages:
|
||||
# com.xyz.foo.level = SEVERE
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
######################################################################
|
||||
# Default Access Control File for Remote JMX(TM) Monitoring
|
||||
######################################################################
|
||||
#
|
||||
# Access control file for Remote JMX API access to monitoring.
|
||||
# This file defines the allowed access for different roles. The
|
||||
# password file (jmxremote.password by default) defines the roles and their
|
||||
# passwords. To be functional, a role must have an entry in
|
||||
# both the password and the access files.
|
||||
#
|
||||
# The default location of this file is $JRE/conf/management/jmxremote.access
|
||||
# You can specify an alternate location by specifying a property in
|
||||
# the management config file $JRE/conf/management/management.properties
|
||||
# (See that file for details)
|
||||
#
|
||||
# The file format for password and access files is syntactically the same
|
||||
# as the Properties file format. The syntax is described in the Javadoc
|
||||
# for java.util.Properties.load.
|
||||
# A typical access file has multiple lines, where each line is blank,
|
||||
# a comment (like this one), or an access control entry.
|
||||
#
|
||||
# An access control entry consists of a role name, and an
|
||||
# associated access level. The role name is any string that does not
|
||||
# itself contain spaces or tabs. It corresponds to an entry in the
|
||||
# password file (jmxremote.password). The access level is one of the
|
||||
# following:
|
||||
# "readonly" grants access to read attributes of MBeans.
|
||||
# For monitoring, this means that a remote client in this
|
||||
# role can read measurements but cannot perform any action
|
||||
# that changes the environment of the running program.
|
||||
# "readwrite" grants access to read and write attributes of MBeans,
|
||||
# to invoke operations on them, and optionally
|
||||
# to create or remove them. This access should be granted
|
||||
# only to trusted clients, since they can potentially
|
||||
# interfere with the smooth operation of a running program.
|
||||
#
|
||||
# The "readwrite" access level can optionally be followed by the "create" and/or
|
||||
# "unregister" keywords. The "unregister" keyword grants access to unregister
|
||||
# (delete) MBeans. The "create" keyword grants access to create MBeans of a
|
||||
# particular class or of any class matching a particular pattern. Access
|
||||
# should only be granted to create MBeans of known and trusted classes.
|
||||
#
|
||||
# For example, the following entry would grant readwrite access
|
||||
# to "controlRole", as well as access to create MBeans of the class
|
||||
# javax.management.monitor.CounterMonitor and to unregister any MBean:
|
||||
# controlRole readwrite \
|
||||
# create javax.management.monitor.CounterMonitorMBean \
|
||||
# unregister
|
||||
# or equivalently:
|
||||
# controlRole readwrite unregister create javax.management.monitor.CounterMBean
|
||||
#
|
||||
# The following entry would grant readwrite access as well as access to create
|
||||
# MBeans of any class in the packages javax.management.monitor and
|
||||
# javax.management.timer:
|
||||
# controlRole readwrite \
|
||||
# create javax.management.monitor.*,javax.management.timer.* \
|
||||
# unregister
|
||||
#
|
||||
# The \ character is defined in the Properties file syntax to allow continuation
|
||||
# lines as shown here. A * in a class pattern matches a sequence of characters
|
||||
# other than dot (.), so javax.management.monitor.* matches
|
||||
# javax.management.monitor.CounterMonitor but not
|
||||
# javax.management.monitor.foo.Bar.
|
||||
#
|
||||
# A given role should have at most one entry in this file. If a role
|
||||
# has no entry, it has no access.
|
||||
# If multiple entries are found for the same role name, then the last
|
||||
# access entry is used.
|
||||
#
|
||||
#
|
||||
# Default access control entries:
|
||||
# o The "monitorRole" role has readonly access.
|
||||
# o The "controlRole" role has readwrite access and can create the standard
|
||||
# Timer and Monitor MBeans defined by the JMX API.
|
||||
|
||||
monitorRole readonly
|
||||
controlRole readwrite \
|
||||
create javax.management.monitor.*,javax.management.timer.* \
|
||||
unregister
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
# ----------------------------------------------------------------------
|
||||
# Template for jmxremote.password
|
||||
#
|
||||
# o Copy this template to jmxremote.password
|
||||
# o Set the user/password entries in jmxremote.password
|
||||
# o Change the permission of jmxremote.password to be accessible
|
||||
# only by the owner.
|
||||
# o The jmxremote.passwords file will be re-written by the server
|
||||
# to replace all plain text passwords with hashed passwords when
|
||||
# the file is read by the server.
|
||||
#
|
||||
|
||||
##############################################################
|
||||
# Password File for Remote JMX Monitoring
|
||||
##############################################################
|
||||
#
|
||||
# Password file for Remote JMX API access to monitoring. This
|
||||
# file defines the different roles and their passwords. The access
|
||||
# control file (jmxremote.access by default) defines the allowed
|
||||
# access for each role. To be functional, a role must have an entry
|
||||
# in both the password and the access files.
|
||||
#
|
||||
# Default location of this file is $JRE/conf/management/jmxremote.password
|
||||
# You can specify an alternate location by specifying a property in
|
||||
# the management config file $JRE/conf/management/management.properties
|
||||
# or by specifying a system property (See that file for details).
|
||||
|
||||
##############################################################
|
||||
# File format of the jmxremote.password file
|
||||
##############################################################
|
||||
#
|
||||
# The file contains multiple lines where each line is blank,
|
||||
# a comment (like this one), or a password entry.
|
||||
#
|
||||
# password entry follows the below syntax
|
||||
# role_name W [clearPassword|hashedPassword]
|
||||
#
|
||||
# role_name is any string that does not itself contain spaces or tabs.
|
||||
# W = spaces or tabs
|
||||
#
|
||||
# Passwords can be specified via clear text or via a hash. Clear text password
|
||||
# is any string that does not contain spaces or tabs. Hashed passwords must
|
||||
# follow the below format.
|
||||
# hashedPassword = base64_encoded_64_byte_salt W base64_encoded_hash W hash_algorithm
|
||||
# where,
|
||||
# base64_encoded_64_byte_salt = 64 byte random salt
|
||||
# base64_encoded_hash = Hash_algorithm(password + salt)
|
||||
# W = spaces or tabs
|
||||
# hash_algorithm = Algorithm string specified using the format below
|
||||
# https://docs.oracle.com/javase/9/docs/specs/security/standard-names.html#messagedigest-algorithms
|
||||
# This is an optional field. If not specified, SHA3-512 will be assumed.
|
||||
#
|
||||
# If passwords are in clear, they will be overwritten by their hash if all of
|
||||
# the below criteria are met.
|
||||
# * com.sun.management.jmxremote.password.toHashes property is set to true in
|
||||
# management.properties file
|
||||
# * the password file is writable
|
||||
# * the system security policy allows writing into the password file, if a
|
||||
# security manager is configured
|
||||
#
|
||||
# In order to change the password for a role, replace the hashed password entry
|
||||
# with a new clear text password or a new hashed password. If the new password
|
||||
# is in clear, it will be replaced with its hash when a new login attempt is made.
|
||||
#
|
||||
# A given role should have at most one entry in this file. If a role
|
||||
# has no entry, it has no access.
|
||||
# If multiple entries are found for the same role name, then the last one
|
||||
# is used.
|
||||
#
|
||||
# A user generated hashed password file can also be used instead of clear-text
|
||||
# password file. If generated by the user, hashed passwords must follow the
|
||||
# format specified above.
|
||||
#
|
||||
# Caution: It is recommended not to edit the password file while the
|
||||
# agent is running, as edits could be lost if a client connection triggers the
|
||||
# hashing of the password file at the same time that the file is externally modified.
|
||||
# The integrity of the file is guaranteed, but any external edits made to the
|
||||
# file during the short period between the time that the agent reads the file
|
||||
# and the time that it writes it back might get lost
|
||||
|
||||
##############################################################
|
||||
# File permissions of the jmxremote.password file
|
||||
##############################################################
|
||||
# This file must be made accessible by ONLY the owner,
|
||||
# otherwise the program will exit with an error.
|
||||
#
|
||||
# In a typical installation, this file can be accessed by anybody on the
|
||||
# local machine, and possibly by people on other machines.
|
||||
# For security, you should either restrict the access to this file except for owner,
|
||||
# or specify another, less accessible file in the management config file
|
||||
# as described above.
|
||||
#
|
||||
# In order to prevent inadverent edits to the password file in the
|
||||
# production environment, it is recommended to deploy a read-only
|
||||
# hashed password file. The hashed entries for clear passwords can be generated
|
||||
# in advance by running the JMX agent.
|
||||
#
|
||||
|
||||
##############################################################
|
||||
# Sample of the jmxremote.password file
|
||||
##############################################################
|
||||
# Following are two commented-out entries. The "monitorRole" role has
|
||||
# password "QED". The "controlRole" role has password "R&D". This is an example
|
||||
# of specifying passwords in the clear
|
||||
#
|
||||
# monitorRole QED
|
||||
# controlRole R&D
|
||||
#
|
||||
# Once a login attempt is made, passwords will be hashed and the file will have
|
||||
# below entries with clear passwords overwritten by their respective
|
||||
# SHA3-512 hash
|
||||
#
|
||||
# monitorRole trilby APzBTt34rV2l+OMbuvbnOQ4si8UZmfRCVbIY1+fAofV5CkQzXS/FDMGteQQk/R3q1wtt104qImzJEA7gCwl6dw== 4EeTdSJ7X6Imu0Mb+dWqIns7a7QPIBoM3NB/XlpMQSPSicE7PnlALVWn2pBY3Q3pGDHyAb32Hd8GUToQbUhAjA== SHA3-512
|
||||
# controlRole roHEJSbRqSSTII4Z4+NOCV2OJaZVQ/dw153Fy2u4ILDP9XiZ426GwzCzc3RtpoqNMwqYIcfdd74xWXSMrWtGaA== w9qDsekgKn0WOVJycDyU0kLBa081zbStcCjUAVEqlfon5Sgx7XHtaodbmzpLegA1jT7Ag36T0zHaEWRHJe2fdA== SHA3-512
|
||||
#
|
||||
-326
@@ -1,326 +0,0 @@
|
||||
#####################################################################
|
||||
# Default Configuration File for Java Platform Management
|
||||
#####################################################################
|
||||
#
|
||||
# The Management Configuration file (in java.util.Properties format)
|
||||
# will be read if one of the following system properties is set:
|
||||
# -Dcom.sun.management.jmxremote.port=<port-number>
|
||||
# or -Dcom.sun.management.config.file=<this-file>
|
||||
#
|
||||
# The default Management Configuration file is:
|
||||
#
|
||||
# $JRE/conf/management/management.properties
|
||||
#
|
||||
# Another location for the Management Configuration File can be specified
|
||||
# by the following property on the Java command line:
|
||||
#
|
||||
# -Dcom.sun.management.config.file=<this-file>
|
||||
#
|
||||
# If -Dcom.sun.management.config.file=<this-file> is set, the port
|
||||
# number for the management agent can be specified in the config file
|
||||
# using the following lines:
|
||||
#
|
||||
# ################ Management Agent Port #########################
|
||||
#
|
||||
# For setting the JMX RMI agent port use the following line
|
||||
# com.sun.management.jmxremote.port=<port-number>
|
||||
#
|
||||
# For setting the JMX local server port use the following line
|
||||
# com.sun.management.jmxremote.local.port=<port-number>
|
||||
|
||||
#####################################################################
|
||||
# Optional Instrumentation
|
||||
#####################################################################
|
||||
#
|
||||
# By default only the basic instrumentation with low overhead is on.
|
||||
# The following properties allow to selectively turn on optional
|
||||
# instrumentation which are off by default and may have some
|
||||
# additional overhead.
|
||||
#
|
||||
# com.sun.management.enableThreadContentionMonitoring
|
||||
#
|
||||
# This option enables thread contention monitoring if the
|
||||
# Java virtual machine supports such instrumentation.
|
||||
# Refer to the specification for the java.lang.management.ThreadMXBean
|
||||
# interface - see isThreadContentionMonitoringSupported() method.
|
||||
#
|
||||
|
||||
# To enable thread contention monitoring, uncomment the following line
|
||||
# com.sun.management.enableThreadContentionMonitoring
|
||||
|
||||
#####################################################################
|
||||
# RMI Management Properties
|
||||
#####################################################################
|
||||
#
|
||||
# If system property -Dcom.sun.management.jmxremote.port=<port-number>
|
||||
# is set then
|
||||
# - A MBean server is started
|
||||
# - JRE Platform MBeans are registered in the MBean server
|
||||
# - RMI connector is published in a private readonly registry at
|
||||
# specified port using a well known name, "jmxrmi"
|
||||
# - the following properties are read for JMX remote management.
|
||||
#
|
||||
# The configuration can be specified only at startup time.
|
||||
# Later changes to above system property (e.g. via setProperty method),
|
||||
# this config file, the password file, or the access file have no effect to the
|
||||
# running MBean server, the connector, or the registry.
|
||||
#
|
||||
|
||||
#
|
||||
# ########## RMI connector settings for local management ##########
|
||||
#
|
||||
# com.sun.management.jmxremote.local.only=true|false
|
||||
# Default for this property is true. (Case for true/false ignored)
|
||||
# If this property is specified as true then the local JMX RMI connector
|
||||
# server will only accept connection requests from clients running on
|
||||
# the host where the out-of-the-box JMX management agent is running.
|
||||
# In order to ensure backwards compatibility this property could be
|
||||
# set to false. However, deploying the local management agent in this
|
||||
# way is discouraged because the local JMX RMI connector server will
|
||||
# accept connection requests from any client either local or remote.
|
||||
# For remote management the remote JMX RMI connector server should
|
||||
# be used instead with authentication and SSL/TLS encryption enabled.
|
||||
#
|
||||
|
||||
# For allowing the local management agent accept local
|
||||
# and remote connection requests use the following line
|
||||
# com.sun.management.jmxremote.local.only=false
|
||||
|
||||
#
|
||||
# ###################### RMI SSL #############################
|
||||
#
|
||||
# com.sun.management.jmxremote.ssl=true|false
|
||||
# Default for this property is true. (Case for true/false ignored)
|
||||
# If this property is specified as false then SSL is not used.
|
||||
#
|
||||
|
||||
# For RMI monitoring without SSL use the following line
|
||||
# com.sun.management.jmxremote.ssl=false
|
||||
|
||||
# com.sun.management.jmxremote.ssl.config.file=filepath
|
||||
# Specifies the location of the SSL configuration file. A properties
|
||||
# file can be used to supply the keystore and truststore location and
|
||||
# password settings thus avoiding to pass them as cleartext in the
|
||||
# command-line.
|
||||
#
|
||||
# The current implementation of the out-of-the-box management agent will
|
||||
# look up and use the properties specified below to configure the SSL
|
||||
# keystore and truststore, if present:
|
||||
# javax.net.ssl.keyStore=<keystore-location>
|
||||
# javax.net.ssl.keyStorePassword=<keystore-password>
|
||||
# javax.net.ssl.trustStore=<truststore-location>
|
||||
# javax.net.ssl.trustStorePassword=<truststore-password>
|
||||
# Any other properties in the file will be ignored. This will allow us
|
||||
# to extend the property set in the future if required by the default
|
||||
# SSL implementation.
|
||||
#
|
||||
# If the property "com.sun.management.jmxremote.ssl" is set to false,
|
||||
# then this property is ignored.
|
||||
#
|
||||
|
||||
# For supplying the keystore settings in a file use the following line
|
||||
# com.sun.management.jmxremote.ssl.config.file=filepath
|
||||
|
||||
# com.sun.management.jmxremote.ssl.enabled.cipher.suites=<cipher-suites>
|
||||
# The value of this property is a string that is a comma-separated list
|
||||
# of SSL/TLS cipher suites to enable. This property can be specified in
|
||||
# conjunction with the previous property "com.sun.management.jmxremote.ssl"
|
||||
# in order to control which particular SSL/TLS cipher suites are enabled
|
||||
# for use by accepted connections. If this property is not specified then
|
||||
# the SSL/TLS RMI Server Socket Factory uses the SSL/TLS cipher suites that
|
||||
# are enabled by default.
|
||||
#
|
||||
|
||||
# com.sun.management.jmxremote.ssl.enabled.protocols=<protocol-versions>
|
||||
# The value of this property is a string that is a comma-separated list
|
||||
# of SSL/TLS protocol versions to enable. This property can be specified in
|
||||
# conjunction with the previous property "com.sun.management.jmxremote.ssl"
|
||||
# in order to control which particular SSL/TLS protocol versions are
|
||||
# enabled for use by accepted connections. If this property is not
|
||||
# specified then the SSL/TLS RMI Server Socket Factory uses the SSL/TLS
|
||||
# protocol versions that are enabled by default.
|
||||
#
|
||||
|
||||
# com.sun.management.jmxremote.ssl.need.client.auth=true|false
|
||||
# Default for this property is false. (Case for true/false ignored)
|
||||
# If this property is specified as true in conjunction with the previous
|
||||
# property "com.sun.management.jmxremote.ssl" then the SSL/TLS RMI Server
|
||||
# Socket Factory will require client authentication.
|
||||
#
|
||||
|
||||
# For RMI monitoring with SSL client authentication use the following line
|
||||
# com.sun.management.jmxremote.ssl.need.client.auth=true
|
||||
|
||||
# com.sun.management.jmxremote.registry.ssl=true|false
|
||||
# Default for this property is false. (Case for true/false ignored)
|
||||
# If this property is specified as true then the RMI registry used
|
||||
# to bind the RMIServer remote object is protected with SSL/TLS
|
||||
# RMI Socket Factories that can be configured with the properties:
|
||||
# com.sun.management.jmxremote.ssl.config.file
|
||||
# com.sun.management.jmxremote.ssl.enabled.cipher.suites
|
||||
# com.sun.management.jmxremote.ssl.enabled.protocols
|
||||
# com.sun.management.jmxremote.ssl.need.client.auth
|
||||
# If the two properties below are true at the same time, i.e.
|
||||
# com.sun.management.jmxremote.ssl=true
|
||||
# com.sun.management.jmxremote.registry.ssl=true
|
||||
# then the RMIServer remote object and the RMI registry are
|
||||
# both exported with the same SSL/TLS RMI Socket Factories.
|
||||
#
|
||||
|
||||
# For using an SSL/TLS protected RMI registry use the following line
|
||||
# com.sun.management.jmxremote.registry.ssl=true
|
||||
|
||||
#
|
||||
# ################ RMI User authentication ################
|
||||
#
|
||||
# com.sun.management.jmxremote.authenticate=true|false
|
||||
# Default for this property is true. (Case for true/false ignored)
|
||||
# If this property is specified as false then no authentication is
|
||||
# performed and all users are allowed all access.
|
||||
#
|
||||
|
||||
# For RMI monitoring without any checking use the following line
|
||||
# com.sun.management.jmxremote.authenticate=false
|
||||
|
||||
#
|
||||
# ################ RMI Login configuration ###################
|
||||
#
|
||||
# com.sun.management.jmxremote.login.config=<config-name>
|
||||
# Specifies the name of a JAAS login configuration entry to use when
|
||||
# authenticating users of RMI monitoring.
|
||||
#
|
||||
# Setting this property is optional - the default login configuration
|
||||
# specifies a file-based authentication that uses the password file.
|
||||
#
|
||||
# When using this property to override the default login configuration
|
||||
# then the named configuration entry must be in a file that gets loaded
|
||||
# by JAAS. In addition, the login module(s) specified in the configuration
|
||||
# should use the name and/or password callbacks to acquire the user's
|
||||
# credentials. See the NameCallback and PasswordCallback classes in the
|
||||
# javax.security.auth.callback package for more details.
|
||||
#
|
||||
# If the property "com.sun.management.jmxremote.authenticate" is set to
|
||||
# false, then this property and the password & access files are ignored.
|
||||
#
|
||||
|
||||
# For a non-default login configuration use the following line
|
||||
# com.sun.management.jmxremote.login.config=<config-name>
|
||||
|
||||
#
|
||||
# ################ RMI Password file location ##################
|
||||
#
|
||||
# com.sun.management.jmxremote.password.file=filepath
|
||||
# Specifies location for password file
|
||||
# This is optional - default location is
|
||||
# $JRE/conf/management/jmxremote.password
|
||||
#
|
||||
# If the property "com.sun.management.jmxremote.authenticate" is set to
|
||||
# false, then this property and the password & access files are ignored.
|
||||
# Otherwise the password file must exist and be in the valid format.
|
||||
# If the password file is empty or non-existent then no access is allowed.
|
||||
#
|
||||
|
||||
# For a non-default password file location use the following line
|
||||
# com.sun.management.jmxremote.password.file=filepath
|
||||
|
||||
#
|
||||
# ################# Hash passwords in password file ##############
|
||||
# com.sun.management.jmxremote.password.toHashes = true|false
|
||||
# Default for this property is true.
|
||||
# Specifies if passwords in the password file should be hashed or not.
|
||||
# If this property is true, and if the password file is writable, and if the
|
||||
# system security policy allows writing into the password file,
|
||||
# all the clear passwords in the password file will be replaced by
|
||||
# their SHA3-512 hash when the file is read by the server
|
||||
#
|
||||
|
||||
#
|
||||
# ################ RMI Access file location #####################
|
||||
#
|
||||
# com.sun.management.jmxremote.access.file=filepath
|
||||
# Specifies location for access file
|
||||
# This is optional - default location is
|
||||
# $JRE/conf/management/jmxremote.access
|
||||
#
|
||||
# If the property "com.sun.management.jmxremote.authenticate" is set to
|
||||
# false, then this property and the password & access files are ignored.
|
||||
# Otherwise, the access file must exist and be in the valid format.
|
||||
# If the access file is empty or non-existent then no access is allowed.
|
||||
#
|
||||
|
||||
# For a non-default password file location use the following line
|
||||
# com.sun.management.jmxremote.access.file=filepath
|
||||
#
|
||||
|
||||
# ################ Management agent listen interface #########################
|
||||
#
|
||||
# com.sun.management.jmxremote.host=<host-name-or-address>
|
||||
# The local host name or IP address on which the JMX RMI agent will bind.
|
||||
# This is useful when running on machines which have several
|
||||
# interfaces defined. It makes it possible to listen to a specific
|
||||
# subnet accessible through that interface.
|
||||
#
|
||||
# The format of the value for that property is any string accepted
|
||||
# by java.net.InetAddress.getByName(String).
|
||||
#
|
||||
|
||||
# ################ Filter for ObjectInputStream #############################
|
||||
# com.sun.management.jmxremote.serial.filter.pattern=<filter-string>
|
||||
# A filter, if configured, is used by java.io.ObjectInputStream during
|
||||
# deserialization of parameters sent to the JMX default agent to validate the
|
||||
# contents of the stream.
|
||||
# A filter is configured as a sequence of patterns, each pattern is either
|
||||
# matched against the name of a class in the stream or defines a limit.
|
||||
# Patterns are separated by ";" (semicolon).
|
||||
# Whitespace is significant and is considered part of the pattern.
|
||||
#
|
||||
# If a pattern includes a "=", it sets a limit.
|
||||
# If a limit appears more than once the last value is used.
|
||||
# Limits are checked before classes regardless of the order in the sequence of patterns.
|
||||
# If any of the limits are exceeded, the filter status is REJECTED.
|
||||
#
|
||||
# maxdepth=value - the maximum depth of a graph
|
||||
# maxrefs=value - the maximum number of internal references
|
||||
# maxbytes=value - the maximum number of bytes in the input stream
|
||||
# maxarray=value - the maximum array length allowed
|
||||
#
|
||||
# Other patterns, from left to right, match the class or package name as
|
||||
# returned from Class.getName.
|
||||
# If the class is an array type, the class or package to be matched is the element type.
|
||||
# Arrays of any number of dimensions are treated the same as the element type.
|
||||
# For example, a pattern of "!example.Foo", rejects creation of any instance or
|
||||
# array of example.Foo.
|
||||
#
|
||||
# If the pattern starts with "!", the status is REJECTED if the remaining pattern
|
||||
# is matched; otherwise the status is ALLOWED if the pattern matches.
|
||||
# If the pattern contains "/", the non-empty prefix up to the "/" is the module name;
|
||||
# if the module name matches the module name of the class then
|
||||
# the remaining pattern is matched with the class name.
|
||||
# If there is no "/", the module name is not compared.
|
||||
# If the pattern ends with ".**" it matches any class in the package and all subpackages.
|
||||
# If the pattern ends with ".*" it matches any class in the package.
|
||||
# If the pattern ends with "*", it matches any class with the pattern as a prefix.
|
||||
# If the pattern is equal to the class name, it matches.
|
||||
# Otherwise, the status is UNDECIDED.
|
||||
#
|
||||
# Ending with !* ensures we reject classes which are otherwise unmatched.
|
||||
com.sun.management.jmxremote.serial.filter.pattern=\
|
||||
java.lang.*;\
|
||||
java.lang.reflect.Proxy;\
|
||||
java.math.BigInteger;\
|
||||
java.math.BigDecimal;\
|
||||
java.util.*;\
|
||||
javax.management.*;\
|
||||
javax.management.modelmbean.*;\
|
||||
javax.management.monitor.*;\
|
||||
javax.management.openmbean.*;\
|
||||
javax.management.relation.*;\
|
||||
javax.management.remote.*;\
|
||||
javax.management.remote.rmi.*;\
|
||||
javax.management.timer.*;\
|
||||
javax.rmi.ssl.*;\
|
||||
java.rmi.MarshalledObject;\
|
||||
java.rmi.dgc.*;\
|
||||
java.rmi.server.*;\
|
||||
javax.security.auth.Subject;\
|
||||
!*
|
||||
-162
@@ -1,162 +0,0 @@
|
||||
############################################################
|
||||
# Default Networking Configuration File
|
||||
#
|
||||
# This file may contain default values for the networking system properties.
|
||||
# These values are only used when the system properties are not specified
|
||||
# on the command line or set programmatically.
|
||||
# For now, only the various proxy settings can be configured here.
|
||||
############################################################
|
||||
|
||||
# Whether or not the DefaultProxySelector will default to System Proxy
|
||||
# settings when they do exist.
|
||||
# Set it to 'true' to enable this feature and check for platform
|
||||
# specific proxy settings
|
||||
# Note that the system properties that do explicitly set proxies
|
||||
# (like http.proxyHost) do take precedence over the system settings
|
||||
# even if java.net.useSystemProxies is set to true.
|
||||
|
||||
java.net.useSystemProxies=false
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
# Proxy configuration for the various protocol handlers.
|
||||
# DO NOT uncomment these lines if you have set java.net.useSystemProxies
|
||||
# to true as the protocol specific properties will take precedence over
|
||||
# system settings.
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
# HTTP Proxy settings. proxyHost is the name of the proxy server
|
||||
# (e.g. proxy.mydomain.com), proxyPort is the port number to use (default
|
||||
# value is 80) and nonProxyHosts is a '|' separated list of hostnames which
|
||||
# should be accessed directly, ignoring the proxy server (default value is
|
||||
# localhost & 127.0.0.1).
|
||||
#
|
||||
# http.proxyHost=
|
||||
# http.proxyPort=80
|
||||
http.nonProxyHosts=localhost|127.*|[::1]
|
||||
#
|
||||
# HTTPS Proxy Settings. proxyHost is the name of the proxy server
|
||||
# (e.g. proxy.mydomain.com), proxyPort is the port number to use (default
|
||||
# value is 443). The HTTPS protocol handlers uses the http nonProxyHosts list.
|
||||
#
|
||||
# https.proxyHost=
|
||||
# https.proxyPort=443
|
||||
#
|
||||
# FTP Proxy settings. proxyHost is the name of the proxy server
|
||||
# (e.g. proxy.mydomain.com), proxyPort is the port number to use (default
|
||||
# value is 80) and nonProxyHosts is a '|' separated list of hostnames which
|
||||
# should be accessed directly, ignoring the proxy server (default value is
|
||||
# localhost & 127.0.0.1).
|
||||
#
|
||||
# ftp.proxyHost=
|
||||
# ftp.proxyPort=80
|
||||
ftp.nonProxyHosts=localhost|127.*|[::1]
|
||||
#
|
||||
# Socks proxy settings. socksProxyHost is the name of the proxy server
|
||||
# (e.g. socks.domain.com), socksProxyPort is the port number to use
|
||||
# (default value is 1080)
|
||||
#
|
||||
# socksProxyHost=
|
||||
# socksProxyPort=1080
|
||||
#
|
||||
# HTTP Keep Alive settings. remainingData is the maximum amount of data
|
||||
# in kilobytes that will be cleaned off the underlying socket so that it
|
||||
# can be reused (default value is 512K), queuedConnections is the maximum
|
||||
# number of Keep Alive connections to be on the queue for clean up (default
|
||||
# value is 10).
|
||||
# http.KeepAlive.remainingData=512
|
||||
# http.KeepAlive.queuedConnections=10
|
||||
|
||||
# Authentication Scheme restrictions for HTTP and HTTPS.
|
||||
#
|
||||
# In some environments certain authentication schemes may be undesirable
|
||||
# when proxying HTTP or HTTPS. For example, "Basic" results in effectively the
|
||||
# cleartext transmission of the user's password over the physical network.
|
||||
# This section describes the mechanism for disabling authentication schemes
|
||||
# based on the scheme name. Disabled schemes will be treated as if they are not
|
||||
# supported by the implementation.
|
||||
#
|
||||
# The 'jdk.http.auth.tunneling.disabledSchemes' property lists the authentication
|
||||
# schemes that will be disabled when tunneling HTTPS over a proxy, HTTP CONNECT.
|
||||
# The 'jdk.http.auth.proxying.disabledSchemes' property lists the authentication
|
||||
# schemes that will be disabled when proxying HTTP.
|
||||
#
|
||||
# In both cases the property is a comma-separated list of, case-insensitive,
|
||||
# authentication scheme names, as defined by their relevant RFCs. An
|
||||
# implementation may, but is not required to, support common schemes whose names
|
||||
# include: 'Basic', 'Digest', 'NTLM', 'Kerberos', 'Negotiate'. A scheme that
|
||||
# is not known, or not supported, by the implementation is ignored.
|
||||
#
|
||||
# Note: This property is currently used by the JDK Reference implementation. It
|
||||
# is not guaranteed to be examined and used by other implementations.
|
||||
#
|
||||
#jdk.http.auth.proxying.disabledSchemes=
|
||||
jdk.http.auth.tunneling.disabledSchemes=Basic
|
||||
|
||||
#
|
||||
# Allow restricted HTTP request headers
|
||||
#
|
||||
# By default, the following request headers are not allowed to be set by user code
|
||||
# in HttpRequests: "connection", "content-length", "expect", "host" and "upgrade".
|
||||
# The 'jdk.httpclient.allowRestrictedHeaders' property allows one or more of these
|
||||
# headers to be specified as a comma separated list to override the default restriction.
|
||||
# The names are case-insensitive and white-space is ignored (removed before processing
|
||||
# the list). Note, this capability is mostly intended for testing and isn't expected
|
||||
# to be used in real deployments. Protocol errors or other undefined behavior is likely
|
||||
# to occur when using them. The property is not set by default.
|
||||
# Note also, that there may be other headers that are restricted from being set
|
||||
# depending on the context. These restrictions cannot be overridden by this property.
|
||||
#
|
||||
# jdk.httpclient.allowRestrictedHeaders=host
|
||||
#
|
||||
#
|
||||
# Transparent NTLM HTTP authentication mode on Windows. Transparent authentication
|
||||
# can be used for the NTLM scheme, where the security credentials based on the
|
||||
# currently logged in user's name and password can be obtained directly from the
|
||||
# operating system, without prompting the user. This property has three possible
|
||||
# values which regulate the behavior as shown below. Other unrecognized values
|
||||
# are handled the same as 'disabled'. Note, that NTLM is not considered to be a
|
||||
# strongly secure authentication scheme and care should be taken before enabling
|
||||
# this mechanism.
|
||||
#
|
||||
# Transparent authentication never used.
|
||||
#jdk.http.ntlm.transparentAuth=disabled
|
||||
#
|
||||
# Enabled for all hosts.
|
||||
#jdk.http.ntlm.transparentAuth=allHosts
|
||||
#
|
||||
# Enabled for hosts that are trusted in Windows Internet settings
|
||||
#jdk.http.ntlm.transparentAuth=trustedHosts
|
||||
#
|
||||
jdk.http.ntlm.transparentAuth=disabled
|
||||
|
||||
#
|
||||
# Maximum HTTP field section size that a client is prepared to accept
|
||||
#
|
||||
# jdk.http.maxHeaderSize=393216
|
||||
#
|
||||
# This is the maximum header field section size that a client is prepared to accept.
|
||||
# This is computed as the sum of the size of the uncompressed header name, plus
|
||||
# the size of the uncompressed header value, plus an overhead of 32 bytes for
|
||||
# each field section line. If a peer sends a field section that exceeds this
|
||||
# size a {@link java.net.ProtocolException ProtocolException} will be raised.
|
||||
# This applies to all versions of the HTTP protocol. A value of zero or a negative
|
||||
# value means no limit. If left unspecified, the default value is 393216 bytes
|
||||
# or 384kB.
|
||||
#
|
||||
# Note: This property is currently used by the JDK Reference implementation. It
|
||||
# is not guaranteed to be examined and used by other implementations.
|
||||
#
|
||||
# Default directory where automatically bound Unix domain server
|
||||
# sockets are stored. Sockets are automatically bound when bound
|
||||
# with a null address.
|
||||
#
|
||||
# On Unix the search order to determine this directory is:
|
||||
#
|
||||
# 1. System property jdk.net.unixdomain.tmpdir
|
||||
#
|
||||
# 2. Networking property jdk.net.unixdomain.tmpdir specified
|
||||
# in this file (effective default)
|
||||
#
|
||||
# 3. System property java.io.tmpdir
|
||||
#
|
||||
jdk.net.unixdomain.tmpdir=/tmp
|
||||
-1566
File diff suppressed because it is too large.
Load diff
-54
@@ -1,54 +0,0 @@
|
||||
|
||||
Java(TM) Cryptography Extension Policy Files
|
||||
for the Java(TM) Platform, Standard Edition Runtime Environment
|
||||
|
||||
README
|
||||
------------------------------------------------------------------------
|
||||
|
||||
Import and export control rules on cryptographic software vary from
|
||||
country to country. The Java Cryptography Extension (JCE) architecture
|
||||
allows flexible cryptographic key strength to be configured via the
|
||||
jurisdiction policy files which are referenced by the "crypto.policy"
|
||||
security property in the <java-home>/conf/security/java.security file.
|
||||
|
||||
By default, Java provides two different sets of cryptographic policy
|
||||
files:
|
||||
|
||||
unlimited: These policy files contain no restrictions on cryptographic
|
||||
strengths or algorithms
|
||||
|
||||
limited: These policy files contain more restricted cryptographic
|
||||
strengths
|
||||
|
||||
These files reside in <java-home>/conf/security/policy in the "unlimited"
|
||||
or "limited" subdirectories respectively.
|
||||
|
||||
Each subdirectory contains a complete policy configuration,
|
||||
and subdirectories can be added/edited/removed to reflect your
|
||||
import or export control product requirements.
|
||||
|
||||
Within a subdirectory, the effective policy is the combined minimum
|
||||
permissions of the grant statements in the file(s) matching the filename
|
||||
pattern "default_*.policy". At least one grant is required. For example:
|
||||
|
||||
limited = Export (all) + Import (limited) = Limited
|
||||
unlimited = Export (all) + Import (all) = Unlimited
|
||||
|
||||
The effective exemption policy is the combined minimum permissions
|
||||
of the grant statements in the file(s) matching the filename pattern
|
||||
"exempt_*.policy". Exemption grants are optional. For example:
|
||||
|
||||
limited = grants exemption permissions, by which the
|
||||
effective policy can be circumvented.
|
||||
e.g. KeyRecovery/KeyEscrow/KeyWeakening.
|
||||
|
||||
Please see the Java Cryptography Architecture (JCA) documentation for
|
||||
additional information on these files and formats.
|
||||
|
||||
YOU ARE ADVISED TO CONSULT YOUR EXPORT/IMPORT CONTROL COUNSEL OR ATTORNEY
|
||||
TO DETERMINE THE EXACT REQUIREMENTS.
|
||||
|
||||
Please note that the JCE for Java SE, including the JCE framework,
|
||||
cryptographic policy files, and standard JCE providers provided with
|
||||
the Java SE, have been reviewed and approved for export as mass market
|
||||
encryption item by the US Bureau of Industry and Security.
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
// Default US Export policy file.
|
||||
|
||||
grant {
|
||||
// There is no restriction to any algorithms.
|
||||
permission javax.crypto.CryptoAllPermission;
|
||||
};
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
// Some countries have import limits on crypto strength. This policy file
|
||||
// is worldwide importable.
|
||||
|
||||
grant {
|
||||
permission javax.crypto.CryptoPermission "DES", 64;
|
||||
permission javax.crypto.CryptoPermission "DESede", *;
|
||||
permission javax.crypto.CryptoPermission "RC2", 128,
|
||||
"javax.crypto.spec.RC2ParameterSpec", 128;
|
||||
permission javax.crypto.CryptoPermission "RC4", 128;
|
||||
permission javax.crypto.CryptoPermission "RC5", 128,
|
||||
"javax.crypto.spec.RC5ParameterSpec", *, 12, *;
|
||||
permission javax.crypto.CryptoPermission "RSA", *;
|
||||
permission javax.crypto.CryptoPermission *, 128;
|
||||
};
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
// Some countries have import limits on crypto strength, but may allow for
|
||||
// these exemptions if the exemption mechanism is used.
|
||||
|
||||
grant {
|
||||
// There is no restriction to any algorithms if KeyRecovery is enforced.
|
||||
permission javax.crypto.CryptoPermission *, "KeyRecovery";
|
||||
|
||||
// There is no restriction to any algorithms if KeyEscrow is enforced.
|
||||
permission javax.crypto.CryptoPermission *, "KeyEscrow";
|
||||
|
||||
// There is no restriction to any algorithms if KeyWeakening is enforced.
|
||||
permission javax.crypto.CryptoPermission *, "KeyWeakening";
|
||||
};
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
// Default US Export policy file.
|
||||
|
||||
grant {
|
||||
// There is no restriction to any algorithms.
|
||||
permission javax.crypto.CryptoAllPermission;
|
||||
};
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
// Country-specific policy file for countries with no limits on crypto strength.
|
||||
|
||||
grant {
|
||||
// There is no restriction to any algorithms.
|
||||
permission javax.crypto.CryptoAllPermission;
|
||||
};
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
############################################################
|
||||
# Sound Configuration File
|
||||
############################################################
|
||||
#
|
||||
# This properties file is used to specify default service
|
||||
# providers for javax.sound.midi.MidiSystem and
|
||||
# javax.sound.sampled.AudioSystem.
|
||||
#
|
||||
# The following keys are recognized by MidiSystem methods:
|
||||
#
|
||||
# javax.sound.midi.Receiver
|
||||
# javax.sound.midi.Sequencer
|
||||
# javax.sound.midi.Synthesizer
|
||||
# javax.sound.midi.Transmitter
|
||||
#
|
||||
# The following keys are recognized by AudioSystem methods:
|
||||
#
|
||||
# javax.sound.sampled.Clip
|
||||
# javax.sound.sampled.Port
|
||||
# javax.sound.sampled.SourceDataLine
|
||||
# javax.sound.sampled.TargetDataLine
|
||||
#
|
||||
# The values specify the full class name of the service
|
||||
# provider, or the device name.
|
||||
#
|
||||
# See the class descriptions for details.
|
||||
#
|
||||
# Example 1:
|
||||
# Use MyDeviceProvider as default for SourceDataLines:
|
||||
# javax.sound.sampled.SourceDataLine=com.xyz.MyDeviceProvider
|
||||
#
|
||||
# Example 2:
|
||||
# Specify the default Synthesizer by its name "InternalSynth".
|
||||
# javax.sound.midi.Synthesizer=#InternalSynth
|
||||
#
|
||||
# Example 3:
|
||||
# Specify the default Receiver by provider and name:
|
||||
# javax.sound.midi.Receiver=com.sun.media.sound.MidiProvider#SunMIDI1
|
||||
#
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
ADDITIONAL INFORMATION ABOUT LICENSING
|
||||
|
||||
Certain files distributed by Oracle America, Inc. and/or its affiliates are
|
||||
subject to the following clarification and special exception to the GPLv2,
|
||||
based on the GNU Project exception for its Classpath libraries, known as the
|
||||
GNU Classpath Exception.
|
||||
|
||||
Note that Oracle includes multiple, independent programs in this software
|
||||
package. Some of those programs are provided under licenses deemed
|
||||
incompatible with the GPLv2 by the Free Software Foundation and others.
|
||||
For example, the package includes programs licensed under the Apache
|
||||
License, Version 2.0 and may include FreeType. Such programs are licensed
|
||||
to you under their original licenses.
|
||||
|
||||
Oracle facilitates your further distribution of this package by adding the
|
||||
Classpath Exception to the necessary parts of its GPLv2 code, which permits
|
||||
you to use that code in combination with other independent modules not
|
||||
licensed under the GPLv2. However, note that this would not permit you to
|
||||
commingle code under an incompatible license with Oracle's GPLv2 licensed
|
||||
code by, for example, cutting and pasting such code into a file also
|
||||
containing Oracle's GPLv2 licensed code and then distributing the result.
|
||||
|
||||
Additionally, if you were to remove the Classpath Exception from any of the
|
||||
files to which it applies and distribute the result, you would likely be
|
||||
required to license some or all of the other code in that distribution under
|
||||
the GPLv2 as well, and since the GPLv2 is incompatible with the license terms
|
||||
of some items included in the distribution by Oracle, removing the Classpath
|
||||
Exception could therefore effectively compromise your ability to further
|
||||
distribute the package.
|
||||
|
||||
Failing to distribute notices associated with some files may also create
|
||||
unexpected legal consequences.
|
||||
|
||||
Proceed with caution and we recommend that you obtain the advice of a lawyer
|
||||
skilled in open source matters before removing the Classpath Exception or
|
||||
making modifications to this package which may subsequently be redistributed
|
||||
and/or involve the use of third party software.
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
|
||||
OPENJDK ASSEMBLY EXCEPTION
|
||||
|
||||
The OpenJDK source code made available by Oracle America, Inc. (Oracle) at
|
||||
openjdk.org ("OpenJDK Code") is distributed under the terms of the GNU
|
||||
General Public License <https://www.gnu.org/copyleft/gpl.html> version 2
|
||||
only ("GPL2"), with the following clarification and special exception.
|
||||
|
||||
Linking this OpenJDK Code statically or dynamically with other code
|
||||
is making a combined work based on this library. Thus, the terms
|
||||
and conditions of GPL2 cover the whole combination.
|
||||
|
||||
As a special exception, Oracle gives you permission to link this
|
||||
OpenJDK Code with certain code licensed by Oracle as indicated at
|
||||
https://openjdk.org/legal/exception-modules-2007-05-08.html
|
||||
("Designated Exception Modules") to produce an executable,
|
||||
regardless of the license terms of the Designated Exception Modules,
|
||||
and to copy and distribute the resulting executable under GPL2,
|
||||
provided that the Designated Exception Modules continue to be
|
||||
governed by the licenses under which they were offered by Oracle.
|
||||
|
||||
As such, it allows licensees and sublicensees of Oracle's GPL2 OpenJDK Code
|
||||
to build an executable that includes those portions of necessary code that
|
||||
Oracle could not provide under GPL2 (or that Oracle has provided under GPL2
|
||||
with the Classpath exception). If you modify or add to the OpenJDK code,
|
||||
that new GPL2 code may still be combined with Designated Exception Modules
|
||||
if the new code is made subject to this exception by its copyright holder.
|
||||
-347
@@ -1,347 +0,0 @@
|
||||
The GNU General Public License (GPL)
|
||||
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this license
|
||||
document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your freedom to share
|
||||
and change it. By contrast, the GNU General Public License is intended to
|
||||
guarantee your freedom to share and change free software--to make sure the
|
||||
software is free for all its users. This General Public License applies to
|
||||
most of the Free Software Foundation's software and to any other program whose
|
||||
authors commit to using it. (Some other Free Software Foundation software is
|
||||
covered by the GNU Library General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not price. Our
|
||||
General Public Licenses are designed to make sure that you have the freedom to
|
||||
distribute copies of free software (and charge for this service if you wish),
|
||||
that you receive source code or can get it if you want it, that you can change
|
||||
the software or use pieces of it in new free programs; and that you know you
|
||||
can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid anyone to deny
|
||||
you these rights or to ask you to surrender the rights. These restrictions
|
||||
translate to certain responsibilities for you if you distribute copies of the
|
||||
software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether gratis or for
|
||||
a fee, you must give the recipients all the rights that you have. You must
|
||||
make sure that they, too, receive or can get the source code. And you must
|
||||
show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and (2)
|
||||
offer you this license which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain that
|
||||
everyone understands that there is no warranty for this free software. If the
|
||||
software is modified by someone else and passed on, we want its recipients to
|
||||
know that what they have is not the original, so that any problems introduced
|
||||
by others will not reflect on the original authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software patents. We
|
||||
wish to avoid the danger that redistributors of a free program will
|
||||
individually obtain patent licenses, in effect making the program proprietary.
|
||||
To prevent this, we have made it clear that any patent must be licensed for
|
||||
everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and modification
|
||||
follow.
|
||||
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains a notice
|
||||
placed by the copyright holder saying it may be distributed under the terms of
|
||||
this General Public License. The "Program", below, refers to any such program
|
||||
or work, and a "work based on the Program" means either the Program or any
|
||||
derivative work under copyright law: that is to say, a work containing the
|
||||
Program or a portion of it, either verbatim or with modifications and/or
|
||||
translated into another language. (Hereinafter, translation is included
|
||||
without limitation in the term "modification".) Each licensee is addressed as
|
||||
"you".
|
||||
|
||||
Activities other than copying, distribution and modification are not covered by
|
||||
this License; they are outside its scope. The act of running the Program is
|
||||
not restricted, and the output from the Program is covered only if its contents
|
||||
constitute a work based on the Program (independent of having been made by
|
||||
running the Program). Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's source code as
|
||||
you receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice and
|
||||
disclaimer of warranty; keep intact all the notices that refer to this License
|
||||
and to the absence of any warranty; and give any other recipients of the
|
||||
Program a copy of this License along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and you may
|
||||
at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion of it, thus
|
||||
forming a work based on the Program, and copy and distribute such modifications
|
||||
or work under the terms of Section 1 above, provided that you also meet all of
|
||||
these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices stating
|
||||
that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in whole or
|
||||
in part contains or is derived from the Program or any part thereof, to be
|
||||
licensed as a whole at no charge to all third parties under the terms of
|
||||
this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively when run,
|
||||
you must cause it, when started running for such interactive use in the
|
||||
most ordinary way, to print or display an announcement including an
|
||||
appropriate copyright notice and a notice that there is no warranty (or
|
||||
else, saying that you provide a warranty) and that users may redistribute
|
||||
the program under these conditions, and telling the user how to view a copy
|
||||
of this License. (Exception: if the Program itself is interactive but does
|
||||
not normally print such an announcement, your work based on the Program is
|
||||
not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If identifiable
|
||||
sections of that work are not derived from the Program, and can be reasonably
|
||||
considered independent and separate works in themselves, then this License, and
|
||||
its terms, do not apply to those sections when you distribute them as separate
|
||||
works. But when you distribute the same sections as part of a whole which is a
|
||||
work based on the Program, the distribution of the whole must be on the terms
|
||||
of this License, whose permissions for other licensees extend to the entire
|
||||
whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest your
|
||||
rights to work written entirely by you; rather, the intent is to exercise the
|
||||
right to control the distribution of derivative or collective works based on
|
||||
the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program with the
|
||||
Program (or with a work based on the Program) on a volume of a storage or
|
||||
distribution medium does not bring the other work under the scope of this
|
||||
License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it, under
|
||||
Section 2) in object code or executable form under the terms of Sections 1 and
|
||||
2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable source
|
||||
code, which must be distributed under the terms of Sections 1 and 2 above
|
||||
on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three years, to
|
||||
give any third party, for a charge no more than your cost of physically
|
||||
performing source distribution, a complete machine-readable copy of the
|
||||
corresponding source code, to be distributed under the terms of Sections 1
|
||||
and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer to
|
||||
distribute corresponding source code. (This alternative is allowed only
|
||||
for noncommercial distribution and only if you received the program in
|
||||
object code or executable form with such an offer, in accord with
|
||||
Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for making
|
||||
modifications to it. For an executable work, complete source code means all
|
||||
the source code for all modules it contains, plus any associated interface
|
||||
definition files, plus the scripts used to control compilation and installation
|
||||
of the executable. However, as a special exception, the source code
|
||||
distributed need not include anything that is normally distributed (in either
|
||||
source or binary form) with the major components (compiler, kernel, and so on)
|
||||
of the operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the source
|
||||
code from the same place counts as distribution of the source code, even though
|
||||
third parties are not compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program except as
|
||||
expressly provided under this License. Any attempt otherwise to copy, modify,
|
||||
sublicense or distribute the Program is void, and will automatically terminate
|
||||
your rights under this License. However, parties who have received copies, or
|
||||
rights, from you under this License will not have their licenses terminated so
|
||||
long as such parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not signed it.
|
||||
However, nothing else grants you permission to modify or distribute the Program
|
||||
or its derivative works. These actions are prohibited by law if you do not
|
||||
accept this License. Therefore, by modifying or distributing the Program (or
|
||||
any work based on the Program), you indicate your acceptance of this License to
|
||||
do so, and all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the Program),
|
||||
the recipient automatically receives a license from the original licensor to
|
||||
copy, distribute or modify the Program subject to these terms and conditions.
|
||||
You may not impose any further restrictions on the recipients' exercise of the
|
||||
rights granted herein. You are not responsible for enforcing compliance by
|
||||
third parties to this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues), conditions
|
||||
are imposed on you (whether by court order, agreement or otherwise) that
|
||||
contradict the conditions of this License, they do not excuse you from the
|
||||
conditions of this License. If you cannot distribute so as to satisfy
|
||||
simultaneously your obligations under this License and any other pertinent
|
||||
obligations, then as a consequence you may not distribute the Program at all.
|
||||
For example, if a patent license would not permit royalty-free redistribution
|
||||
of the Program by all those who receive copies directly or indirectly through
|
||||
you, then the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply and
|
||||
the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any patents or
|
||||
other property right claims or to contest validity of any such claims; this
|
||||
section has the sole purpose of protecting the integrity of the free software
|
||||
distribution system, which is implemented by public license practices. Many
|
||||
people have made generous contributions to the wide range of software
|
||||
distributed through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing to
|
||||
distribute software through any other system and a licensee cannot impose that
|
||||
choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to be a
|
||||
consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in certain
|
||||
countries either by patents or by copyrighted interfaces, the original
|
||||
copyright holder who places the Program under this License may add an explicit
|
||||
geographical distribution limitation excluding those countries, so that
|
||||
distribution is permitted only in or among countries not thus excluded. In
|
||||
such case, this License incorporates the limitation as if written in the body
|
||||
of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions of the
|
||||
General Public License from time to time. Such new versions will be similar in
|
||||
spirit to the present version, but may differ in detail to address new problems
|
||||
or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any later
|
||||
version", you have the option of following the terms and conditions either of
|
||||
that version or of any later version published by the Free Software Foundation.
|
||||
If the Program does not specify a version number of this License, you may
|
||||
choose any version ever published by the Free Software Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free programs
|
||||
whose distribution conditions are different, write to the author to ask for
|
||||
permission. For software which is copyrighted by the Free Software Foundation,
|
||||
write to the Free Software Foundation; we sometimes make exceptions for this.
|
||||
Our decision will be guided by the two goals of preserving the free status of
|
||||
all derivatives of our free software and of promoting the sharing and reuse of
|
||||
software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
|
||||
THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
|
||||
STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE
|
||||
PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,
|
||||
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
|
||||
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE,
|
||||
YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL
|
||||
ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE
|
||||
PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR
|
||||
INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA
|
||||
BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||
FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER
|
||||
OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest possible
|
||||
use to the public, the best way to achieve this is to make it free software
|
||||
which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest to attach
|
||||
them to the start of each source file to most effectively convey the exclusion
|
||||
of warranty; and each file should have at least the "copyright" line and a
|
||||
pointer to where the full notice is found.
|
||||
|
||||
One line to give the program's name and a brief idea of what it does.
|
||||
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify it
|
||||
under the terms of the GNU General Public License as published by the Free
|
||||
Software Foundation; either version 2 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this when it
|
||||
starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author Gnomovision comes
|
||||
with ABSOLUTELY NO WARRANTY; for details type 'show w'. This is free
|
||||
software, and you are welcome to redistribute it under certain conditions;
|
||||
type 'show c' for details.
|
||||
|
||||
The hypothetical commands 'show w' and 'show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may be
|
||||
called something other than 'show w' and 'show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. Here
|
||||
is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
'Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
signature of Ty Coon, 1 April 1989
|
||||
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Library General Public
|
||||
License instead of this License.
|
||||
|
||||
|
||||
"CLASSPATH" EXCEPTION TO THE GPL
|
||||
|
||||
Certain source files distributed by Oracle America and/or its affiliates are
|
||||
subject to the following clarification and special exception to the GPL, but
|
||||
only where Oracle has expressly included in the particular source file's header
|
||||
the words "Oracle designates this particular file as subject to the "Classpath"
|
||||
exception as provided by Oracle in the LICENSE file that accompanied this code."
|
||||
|
||||
Linking this library statically or dynamically with other modules is making
|
||||
a combined work based on this library. Thus, the terms and conditions of
|
||||
the GNU General Public License cover the whole combination.
|
||||
|
||||
As a special exception, the copyright holders of this library give you
|
||||
permission to link this library with independent modules to produce an
|
||||
executable, regardless of the license terms of these independent modules,
|
||||
and to copy and distribute the resulting executable under terms of your
|
||||
choice, provided that you also meet, for each linked independent module,
|
||||
the terms and conditions of the license of that module. An independent
|
||||
module is a module which is not derived from or based on this library. If
|
||||
you modify this library, you may extend this exception to your version of
|
||||
the library, but you are not obligated to do so. If you do not wish to do
|
||||
so, delete this exception statement from your version.
|
||||
Loaded 100 of 406 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user