fix(#6153): 导入包含启动器的整合包时提示"无法识别"#6225
Conversation
…llWizardProvider.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces support for detecting and unwrapping HMCL launcher wrapper ZIPs that embed the actual modpack (such as modpack.zip or modpack.mrpack). The current implementation attempts to pass the inner Zip FileSystem's Path and manage its lifecycle across the installation wizard. However, the reviewer points out that using a non-default FileSystem Path can lead to compatibility issues (such as UnsupportedOperationException) with existing tools. The reviewer suggests a robust alternative: extracting the embedded modpack to a temporary file on the default file system, which simplifies lifecycle management and prevents potential resource leaks.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| public static Pair<Path, FileSystem> 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)) { | ||
| FileSystem fs = outerFs; | ||
| outerFs = null; | ||
| return pair(entryPath, fs); | ||
| } | ||
| } | ||
| } catch (IOException ignored) { | ||
| } finally { | ||
| if (outerFs != null) { | ||
| try { | ||
| outerFs.close(); | ||
| } catch (IOException ignored) { | ||
| // Ignore close errors for wrapper filesystem | ||
| } | ||
| } | ||
| } | ||
| return null; | ||
| } |
There was a problem hiding this comment.
使用 Zip FileSystem 的 Path 作为 MODPACK_FILE 会导致后续的安装步骤或解析步骤中抛出 UnsupportedOperationException 或 ProviderMismatchException。这是因为 Java 的 ZipFileSystemProvider 在许多 JDK 版本中不支持非默认文件系统(Default FileSystem)的 Path 对象,且 HMCL 的许多安装器和工具类(例如 CompressingUtils.createReadOnlyZipFileSystem)会直接或间接调用 toAbsolutePath()、toFile() 或 FileSystems.newFileSystem(Path, ...),这些操作在非默认文件系统的 Path 上会失败。
此外,在 LocalModpackPage 和 ModpackInstallWizardProvider 中手动管理 MODPACK_WRAPPER_FS 的生命周期非常繁琐且容易出错(需要在 cleanup、finish、异常处理等多个地方手动 close)。
建议的改进方案:
在 unwrapIfLauncherWrapper 中,如果检测到是包裹整合包,直接将内部的 modpack.zip 或 modpack.mrpack 解压到一个临时文件中(使用 Files.createTempFile),并返回该临时文件的 Path。这样:
- 临时文件位于默认文件系统上,所有现有的安装器和工具类都能完美兼容,不会有任何
UnsupportedOperationException风险。 - 无需在 settings 中传递和管理复杂的
FileSystem对象,只需在安装任务完成或向导清理时删除该临时文件即可,代码会更加简洁和健壮。
@Nullable
public static Path unwrapIfLauncherWrapper(Path file, Charset charset) {
try (FileSystem outerFs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
for (String innerName : new String[]{"modpack.zip", "modpack.mrpack"}) {
Path entryPath = outerFs.getPath("/" + innerName);
if (Files.isRegularFile(entryPath)) {
String ext = FileUtils.getExtension(innerName);
Path tempFile = Files.createTempFile("hmcl-modpack-", "." + ext);
Files.copy(entryPath, tempFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
tempFile.toFile().deleteOnExit();
return tempFile;
}
}
} catch (IOException ignored) {
}
return null;
}There was a problem hiding this comment.
测试中没有任何Exception出现;
根据反编译检查和实际测试,ZipFileSystemProvider 的 UnsupportedOperationException 仅在 JDK 8下出现,JDK 11及以上没有相关问题
| if (selectedFile.getFileSystem() == FileSystems.getDefault()) { | ||
| var wrapper = ModpackHelper.unwrapIfLauncherWrapper(selectedFile, encoding); | ||
| if (wrapper != 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 | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
配合 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) {
}
}
}
}| FileSystem wrapperFs = settings.remove(MODPACK_WRAPPER_FS); | ||
| if (wrapperFs != null) { | ||
| try { | ||
| wrapperFs.close(); | ||
| } catch (IOException ignored) { | ||
| // Ignore close errors for wrapper filesystem | ||
| } | ||
| } |
| } | ||
|
|
||
| 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"); |
There was a problem hiding this comment.
| FileSystem wrapperFs = settings.get(LocalModpackPage.MODPACK_WRAPPER_FS); | ||
| Task<?> task; | ||
| try { | ||
| task = finishModpackInstallingAsync(settings); | ||
| } catch (Throwable t) { | ||
| if (wrapperFs != null) { | ||
| try { | ||
| wrapperFs.close(); | ||
| } catch (IOException ignored) { | ||
| // Ignore close errors for wrapper filesystem | ||
| } | ||
| } | ||
| throw t; | ||
| } | ||
| if (wrapperFs != null) { | ||
| if (task != null) { | ||
| settings.remove(LocalModpackPage.MODPACK_WRAPPER_FS); | ||
| task = task.whenComplete(Schedulers.defaultScheduler(), ignored -> { | ||
| try { | ||
| wrapperFs.close(); | ||
| } catch (IOException e) { | ||
| // Ignore close errors for wrapper filesystem | ||
| } | ||
| }); | ||
| } else { | ||
| try { | ||
| wrapperFs.close(); | ||
| } catch (IOException ignored) { | ||
| // Ignore close errors for wrapper filesystem | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
配合临时文件方案,在安装任务结束或异常时,清理临时文件 MODPACK_TEMP_FILE。
Path tempFile = settings.get(LocalModpackPage.MODPACK_TEMP_FILE);
Task<?> task;
try {
task = finishModpackInstallingAsync(settings);
} catch (Throwable t) {
if (tempFile != null) {
try {
java.nio.file.Files.deleteIfExists(tempFile);
} catch (IOException ignored) {
}
}
throw t;
}
if (tempFile != null) {
if (task != null) {
settings.remove(LocalModpackPage.MODPACK_TEMP_FILE);
task = task.whenComplete(Schedulers.defaultScheduler(), ignored -> {
try {
java.nio.file.Files.deleteIfExists(tempFile);
} catch (IOException e) {
// Ignore
}
});
} else {
try {
java.nio.file.Files.deleteIfExists(tempFile);
} catch (IOException ignored) {
}
}
}|
测试中没有任何Exception出现; 根据反编译检查和实际测试,ZipFileSystemProvider 的 UnsupportedOperationException 仅在 JDK 8下出现,JDK 11及以上没有相关问题 |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request adds support for detecting and unwrapping HMCL launcher wrapper ZIPs that embed actual modpacks (such as modpack.zip or modpack.mrpack), managing the lifecycle of the wrapper's FileSystem during the installation wizard. The review feedback correctly identifies a potential race condition and resource leak if the asynchronous task completes after the page is cleaned up, and suggests introducing a volatile boolean flag to safely handle cancellation and prevent state pollution.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| .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.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) -> { |
There was a problem hiding this comment.
在锠包包含启刨器的整合包包含启刨器的恐怕在快递切页或取消的情况下,帽步任务 Task.supplyAsync 可能会在页面已被清理(cleanup 已被调用)后才执行完毟。这会导致两个严重问题:
- 资源泄露:新创建的
wrapper.getValue()(即FileSystem)会被放入controller.getSettings()中,但由于cleanup已经执行完毟,该文件系统将永远不会被关闭。 - 状态污染:已销毁页面的帽步任务会向共享的
controller.getSettings()写入数据,从而覆盖新页面的设置。
解决方案:
在 LocalModpackPage 中引入一个 private volatile boolean cleanedUp = false; 字段。在 cleanup 方法中将其置为 true。在帽步任务的 thenApplyAsync 和 whenComplete 中检查该标志,如果已清理,将立即关闭新创建的 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;| public void cleanup(SettingsMap settings) { | ||
| 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 | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
配合上述的 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
}
}
}| public static Pair<Path, FileSystem> unwrapIfLauncherWrapper(Path file, Charset charset) { | ||
| FileSystem outerFs = null; | ||
| try { | ||
| outerFs = CompressingUtils.createReadOnlyZipFileSystem(file, charset); |
There was a problem hiding this comment.
可以在代码改动少且不解压临时文件的情况下实现这个功能
getPath 直接返回 Path,与现有代码兼容
#6153