mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-08-03 19:36:53 -04:00
使用独立线程保存设置 (#3929)
This commit is contained in:
parent
bc911b95a0
commit
8889cca878
@ -27,6 +27,7 @@ import javafx.scene.input.DataFormat;
|
|||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||||
import org.jackhuang.hmcl.setting.SambaException;
|
import org.jackhuang.hmcl.setting.SambaException;
|
||||||
|
import org.jackhuang.hmcl.util.FileSaver;
|
||||||
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
import org.jackhuang.hmcl.ui.Controllers;
|
import org.jackhuang.hmcl.ui.Controllers;
|
||||||
@ -214,6 +215,7 @@ public final class Launcher extends Application {
|
|||||||
@Override
|
@Override
|
||||||
public void stop() throws Exception {
|
public void stop() throws Exception {
|
||||||
Controllers.onApplicationStop();
|
Controllers.onApplicationStop();
|
||||||
|
FileSaver.shutdown();
|
||||||
LOG.shutdown();
|
LOG.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ package org.jackhuang.hmcl;
|
|||||||
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
|
import org.jackhuang.hmcl.util.FileSaver;
|
||||||
import org.jackhuang.hmcl.ui.AwtUtils;
|
import org.jackhuang.hmcl.ui.AwtUtils;
|
||||||
import org.jackhuang.hmcl.util.ModuleHelper;
|
import org.jackhuang.hmcl.util.ModuleHelper;
|
||||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
||||||
@ -79,6 +80,7 @@ public final class Main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void exit(int exitCode) {
|
public static void exit(int exitCode) {
|
||||||
|
FileSaver.shutdown();
|
||||||
LOG.shutdown();
|
LOG.shutdown();
|
||||||
System.exit(exitCode);
|
System.exit(exitCode);
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ import org.jackhuang.hmcl.mod.Modpack;
|
|||||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||||
import org.jackhuang.hmcl.mod.ModpackProvider;
|
import org.jackhuang.hmcl.mod.ModpackProvider;
|
||||||
import org.jackhuang.hmcl.setting.Profile;
|
import org.jackhuang.hmcl.setting.Profile;
|
||||||
|
import org.jackhuang.hmcl.util.FileSaver;
|
||||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
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))
|
if (!localVersionSettings.containsKey(id))
|
||||||
return false;
|
return;
|
||||||
File file = getLocalVersionSettingFile(id);
|
Path file = getLocalVersionSettingFile(id).toPath().toAbsolutePath().normalize();
|
||||||
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
LOG.info("Saving version setting: " + id);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
FileUtils.writeText(file, GSON.toJson(localVersionSettings.get(id)));
|
Files.createDirectories(file.getParent());
|
||||||
return true;
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.error("Unable to save version setting of " + id, e);
|
LOG.warning("Failed to create directory: " + file.getParent(), e);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileSaver.save(file, GSON.toJson(localVersionSettings.get(id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,9 +35,7 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
|||||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||||
import org.jackhuang.hmcl.game.OAuthServer;
|
import org.jackhuang.hmcl.game.OAuthServer;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
import org.jackhuang.hmcl.util.FileSaver;
|
||||||
import org.jackhuang.hmcl.util.Lang;
|
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
|
||||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||||
|
|
||||||
import javax.net.ssl.SSLException;
|
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(() ->
|
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) {
|
private static Account parseAccount(Map<Object, Object> storage) {
|
||||||
|
@ -19,8 +19,7 @@ package org.jackhuang.hmcl.setting;
|
|||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import org.jackhuang.hmcl.Metadata;
|
import org.jackhuang.hmcl.Metadata;
|
||||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
import org.jackhuang.hmcl.util.FileSaver;
|
||||||
import org.jackhuang.hmcl.util.Lang;
|
|
||||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||||
@ -83,18 +82,20 @@ public final class ConfigHolder {
|
|||||||
LOG.info("Config location: " + configLocation);
|
LOG.info("Config location: " + configLocation);
|
||||||
|
|
||||||
configInstance = loadConfig();
|
configInstance = loadConfig();
|
||||||
configInstance.addListener(source -> markConfigDirty());
|
configInstance.addListener(source -> FileSaver.save(configLocation, configInstance.toJson()));
|
||||||
|
|
||||||
globalConfigInstance = loadGlobalConfig();
|
globalConfigInstance = loadGlobalConfig();
|
||||||
globalConfigInstance.addListener(source -> markGlobalConfigDirty());
|
globalConfigInstance.addListener(source -> FileSaver.save(GLOBAL_CONFIG_PATH, globalConfigInstance.toJson()));
|
||||||
|
|
||||||
Locale.setDefault(config().getLocalization().getLocale());
|
Locale.setDefault(config().getLocalization().getLocale());
|
||||||
I18n.setLocale(configInstance.getLocalization());
|
I18n.setLocale(configInstance.getLocalization());
|
||||||
LOG.setLogRetention(globalConfig().getLogRetention());
|
LOG.setLogRetention(globalConfig().getLogRetention());
|
||||||
Settings.init();
|
Settings.init();
|
||||||
|
|
||||||
if (newlyCreated)
|
if (newlyCreated) {
|
||||||
saveConfigSync();
|
LOG.info("Creating config file " + configLocation);
|
||||||
|
FileUtils.saveSafely(configLocation, configInstance.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
if (!Files.isWritable(configLocation)) {
|
if (!Files.isWritable(configLocation)) {
|
||||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||||
@ -169,34 +170,10 @@ public final class ConfigHolder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG.info("Creating an empty config");
|
|
||||||
newlyCreated = true;
|
newlyCreated = true;
|
||||||
return new Config();
|
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
|
// Global Config
|
||||||
|
|
||||||
private static GlobalConfig loadGlobalConfig() throws IOException {
|
private static GlobalConfig loadGlobalConfig() throws IOException {
|
||||||
@ -218,22 +195,4 @@ public final class ConfigHolder {
|
|||||||
return new GlobalConfig();
|
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -111,6 +111,7 @@ public final class CrashReporter implements Thread.UncaughtExceptionHandler {
|
|||||||
LOG.error("Unable to handle uncaught exception", handlingException);
|
LOG.error("Unable to handle uncaught exception", handlingException);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileSaver.shutdown();
|
||||||
LOG.shutdown();
|
LOG.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
157
HMCL/src/main/java/org/jackhuang/hmcl/util/FileSaver.java
Normal file
157
HMCL/src/main/java/org/jackhuang/hmcl/util/FileSaver.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user