From 93c56b21315733911898c0e09855cba7675d90d6 Mon Sep 17 00:00:00 2001 From: dwgx <143298346+dwgx@users.noreply.github.com> Date: Mon, 3 Aug 2026 19:35:21 +0900 Subject: [PATCH] =?UTF-8?q?Fix:=20=E5=88=A0=E9=99=A4=E5=AE=9E=E4=BE=8B?= =?UTF-8?q?=E6=97=B6=E5=85=88=E8=90=BD=E7=9B=98=E5=BE=85=E5=86=99=E5=85=A5?= =?UTF-8?q?=E7=9A=84=E8=AE=BE=E7=BD=AE=EF=BC=8C=E9=81=BF=E5=85=8D=E5=AE=9E?= =?UTF-8?q?=E4=BE=8B=E7=9B=AE=E5=BD=95=E8=A2=AB=E9=87=8D=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit saveGameSettings 通过 FileSaver 异步写入实例设置文件。FileSaver 在后台线程 上合并写入,收到第一个写入请求后会先 sleep 200ms 等待更多变更再统一落盘, 因此提交写入与实际落盘之间存在一个时间窗口。 实例设置文件位于将被删除的实例目录内部: <游戏目录>/versions/<实例>///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 上通过。本修复后该测试无需改动 即可通过,测试本身不含任何变更。 --- .../hmcl/game/HMCLGameRepository.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index 0b704310602..ce9642f353e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -244,8 +244,16 @@ public void clean(GameInstanceID instanceId) throws IOException { } /// Removes an instance from disk and clears its cached HMCL settings state. + /// + /// Pending instance settings writes are flushed first. [#saveGameSettings(GameInstanceID)] hands the file to + /// [FileSaver], which batches writes on a background thread, and the settings file lives inside the instance + /// root that is about to be removed. Because [FileUtils#saveSafely] recreates missing parent directories, a + /// write that lands after the removal would recreate the instance directory and bring the removed settings + /// back on the next refresh. @Override public boolean removeInstanceFromDisk(GameInstanceID instanceId) { + flushPendingSettingsWrites(); + boolean removed = super.removeInstanceFromDisk(instanceId); if (removed) { instanceGameSettings.remove(instanceId); @@ -256,6 +264,18 @@ public boolean removeInstanceFromDisk(GameInstanceID instanceId) { return removed; } + /// Waits until [FileSaver] has written everything queued so far. + /// + /// Called before removing files that queued writes may target, so that a pending write cannot recreate them. + private static void flushPendingSettingsWrites() { + try { + FileSaver.waitForAllSaves(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.warning("Interrupted while flushing pending settings writes", e); + } + } + public void duplicateInstance(GameInstanceID srcId, GameInstanceID dstId, boolean copySaves) throws IOException { Path srcDir = getInstanceRoot(srcId); Path dstDir = getInstanceRoot(dstId);