Skip to content

fix(#6153): 导入包含启动器的整合包时提示"无法识别"#6225

Open
YELANDAOKONG wants to merge 10 commits into
HMCL-dev:mainfrom
YELANDAOKONG:fix-6153
Open

fix(#6153): 导入包含启动器的整合包时提示"无法识别"#6225
YELANDAOKONG wants to merge 10 commits into
HMCL-dev:mainfrom
YELANDAOKONG:fix-6153

Conversation

@YELANDAOKONG

Copy link
Copy Markdown
Contributor

@YELANDAOKONG

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

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.

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.

Comment on lines +134 to +157
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;
}

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

使用 Zip FileSystem 的 Path 作为 MODPACK_FILE 会导致后续的安装步骤或解析步骤中抛出 UnsupportedOperationExceptionProviderMismatchException。这是因为 Java 的 ZipFileSystemProvider 在许多 JDK 版本中不支持非默认文件系统(Default FileSystem)的 Path 对象,且 HMCL 的许多安装器和工具类(例如 CompressingUtils.createReadOnlyZipFileSystem)会直接或间接调用 toAbsolutePath()toFile()FileSystems.newFileSystem(Path, ...),这些操作在非默认文件系统的 Path 上会失败。

此外,在 LocalModpackPageModpackInstallWizardProvider 中手动管理 MODPACK_WRAPPER_FS 的生命周期非常繁琐且容易出错(需要在 cleanup、finish、异常处理等多个地方手动 close)。

建议的改进方案:
unwrapIfLauncherWrapper 中,如果检测到是包裹整合包,直接将内部的 modpack.zipmodpack.mrpack 解压到一个临时文件中(使用 Files.createTempFile),并返回该临时文件的 Path。这样:

  1. 临时文件位于默认文件系统上,所有现有的安装器和工具类都能完美兼容,不会有任何 UnsupportedOperationException 风险。
  2. 无需在 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;
    }

@YELANDAOKONG YELANDAOKONG Jun 26, 2026

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.

测试中没有任何Exception出现;

根据反编译检查和实际测试,ZipFileSystemProvider 的 UnsupportedOperationException 仅在 JDK 8下出现,JDK 11及以上没有相关问题

Comment on lines +111 to +134
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
}
}
}
}

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) {
                                }
                            }
                        }
                    }

Comment on lines +178 to +185
FileSystem wrapperFs = settings.remove(MODPACK_WRAPPER_FS);
if (wrapperFs != null) {
try {
wrapperFs.close();
} catch (IOException ignored) {
// Ignore close errors for wrapper filesystem
}
}

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

配合临时文件方案,在 cleanup 时删除临时文件即可。

        Path tempFile = settings.remove(MODPACK_TEMP_FILE);
        if (tempFile != null) {
            try {
                java.nio.file.Files.deleteIfExists(tempFile);
            } catch (IOException ignored) {
            }
        }

}

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");

Comment on lines +149 to +180
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
}
}
}

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_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) {
                }
            }
        }

@YELANDAOKONG

Copy link
Copy Markdown
Contributor Author

测试中没有任何Exception出现;

根据反编译检查和实际测试,ZipFileSystemProvider 的 UnsupportedOperationException 仅在 JDK 8下出现,JDK 11及以上没有相关问题

@YELANDAOKONG

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

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.

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.

Comment on lines 108 to 138
.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) -> {

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;

Comment on lines 176 to 186
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
}
}
}

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
            }
        }
    }

public static Pair<Path, FileSystem> 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,与现有代码兼容

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants