From 622d84182e0d1479e4faafaa22445ccfe41e06ea Mon Sep 17 00:00:00 2001 From: HaoHaoLucas <2313583830@qq.com> Date: Thu, 7 May 2026 01:35:16 +0800 Subject: [PATCH 1/5] feat: background download launcher updates (#4352) Co-authored-by: Cursor --- .../org/jackhuang/hmcl/setting/Config.java | 15 ++ .../org/jackhuang/hmcl/ui/Controllers.java | 17 ++ .../ui/decorator/DecoratorController.java | 7 + .../jackhuang/hmcl/ui/main/SettingsPage.java | 13 + .../jackhuang/hmcl/upgrade/UpdateChecker.java | 1 + .../jackhuang/hmcl/upgrade/UpdateHandler.java | 226 +++++++++++++++--- .../resources/assets/lang/I18N.properties | 4 + .../assets/lang/I18N_zh_CN.properties | 4 + 8 files changed, 260 insertions(+), 27 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java index 0a845929b5..f38af18ffe 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java @@ -237,6 +237,21 @@ public void setDisableAutoShowUpdateDialog(boolean disableAutoShowUpdateDialog) this.disableAutoShowUpdateDialog.set(disableAutoShowUpdateDialog); } + @SerializedName("backgroundAutoDownloadUpdate") + private final BooleanProperty backgroundAutoDownloadUpdate = new SimpleBooleanProperty(false); + + public BooleanProperty backgroundAutoDownloadUpdateProperty() { + return backgroundAutoDownloadUpdate; + } + + public boolean isBackgroundAutoDownloadUpdate() { + return backgroundAutoDownloadUpdate.get(); + } + + public void setBackgroundAutoDownloadUpdate(boolean backgroundAutoDownloadUpdate) { + this.backgroundAutoDownloadUpdate.set(backgroundAutoDownloadUpdate); + } + @SerializedName("disableAprilFools") private final BooleanProperty disableAprilFools = new SimpleBooleanProperty(false); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index f68bedac09..72263fa3e4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -49,6 +49,8 @@ import org.jackhuang.hmcl.setting.*; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskExecutor; +import org.jackhuang.hmcl.upgrade.RemoteVersion; +import org.jackhuang.hmcl.upgrade.UpdateHandler; import org.jackhuang.hmcl.ui.account.AccountListPage; import org.jackhuang.hmcl.ui.animation.AnimationUtils; import org.jackhuang.hmcl.ui.animation.ContainerAnimations; @@ -119,6 +121,8 @@ public final class Controllers { }); private static Lazy rootPage = new Lazy<>(RootPage::new); private static DecoratorController decorator; + @Nullable + private static Path lastPendingUpdateNotificationJar; private static DownloadPage downloadPage; private static Lazy accountListPage = new Lazy<>(() -> { AccountListPage accountListPage = new AccountListPage(); @@ -622,6 +626,18 @@ public static void showToast(String content) { decorator.showToast(content); } + public static void showPendingUpdateNotification(Path stagedJar, RemoteVersion version) { + FXUtils.checkFxUserThread(); + if (stagedJar.equals(lastPendingUpdateNotificationJar)) { + return; + } + lastPendingUpdateNotificationJar = stagedJar; + decorator.showPersistentSnackbar( + i18n("update.background_downloaded.toast", version.version()), + i18n("update.restart_to_apply"), + () -> UpdateHandler.applyStagedUpdate(stagedJar)); + } + public static void onHyperlinkAction(String href) { if (href.startsWith("hmcl://")) { switch (href) { @@ -644,6 +660,7 @@ public static boolean isStopped() { } public static void shutdown() { + lastPendingUpdateNotificationJar = null; rootPage = null; versionPage = null; gameListPage = null; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java index ee2305acbf..6a415a0534 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java @@ -433,6 +433,13 @@ public void showToast(String content) { decorator.getSnackbar().fireEvent(new JFXSnackbar.SnackbarEvent(new JFXSnackbarLayout(content))); } + /// Shows a snackbar until the user triggers the action or another snackbar replaces it. + public void showPersistentSnackbar(String message, String actionText, Runnable action) { + FXUtils.checkFxUserThread(); + JFXSnackbarLayout layout = new JFXSnackbarLayout(message, actionText, e -> action.run()); + decorator.getSnackbar().enqueue(new JFXSnackbar.SnackbarEvent(layout, Duration.INDEFINITE)); + } + // ==== Wizard ==== public void startWizard(WizardProvider wizardProvider) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java index 89913b23df..1ca775e1c9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java @@ -162,6 +162,19 @@ protected int getTrailingTextIndex() { updatePaneList.getContent().add(disableAutoShowUpdateDialogPane); } + { + LineToggleButton backgroundDownloadPane = new LineToggleButton(); + backgroundDownloadPane.setTitle(i18n("settings.launcher.update.background_auto_download")); + backgroundDownloadPane.setSubtitle(i18n("settings.launcher.update.background_auto_download.subtitle")); + backgroundDownloadPane.selectedProperty().bindBidirectional(config().backgroundAutoDownloadUpdateProperty()); + backgroundDownloadPane.selectedProperty().addListener((obs, oldVal, newVal) -> { + if (Boolean.TRUE.equals(newVal) && !Boolean.TRUE.equals(oldVal)) { + UpdateHandler.tryAutoDownloadIfOutdated(); + } + }); + updatePaneList.getContent().add(backgroundDownloadPane); + } + rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("update")), updatePaneList); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java index df15a3fac1..38d89d2e73 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java @@ -57,6 +57,7 @@ private UpdateChecker() { public static void init() { requestCheckUpdate(UpdateChannel.getChannel(), config().isAcceptPreviewUpdate()); + outdated.addListener(obs -> UpdateHandler.onOutdatedStateMayHaveChanged()); } public static RemoteVersion getLatestVersion() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java index 95c8f1b772..83f2953315 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java @@ -45,18 +45,213 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Stream; +import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.ui.FXUtils.checkFxUserThread; import static org.jackhuang.hmcl.util.Lang.thread; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; public final class UpdateHandler { + private static final AtomicBoolean backgroundDownloadInProgress = new AtomicBoolean(false); + private UpdateHandler() { } + private static Path getUpdateCacheDirectory() { + return Metadata.HMCL_GLOBAL_DIRECTORY.resolve("cache").resolve("update"); + } + + private static Path getStagedJarPath(String version) { + return getUpdateCacheDirectory().resolve("HMCL-" + version + ".jar"); + } + + public static void onOutdatedStateMayHaveChanged() { + if (!config().isBackgroundAutoDownloadUpdate()) { + return; + } + if (!UpdateChecker.isOutdated()) { + return; + } + RemoteVersion latest = UpdateChecker.getLatestVersion(); + if (latest == null) { + return; + } + thread(() -> downloadUpdateInBackground(latest), "HMCL Background Update Download", true); + } + + public static void tryAutoDownloadIfOutdated() { + checkFxUserThread(); + onOutdatedStateMayHaveChanged(); + } + + private static void downloadUpdateInBackground(RemoteVersion version) { + if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && !OperatingSystem.isWindows7OrLater()) { + return; + } + + Path target = getStagedJarPath(version.version()); + + try { + Files.createDirectories(target.getParent()); + } catch (IOException e) { + LOG.warning("Failed to create update cache directory", e); + return; + } + + if (Files.isRegularFile(target)) { + try { + IntegrityChecker.verifyJar(target); + notifyStagedUpdateReady(target, version); + return; + } catch (IOException | RuntimeException e) { + LOG.info("Replacing invalid cached update file: " + target, e); + try { + Files.deleteIfExists(target); + } catch (IOException ex) { + LOG.warning("Failed to delete invalid cached update file", ex); + } + } + } + + if (!backgroundDownloadInProgress.compareAndSet(false, true)) { + return; + } + try { + cleanOldStagedJarsExcept(target); + Task task = new HMCLDownloadTask(version, target); + TaskExecutor executor = task.executor(); + boolean success = executor.test(); + if (!success) { + Exception ex = executor.getException(); + if (ex instanceof CancellationException) { + try { + Files.deleteIfExists(target); + } catch (IOException ignored) { + } + return; + } + LOG.warning("Background HMCL update download failed", ex); + try { + Files.deleteIfExists(target); + } catch (IOException ignored) { + } + return; + } + + if (!IntegrityChecker.DISABLE_SELF_INTEGRITY_CHECK && !IntegrityChecker.isSelfVerified()) { + LOG.warning("Background update download skipped: current JAR is not verified"); + try { + Files.deleteIfExists(target); + } catch (IOException ignored) { + } + return; + } + try { + IntegrityChecker.verifyJar(target); + } catch (IOException e) { + LOG.warning("Downloaded update jar failed integrity check", e); + try { + Files.deleteIfExists(target); + } catch (IOException ignored) { + } + return; + } + + notifyStagedUpdateReady(target, version); + } finally { + backgroundDownloadInProgress.set(false); + } + } + + private static void cleanOldStagedJarsExcept(Path keep) { + Path dir = keep.getParent(); + if (dir == null || !Files.isDirectory(dir)) { + return; + } + try (Stream stream = Files.list(dir)) { + stream.filter(p -> !p.equals(keep)).forEach(p -> { + try { + Files.deleteIfExists(p); + } catch (IOException e) { + LOG.warning("Failed to delete stale update file " + p, e); + } + }); + } catch (IOException e) { + LOG.warning("Failed to list update cache directory", e); + } + } + + private static void notifyStagedUpdateReady(Path stagedJar, RemoteVersion version) { + Platform.runLater(() -> Controllers.showPendingUpdateNotification(stagedJar, version)); + } + + public static void applyStagedUpdate(Path stagedJar) { + checkFxUserThread(); + + if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && !OperatingSystem.isWindows7OrLater()) { + Controllers.dialog(i18n("fatal.apply_update_need_win7", Metadata.PUBLISH_URL), i18n("message.error"), MessageType.ERROR); + return; + } + + try { + if (!Files.isRegularFile(stagedJar)) { + Controllers.dialog(i18n("update.failed"), i18n("message.error"), MessageType.ERROR); + return; + } + IntegrityChecker.verifyJar(stagedJar); + } catch (IOException e) { + LOG.warning("Staged update file is missing or invalid", e); + Controllers.dialog(e.toString(), i18n("update.failed"), MessageType.ERROR); + return; + } + + try { + prepareExitForUpdate(); + requestUpdate(stagedJar, getCurrentLocation()); + EntryPoint.exit(0); + } catch (IOException e) { + LOG.warning("Failed to apply staged update", e); + Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageType.ERROR); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageType.ERROR); + } + } + + private static void prepareExitForUpdate() throws IOException, InterruptedException { + if (!IntegrityChecker.DISABLE_SELF_INTEGRITY_CHECK && !IntegrityChecker.isSelfVerified()) { + throw new IOException("Current JAR is not verified"); + } + + CompletableFuture future = new CompletableFuture<>(); + Platform.runLater(() -> { + try { + Controllers.saveWindowStates(); + } finally { + future.complete(null); + } + }); + try { + future.get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + throw new IOException(cause); + } + + FileSaver.waitForAllSaves(); + } + /** * @return whether to exit */ @@ -123,38 +318,15 @@ public static void updateFrom(RemoteVersion version) { if (success) { try { - if (!IntegrityChecker.isSelfVerified() && !IntegrityChecker.DISABLE_SELF_INTEGRITY_CHECK) { - throw new IOException("Current JAR is not verified"); - } - - CompletableFuture future = new CompletableFuture<>(); - - Platform.runLater(() -> { - try { - Controllers.saveWindowStates(); - } finally { - future.complete(null); - } - }); - - try { - future.get(); - } catch (ExecutionException | InterruptedException ignored) { - // Ignore - } - - - try { - FileSaver.waitForAllSaves(); - } catch (InterruptedException ignored) { - // Ignore - } - + prepareExitForUpdate(); requestUpdate(downloaded, getCurrentLocation()); EntryPoint.exit(0); } catch (IOException e) { LOG.warning("Failed to update to " + version, e); Platform.runLater(() -> Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageType.ERROR)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Platform.runLater(() -> Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageType.ERROR)); } } else { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 731eaee788..4b3867fd72 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1649,6 +1649,10 @@ update.channel.stable=Stable update.checking=Checking for updates update.disable_auto_show_update_dialog=Do not show update dialog automatically update.disable_auto_show_update_dialog.subtitle=Enable this option to prevent HMCL from automatically showing the update dialog. +settings.launcher.update.background_auto_download=Download updates in the background +settings.launcher.update.background_auto_download.subtitle=When an update is available, HMCL downloads the installer automatically. Restart from the notification when you are ready to apply it. +update.background_downloaded.toast=HMCL %s has been downloaded and is ready to install. +update.restart_to_apply=Restart to update update.failed=Failed to update update.found=Update Available! update.newest_version=Latest version: %s diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 1180eddfa1..c0ab12ac7d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1440,6 +1440,10 @@ update.channel.stable=稳定版 update.checking=正在检查更新 update.disable_auto_show_update_dialog=不自动显示更新弹窗 update.disable_auto_show_update_dialog.subtitle=启用此选项,HMCL 将不会自动弹出更新弹窗。 +settings.launcher.update.background_auto_download=在后台自动下载更新 +settings.launcher.update.background_auto_download.subtitle=发现新版本时自动下载安装包;准备好后在通知中重启启动器以完成更新。 +update.background_downloaded.toast=HMCL %s 已下载完成,可安装更新。 +update.restart_to_apply=重启以更新 update.failed=更新失败 update.found=发现更新 update.newest_version=最新版本为:%s From 8f926e2f4399f962ec443b79ded0e21e9beba8fa Mon Sep 17 00:00:00 2001 From: HaoHaoLucas <2313583830@qq.com> Date: Fri, 8 May 2026 00:28:33 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=AD=BB=E9=94=81=E3=80=81=E5=AD=90=E8=BF=9B?= =?UTF-8?q?=E7=A8=8B=20JVM=20=E5=8F=82=E6=95=B0=E4=B8=8E=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E9=A1=B5=E6=9B=B4=E6=96=B0=E6=8C=89=E9=92=AE=20(#4352)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - prepareExitForUpdate:在 JavaFX 线程上直接保存窗口状态,避免 applyStagedUpdate 死锁\n- startJava:不向子进程传递 hmcl.version.override,避免 --apply-to 后仍显示测试版本号\n- SettingsPage:更新按钮始终可见;无远端版本缓存时触发重新检查并提示 Co-authored-by: Cursor --- .../jackhuang/hmcl/ui/main/SettingsPage.java | 10 +-- .../jackhuang/hmcl/upgrade/UpdateHandler.java | 64 +++++++++++++------ 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java index 1ca775e1c9..defc220e09 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java @@ -88,7 +88,6 @@ public SettingsPage() { { JFXButton updateButton = FXUtils.newToggleButton4(SVG.UPDATE, 20); - updateButton.setOnAction(e -> onUpdate()); updateButton.setPadding(Insets.EMPTY); FXUtils.installFastTooltip(updateButton, i18n("update.tooltip")); @@ -108,6 +107,8 @@ protected int getTrailingTextIndex() { updatePane.setTitle(i18n("update")); updatePane.setValue(UpdateChannel.getChannel()); + updateButton.setOnAction(e -> onUpdate(updateChannel)); + updatePane.setConverter(channel -> i18n("update.channel." + channel.channelName)); updatePane.setItems(List.of(UpdateChannel.STABLE, UpdateChannel.DEVELOPMENT)); updatePane.setDescriptionConverter(channel -> i18n("update.note." + channel.channelName)); @@ -118,8 +119,6 @@ protected int getTrailingTextIndex() { updateListener = any -> { boolean outdated = UpdateChecker.isOutdated(); - updateButton.setVisible(outdated); - updateButton.setManaged(outdated); updatePane.pseudoClassStateChanged(PseudoClass.getPseudoClass("active"), outdated); if (UpdateChecker.isOutdated()) { @@ -283,9 +282,12 @@ private void openLogFolder() { FXUtils.openFolder(LOG.getLogFile().getParent()); } - private void onUpdate() { + /// Opens the update flow for the latest known remote build, or re-requests a check when none is cached yet. + private void onUpdate(ObjectProperty updateChannel) { RemoteVersion target = UpdateChecker.getLatestVersion(); if (target == null) { + UpdateChecker.requestCheckUpdate(updateChannel.get(), config().isAcceptPreviewUpdate()); + Controllers.showToast(i18n("update.checking")); return; } UpdateHandler.updateFrom(target); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java index 83f2953315..58496b4457 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java @@ -222,31 +222,34 @@ public static void applyStagedUpdate(Path stagedJar) { Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageType.ERROR); } } - private static void prepareExitForUpdate() throws IOException, InterruptedException { if (!IntegrityChecker.DISABLE_SELF_INTEGRITY_CHECK && !IntegrityChecker.isSelfVerified()) { throw new IOException("Current JAR is not verified"); } - CompletableFuture future = new CompletableFuture<>(); - Platform.runLater(() -> { + if (Platform.isFxApplicationThread()) { + Controllers.saveWindowStates(); + } else { + CompletableFuture future = new CompletableFuture<>(); + Platform.runLater(() -> { + try { + Controllers.saveWindowStates(); + } finally { + future.complete(null); + } + }); try { - Controllers.saveWindowStates(); - } finally { - future.complete(null); - } - }); - try { - future.get(); - } catch (ExecutionException e) { - Throwable cause = e.getCause(); - if (cause instanceof IOException) { - throw (IOException) cause; - } - if (cause instanceof RuntimeException) { - throw (RuntimeException) cause; + future.get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + throw new IOException(cause); } - throw new IOException(cause); } FileSaver.waitForAllSaves(); @@ -372,13 +375,33 @@ private static void requestUpdate(Path updateTo, Path self) throws IOException { startJava(updateTo, "--apply-to", self.toString()); } + /// Whether an {@code hmcl.*} system property key should be passed to a child HMCL JVM. + private static boolean shouldForwardChildHmclPropertyKey(String key) { + return !"hmcl.version.override".equals(key); + } + + /// Parses the property key from a {@code -Dkey=value} (or {@code -Dkey}) argument for forwarding rules. + private static boolean shouldForwardChildJvmDefinition(String inputArgument) { + int eq = inputArgument.indexOf('=', 2); + String key = eq == -1 ? inputArgument.substring(2) : inputArgument.substring(2, eq); + return shouldForwardChildHmclPropertyKey(key); + } + + /// Starts a new JVM running {@code jar} with the given trailing arguments. + /// + /// Input JVM definitions from the current process are copied so child behaviour matches the parent (memory, + /// exports, etc.). {@code hmcl.version.override} is intentionally omitted so an update subprocess and the + /// post-update launcher read the real version from the target JAR manifest instead of a test-only override + /// that would otherwise stick across {@code --apply-to} and make {@link Metadata#VERSION} wrong for renames. public static void startJava(Path jar, String... appArgs) throws IOException { List commandline = new ArrayList<>(); commandline.add(JavaRuntime.getDefault().getBinary().toString()); try { for (String inputArgument : ManagementFactory.getRuntimeMXBean().getInputArguments()) { - if (inputArgument.startsWith("-D") || inputArgument.startsWith("-X")) { + if (inputArgument.startsWith("-X")) { + commandline.add(inputArgument); + } else if (inputArgument.startsWith("-D") && shouldForwardChildJvmDefinition(inputArgument)) { commandline.add(inputArgument); } } @@ -386,6 +409,9 @@ public static void startJava(Path jar, String... appArgs) throws IOException { // ManagementFactory not available for (Map.Entry entry : System.getProperties().entrySet()) { if (entry.getKey() instanceof String key && key.startsWith("hmcl.")) { + if (!shouldForwardChildHmclPropertyKey(key)) { + continue; + } commandline.add("-D" + key + "=" + entry.getValue()); } } From e8f66d93a53ef923b758fb0b98e22b1a2acd0a36 Mon Sep 17 00:00:00 2001 From: HaoHaoLucas <2313583830@qq.com> Date: Fri, 8 May 2026 00:35:51 +0800 Subject: [PATCH 3/5] 2 --- .../jackhuang/hmcl/ui/decorator/DecoratorController.java | 1 - .../java/org/jackhuang/hmcl/ui/main/SettingsPage.java | 1 - .../java/org/jackhuang/hmcl/upgrade/UpdateHandler.java | 8 -------- 3 files changed, 10 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java index 6a415a0534..dab2d24468 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java @@ -433,7 +433,6 @@ public void showToast(String content) { decorator.getSnackbar().fireEvent(new JFXSnackbar.SnackbarEvent(new JFXSnackbarLayout(content))); } - /// Shows a snackbar until the user triggers the action or another snackbar replaces it. public void showPersistentSnackbar(String message, String actionText, Runnable action) { FXUtils.checkFxUserThread(); JFXSnackbarLayout layout = new JFXSnackbarLayout(message, actionText, e -> action.run()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java index defc220e09..bc38730801 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java @@ -282,7 +282,6 @@ private void openLogFolder() { FXUtils.openFolder(LOG.getLogFile().getParent()); } - /// Opens the update flow for the latest known remote build, or re-requests a check when none is cached yet. private void onUpdate(ObjectProperty updateChannel) { RemoteVersion target = UpdateChecker.getLatestVersion(); if (target == null) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java index 58496b4457..51688793c8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java @@ -375,24 +375,16 @@ private static void requestUpdate(Path updateTo, Path self) throws IOException { startJava(updateTo, "--apply-to", self.toString()); } - /// Whether an {@code hmcl.*} system property key should be passed to a child HMCL JVM. private static boolean shouldForwardChildHmclPropertyKey(String key) { return !"hmcl.version.override".equals(key); } - /// Parses the property key from a {@code -Dkey=value} (or {@code -Dkey}) argument for forwarding rules. private static boolean shouldForwardChildJvmDefinition(String inputArgument) { int eq = inputArgument.indexOf('=', 2); String key = eq == -1 ? inputArgument.substring(2) : inputArgument.substring(2, eq); return shouldForwardChildHmclPropertyKey(key); } - /// Starts a new JVM running {@code jar} with the given trailing arguments. - /// - /// Input JVM definitions from the current process are copied so child behaviour matches the parent (memory, - /// exports, etc.). {@code hmcl.version.override} is intentionally omitted so an update subprocess and the - /// post-update launcher read the real version from the target JAR manifest instead of a test-only override - /// that would otherwise stick across {@code --apply-to} and make {@link Metadata#VERSION} wrong for renames. public static void startJava(Path jar, String... appArgs) throws IOException { List commandline = new ArrayList<>(); commandline.add(JavaRuntime.getDefault().getBinary().toString()); From 6605026297185d37abebd9a066016aa2dacd3522 Mon Sep 17 00:00:00 2001 From: HaoHaoLucas <2313583830@qq.com> Date: Fri, 8 May 2026 01:13:42 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E7=B9=81=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 90f2157ece..22e2ef0888 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1302,6 +1302,8 @@ settings.launcher.theme=主題色 settings.launcher.title_transparent=標題欄透明 settings.launcher.turn_off_animations=關閉動畫 settings.launcher.version_list_source=版本清單來源 +settings.launcher.update.background_auto_download=在後台自動下載更新 +settings.launcher.update.background_auto_download.subtitle=當有可用更新時,HMCL 會自動下載安裝程式。準備好後請從通知重新啟動以套用更新。 settings.launcher.background.settings.opacity=不透明度 settings.memory=遊戲記憶體 @@ -1435,6 +1437,8 @@ update.channel.stable=穩定版 update.checking=正在檢查更新 update.disable_auto_show_update_dialog=不自動顯示更新彈窗 update.disable_auto_show_update_dialog.subtitle=啟用此選項,HMCL 將不會自動彈出更新彈窗。 +update.background_downloaded.toast=HMCL %s 已下載完成,可安裝更新。 +update.restart_to_apply=重新啟動以更新 update.failed=更新失敗 update.found=發現到更新 update.newest_version=最新版本為:%s From 4c2c01d79848bb4b317dc0a7fcb66d6a41e9933a Mon Sep 17 00:00:00 2001 From: HaoHaoLucas <2313583830@qq.com> Date: Fri, 8 May 2026 01:20:17 +0800 Subject: [PATCH 5/5] error fixed --- HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java index 51688793c8..81b6374a6f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java @@ -222,6 +222,7 @@ public static void applyStagedUpdate(Path stagedJar) { Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageType.ERROR); } } + private static void prepareExitForUpdate() throws IOException, InterruptedException { if (!IntegrityChecker.DISABLE_SELF_INTEGRITY_CHECK && !IntegrityChecker.isSelfVerified()) { throw new IOException("Current JAR is not verified");