Reload config if changed

This commit is contained in:
Johannes Frohnmeyer 2022-02-12 12:16:19 +01:00
parent e0e5aaba99
commit d6b593b6e5
Signed by: Johannes
GPG Key ID: E76429612C2929F4
18 changed files with 235 additions and 53 deletions

View File

@ -6,8 +6,9 @@ It includes:
- a Gson strategy to ignore fields annotated with GsonHidden
- LazySupplier, a supplier that caches the result of another supplier to which it delegates
- ThrowingRunnable and ThrowingSupplier, counterparts of their default lambdas which allow exceptions
- a "flags" system to allow dynamically enabling LibJF features through system properties and fabric.mod.jsons
- a "flags" system to allow dynamically enabling LibJF features through system properties and fabric.mod.json
- a shared logger and Gson instance
- the ResourcePath abstraction to describe locations of resources as simple strings
- the CoProcess system used internally to control threads separate from the game thread
All of these are implemented in one reusable class (except the Gson strategy, whose annotation is separate), which should be simple to read

View File

@ -1,4 +1,5 @@
archivesBaseName = "libjf-base"
dependencies {
include modImplementation(fabricApi.module("fabric-lifecycle-events-v1", "${rootProject.fabric_version}"))
}

View File

@ -0,0 +1,6 @@
package io.gitlab.jfronny.libjf.coprocess;
public interface CoProcess {
void start();
void stop();
}

View File

@ -0,0 +1,31 @@
package io.gitlab.jfronny.libjf.coprocess;
import io.gitlab.jfronny.libjf.LibJf;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.loader.api.FabricLoader;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class CoProcessManager implements ModInitializer {
private final List<CoProcess> coProcesses = new ArrayList<>();
@Override
public void onInitialize() {
coProcesses.addAll(FabricLoader.getInstance().getEntrypoints(LibJf.MOD_ID + ":coprocess", CoProcess.class));
Runtime.getRuntime().addShutdownHook(new Thread(() -> coProcesses.forEach(coProcess -> {
coProcess.stop();
if (coProcess instanceof Closeable cl) {
try {
cl.close();
} catch (IOException e) {
LibJf.LOGGER.error("Could not close co-process", e);
}
}
})));
for (CoProcess coProcess : coProcesses) {
coProcess.start();
}
}
}

View File

@ -0,0 +1,34 @@
package io.gitlab.jfronny.libjf.coprocess;
public abstract class ThreadCoProcess implements CoProcess, Runnable {
private Thread th = null;
private boolean closed = true;
@Override
public void start() {
if (th != null) stop();
closed = false;
th = new Thread(this);
th.start();
}
@Override
public void stop() {
if (th == null) return;
closed = true;
try {
th.join();
} catch (InterruptedException e) {
throw new RuntimeException("Could not join co-process thread", e);
}
th = null;
}
@Override
public void run() {
while (!closed) {
executeIteration();
}
}
public abstract void executeIteration();
}

View File

@ -16,6 +16,9 @@
"fabricloader": ">=0.12.0",
"minecraft": "*"
},
"entrypoints": {
"main": ["io.gitlab.jfronny.libjf.coprocess.CoProcessManager"]
},
"custom": {
"modmenu": {
"parent": "libjf",

View File

@ -2,6 +2,7 @@ package io.gitlab.jfronny.libjf.config.api;
import io.gitlab.jfronny.libjf.config.impl.ConfigHolderImpl;
import java.nio.file.Path;
import java.util.Map;
public interface ConfigHolder {
@ -12,5 +13,8 @@ public interface ConfigHolder {
Map<String, ConfigInstance> getRegistered();
ConfigInstance get(Class<?> configClass);
ConfigInstance get(String modId);
boolean isRegistered(Class<?> config);
ConfigInstance get(Path configPath);
boolean isRegistered(Class<?> configClass);
boolean isRegistered(String modId);
boolean isRegistered(Path configPath);
}

View File

@ -1,23 +1,38 @@
package io.gitlab.jfronny.libjf.config.impl;
import com.google.common.collect.ImmutableMap;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.ConfigHolder;
import io.gitlab.jfronny.libjf.config.api.ConfigInstance;
import org.jetbrains.annotations.ApiStatus;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
public class ConfigHolderImpl implements ConfigHolder {
@ApiStatus.Internal
public static final ConfigHolderImpl INSTANCE = new ConfigHolderImpl();
private ConfigHolderImpl() {}
private ConfigHolderImpl() {
}
private final Map<String, ConfigInstance> configs = new HashMap<>();
private final Map<Path, ConfigInstance> configsByPath = new HashMap<>();
@Override
public void register(String modId, Class<?> config) {
if (!isRegistered(config))
configs.put(modId, new ConfigInstanceImpl(modId, config));
if (isRegistered(modId)) {
if (get(modId).matchesConfigClass(config)) {
LibJf.LOGGER.warn("Attempted to set config of " + modId + " twice, skipping");
return;
}
LibJf.LOGGER.warn("Overriding config class of " + modId + " to " + config);
}
if (isRegistered(config)) {
LibJf.LOGGER.warn("Attempted to reuse config class " + config + ", this is unsupported");
}
ConfigInstanceImpl instance = new ConfigInstanceImpl(modId, config);
configs.put(modId, instance);
configsByPath.put(instance.path, instance);
}
@Override
@ -39,6 +54,12 @@ public class ConfigHolderImpl implements ConfigHolder {
return configs.get(configClass);
}
@Override
public ConfigInstance get(Path configPath) {
return configsByPath.get(configPath);
}
@Override
public boolean isRegistered(Class<?> config) {
for (ConfigInstance value : configs.values()) {
if (value.matchesConfigClass(config))
@ -46,4 +67,14 @@ public class ConfigHolderImpl implements ConfigHolder {
}
return false;
}
@Override
public boolean isRegistered(String modId) {
return configs.containsKey(modId);
}
@Override
public boolean isRegistered(Path configPath) {
return configsByPath.containsKey(configPath);
}
}

View File

@ -5,6 +5,7 @@ import io.gitlab.jfronny.libjf.config.api.ConfigInstance;
import io.gitlab.jfronny.libjf.config.api.Entry;
import io.gitlab.jfronny.libjf.config.api.Preset;
import io.gitlab.jfronny.libjf.config.api.Verifier;
import io.gitlab.jfronny.libjf.config.impl.entrypoint.JfConfigWatchService;
import net.fabricmc.loader.api.FabricLoader;
import java.lang.reflect.Field;
@ -145,8 +146,10 @@ public class ConfigInstanceImpl implements ConfigInstance {
@Override
public void write() {
try {
if (!Files.exists(path)) Files.createFile(path);
Files.write(path, LibJf.GSON.toJson(configClass.getDeclaredConstructor().newInstance()).getBytes());
JfConfigWatchService.lock(path, () -> {
if (!Files.exists(path)) Files.createFile(path);
Files.write(path, LibJf.GSON.toJson(configClass.getDeclaredConstructor().newInstance()).getBytes());
});
} catch (Exception e) {
e.printStackTrace();
}

View File

@ -1,4 +1,4 @@
package io.gitlab.jfronny.libjf.config.impl.entry;
package io.gitlab.jfronny.libjf.config.impl.entrypoint;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.ConfigHolder;
@ -10,7 +10,7 @@ public class JfConfigClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
for (ConfigInstance config : ConfigHolder.getInstance().getRegistered().values()) {
LibJf.LOGGER.info("Registring config UI for " + config.getModId());
LibJf.LOGGER.info("Registering config UI for " + config.getModId());
EntryInfoWidgetBuilder.initConfig(config);
}
}

View File

@ -1,4 +1,4 @@
package io.gitlab.jfronny.libjf.config.impl;
package io.gitlab.jfronny.libjf.config.impl.entrypoint;
import com.mojang.brigadier.Command;
import io.gitlab.jfronny.libjf.LibJf;
@ -9,7 +9,7 @@ import net.minecraft.text.LiteralText;
import static net.minecraft.server.command.CommandManager.literal;
public class ConfigCommand implements ModInitializer {
public class JfConfigCommand implements ModInitializer {
@Override
public void onInitialize() {
CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> {

View File

@ -1,4 +1,4 @@
package io.gitlab.jfronny.libjf.config.impl.entry;
package io.gitlab.jfronny.libjf.config.impl.entrypoint;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.ConfigHolder;
@ -11,8 +11,14 @@ public class JfConfigSafe implements PreLaunchEntrypoint {
@Override
public void onPreLaunch() {
for (EntrypointContainer<JfConfig> config : FabricLoader.getInstance().getEntrypointContainers(LibJf.MOD_ID + ":config", JfConfig.class)) {
ConfigHolder.getInstance().register(config.getProvider().getMetadata().getId(), config.getEntrypoint().getClass());
LibJf.LOGGER.info("Registering config for " + config.getProvider().getMetadata().getId());
registerIfMissing(config.getProvider().getMetadata().getId(), config.getEntrypoint().getClass());
}
}
public static void registerIfMissing(String modId, Class<?> klazz) {
if (!ConfigHolder.getInstance().isRegistered(modId)) {
LibJf.LOGGER.info("Registering config for " + modId);
ConfigHolder.getInstance().register(modId, klazz);
}
}
}

View File

@ -1,8 +1,7 @@
package io.gitlab.jfronny.libjf.config.impl.entry;
package io.gitlab.jfronny.libjf.config.impl.entrypoint;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.ConfigHolder;
import io.gitlab.jfronny.libjf.config.impl.ConfigHolderImpl;
import io.gitlab.jfronny.libjf.config.api.JfConfig;
import io.gitlab.jfronny.libjf.unsafe.DynamicEntry;
import io.gitlab.jfronny.libjf.unsafe.UltraEarlyInit;
@ -10,10 +9,9 @@ import io.gitlab.jfronny.libjf.unsafe.UltraEarlyInit;
public class JfConfigUnsafe implements UltraEarlyInit {
@Override
public void init() {
DynamicEntry.execute(LibJf.MOD_ID + ":config", JfConfig.class, s -> {
ConfigHolder.getInstance().register(s.modId(), s.instance().getClass());
LibJf.LOGGER.info("Registering config for " + s.modId());
});
DynamicEntry.execute(LibJf.MOD_ID + ":config", JfConfig.class,
s -> JfConfigSafe.registerIfMissing(s.modId(), s.instance().getClass())
);
LibJf.LOGGER.info("Finished LibJF config entrypoint");
}
}

View File

@ -0,0 +1,84 @@
package io.gitlab.jfronny.libjf.config.impl.entrypoint;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.ConfigHolder;
import io.gitlab.jfronny.libjf.coprocess.ThreadCoProcess;
import io.gitlab.jfronny.libjf.interfaces.ThrowingRunnable;
import net.fabricmc.loader.api.FabricLoader;
import java.io.Closeable;
import java.io.IOException;
import java.nio.file.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static java.nio.file.StandardWatchEventKinds.*;
public class JfConfigWatchService extends ThreadCoProcess implements Closeable {
private static final Path CONFIG_DIR = FabricLoader.getInstance().getConfigDir();
private static final Set<JfConfigWatchService> REGISTERED_INSTANCES = new HashSet<>();
private final WatchService service;
private static final Map<Path, Integer> locked = new HashMap<>();
public static <TEx extends Throwable> void lock(Path p, ThrowingRunnable<TEx> task) throws TEx {
synchronized (CONFIG_DIR) {
locked.compute(p, (p1, val) -> val == null ? 1 : val + 1);
task.run();
for (JfConfigWatchService instance : REGISTERED_INSTANCES) {
instance.executeIteration();
}
}
}
public JfConfigWatchService() {
WatchService ws = null;
try {
ws = FileSystems.getDefault().newWatchService();
CONFIG_DIR.register(ws, ENTRY_MODIFY, ENTRY_CREATE, ENTRY_DELETE);
} catch (IOException e) {
if (ws != null) {
try {
ws.close();
} catch (IOException ignored) {
}
ws = null;
}
LibJf.LOGGER.error("Could not initialize FS watcher for configs");
}
service = ws;
REGISTERED_INSTANCES.add(this);
}
@Override
public void executeIteration() {
synchronized (CONFIG_DIR) {
WatchKey key = service.poll();
if (key != null) {
ConfigHolder ch = ConfigHolder.getInstance();
for (WatchEvent<?> event : key.pollEvents()) {
if (event.context() instanceof Path p) {
p = CONFIG_DIR.resolve(p);
if (ch.isRegistered(p)) {
int lockCurr = locked.getOrDefault(p, 0);
if (lockCurr == 0) {
LibJf.LOGGER.info("Detected updated config: " + p + ", reloading");
ch.get(p).load();
}
else {
locked.put(p, lockCurr - 1);
}
}
}
}
if (!key.reset()) LibJf.LOGGER.error("Could not reset config watch key");
}
}
}
@Override
public void close() throws IOException {
service.close();
REGISTERED_INSTANCES.remove(this);
}
}

View File

@ -13,21 +13,12 @@
"license": "MIT",
"environment": "*",
"entrypoints": {
"modmenu": [
"io.gitlab.jfronny.libjf.config.impl.ModMenu"
],
"client": [
"io.gitlab.jfronny.libjf.config.impl.entry.JfConfigClient"
],
"libjf:preEarly": [
"io.gitlab.jfronny.libjf.config.impl.entry.JfConfigUnsafe"
],
"preLaunch": [
"io.gitlab.jfronny.libjf.config.impl.entry.JfConfigSafe"
],
"main": [
"io.gitlab.jfronny.libjf.config.impl.ConfigCommand"
]
"modmenu": ["io.gitlab.jfronny.libjf.config.impl.ModMenu"],
"client": ["io.gitlab.jfronny.libjf.config.impl.entrypoint.JfConfigClient"],
"libjf:preEarly": ["io.gitlab.jfronny.libjf.config.impl.entrypoint.JfConfigUnsafe"],
"preLaunch": ["io.gitlab.jfronny.libjf.config.impl.entrypoint.JfConfigSafe"],
"main": ["io.gitlab.jfronny.libjf.config.impl.entrypoint.JfConfigCommand"],
"libjf:coprocess": ["io.gitlab.jfronny.libjf.config.impl.entrypoint.JfConfigWatchService"]
},
"depends": {
"fabricloader": ">=0.12.0",

View File

@ -2,6 +2,5 @@ archivesBaseName = "libjf-web-v0"
dependencies {
moduleDependencies(project, ["libjf-base", "libjf-config-v0"])
include modImplementation(fabricApi.module("fabric-lifecycle-events-v1", "${rootProject.fabric_version}"))
include modImplementation(fabricApi.module("fabric-command-api-v1", "${rootProject.fabric_version}"))
}

View File

@ -3,18 +3,15 @@ package io.gitlab.jfronny.libjf.web.impl;
import com.mojang.brigadier.Command;
import io.gitlab.jfronny.libjf.Flags;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.coprocess.CoProcess;
import io.gitlab.jfronny.libjf.web.api.WebServer;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.api.DedicatedServerModInitializer;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.minecraft.text.LiteralText;
import static net.minecraft.server.command.CommandManager.literal;
public class JfWeb implements ClientModInitializer, DedicatedServerModInitializer, ModInitializer {
public class JfWeb implements CoProcess, ModInitializer {
public static final WebServer SERVER;
static {
JfWebConfig.ensureValidPort();
@ -22,19 +19,13 @@ public class JfWeb implements ClientModInitializer, DedicatedServerModInitialize
}
@Override
public void onInitializeClient() {
if (isEnabled()) {
ClientLifecycleEvents.CLIENT_STARTED.register(client -> SERVER.restart());
ClientLifecycleEvents.CLIENT_STOPPING.register(client -> SERVER.stop());
}
public void start() {
if (isEnabled()) SERVER.restart();
}
@Override
public void onInitializeServer() {
if (isEnabled()) {
ServerLifecycleEvents.SERVER_STARTED.register(server -> SERVER.restart());
ServerLifecycleEvents.SERVER_STOPPED.register(server -> SERVER.stop());
}
public void stop() {
if (isEnabled()) SERVER.stop();
}
@Override

View File

@ -14,8 +14,7 @@
"environment": "*",
"entrypoints": {
"main": ["io.gitlab.jfronny.libjf.web.impl.JfWeb"],
"client": ["io.gitlab.jfronny.libjf.web.impl.JfWeb"],
"server": ["io.gitlab.jfronny.libjf.web.impl.JfWeb"],
"libjf:coprocess": ["io.gitlab.jfronny.libjf.web.impl.JfWeb"],
"libjf:config": ["io.gitlab.jfronny.libjf.web.impl.JfWebConfig"]
},
"depends": {