From 8889cca8787a4b74fa8202d51e644d8aadd04107 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 8 Jun 2025 12:33:13 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=8B=AC=E7=AB=8B=E7=BA=BF?= =?UTF-8?q?=E7=A8=8B=E4=BF=9D=E5=AD=98=E8=AE=BE=E7=BD=AE=20(#3929)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/Launcher.java | 2 + .../main/java/org/jackhuang/hmcl/Main.java | 2 + .../hmcl/game/HMCLGameRepository.java | 20 +-- .../org/jackhuang/hmcl/setting/Accounts.java | 19 +-- .../jackhuang/hmcl/setting/ConfigHolder.java | 55 +----- .../jackhuang/hmcl/util/CrashReporter.java | 1 + .../org/jackhuang/hmcl/util/FileSaver.java | 157 ++++++++++++++++++ 7 files changed, 179 insertions(+), 77 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/util/FileSaver.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java index 3b77a208e..548c3c368 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java @@ -27,6 +27,7 @@ import javafx.scene.input.DataFormat; import javafx.stage.Stage; import org.jackhuang.hmcl.setting.ConfigHolder; import org.jackhuang.hmcl.setting.SambaException; +import org.jackhuang.hmcl.util.FileSaver; import org.jackhuang.hmcl.task.AsyncTaskExecutor; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.ui.Controllers; @@ -214,6 +215,7 @@ public final class Launcher extends Application { @Override public void stop() throws Exception { Controllers.onApplicationStop(); + FileSaver.shutdown(); LOG.shutdown(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Main.java b/HMCL/src/main/java/org/jackhuang/hmcl/Main.java index 0a6611ef9..886ac1bc1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Main.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Main.java @@ -19,6 +19,7 @@ package org.jackhuang.hmcl; import javafx.application.Platform; import javafx.scene.control.Alert; +import org.jackhuang.hmcl.util.FileSaver; import org.jackhuang.hmcl.ui.AwtUtils; import org.jackhuang.hmcl.util.ModuleHelper; import org.jackhuang.hmcl.util.SelfDependencyPatcher; @@ -79,6 +80,7 @@ public final class Main { } public static void exit(int exitCode) { + FileSaver.shutdown(); LOG.shutdown(); System.exit(exitCode); } 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 54f699c57..bae19d6e5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -30,6 +30,7 @@ import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.mod.ModpackConfiguration; import org.jackhuang.hmcl.mod.ModpackProvider; import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.util.FileSaver; import org.jackhuang.hmcl.setting.VersionIconType; import org.jackhuang.hmcl.setting.VersionSetting; import org.jackhuang.hmcl.ui.FXUtils; @@ -329,22 +330,17 @@ public class HMCLGameRepository extends DefaultGameRepository { } } - public boolean saveVersionSetting(String id) { + public void saveVersionSetting(String id) { if (!localVersionSettings.containsKey(id)) - return false; - File file = getLocalVersionSettingFile(id); - if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile())) - return false; - - LOG.info("Saving version setting: " + id); - + return; + Path file = getLocalVersionSettingFile(id).toPath().toAbsolutePath().normalize(); try { - FileUtils.writeText(file, GSON.toJson(localVersionSettings.get(id))); - return true; + Files.createDirectories(file.getParent()); } catch (IOException e) { - LOG.error("Unable to save version setting of " + id, e); - return false; + LOG.warning("Failed to create directory: " + file.getParent(), e); } + + FileSaver.save(file, GSON.toJson(localVersionSettings.get(id))); } /** diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java index d20bd6fbf..2dd47aca5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java @@ -35,9 +35,7 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory; import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException; import org.jackhuang.hmcl.game.OAuthServer; import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.util.InvocationDispatcher; -import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.FileSaver; import org.jackhuang.hmcl.util.skin.InvalidSkinException; import javax.net.ssl.SSLException; @@ -184,21 +182,8 @@ public final class Accounts { } } - InvocationDispatcher dispatcher = InvocationDispatcher.runOn(Lang::thread, json -> { - LOG.info("Saving global accounts"); - synchronized (globalAccountsFile) { - try { - synchronized (globalAccountsFile) { - FileUtils.saveSafely(globalAccountsFile, json); - } - } catch (IOException e) { - LOG.error("Failed to save global accounts", e); - } - } - }); - globalAccountStorages.addListener(onInvalidating(() -> - dispatcher.accept(Config.CONFIG_GSON.toJson(globalAccountStorages)))); + FileSaver.save(globalAccountsFile, Config.CONFIG_GSON.toJson(globalAccountStorages)))); } private static Account parseAccount(Map storage) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/ConfigHolder.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/ConfigHolder.java index 4b79b6be7..7bc0f1a64 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/ConfigHolder.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/ConfigHolder.java @@ -19,8 +19,7 @@ package org.jackhuang.hmcl.setting; import com.google.gson.JsonParseException; import org.jackhuang.hmcl.Metadata; -import org.jackhuang.hmcl.util.InvocationDispatcher; -import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.FileSaver; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.JarUtils; @@ -83,18 +82,20 @@ public final class ConfigHolder { LOG.info("Config location: " + configLocation); configInstance = loadConfig(); - configInstance.addListener(source -> markConfigDirty()); + configInstance.addListener(source -> FileSaver.save(configLocation, configInstance.toJson())); globalConfigInstance = loadGlobalConfig(); - globalConfigInstance.addListener(source -> markGlobalConfigDirty()); + globalConfigInstance.addListener(source -> FileSaver.save(GLOBAL_CONFIG_PATH, globalConfigInstance.toJson())); Locale.setDefault(config().getLocalization().getLocale()); I18n.setLocale(configInstance.getLocalization()); LOG.setLogRetention(globalConfig().getLogRetention()); Settings.init(); - if (newlyCreated) - saveConfigSync(); + if (newlyCreated) { + LOG.info("Creating config file " + configLocation); + FileUtils.saveSafely(configLocation, configInstance.toJson()); + } if (!Files.isWritable(configLocation)) { if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS @@ -169,34 +170,10 @@ public final class ConfigHolder { } } - LOG.info("Creating an empty config"); newlyCreated = true; return new Config(); } - private static final InvocationDispatcher configWriter = InvocationDispatcher.runOn(Lang::thread, content -> { - try { - writeToConfig(content); - } catch (IOException e) { - LOG.error("Failed to save config", e); - } - }); - - private static void writeToConfig(String content) throws IOException { - LOG.info("Saving config"); - synchronized (configLocation) { - FileUtils.saveSafely(configLocation, content); - } - } - - private static void markConfigDirty() { - configWriter.accept(configInstance.toJson()); - } - - private static void saveConfigSync() throws IOException { - writeToConfig(configInstance.toJson()); - } - // Global Config private static GlobalConfig loadGlobalConfig() throws IOException { @@ -218,22 +195,4 @@ public final class ConfigHolder { return new GlobalConfig(); } - private static final InvocationDispatcher globalConfigWriter = InvocationDispatcher.runOn(Lang::thread, content -> { - try { - writeToGlobalConfig(content); - } catch (IOException e) { - LOG.error("Failed to save config", e); - } - }); - - private static void writeToGlobalConfig(String content) throws IOException { - LOG.info("Saving global config"); - synchronized (GLOBAL_CONFIG_PATH) { - FileUtils.saveSafely(GLOBAL_CONFIG_PATH, content); - } - } - - private static void markGlobalConfigDirty() { - globalConfigWriter.accept(globalConfigInstance.toJson()); - } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java index bb664e713..3eb8d38cd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java @@ -111,6 +111,7 @@ public final class CrashReporter implements Thread.UncaughtExceptionHandler { LOG.error("Unable to handle uncaught exception", handlingException); } + FileSaver.shutdown(); LOG.shutdown(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/FileSaver.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/FileSaver.java new file mode 100644 index 000000000..a15d7a8e8 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/FileSaver.java @@ -0,0 +1,157 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.util; + +import org.jackhuang.hmcl.util.io.FileUtils; + +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +/** + * @author Glavo + */ +public final class FileSaver extends Thread { + + private static final Pair SHUTDOWN = Pair.pair(null, null); + + private static final BlockingQueue> queue = new LinkedBlockingQueue<>(); + private static final AtomicBoolean running = new AtomicBoolean(false); + private static final ReentrantLock runningLock = new ReentrantLock(); + private static volatile boolean shutdown = false; + + private static void doSave(Map map) { + for (Map.Entry entry : map.entrySet()) { + saveSync(entry.getKey(), entry.getValue()); + } + } + + public static void save(Path file, String content) { + Objects.requireNonNull(file); + Objects.requireNonNull(content); + + ShutdownHook.ensureInstalled(); + + queue.add(Pair.pair(file, content)); + if (running.compareAndSet(false, true)) { + new FileSaver().start(); + } + } + + public static void saveSync(Path file, String content) { + LOG.info("Saving file " + file); + try { + FileUtils.saveSafely(file, content); + } catch (Throwable e) { + LOG.warning("Failed to save " + file, e); + } + } + + public static void shutdown() { + shutdown = true; + queue.add(SHUTDOWN); + } + + private FileSaver() { + super("FileSaver"); + } + + private boolean stopped = false; + + private void stopCurrentSaver() { + // Ensure that each saver calls `running.set(false)` at most once + if (!stopped) { + stopped = true; + running.set(false); + } + } + + @Override + public void run() { + runningLock.lock(); + try { + HashMap map = new HashMap<>(); + ArrayList> buffer = new ArrayList<>(); + + while (!stopped) { + if (shutdown) { + stopCurrentSaver(); + } else { + Pair head = queue.poll(30, TimeUnit.SECONDS); + if (head == null || head == SHUTDOWN) { + stopCurrentSaver(); + } else { + map.put(head.getKey(), head.getValue()); + //noinspection BusyWait + Thread.sleep(200); // Waiting for more changes + } + } + + while (queue.drainTo(buffer) > 0) { + for (Pair pair : buffer) { + if (pair == SHUTDOWN) + stopCurrentSaver(); + else + map.put(pair.getKey(), pair.getValue()); + } + buffer.clear(); + } + + doSave(map); + map.clear(); + } + } catch (InterruptedException e) { + throw new AssertionError("This thread cannot be interrupted", e); + } finally { + runningLock.unlock(); + } + } + + private static final class ShutdownHook extends Thread { + + static { + Runtime.getRuntime().addShutdownHook(new ShutdownHook()); + } + + static void ensureInstalled() { + // Ensure the shutdown hook is installed + } + + @Override + public void run() { + shutdown(); + runningLock.lock(); + try { + HashMap map = new HashMap<>(); + for (Pair pair : queue) { + if (pair != SHUTDOWN) + map.put(pair.getKey(), pair.getValue()); + } + doSave(map); + } finally { + runningLock.unlock(); + } + } + } +}