Reload config if changed
This commit is contained in:
parent
e0e5aaba99
commit
d6b593b6e5
|
@ -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
|
|
@ -1,4 +1,5 @@
|
|||
archivesBaseName = "libjf-base"
|
||||
|
||||
dependencies {
|
||||
include modImplementation(fabricApi.module("fabric-lifecycle-events-v1", "${rootProject.fabric_version}"))
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package io.gitlab.jfronny.libjf.coprocess;
|
||||
|
||||
public interface CoProcess {
|
||||
void start();
|
||||
void stop();
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -16,6 +16,9 @@
|
|||
"fabricloader": ">=0.12.0",
|
||||
"minecraft": "*"
|
||||
},
|
||||
"entrypoints": {
|
||||
"main": ["io.gitlab.jfronny.libjf.coprocess.CoProcessManager"]
|
||||
},
|
||||
"custom": {
|
||||
"modmenu": {
|
||||
"parent": "libjf",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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) -> {
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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}"))
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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": {
|
||||
|
|
Loading…
Reference in New Issue
Block a user