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..396218d2c41 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -67,6 +67,7 @@ import java.nio.file.Path; import java.time.Instant; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -109,6 +110,8 @@ public record InstanceReference(HMCLGameRepository repository, @Nullable GameIns private final Set loadedInstanceGameSettings = new HashSet<>(); private final Set readOnlyInstanceGameSettings = new HashSet<>(); private final Set beingModpackInstances = new HashSet<>(); + /// Instance IDs whose instance game settings must not be written back to disk + private final Set beingRemovedInstances = ConcurrentHashMap.newKeySet(); public final EventManager onInstanceIconChanged = new EventManager<>(); @@ -243,17 +246,43 @@ public void clean(GameInstanceID instanceId) throws IOException { clean(getRunDirectory(instanceId)); } + /// Marks the instance as being removed so that instance game settings changes + /// are not written back to disk + public void markInstanceBeingRemoved(GameInstanceID instanceId) { + beingRemovedInstances.add(instanceId); + } + + /// Unmarks the instance so that instance game settings can be saved again, + /// e.g. when the removal failed. + public void unmarkInstanceBeingRemoved(GameInstanceID instanceId) { + beingRemovedInstances.remove(instanceId); + } + /// Removes an instance from disk and clears its cached HMCL settings state. @Override public boolean removeInstanceFromDisk(GameInstanceID instanceId) { - boolean removed = super.removeInstanceFromDisk(instanceId); - if (removed) { - instanceGameSettings.remove(instanceId); - loadedInstanceGameSettings.remove(instanceId); - readOnlyInstanceGameSettings.remove(instanceId); - beingModpackInstances.remove(instanceId); + beingRemovedInstances.add(instanceId); + try { + // Flush settings saves that were enqueued before the removal started, so that they + // cannot recreate the instance directory after it has been moved + try { + FileSaver.waitForAllSaves(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.warning("Interrupted while waiting for pending file saves", e); + } + + boolean removed = super.removeInstanceFromDisk(instanceId); + if (removed) { + instanceGameSettings.remove(instanceId); + loadedInstanceGameSettings.remove(instanceId); + readOnlyInstanceGameSettings.remove(instanceId); + beingModpackInstances.remove(instanceId); + } + return removed; + } finally { + beingRemovedInstances.remove(instanceId); } - return removed; } public void duplicateInstance(GameInstanceID srcId, GameInstanceID dstId, boolean copySaves) throws IOException { @@ -680,7 +709,9 @@ else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE)) } public void saveGameSettings(GameInstanceID instanceId) { - if (!instanceGameSettings.containsKey(instanceId) || readOnlyInstanceGameSettings.contains(instanceId)) + if (beingRemovedInstances.contains(instanceId) + || !instanceGameSettings.containsKey(instanceId) + || readOnlyInstanceGameSettings.contains(instanceId)) return; GameSettings.Instance setting = instanceGameSettings.get(instanceId); if (setting == null) { @@ -705,7 +736,9 @@ public void saveGameSettings(GameInstanceID instanceId) { /// @param instanceId the instance ID /// @throws IOException if saving the file fails private void saveGameSettingsSync(GameInstanceID instanceId) throws IOException { - if (!instanceGameSettings.containsKey(instanceId) || readOnlyInstanceGameSettings.contains(instanceId)) { + if (beingRemovedInstances.contains(instanceId) + || !instanceGameSettings.containsKey(instanceId) + || readOnlyInstanceGameSettings.contains(instanceId)) { return; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java index 4d62e3263b3..31b007d4f95 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java @@ -128,8 +128,13 @@ public static void deleteInstance(HMCLGameRepository repository, GameInstanceID JFXButton deleteButton = new JFXButton(i18n("button.delete")); deleteButton.getStyleClass().add("dialog-error"); deleteButton.setOnAction(e -> { + // Block instance game settings from being written back to disk while the deletion + // task is running, otherwise a settings change during the deletion may recreate + // the instance directory after it has been moved to the trash + repository.markInstanceBeingRemoved(instanceId); Task.supplyAsync(Schedulers.io(), () -> repository.removeInstanceFromDisk(instanceId)) .whenComplete(Schedulers.javafx(), (result, exception) -> { + repository.unmarkInstanceBeingRemoved(instanceId); if (exception != null || !Boolean.TRUE.equals(result)) { Controllers.dialog(i18n("instance.manage.remove.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR); }