From 59ef1d2817ecd351e1857b32a57adbe79da6925c Mon Sep 17 00:00:00 2001 From: mezz Date: Mon, 20 Feb 2023 16:50:59 -0800 Subject: [PATCH] Improve config file watcher, avoid creating excess threads on world reload --- .../jei/common/config/file/ConfigSchema.java | 12 +- .../jei/common/config/file/FileWatcher.java | 171 ++++------------ .../common/config/file/FileWatcherThread.java | 185 ++++++++++++++++++ .../jei/common/config/file/IConfigSchema.java | 2 +- .../plugins/fabric/FabricGuiPlugin.java | 6 +- .../forge/plugins/forge/ForgeGuiPlugin.java | 6 +- .../mezz/jei/gui/config/JeiClientConfigs.java | 5 +- .../mezz/jei/gui/startup/GuiConfigData.java | 5 +- .../mezz/jei/gui/startup/JeiGuiStarter.java | 6 +- .../mezz/jei/library/startup/JeiStarter.java | 11 +- 10 files changed, 256 insertions(+), 153 deletions(-) create mode 100644 Common/src/main/java/mezz/jei/common/config/file/FileWatcherThread.java diff --git a/Common/src/main/java/mezz/jei/common/config/file/ConfigSchema.java b/Common/src/main/java/mezz/jei/common/config/file/ConfigSchema.java index 2bef9fff8..637f0ba06 100644 --- a/Common/src/main/java/mezz/jei/common/config/file/ConfigSchema.java +++ b/Common/src/main/java/mezz/jei/common/config/file/ConfigSchema.java @@ -11,7 +11,6 @@ import java.nio.file.Path; import java.time.Duration; import java.util.List; -import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.atomic.AtomicBoolean; @@ -56,19 +55,12 @@ private void onFileChanged() { } @Override - public void register() { + public void register(FileWatcher fileWatcher) { if (!Files.exists(path)) { save(); } - try { - Map callbacks = Map.of(path, this::onFileChanged); - FileWatcher fileWatcher = new FileWatcher(callbacks); - Thread thread = new Thread(fileWatcher::run, "JEI Config file watcher"); - thread.start(); - } catch (IOException e) { - LOGGER.error("Failed to create FileWatcher Thread for config file: '{}'", path, e); - } + fileWatcher.addCallback(path, this::onFileChanged); } private void save() { diff --git a/Common/src/main/java/mezz/jei/common/config/file/FileWatcher.java b/Common/src/main/java/mezz/jei/common/config/file/FileWatcher.java index 77d8f0196..c86c63bf1 100644 --- a/Common/src/main/java/mezz/jei/common/config/file/FileWatcher.java +++ b/Common/src/main/java/mezz/jei/common/config/file/FileWatcher.java @@ -2,161 +2,70 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.Unmodifiable; +import org.jetbrains.annotations.Nullable; import java.io.IOException; -import java.nio.file.FileSystem; -import java.nio.file.FileSystems; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardWatchEventKinds; -import java.nio.file.WatchEvent; -import java.nio.file.WatchKey; -import java.nio.file.WatchService; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; public class FileWatcher { private static final Logger LOGGER = LogManager.getLogger(); - /** - * To avoid calling the callbacks many times while a file is being edited, - * wait a little while for there to be no more changes before we call them. - */ - private static final int quietTimeMs = 1_000; - /** - * If a directory we want to watch does not exist, we should periodically check for it. - */ - private static final int recheckDirectoriesMs = 60_000; - - private final WatchService watchService; - @Unmodifiable - private final Map callbacks; - @Unmodifiable - private final Set directoriesToWatch; - - private final Map watchedDirectories = new HashMap<>(); - private final Set changedPaths = new HashSet<>(); - private long lastRecheckTime = 0; + private @Nullable FileWatcherThread thread; - /** - * @param callbacks a map of files to watch and callbacks to call when a file changes - */ - public FileWatcher(Map callbacks) throws IOException { - this.callbacks = Map.copyOf(callbacks); - this.directoriesToWatch = callbacks.keySet().stream() - .map(Path::getParent) - .collect(Collectors.toUnmodifiableSet()); - FileSystem fileSystem = FileSystems.getDefault(); - this.watchService = fileSystem.newWatchService(); + public FileWatcher(String threadName) { + this.thread = createThread(threadName); } - @SuppressWarnings("InfiniteLoopStatement") - public void run() { - try (watchService) { - while (true) { - runIteration(); - } - } catch (InterruptedException e) { - LOGGER.error("FileWatcher was interrupted, stopping.", e); - } catch (IOException e) { - LOGGER.error("FileWatcher encountered an unhandled IOException, stopping.", e); + @Nullable + private static FileWatcherThread createThread(String threadName) { + try { + return new FileWatcherThread(threadName); + } catch (UnsupportedOperationException | IOException e) { + LOGGER.error("Unable to create file watcher: ", e); + return null; } - notifyChanges(); } - private void runIteration() throws InterruptedException { - long time = System.currentTimeMillis(); - if (time > lastRecheckTime + recheckDirectoriesMs) { - lastRecheckTime = time; - watchDirectories(); + /** + * @param path a config file to watch + * @param callback a callbacks to call when the file changes. + * Callbacks must be thread-safe, they will be called from this thread. + */ + public void addCallback(Path path, Runnable callback) { + if (thread != null) { + this.thread.addCallback(path, callback); } + } - if (changedPaths.isEmpty()) { - // There are no changes yet. - // Just block and wait for some changes. - WatchKey watchKey = watchService.take(); - if (watchKey != null) { - pollWatchKey(watchKey); - } - } else { - // We have some detected some changes already. - // Collect more changes, or notify the callbacks if there are no new changes. - WatchKey watchKey = watchService.poll(quietTimeMs, TimeUnit.MILLISECONDS); - if (watchKey != null) { - pollWatchKey(watchKey); - } else { - notifyChanges(); - } + /** + * Start the file watcher thread + */ + public void start() { + if (thread != null) { + this.thread.start(); } } - private void pollWatchKey(WatchKey watchKey) { - Path watchedDirectory = watchedDirectories.get(watchKey); - if (watchedDirectory == null) { + /** + * Stop the file watcher thread and clear all callbacks. + */ + public void reset() { + if (this.thread == null) { return; } - List> events = watchKey.pollEvents(); - for (WatchEvent event : events) { - if (event.kind() == StandardWatchEventKinds.OVERFLOW) { - // we missed some events, - // so we must assume every watched file in the directory has changed - callbacks.keySet().stream() - .filter(path -> path.getParent().equals(watchedDirectory)) - .forEach(changedPaths::add); - break; - } else if (event.context() instanceof Path eventPath) { - Path fullPath = watchedDirectory.resolve(eventPath); - if (callbacks.containsKey(fullPath)) { - changedPaths.add(fullPath); - } - } + String threadName = this.thread.getName(); + this.thread.interrupt(); + try { + this.thread.join(1000); + } catch (InterruptedException consumed) { + Thread.currentThread().interrupt(); } - if (!watchKey.reset()) { - LOGGER.info("Failed to re-watch directory {}. It may have been deleted.", watchedDirectory); - watchedDirectories.remove(watchKey); + if (this.thread.isAlive()) { + LOGGER.error("File Watcher thread could not be stopped and will be abandoned."); } - } - private void notifyChanges() { - if (changedPaths.isEmpty()) { - return; - } - LOGGER.info("Detected changes in files:\n{}", changedPaths.stream().map(Path::toString).collect(Collectors.joining("\n"))); - for (Path changedPath : changedPaths) { - Runnable runnable = callbacks.get(changedPath); - if (runnable != null) { - runnable.run(); - } - } - changedPaths.clear(); - } - - private void watchDirectories() { - for (Path directory : directoriesToWatch) { - if (!watchedDirectories.containsValue(directory) && - Files.isDirectory(directory) - ) { - try { - WatchKey key = directory.register( - watchService, - StandardWatchEventKinds.ENTRY_DELETE, - StandardWatchEventKinds.ENTRY_CREATE, - StandardWatchEventKinds.ENTRY_MODIFY, - StandardWatchEventKinds.OVERFLOW - ); - watchedDirectories.put(key, directory); - } catch (IOException e) { - LOGGER.error("Failed to watch directory: {}", directory, e); - } - } - } + this.thread = createThread(threadName); } } diff --git a/Common/src/main/java/mezz/jei/common/config/file/FileWatcherThread.java b/Common/src/main/java/mezz/jei/common/config/file/FileWatcherThread.java new file mode 100644 index 000000000..bc9bcceca --- /dev/null +++ b/Common/src/main/java/mezz/jei/common/config/file/FileWatcherThread.java @@ -0,0 +1,185 @@ +package mezz.jei.common.config.file; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.annotation.concurrent.ThreadSafe; +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@ThreadSafe +public class FileWatcherThread extends Thread { + private static final Logger LOGGER = LogManager.getLogger(); + + /** + * To avoid calling the callbacks many times while a file is being edited, + * wait a little while for there to be no more changes before we call them. + */ + private static final int quietTimeMs = 500; + /** + * If a directory we want to watch does not exist, we should periodically check for it. + */ + private static final int recheckDirectoriesMs = 60_000; + + private final WatchService watchService; + private final Map callbacks; + private final Set directoriesToWatch; + + private final Map watchedDirectories = new HashMap<>(); + private final Set changedPaths = new HashSet<>(); + private long lastDirectoryCheckTime = 0; + + /** + * @param name the name of the new thread + */ + public FileWatcherThread(String name) throws IOException { + super(name); + this.callbacks = new HashMap<>(); + this.directoriesToWatch = new HashSet<>(); + FileSystem fileSystem = FileSystems.getDefault(); + this.watchService = fileSystem.newWatchService(); + } + + /** + * @param path a config file to watch + * @param callback a callbacks to call when the file changes. + * Callbacks must be thread-safe, they will be called from this thread. + */ + public synchronized void addCallback(Path path, Runnable callback) { + this.callbacks.put(path, callback); + if (this.directoriesToWatch.add(path.getParent())) { + this.lastDirectoryCheckTime = 0; + } + } + + @Override + public void run() { + try (watchService) { + while (!Thread.currentThread().isInterrupted()) { + synchronized (this) { + runIteration(); + } + } + } catch (InterruptedException consumed) { + LOGGER.info("FileWatcher was interrupted, stopping."); + } catch (IOException e) { + LOGGER.error("FileWatcher encountered an unhandled IOException, stopping.", e); + } finally { + synchronized (this) { + watchedDirectories + .keySet() + .forEach(WatchKey::cancel); + } + } + } + + private void runIteration() throws InterruptedException { + long time = System.currentTimeMillis(); + if (time > lastDirectoryCheckTime + recheckDirectoriesMs) { + lastDirectoryCheckTime = time; + watchDirectories(); + } + + if (changedPaths.isEmpty()) { + // There are no changes yet. + // Just block and wait for some changes. + WatchKey watchKey = watchService.take(); + if (watchKey != null) { + pollWatchKey(watchKey); + } + } else { + // We have some detected some changes already. + // Collect more changes, or notify the callbacks if there are no new changes. + WatchKey watchKey = watchService.poll(quietTimeMs, TimeUnit.MILLISECONDS); + if (watchKey != null) { + pollWatchKey(watchKey); + } else { + notifyChanges(); + } + } + } + + private void pollWatchKey(WatchKey watchKey) throws InterruptedException { + Path watchedDirectory = watchedDirectories.get(watchKey); + if (watchedDirectory == null) { + return; + } + + List> events = watchKey.pollEvents(); + for (WatchEvent event : events) { + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } + if (event.kind() == StandardWatchEventKinds.OVERFLOW) { + // we missed some events, + // so we must assume every watched file in the directory has changed + callbacks.keySet().stream() + .filter(path -> path.getParent().equals(watchedDirectory)) + .forEach(changedPaths::add); + break; + } else if (event.context() instanceof Path eventPath) { + Path fullPath = watchedDirectory.resolve(eventPath); + if (callbacks.containsKey(fullPath)) { + changedPaths.add(fullPath); + } + } + } + + if (!watchKey.reset()) { + LOGGER.info("Failed to re-watch directory {}. It may have been deleted.", watchedDirectory); + watchedDirectories.remove(watchKey); + } + } + + private void notifyChanges() { + if (changedPaths.isEmpty()) { + return; + } + LOGGER.info("Detected changes in files:\n{}", changedPaths.stream().map(Path::toString).collect(Collectors.joining("\n"))); + for (Path changedPath : changedPaths) { + Runnable runnable = callbacks.get(changedPath); + if (runnable != null) { + runnable.run(); + } + } + changedPaths.clear(); + } + + private void watchDirectories() { + for (Path directory : directoriesToWatch) { + if (Thread.currentThread().isInterrupted()) { + return; + } + if (!watchedDirectories.containsValue(directory) && + Files.isDirectory(directory) + ) { + try { + WatchKey key = directory.register( + watchService, + StandardWatchEventKinds.ENTRY_DELETE, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.OVERFLOW + ); + watchedDirectories.put(key, directory); + } catch (IOException e) { + LOGGER.error("Failed to watch directory: {}", directory, e); + } + } + } + } +} diff --git a/Common/src/main/java/mezz/jei/common/config/file/IConfigSchema.java b/Common/src/main/java/mezz/jei/common/config/file/IConfigSchema.java index ce2162ab6..94fc18ba8 100644 --- a/Common/src/main/java/mezz/jei/common/config/file/IConfigSchema.java +++ b/Common/src/main/java/mezz/jei/common/config/file/IConfigSchema.java @@ -3,7 +3,7 @@ import mezz.jei.api.runtime.config.IJeiConfigFile; public interface IConfigSchema extends IJeiConfigFile { - void register(); + void register(FileWatcher fileWatcher); void loadIfNeeded(); diff --git a/Fabric/src/main/java/mezz/jei/fabric/plugins/fabric/FabricGuiPlugin.java b/Fabric/src/main/java/mezz/jei/fabric/plugins/fabric/FabricGuiPlugin.java index 6ae9f2c82..1ac77520c 100644 --- a/Fabric/src/main/java/mezz/jei/fabric/plugins/fabric/FabricGuiPlugin.java +++ b/Fabric/src/main/java/mezz/jei/fabric/plugins/fabric/FabricGuiPlugin.java @@ -5,6 +5,7 @@ import mezz.jei.api.constants.ModIds; import mezz.jei.api.registration.IRuntimeRegistration; import mezz.jei.api.runtime.IJeiRuntime; +import mezz.jei.common.config.file.FileWatcher; import mezz.jei.fabric.startup.EventRegistration; import mezz.jei.gui.startup.JeiEventHandlers; import mezz.jei.gui.startup.JeiGuiStarter; @@ -21,6 +22,7 @@ public class FabricGuiPlugin implements IModPlugin { private static @Nullable IJeiRuntime runtime; private final EventRegistration eventRegistration = new EventRegistration(); + private final FileWatcher fileWatcher = new FileWatcher("JEI GUI Config file watcher"); @Override public ResourceLocation getPluginUid() { @@ -29,8 +31,9 @@ public ResourceLocation getPluginUid() { @Override public void registerRuntime(IRuntimeRegistration registration) { - JeiEventHandlers eventHandlers = JeiGuiStarter.start(registration); + JeiEventHandlers eventHandlers = JeiGuiStarter.start(registration, fileWatcher); eventRegistration.setEventHandlers(eventHandlers); + fileWatcher.start(); } @Override @@ -43,6 +46,7 @@ public void onRuntimeUnavailable() { runtime = null; LOGGER.info("Stopping JEI GUI"); eventRegistration.clear(); + fileWatcher.reset(); } public static Optional getRuntime() { diff --git a/Forge/src/main/java/mezz/jei/forge/plugins/forge/ForgeGuiPlugin.java b/Forge/src/main/java/mezz/jei/forge/plugins/forge/ForgeGuiPlugin.java index b19fed470..8034e1c20 100644 --- a/Forge/src/main/java/mezz/jei/forge/plugins/forge/ForgeGuiPlugin.java +++ b/Forge/src/main/java/mezz/jei/forge/plugins/forge/ForgeGuiPlugin.java @@ -4,6 +4,7 @@ import mezz.jei.api.JeiPlugin; import mezz.jei.api.constants.ModIds; import mezz.jei.api.registration.IRuntimeRegistration; +import mezz.jei.common.config.file.FileWatcher; import mezz.jei.forge.events.RuntimeEventSubscriptions; import mezz.jei.forge.startup.EventRegistration; import mezz.jei.gui.startup.JeiEventHandlers; @@ -18,6 +19,7 @@ public class ForgeGuiPlugin implements IModPlugin { private static final Logger LOGGER = LogManager.getLogger(); private final RuntimeEventSubscriptions runtimeSubscriptions = new RuntimeEventSubscriptions(MinecraftForge.EVENT_BUS); + private final FileWatcher fileWatcher = new FileWatcher("JEI GUI Config file watcher"); @Override public ResourceLocation getPluginUid() { @@ -31,14 +33,16 @@ public void registerRuntime(IRuntimeRegistration registration) { runtimeSubscriptions.clear(); } - JeiEventHandlers eventHandlers = JeiGuiStarter.start(registration); + JeiEventHandlers eventHandlers = JeiGuiStarter.start(registration, fileWatcher); EventRegistration.registerEvents(runtimeSubscriptions, eventHandlers); + fileWatcher.start(); } @Override public void onRuntimeUnavailable() { LOGGER.info("Stopping JEI GUI"); runtimeSubscriptions.clear(); + fileWatcher.reset(); } } diff --git a/Gui/src/main/java/mezz/jei/gui/config/JeiClientConfigs.java b/Gui/src/main/java/mezz/jei/gui/config/JeiClientConfigs.java index 44ecf4baa..d096eddb8 100644 --- a/Gui/src/main/java/mezz/jei/gui/config/JeiClientConfigs.java +++ b/Gui/src/main/java/mezz/jei/gui/config/JeiClientConfigs.java @@ -3,6 +3,7 @@ import mezz.jei.common.config.file.ConfigSchemaBuilder; import mezz.jei.common.config.file.IConfigSchema; import mezz.jei.common.config.file.IConfigSchemaBuilder; +import mezz.jei.common.config.file.FileWatcher; import mezz.jei.gui.util.HorizontalAlignment; import java.nio.file.Path; @@ -26,8 +27,8 @@ public JeiClientConfigs(Path configFile) { schema = builder.build(); } - public void register() { - schema.register(); + public void register(FileWatcher fileWatcher) { + schema.register(fileWatcher); } @Override diff --git a/Gui/src/main/java/mezz/jei/gui/startup/GuiConfigData.java b/Gui/src/main/java/mezz/jei/gui/startup/GuiConfigData.java index 12463330c..123517fb1 100644 --- a/Gui/src/main/java/mezz/jei/gui/startup/GuiConfigData.java +++ b/Gui/src/main/java/mezz/jei/gui/startup/GuiConfigData.java @@ -1,5 +1,6 @@ package mezz.jei.gui.startup; +import mezz.jei.common.config.file.FileWatcher; import mezz.jei.gui.config.IngredientTypeSortingConfig; import mezz.jei.gui.config.ModNameSortingConfig; import mezz.jei.common.platform.Services; @@ -16,10 +17,10 @@ public record GuiConfigData( ModNameSortingConfig modNameSortingConfig, IngredientTypeSortingConfig ingredientTypeSortingConfig ) { - public static GuiConfigData create() { + public static GuiConfigData create(FileWatcher fileWatcher) { Path configDir = Services.PLATFORM.getConfigHelper().createJeiConfigDir(); JeiClientConfigs jeiClientConfigs = new JeiClientConfigs(configDir.resolve("jei-client.ini")); - jeiClientConfigs.register(); + jeiClientConfigs.register(fileWatcher); IBookmarkConfig bookmarkConfig = new BookmarkConfig(configDir); ModNameSortingConfig ingredientModNameSortingConfig = new ModNameSortingConfig(configDir.resolve("ingredient-list-mod-sort-order.ini")); diff --git a/Gui/src/main/java/mezz/jei/gui/startup/JeiGuiStarter.java b/Gui/src/main/java/mezz/jei/gui/startup/JeiGuiStarter.java index e1c507b32..f076ba029 100644 --- a/Gui/src/main/java/mezz/jei/gui/startup/JeiGuiStarter.java +++ b/Gui/src/main/java/mezz/jei/gui/startup/JeiGuiStarter.java @@ -18,6 +18,7 @@ import mezz.jei.common.network.IConnectionToServer; import mezz.jei.core.util.LoggedTimer; import mezz.jei.common.config.IWorldConfig; +import mezz.jei.common.config.file.FileWatcher; import mezz.jei.gui.bookmarks.BookmarkList; import mezz.jei.gui.config.IBookmarkConfig; import mezz.jei.gui.config.IClientConfig; @@ -59,7 +60,7 @@ public class JeiGuiStarter { private static final Logger LOGGER = LogManager.getLogger(); - public static JeiEventHandlers start(IRuntimeRegistration registration) { + public static JeiEventHandlers start(IRuntimeRegistration registration, FileWatcher fileWatcher) { LOGGER.info("Starting JEI GUI"); LoggedTimer timer = new LoggedTimer(); @@ -86,7 +87,8 @@ public static JeiEventHandlers start(IRuntimeRegistration registration) { timer.stop(); timer.start("Building ingredient filter"); - GuiConfigData configData = GuiConfigData.create(); + GuiConfigData configData = GuiConfigData.create(fileWatcher); + ModNameSortingConfig modNameSortingConfig = configData.modNameSortingConfig(); IngredientTypeSortingConfig ingredientTypeSortingConfig = configData.ingredientTypeSortingConfig(); IWorldConfig worldConfig = Internal.getWorldConfig(); diff --git a/Library/src/main/java/mezz/jei/library/startup/JeiStarter.java b/Library/src/main/java/mezz/jei/library/startup/JeiStarter.java index 63f1ababb..97b8bbf0a 100644 --- a/Library/src/main/java/mezz/jei/library/startup/JeiStarter.java +++ b/Library/src/main/java/mezz/jei/library/startup/JeiStarter.java @@ -13,6 +13,7 @@ import mezz.jei.common.Internal; import mezz.jei.common.config.DebugConfig; import mezz.jei.common.config.IWorldConfig; +import mezz.jei.common.config.file.FileWatcher; import mezz.jei.common.platform.Services; import mezz.jei.common.util.ErrorUtil; import mezz.jei.core.util.LoggedTimer; @@ -47,6 +48,7 @@ public final class JeiStarter { private static final Logger LOGGER = LogManager.getLogger(); private final StartData data; + private final FileWatcher fileWatcher = new FileWatcher("JEI Library Config file watcher"); public JeiStarter(StartData data) { ErrorUtil.checkNotEmpty(data.plugins(), "plugins"); @@ -73,15 +75,17 @@ public void start() { IConfigSchemaBuilder debugFileBuilder = new ConfigSchemaBuilder(configDir.resolve("jei-debug.ini")); DebugConfig.create(debugFileBuilder); - debugFileBuilder.build().register(); + debugFileBuilder.build().register(fileWatcher); IConfigSchemaBuilder modFileBuilder = new ConfigSchemaBuilder(configDir.resolve("jei-mod-id-format.ini")); ModIdFormatConfig modIdFormatConfig = new ModIdFormatConfig(modFileBuilder); - modFileBuilder.build().register(); + modFileBuilder.build().register(fileWatcher); IConfigSchemaBuilder colorFileBuilder = new ConfigSchemaBuilder(configDir.resolve("jei-colors.ini")); ColorNameConfig colorNameConfig = new ColorNameConfig(colorFileBuilder); - colorFileBuilder.build().register(); + colorFileBuilder.build().register(fileWatcher); + + fileWatcher.start(); IColorHelper colorHelper = new ColorHelper(colorNameConfig); @@ -158,5 +162,6 @@ public void stop() { LOGGER.info("Stopping JEI"); List plugins = data.plugins(); PluginCaller.callOnPlugins("Sending Runtime Unavailable", plugins, IModPlugin::onRuntimeUnavailable); + fileWatcher.reset(); } }