使用独立线程保存设置 (#3929)

This commit is contained in:
Glavo 2025-06-08 12:33:13 +08:00 committed by GitHub
parent bc911b95a0
commit 8889cca878
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 179 additions and 77 deletions

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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)));
}
/**

View File

@ -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<String> 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<Object, Object> storage) {

View File

@ -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<String> 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<String> 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());
}
}

View File

@ -111,6 +111,7 @@ public final class CrashReporter implements Thread.UncaughtExceptionHandler {
LOG.error("Unable to handle uncaught exception", handlingException);
}
FileSaver.shutdown();
LOG.shutdown();
}

View File

@ -0,0 +1,157 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
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<Path, String> SHUTDOWN = Pair.pair(null, null);
private static final BlockingQueue<Pair<Path, String>> 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<Path, String> map) {
for (Map.Entry<Path, String> 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<Path, String> map = new HashMap<>();
ArrayList<Pair<Path, String>> buffer = new ArrayList<>();
while (!stopped) {
if (shutdown) {
stopCurrentSaver();
} else {
Pair<Path, String> 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<Path, String> 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<Path, String> map = new HashMap<>();
for (Pair<Path, String> pair : queue) {
if (pair != SHUTDOWN)
map.put(pair.getKey(), pair.getValue());
}
doSave(map);
} finally {
runningLock.unlock();
}
}
}
}