Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,19 @@
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;
import org.jackhuang.hmcl.util.gson.JsonUtils;
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;
Expand Down Expand Up @@ -124,6 +127,40 @@ public static Modpack readModpackManifest(Path file, Charset charset) throws Uns
throw new UnsupportedModpackException(file.toString());
}

/// 存储解析启动器包装 ZIP 后的结果
/// @param innerPath 包装文件系统内的整合包条目路径
/// @param wrapperFs 包装文件系统;当不再需要 [innerPath] 时必须被关闭
public record LauncherWrapper(Path innerPath, FileSystem wrapperFs) implements Closeable {
/// 关闭包装的 [FileSystem]。
@Override
public void close() throws IOException {
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

为什么要用 ZipFileSystem?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

可以在代码改动少且不解压临时文件的情况下实现这个功能

getPath 直接返回 Path,与现有代码兼容

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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,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.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicReference;

import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
Expand All @@ -53,6 +58,11 @@ public final class LocalModpackPage extends ModpackPage {
private Modpack manifest = null;
private Charset charset;

private final AtomicReference<FileSystem> wrapperFsRef = new AtomicReference<>();
@Nullable
private volatile Path resolvedModpackFile;
private volatile boolean cleanedUp;

public LocalModpackPage(WizardController controller) {
super(controller);

Expand Down Expand Up @@ -104,10 +114,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()) {
var wrapper = ModpackHelper.unwrapIfLauncherWrapper(selectedFile, encoding);
if (wrapper != null) {
actualFile = wrapper.innerPath();
resolvedModpackFile = actualFile;
wrapperFsRef.set(wrapper.wrapperFs());
}
}
Comment on lines +118 to +125

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

配合 ModpackHelper.unwrapIfLauncherWrapper 改为返回临时文件 Path 的方案,这里不需要再处理复杂的 MODPACK_WRAPPER_FS,而是改为记录并管理临时文件 MODPACK_TEMP_FILE 的生命周期。这可以极大简化逻辑并避免文件系统泄漏。

                    if (selectedFile.getFileSystem() == FileSystems.getDefault()) {
                        Path unwrapped = ModpackHelper.unwrapIfLauncherWrapper(selectedFile, encoding);
                        if (unwrapped != null) {
                            actualFile = unwrapped;
                            controller.getSettings().put(MODPACK_FILE, unwrapped);
                            Path oldTemp = controller.getSettings().put(MODPACK_TEMP_FILE, unwrapped);
                            if (oldTemp != null) {
                                try {
                                    java.nio.file.Files.deleteIfExists(oldTemp);
                                } catch (IOException ignored) {
                                }
                            }
                        } else {
                            Path oldTemp = controller.getSettings().remove(MODPACK_TEMP_FILE);
                            if (oldTemp != null) {
                                try {
                                    java.nio.file.Files.deleteIfExists(oldTemp);
                                } catch (IOException ignored) {
                                }
                            }
                        }
                    }

manifest = ModpackHelper.readModpackManifest(actualFile, encoding);
return manifest;
})
.whenComplete(Schedulers.javafx(), (manifest, exception) -> {
Comment on lines 115 to 129

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

在锠包包含启刨器的整合包包含启刨器的恐怕在快递切页或取消的情况下,帽步任务 Task.supplyAsync 可能会在页面已被清理(cleanup 已被调用)后才执行完毟。这会导致两个严重问题:

  1. 资源泄露:新创建的 wrapper.getValue()(即 FileSystem)会被放入 controller.getSettings() 中,但由于 cleanup 已经执行完毟,该文件系统将永远不会被关闭。
  2. 状态污染:已销毁页面的帽步任务会向共享的 controller.getSettings() 写入数据,从而覆盖新页面的设置。

解决方案
LocalModpackPage 中引入一个 private volatile boolean cleanedUp = false; 字段。在 cleanup 方法中将其置为 true。在帽步任务的 thenApplyAsyncwhenComplete 中检查该标志,如果已清理,将立即关闭新创建的 FileSystem 并退出,避免修改共享设置。

                .thenApplyAsync(encoding -> {
                    charset = encoding;
                    Path actualFile = selectedFile;
                    if (selectedFile.getFileSystem() == FileSystems.getDefault()) {
                        var wrapper = ModpackHelper.unwrapIfLauncherWrapper(selectedFile, encoding);
                        if (wrapper != null) {
                            if (cleanedUp) {
                                try {
                                    wrapper.getValue().close();
                                } catch (IOException ignored) {
                                }
                                return null;
                            }
                            actualFile = wrapper.getKey();
                            controller.getSettings().put(MODPACK_FILE, wrapper.getKey());
                            FileSystem oldFs = controller.getSettings().put(MODPACK_WRAPPER_FS, wrapper.getValue());
                            if (oldFs != null) {
                                try {
                                    oldFs.close();
                                } catch (IOException ignored) {
                                    // Ignore close errors for wrapper filesystem
                                }
                            }
                        } else {
                            FileSystem oldFs = controller.getSettings().remove(MODPACK_WRAPPER_FS);
                            if (oldFs != null) {
                                try { 
                                    oldFs.close();
                                } catch (IOException ignored) {
                                    // Ignore close errors for wrapper filesystem
                                }
                            }
                        }
                    }
                    manifest = ModpackHelper.readModpackManifest(actualFile, encoding);
                    return manifest;
                })
                .whenComplete(Schedulers.javafx(), (manifest, exception) -> {
                    if (cleanedUp) return;

FileSystem fs = wrapperFsRef.getAndSet(null);
if (fs != null) {
if (exception != null || cleanedUp) {
IOUtils.closeQuietly(fs);
} else {
Path innerPath = resolvedModpackFile;
if (innerPath != null) {
controller.getSettings().put(MODPACK_FILE, innerPath);
}
controller.getSettings().put(MODPACK_WRAPPER_FS, fs);
}
}

if (exception instanceof ManuallyCreatedModpackException) {
hideSpinner();
nameProperty.set(FileUtils.getName(selectedFile));
Expand Down Expand Up @@ -146,7 +178,12 @@ public LocalModpackPage(WizardController controller) {

@Override
public void cleanup(SettingsMap settings) {
cleanedUp = true;
settings.remove(MODPACK_FILE);
// 同时从 AtomicReference(后台任务可能尚未转移)
// 和 settings(可能已被 whenComplete 转移)中关闭 FS。
IOUtils.closeQuietly(wrapperFsRef.getAndSet(null));
IOUtils.closeQuietly(settings.remove(MODPACK_WRAPPER_FS));
}
Comment on lines 180 to 187

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

配合上述的 cleanedUp 机制,在 cleanup 方法中将 cleanedUp 标志置为 true,以确保在页面销毁后能够正确中止帽步任务并释放资源。请注意需要在 LocalModpackPage 类中声明 private volatile boolean cleanedUp = false; 成员变量。

    @Override
    public void cleanup(SettingsMap settings) {
        this.cleanedUp = true;
        settings.remove(MODPACK_FILE);
        FileSystem wrapperFs = settings.remove(MODPACK_WRAPPER_FS);
        if (wrapperFs != null) {
            try {
                wrapperFs.close();
            } catch (IOException ignored) {
                // Ignore close errors for wrapper filesystem
            }
        }
    }


protected void onInstall() {
Expand Down Expand Up @@ -179,6 +216,7 @@ protected void onDescribe() {
}

public static final SettingsMap.Key<Path> MODPACK_FILE = new SettingsMap.Key<>("MODPACK_FILE");
public static final SettingsMap.Key<FileSystem> MODPACK_WRAPPER_FS = new SettingsMap.Key<>("MODPACK_WRAPPER_FS");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

MODPACK_WRAPPER_FS 替换为 MODPACK_TEMP_FILE

Suggested change
public static final SettingsMap.Key<FileSystem> MODPACK_WRAPPER_FS = new SettingsMap.Key<>("MODPACK_WRAPPER_FS");
public static final SettingsMap.Key<Path> MODPACK_TEMP_FILE = new SettingsMap.Key<>("MODPACK_TEMP_FILE");

public static final SettingsMap.Key<String> MODPACK_NAME = new SettingsMap.Key<>("MODPACK_NAME");
public static final SettingsMap.Key<Modpack> MODPACK_MANIFEST = new SettingsMap.Key<>("MODPACK_MANIFEST");
public static final SettingsMap.Key<Charset> MODPACK_CHARSET = new SettingsMap.Key<>("MODPACK_CHARSET");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,13 @@
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 org.jetbrains.annotations.Nullable;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.FileSystem;
import java.nio.file.Path;

import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
Expand Down Expand Up @@ -145,7 +148,19 @@ public Object finish(SettingsMap settings) {
}
});

return finishModpackInstallingAsync(settings);
@Nullable FileSystem wrapperFs = settings.remove(LocalModpackPage.MODPACK_WRAPPER_FS);
try {
Task<?> task = finishModpackInstallingAsync(settings);
if (task != null && wrapperFs != null) {
FileSystem fs = wrapperFs;
wrapperFs = null;
task = task.whenComplete(Schedulers.defaultScheduler(),
ignored -> IOUtils.closeQuietly(fs));
}
return task;
} finally {
IOUtils.closeQuietly(wrapperFs);
}
}

private static Node createModpackInstallPage(WizardController controller) {
Expand Down