Fix: 删除实例时先落盘待写入的设置,避免实例目录被重建 - #6597
Closed
dwgx wants to merge 1 commit into
Closed
Conversation
saveGameSettings 通过 FileSaver 异步写入实例设置文件。FileSaver 在后台线程 上合并写入,收到第一个写入请求后会先 sleep 200ms 等待更多变更再统一落盘, 因此提交写入与实际落盘之间存在一个时间窗口。 实例设置文件位于将被删除的实例目录内部: <游戏目录>/versions/<实例>/<metadata>/<config>/instance-game-settings.json 而 FileUtils.saveSafely 在写入前会调用 Files.createDirectories 补齐缺失的 父目录。因此如果一次排队中的写入在 removeInstanceFromDisk 之后才落盘,它会 把刚被删除的实例目录整棵重建出来,并写回设置文件;removeInstanceFromDisk 的 finally 块中的 refreshAsync 随后又会把这份设置读回内存。 结果是删除实例后 versions/ 下残留一个只含 HMCL 元数据的目录,且已删除实例 的设置被复原。若该设置包含 PROPERTY_RUNNING_DIRECTORY 覆盖项,getRunDirectory 会继续返回实例目录而非游戏目录。 修复方式是在 removeInstanceFromDisk 删除文件前调用 FileSaver.waitForAllSaves, 确保没有写入还在排队。该方法本就是为此设计,UpdateHandler 与 Controllers 在退出前也是这样使用的。删除操作运行在 Schedulers.io 线程上,不在 FileSaver 线程上,因此不存在自等待。 GameDirectoriesTest.newIsolatedInstallingInstanceUsesVersionRootBeforeVersionExists 此前依赖时序,在 macOS 上稳定失败、在 Linux 上通过。本修复后该测试无需改动 即可通过,测试本身不含任何变更。
Member
|
我怀疑本 PR 是由 AI 完全独立完成的,并没有经过你的审查。介于 HMCL 近期被 AI slop 骚扰的情况,我要求您提供相关证据证明本 PR 确实正常工作,或证明你已经充分审查了该 PR 的代码。 |
Contributor
|
注意你提到的测试失败案例,那个案例在两次签出分支之间只会出现一次,不一定是因为你修好了。 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题是什么
删除一个游戏实例后,
versions/目录下可能会残留一个只含 HMCL 元数据的空壳目录,而且这个实例已被删除的设置会被恢复回来。如果被恢复的设置里带有「独立游戏目录」(
PROPERTY_RUNNING_DIRECTORY)覆盖项,那么getRunDirectory会继续返回实例目录,而不是游戏目录。为什么会这样
这是一个时间窗口的问题。
HMCL 保存实例设置时,走的是
saveGameSettings→FileSaver.save。而FileSaver是异步的:它在后台线程上批量写入,收到第一个写入请求后会先Thread.sleep(200)等一会儿,看还有没有别的变更要一起写,然后才统一落盘。所以从「提交写入」到「真正写进磁盘」之间,有一个大约 200 毫秒的窗口。
问题在于,实例设置文件是存在实例目录里面的:
而
FileUtils.saveSafely在写文件之前,会先调用Files.createDirectories把缺失的父目录补齐 —— 这本身是合理的,第一次保存时目录还不存在。于是就出现了这样的顺序:
FileSaver队列,还没落盘removeInstanceFromDisk把versions/<实例>/删掉Files.createDirectories把刚删掉的目录树重建出来,并写回设置文件removeInstanceFromDisk的finally块里的refreshAsync紧接着又把这份设置读回内存结果就是:目录删了又被建回来,设置删了又被读回来。
怎么改的
在
removeInstanceFromDisk真正删文件之前,先等待所有排队中的写入落盘:这样删除发生时队列已经空了,不会再有写入回来重建目录。
FileSaver.waitForAllSaves()本来就是为这个场景设计的(它的文档写的是 "Wait for all saves to complete"),UpdateHandler和Controllers在退出前也是这样用的,这里只是把同样的做法用在删除之前。关于死锁:
removeInstanceFromDisk运行在Schedulers.io()线程上(见Instances.java中删除按钮的处理),不在FileSaver线程上,所以不存在自己等自己的情况。中断异常按惯例恢复中断标志并记录日志。测试
GameDirectoriesTest.newIsolatedInstallingInstanceUsesVersionRootBeforeVersionExists这个测试正好覆盖了这条路径:它保存设置、删除实例,然后断言getRunDirectory回到游戏目录。这个测试此前依赖时序 —— 在 Linux 上通过,在 macOS 上稳定失败:
失败的正是最后那句断言,也就是「删除之后
getRunDirectory应该回到游戏目录」。本 PR 不含任何测试改动,修复生产代码后该测试即可通过:
GameDirectoriesTest:HMCL:test全量我在未修改的
c8929c8上复跑确认过,该失败在修复前就存在,不是别的改动引入的。环境:macOS 26.5.2 / Apple Silicon (arm64) / JDK 21.0.11。
检查项:
./gradlew test checkstyle checkTranslations --rerun-tasks全部通过补充说明
这个失败之所以一直没被发现,是因为它依赖时序 —— Java CI 工作流跑在
ubuntu-latest上,在那个环境下时序恰好不触发。仓库目前没有任何工作流在 macOS 上运行,而HMCL/build.gradle.kts里声明了"macos-x86_64", "macos-arm64"两个目标平台。我可以另开一个 PR 补一条 macOS 测试通道,如果维护者认为有必要的话。