diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java index 669b3ba3903..c6718d11b42 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java @@ -39,6 +39,7 @@ 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; @@ -46,9 +47,11 @@ import org.jackhuang.hmcl.util.i18n.LocalizedText; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.io.IOUtils; import org.jetbrains.annotations.NotNullByDefault; import org.jetbrains.annotations.Nullable; +import java.io.Closeable; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.charset.Charset; @@ -60,6 +63,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import static org.jackhuang.hmcl.util.Lang.mapOf; @@ -124,6 +128,65 @@ public static Modpack readModpackManifest(Path file, Charset charset) throws Uns throw new UnsupportedModpackException(file.toString()); } + /// Owns the launcher wrapper file system and the embedded modpack path within it. + /// + /// The owner is safe to close repeatedly and from competing terminal paths. + public static final class LauncherWrapper implements Closeable { + /// Embedded modpack path backed by the wrapper file system. + private final Path innerPath; + + /// Wrapper file system, atomically cleared by the first close operation. + private final AtomicReference<@Nullable FileSystem> wrapperFsRef; + + /// Creates an owner for an embedded modpack path and its backing file system. + /// + /// @param innerPath embedded modpack path + /// @param wrapperFs backing wrapper file system + private LauncherWrapper(Path innerPath, FileSystem wrapperFs) { + this.innerPath = innerPath; + this.wrapperFsRef = new AtomicReference<>(wrapperFs); + } + + /// Returns the embedded modpack path. + /// + /// The path is valid only until this owner is closed. + public Path innerPath() { + return innerPath; + } + + /// Closes the backing wrapper file system if it is still open. + + public void close() throws IOException { + FileSystem wrapperFs = wrapperFsRef.getAndSet(null); + if (wrapperFs != null) { + wrapperFs.close(); + } + } + } + + /// 检测 [file] 是否为 HMCL 启动器包装 ZIP(其内部嵌入了实际的整合包 `modpack.zip` 或 `modpack.mrpack`) + /// 返回一个包含内部条目路径和包装文件系统的 [LauncherWrapper], + /// 如果 [file] 不是包装 ZIP,则返回 `null` + @Nullable + public static LauncherWrapper unwrapIfLauncherWrapper(Path file, Charset charset) { + FileSystem outerFs = null; + try { + outerFs = CompressingUtils.createReadOnlyZipFileSystem(file, charset); + for (String innerName : new String[]{"modpack.zip", "modpack.mrpack"}) { + Path entryPath = outerFs.getPath("/" + innerName); + if (Files.isRegularFile(entryPath)) { + LauncherWrapper result = new LauncherWrapper(entryPath, outerFs); + outerFs = null; + return result; + } + } + } catch (IOException ignored) { + } finally { + IOUtils.closeQuietly(outerFs); + } + return null; + } + public static Path findMinecraftDirectoryInManuallyCreatedModpack(String modpackName, FileSystem fs) throws IOException, UnsupportedModpackException { Path root = fs.getPath("/"); if (isMinecraftDirectory(root)) return root; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java index c731a684d6d..47c76fe5082 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java @@ -24,6 +24,7 @@ import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.game.ManuallyCreatedModpackException; import org.jackhuang.hmcl.game.ModpackHelper; +import org.jackhuang.hmcl.game.ModpackHelper.LauncherWrapper; import org.jackhuang.hmcl.modpack.Modpack; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; @@ -40,9 +41,14 @@ import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.io.IOUtils; +import org.jetbrains.annotations.Nullable; import java.nio.charset.Charset; +import java.nio.file.FileSystems; import java.nio.file.Path; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -53,6 +59,10 @@ public final class LocalModpackPage extends ModpackPage { private Modpack manifest = null; private Charset charset; + private final AtomicReference wrapperRef = new AtomicReference<>(); + + private volatile boolean cleanedUp; + public LocalModpackPage(WizardController controller) { super(controller); @@ -91,11 +101,12 @@ public LocalModpackPage(WizardController controller) { FileChooser chooser = new FileChooser(); chooser.setTitle(i18n("modpack.choose")); chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip")); - selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage())); - if (selectedFile == null) { + Path chosenFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage())); + if (chosenFile == null) { controller.onEnd(); return; } + selectedFile = chosenFile; controller.getSettings().put(MODPACK_FILE, selectedFile); } @@ -104,10 +115,32 @@ public LocalModpackPage(WizardController controller) { Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(selectedFile)) .thenApplyAsync(encoding -> { charset = encoding; - manifest = ModpackHelper.readModpackManifest(selectedFile, encoding); + Path actualFile = selectedFile; + if (selectedFile.getFileSystem() == FileSystems.getDefault()) { + LauncherWrapper wrapper = ModpackHelper.unwrapIfLauncherWrapper(selectedFile, encoding); + if (wrapper != null) { + actualFile = wrapper.innerPath(); + wrapperRef.set(wrapper); + } + } + manifest = ModpackHelper.readModpackManifest(actualFile, encoding); return manifest; }) .whenComplete(Schedulers.javafx(), (manifest, exception) -> { + LauncherWrapper wrapper = wrapperRef.getAndSet(null); + if (wrapper != null) { + if (exception != null || cleanedUp) { + IOUtils.closeQuietly(wrapper); + } else { + controller.getSettings().put(MODPACK_FILE, wrapper.innerPath()); + controller.getSettings().put(MODPACK_WRAPPER, wrapper); + } + } + + if (cleanedUp) { + return; + } + if (exception instanceof ManuallyCreatedModpackException) { hideSpinner(); nameProperty.set(FileUtils.getName(selectedFile)); @@ -129,27 +162,36 @@ public LocalModpackPage(WizardController controller) { Platform.runLater(controller::onEnd); } else { hideSpinner(); - controller.getSettings().put(MODPACK_MANIFEST, manifest); - nameProperty.set(manifest.getName()); - versionProperty.set(manifest.getVersion()); - authorProperty.set(manifest.getAuthor()); + Modpack parsedManifest = Objects.requireNonNull(manifest); + controller.getSettings().put(MODPACK_MANIFEST, parsedManifest); + nameProperty.set(parsedManifest.getName()); + versionProperty.set(parsedManifest.getVersion()); + authorProperty.set(parsedManifest.getAuthor()); if (name == null) { // trim: https://github.com/HMCL-dev/HMCL/issues/962 - txtModpackName.setText(manifest.getName().trim()); + txtModpackName.setText(parsedManifest.getName().trim()); } - btnDescription.setVisible(StringUtils.isNotBlank(manifest.getDescription())); + btnDescription.setVisible(StringUtils.isNotBlank(parsedManifest.getDescription())); } }).start(); } @Override public void cleanup(SettingsMap settings) { + cleanedUp = true; settings.remove(MODPACK_FILE); + IOUtils.closeQuietly(wrapperRef.getAndSet(null)); + IOUtils.closeQuietly(settings.remove(MODPACK_WRAPPER)); } protected void onInstall() { + Charset detectedCharset = charset; + if (detectedCharset == null) { + return; + } + String name = txtModpackName.getText(); // Check for non-ASCII characters. @@ -160,7 +202,7 @@ protected void onInstall() { MessageDialogPane.MessageType.QUESTION) .yesOrNo(() -> { controller.getSettings().put(MODPACK_NAME, name); - controller.getSettings().put(MODPACK_CHARSET, charset); + controller.getSettings().put(MODPACK_CHARSET, detectedCharset); controller.onFinish(); }, () -> { // The user selects Cancel and does nothing. @@ -168,7 +210,7 @@ protected void onInstall() { .build()); } else { controller.getSettings().put(MODPACK_NAME, name); - controller.getSettings().put(MODPACK_CHARSET, charset); + controller.getSettings().put(MODPACK_CHARSET, detectedCharset); controller.onFinish(); } } @@ -179,6 +221,9 @@ protected void onDescribe() { } public static final SettingsMap.Key MODPACK_FILE = new SettingsMap.Key<>("MODPACK_FILE"); + public static final SettingsMap.Key MODPACK_WRAPPER = + new SettingsMap.Key<>("MODPACK_WRAPPER"); + public static final SettingsMap.Key MODPACK_NAME = new SettingsMap.Key<>("MODPACK_NAME"); public static final SettingsMap.Key MODPACK_MANIFEST = new SettingsMap.Key<>("MODPACK_MANIFEST"); public static final SettingsMap.Key MODPACK_CHARSET = new SettingsMap.Key<>("MODPACK_CHARSET"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java index cc8fae2ba95..c997da35624 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java @@ -35,6 +35,7 @@ import org.jackhuang.hmcl.ui.wizard.WizardProvider; import org.jackhuang.hmcl.util.SettingsMap; import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.io.IOUtils; import java.io.FileNotFoundException; import java.io.IOException; @@ -94,6 +95,9 @@ private Task finishModpackInstallingAsync(SettingsMap settings) { boolean isManuallyCreated = settings.getOrDefault(LocalModpackPage.MODPACK_MANUALLY_CREATED, false); if (isManuallyCreated) { + if (selected == null || name == null || charset == null) { + return null; + } return ModpackHelper.getInstallManuallyCreatedModpackTask(profile, selected, name, charset); } @@ -145,7 +149,18 @@ public Object finish(SettingsMap settings) { } }); - return finishModpackInstallingAsync(settings); + ModpackHelper.LauncherWrapper wrapper = settings.get(LocalModpackPage.MODPACK_WRAPPER); + try { + Task task = finishModpackInstallingAsync(settings); + if (task != null && wrapper != null) { + ModpackHelper.LauncherWrapper ownedWrapper = wrapper; + wrapper = null; + task = task.whenComplete(Schedulers.defaultScheduler(), ignored -> IOUtils.closeQuietly(ownedWrapper)); + } + return task; + } finally { + IOUtils.closeQuietly(wrapper); + } } private static Node createModpackInstallPage(WizardController controller) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardController.java index c1a0b0bf28d..5790f3c9391 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardController.java @@ -20,6 +20,7 @@ import javafx.scene.Node; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.SettingsMap; +import org.jetbrains.annotations.Nullable; import java.util.*; @@ -55,19 +56,20 @@ public List getPages() { @Override public void onStart() { - Objects.requireNonNull(provider); + WizardProvider activeProvider = Objects.requireNonNull(provider); + stopped = false; settings.clear(); - provider.start(settings); + activeProvider.start(settings); pages.clear(); Node page = navigatingTo(0); - pages.push(page); if (stopped) { // navigatingTo may stop this wizard. return; } + pages.push(page); if (page instanceof WizardPage) ((WizardPage) page).onNavigate(settings); @@ -87,12 +89,11 @@ public void onNext(Node page) { } public void onNext(Node page, NavigationDirection direction) { - pages.push(page); - if (stopped) { // navigatingTo may stop this wizard. return; } + pages.push(page); if (page instanceof WizardPage) ((WizardPage) page).onNavigate(settings); @@ -107,7 +108,7 @@ public void onPrev(boolean cleanUp) { public void onPrev(boolean cleanUp, NavigationDirection direction) { if (!canPrev()) { - if (provider.cancelIfCannotGoBack()) { + if (Objects.requireNonNull(provider).cancelIfCannotGoBack()) { onCancel(); return; } else { @@ -134,7 +135,8 @@ public boolean canPrev() { @Override public void onFinish() { - Object result = provider.finish(settings); + WizardProvider activeProvider = Objects.requireNonNull(provider); + @Nullable Object result = activeProvider.finish(settings); if (result instanceof Summary) displayer.navigateTo(((Summary) result).getComponent(), NavigationDirection.NEXT); else if (result instanceof Task) displayer.handleTask(settings, ((Task) result)); else if (result != null) throw new IllegalStateException("Unrecognized wizard result: " + result); @@ -143,8 +145,17 @@ public void onFinish() { @Override public void onEnd() { stopped = true; + while (!pages.isEmpty()) { + Node page = pages.pop(); + if (page instanceof WizardPage wizardPage) { + try { + wizardPage.cleanup(settings); + } catch (RuntimeException e) { + LOG.warning("Failed to clean up wizard page " + page, e); + } + } + } settings.clear(); - pages.clear(); displayer.onEnd(); } @@ -155,6 +166,6 @@ public void onCancel() { } protected Node navigatingTo(int step) { - return provider.createPage(this, step, settings); + return Objects.requireNonNull(provider).createPage(this, step, settings); } }