diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java index a252c92c8..ecfc6c51b 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java @@ -71,6 +71,7 @@ import me.lucko.luckperms.common.tasks.ExpireTemporaryTask; import me.lucko.luckperms.common.tasks.UpdateTask; import me.lucko.luckperms.common.treeview.PermissionVault; import me.lucko.luckperms.common.utils.BufferedRequest; +import me.lucko.luckperms.common.utils.FileWatcher; import me.lucko.luckperms.common.utils.LoggerImpl; import org.bukkit.World; @@ -106,6 +107,7 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { private GroupManager groupManager; private TrackManager trackManager; private Storage storage; + private FileWatcher fileWatcher = null; private InternalMessagingService messagingService = null; private UuidCache uuidCache; private BukkitListener listener; @@ -168,6 +170,11 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { listener = new BukkitListener(this); pm.registerEvents(listener, this); + if (getConfiguration().get(ConfigKeys.WATCH_FILES)) { + fileWatcher = new FileWatcher(this); + getScheduler().doAsyncRepeating(fileWatcher, 30L); + } + // initialise datastore storage = StorageFactory.getInstance(this, StorageType.H2); @@ -336,6 +343,10 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { getLog().info("Closing datastore..."); storage.shutdown(); + if (fileWatcher != null) { + fileWatcher.close(); + } + if (messagingService != null) { getLog().info("Closing messaging service..."); messagingService.close(); @@ -363,6 +374,7 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { groupManager = null; trackManager = null; storage = null; + fileWatcher = null; messagingService = null; uuidCache = null; listener = null; diff --git a/bukkit/src/main/resources/config.yml b/bukkit/src/main/resources/config.yml index a26d003a1..618820100 100644 --- a/bukkit/src/main/resources/config.yml +++ b/bukkit/src/main/resources/config.yml @@ -192,6 +192,12 @@ group-name-rewrite: # Fill out connection info below if you're using MySQL, PostgreSQL or MongoDB storage-method: h2 +# When using a file-based storage type, LuckPerms can monitor the data files for changes, and then schedule automatic +# updates when changes are detected. +# +# If you don't want this to happen, set this option to false. +watch-files: true + # This block enables support for split datastores. split-storage: enabled: false @@ -217,9 +223,14 @@ data: # This should *not* be set to "lp_" if you have previously ran LuckPerms v2.16.81 or earlier with this database. table_prefix: 'luckperms_' - # Set to -1 to disable. If this is the only instance accessing the datastore, you can disable syncing. - # e.g. if you're using sqlite or flatfile, this can be set to -1 to save resources. - sync-minutes: 3 + # This option controls how frequently LuckPerms will perform a sync task. + # A sync task will refresh all data from the storage, and ensure that the most up-to-date data is being used by the plugin. + # + # This is disabled by default, as most users will not need it. However, if you're using a remote storage type + # without a messaging service setup, you may wish to set this value to something like 3. + # + # Set to -1 to disable the task completely. + sync-minutes: -1 # Settings for the messaging service # diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java b/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java index 186b70a97..30fe3925f 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java @@ -64,6 +64,7 @@ import me.lucko.luckperms.common.tasks.ExpireTemporaryTask; import me.lucko.luckperms.common.tasks.UpdateTask; import me.lucko.luckperms.common.treeview.PermissionVault; import me.lucko.luckperms.common.utils.BufferedRequest; +import me.lucko.luckperms.common.utils.FileWatcher; import me.lucko.luckperms.common.utils.LoggerImpl; import net.md_5.bungee.api.connection.ProxiedPlayer; @@ -88,6 +89,7 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin { private GroupManager groupManager; private TrackManager trackManager; private Storage storage; + private FileWatcher fileWatcher = null; private InternalMessagingService messagingService = null; private UuidCache uuidCache; private ApiProvider apiProvider; @@ -122,6 +124,11 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin { // register events getProxy().getPluginManager().registerListener(this, new BungeeListener(this)); + if (getConfiguration().get(ConfigKeys.WATCH_FILES)) { + fileWatcher = new FileWatcher(this); + getScheduler().doAsyncRepeating(fileWatcher, 30L); + } + // initialise datastore storage = StorageFactory.getInstance(this, StorageType.H2); @@ -226,6 +233,10 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin { getLog().info("Closing datastore..."); storage.shutdown(); + if (fileWatcher != null) { + fileWatcher.close(); + } + if (messagingService != null) { getLog().info("Closing messaging service..."); messagingService.close(); diff --git a/bungee/src/main/resources/config.yml b/bungee/src/main/resources/config.yml index e2d9e9a52..2f75a6824 100644 --- a/bungee/src/main/resources/config.yml +++ b/bungee/src/main/resources/config.yml @@ -134,6 +134,12 @@ meta-formatting: # Fill out connection info below if you're using MySQL, PostgreSQL or MongoDB storage-method: h2 +# When using a file-based storage type, LuckPerms can monitor the data files for changes, and then schedule automatic +# updates when changes are detected. +# +# If you don't want this to happen, set this option to false. +watch-files: true + # This block enables support for split datastores. split-storage: enabled: false @@ -159,9 +165,14 @@ data: # This should *not* be set to "lp_" if you have previously ran LuckPerms v2.16.81 or earlier with this database. table_prefix: 'luckperms_' - # Set to -1 to disable. If this is the only instance accessing the datastore, you can disable syncing. - # e.g. if you're using sqlite or flatfile, this can be set to -1 to save resources. - sync-minutes: 3 + # This option controls how frequently LuckPerms will perform a sync task. + # A sync task will refresh all data from the storage, and ensure that the most up-to-date data is being used by the plugin. + # + # This is disabled by default, as most users will not need it. However, if you're using a remote storage type + # without a messaging service setup, you may wish to set this value to something like 3. + # + # Set to -1 to disable the task completely. + sync-minutes: -1 # Settings for the messaging service # diff --git a/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java b/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java index 94a4c895b..e8f03563a 100644 --- a/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java +++ b/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java @@ -51,7 +51,7 @@ import java.util.Map; public class ConfigKeys { public static final ConfigKey SERVER = StringKey.of("server", "global"); - public static final ConfigKey SYNC_TIME = EnduringKey.wrap(IntegerKey.of("data.sync-minutes", 3)); + public static final ConfigKey SYNC_TIME = EnduringKey.wrap(IntegerKey.of("data.sync-minutes", -1)); public static final ConfigKey DEFAULT_GROUP_NODE = StaticKey.of("group.default"); // constant since 2.6 public static final ConfigKey DEFAULT_GROUP_NAME = StaticKey.of("default"); // constant since 2.6 public static final ConfigKey INCLUDING_GLOBAL_PERMS = BooleanKey.of("include-global", true); @@ -140,6 +140,7 @@ public class ConfigKeys { })); public static final ConfigKey SQL_TABLE_PREFIX = EnduringKey.wrap(StringKey.of("data.table_prefix", "luckperms_")); public static final ConfigKey STORAGE_METHOD = EnduringKey.wrap(StringKey.of("storage-method", "h2")); + public static final ConfigKey WATCH_FILES = BooleanKey.of("watch-files", true); public static final ConfigKey SPLIT_STORAGE = EnduringKey.wrap(BooleanKey.of("split-storage.enabled", false)); public static final ConfigKey> SPLIT_STORAGE_OPTIONS = EnduringKey.wrap(AbstractKey.of(c -> { return ImmutableMap.builder() diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java b/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java index 137c42261..c406b380f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java @@ -46,6 +46,7 @@ import me.lucko.luckperms.common.messaging.InternalMessagingService; import me.lucko.luckperms.common.storage.Storage; import me.lucko.luckperms.common.treeview.PermissionVault; import me.lucko.luckperms.common.utils.BufferedRequest; +import me.lucko.luckperms.common.utils.FileWatcher; import java.io.File; import java.io.InputStream; @@ -54,6 +55,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Set; import java.util.UUID; +import java.util.function.Consumer; /** * Main internal interface for LuckPerms plugins, providing the base for abstraction throughout the project. @@ -216,6 +218,20 @@ public interface LuckPermsPlugin { */ String getServerVersion(); + /** + * Gets the file watcher running on the platform, or null if it's not enabled. + * + * @return the file watcher, or null + */ + FileWatcher getFileWatcher(); + + default void applyToFileWatcher(Consumer consumer) { + FileWatcher fw = getFileWatcher(); + if (fw != null) { + consumer.accept(fw); + } + } + /** * Gets the plugins main data storage directory * diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/backing/FlatfileBacking.java b/common/src/main/java/me/lucko/luckperms/common/storage/backing/FlatfileBacking.java index 5c7ade224..eabfa0880 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/backing/FlatfileBacking.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/backing/FlatfileBacking.java @@ -23,7 +23,9 @@ package me.lucko.luckperms.common.storage.backing; import me.lucko.luckperms.api.LogEntry; +import me.lucko.luckperms.common.commands.utils.Util; import me.lucko.luckperms.common.constants.Constants; +import me.lucko.luckperms.common.core.model.User; import me.lucko.luckperms.common.data.Log; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; @@ -49,23 +51,27 @@ abstract class FlatfileBacking extends AbstractBacking { private static final String LOG_FORMAT = "%s(%s): [%s] %s(%s) --> %s"; private final Logger actionLogger = Logger.getLogger("lp_actions"); + private Map uuidCache = new ConcurrentHashMap<>(); + private final File pluginDir; + private final String fileExtension; + + private File uuidData; + private File actionLog; File usersDir; File groupsDir; File tracksDir; - private Map uuidCache = new ConcurrentHashMap<>(); - private File uuidData; - private File actionLog; - FlatfileBacking(LuckPermsPlugin plugin, String name, File pluginDir) { + FlatfileBacking(LuckPermsPlugin plugin, String name, File pluginDir, String fileExtension) { super(plugin, name); this.pluginDir = pluginDir; + this.fileExtension = fileExtension; } @Override public void init() { try { - makeFiles(); + setupFiles(); } catch (IOException e) { e.printStackTrace(); return; @@ -93,7 +99,7 @@ abstract class FlatfileBacking extends AbstractBacking { setAcceptingLogins(true); } - private void makeFiles() throws IOException { + private void setupFiles() throws IOException { File data = new File(pluginDir, "data"); data.mkdirs(); @@ -111,6 +117,45 @@ abstract class FlatfileBacking extends AbstractBacking { actionLog = new File(data, "actions.log"); actionLog.createNewFile(); + + // Listen for file changes. + plugin.applyToFileWatcher(watcher -> { + watcher.subscribe("users", usersDir.toPath(), s -> { + if (!s.endsWith(fileExtension)) { + return; + } + + String user = s.substring(0, s.length() - fileExtension.length()); + UUID uuid = Util.parseUuid(user); + if (uuid == null) { + return; + } + + User u = plugin.getUserManager().get(uuid); + if (u != null) { + plugin.getLog().info("[FileWatcher] Refreshing user " + u.getName()); + plugin.getStorage().loadUser(uuid, "null"); + } + }); + watcher.subscribe("groups", groupsDir.toPath(), s -> { + if (!s.endsWith(fileExtension)) { + return; + } + + String groupName = s.substring(0, s.length() - fileExtension.length()); + plugin.getLog().info("[FileWatcher] Refreshing group " + groupName); + plugin.getUpdateTaskBuffer().request(); + }); + watcher.subscribe("tracks", tracksDir.toPath(), s -> { + if (!s.endsWith(fileExtension)) { + return; + } + + String trackName = s.substring(0, s.length() - fileExtension.length()); + plugin.getLog().info("[FileWatcher] Refreshing track " + trackName); + plugin.getStorage().loadAllTracks(); + }); + }); } @Override @@ -118,6 +163,10 @@ abstract class FlatfileBacking extends AbstractBacking { saveUUIDCache(uuidCache); } + protected void registerFileAction(String type, File file) { + plugin.applyToFileWatcher(fileWatcher -> fileWatcher.registerChange(type, file.getName())); + } + @Override public boolean logAction(LogEntry entry) { actionLogger.info(String.format(LOG_FORMAT, diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/backing/JSONBacking.java b/common/src/main/java/me/lucko/luckperms/common/storage/backing/JSONBacking.java index 02396db57..8bef881f3 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/backing/JSONBacking.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/backing/JSONBacking.java @@ -70,7 +70,7 @@ public class JSONBacking extends FlatfileBacking { } public JSONBacking(LuckPermsPlugin plugin, File pluginDir) { - super(plugin, "JSON", pluginDir); + super(plugin, "JSON", pluginDir, ".json"); } private boolean fileToWriter(File file, ThrowingFunction writeOperation) { @@ -108,6 +108,8 @@ public class JSONBacking extends FlatfileBacking { try { return call(() -> { File userFile = new File(usersDir, uuid.toString() + ".json"); + registerFileAction("users", userFile); + if (userFile.exists()) { return fileToReader(userFile, reader -> { reader.beginObject(); @@ -178,6 +180,8 @@ public class JSONBacking extends FlatfileBacking { try { return call(() -> { File userFile = new File(usersDir, user.getUuid().toString() + ".json"); + registerFileAction("users", userFile); + if (!GenericUserManager.shouldSave(user)) { if (userFile.exists()) { userFile.delete(); @@ -221,6 +225,8 @@ public class JSONBacking extends FlatfileBacking { if (files == null) return false; for (File file : files) { + registerFileAction("users", file); + Map nodes = new HashMap<>(); fileToReader(file, reader -> { reader.beginObject(); @@ -277,6 +283,8 @@ public class JSONBacking extends FlatfileBacking { if (files == null) return false; for (File file : files) { + registerFileAction("users", file); + UUID holder = UUID.fromString(file.getName().substring(0, file.getName().length() - 5)); Map nodes = new HashMap<>(); fileToReader(file, reader -> { @@ -321,6 +329,8 @@ public class JSONBacking extends FlatfileBacking { try { return call(() -> { File groupFile = new File(groupsDir, name + ".json"); + registerFileAction("groups", groupFile); + if (groupFile.exists()) { return fileToReader(groupFile, reader -> { reader.beginObject(); @@ -374,6 +384,8 @@ public class JSONBacking extends FlatfileBacking { try { return call(() -> { File groupFile = new File(groupsDir, name + ".json"); + registerFileAction("groups", groupFile); + return groupFile.exists() && fileToReader(groupFile, reader -> { reader.beginObject(); reader.nextName(); // name record @@ -420,6 +432,8 @@ public class JSONBacking extends FlatfileBacking { try { return call(() -> { File groupFile = new File(groupsDir, group.getName() + ".json"); + registerFileAction("groups", groupFile); + if (!groupFile.exists()) { try { groupFile.createNewFile(); @@ -453,6 +467,8 @@ public class JSONBacking extends FlatfileBacking { try { return call(() -> { File groupFile = new File(groupsDir, group.getName() + ".json"); + registerFileAction("groups", groupFile); + if (groupFile.exists()) { groupFile.delete(); } @@ -471,6 +487,8 @@ public class JSONBacking extends FlatfileBacking { if (files == null) return false; for (File file : files) { + registerFileAction("groups", file); + String holder = file.getName().substring(0, file.getName().length() - 5); Map nodes = new HashMap<>(); fileToReader(file, reader -> { @@ -511,6 +529,8 @@ public class JSONBacking extends FlatfileBacking { try { return call(() -> { File trackFile = new File(tracksDir, name + ".json"); + registerFileAction("tracks", trackFile); + if (trackFile.exists()) { return fileToReader(trackFile, reader -> { reader.beginObject(); @@ -561,6 +581,8 @@ public class JSONBacking extends FlatfileBacking { try { return call(() -> { File trackFile = new File(tracksDir, name + ".json"); + registerFileAction("tracks", trackFile); + return trackFile.exists() && fileToReader(trackFile, reader -> { reader.beginObject(); reader.nextName(); // name record @@ -606,6 +628,8 @@ public class JSONBacking extends FlatfileBacking { try { return call(() -> { File trackFile = new File(tracksDir, track.getName() + ".json"); + registerFileAction("tracks", trackFile); + if (!trackFile.exists()) { try { trackFile.createNewFile(); @@ -639,6 +663,8 @@ public class JSONBacking extends FlatfileBacking { try { return call(() -> { File trackFile = new File(tracksDir, track.getName() + ".json"); + registerFileAction("tracks", trackFile); + if (trackFile.exists()) { trackFile.delete(); } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/backing/YAMLBacking.java b/common/src/main/java/me/lucko/luckperms/common/storage/backing/YAMLBacking.java index 65f1898db..be6f97d18 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/backing/YAMLBacking.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/backing/YAMLBacking.java @@ -77,7 +77,7 @@ public class YAMLBacking extends FlatfileBacking { } public YAMLBacking(LuckPermsPlugin plugin, File pluginDir) { - super(plugin, "YAML", pluginDir); + super(plugin, "YAML", pluginDir, ".yml"); } private boolean writeMapToFile(File file, Map values) { @@ -110,6 +110,7 @@ public class YAMLBacking extends FlatfileBacking { try { return call(() -> { File userFile = new File(usersDir, uuid.toString() + ".yml"); + registerFileAction("users", userFile); if (userFile.exists()) { return readMapFromFile(userFile, values -> { // User exists, let's load. @@ -159,6 +160,7 @@ public class YAMLBacking extends FlatfileBacking { try { return call(() -> { File userFile = new File(usersDir, user.getUuid().toString() + ".yml"); + registerFileAction("users", userFile); if (!GenericUserManager.shouldSave(user)) { if (userFile.exists()) { userFile.delete(); @@ -194,6 +196,7 @@ public class YAMLBacking extends FlatfileBacking { if (files == null) return false; for (File file : files) { + registerFileAction("users", file); Map nodes = new HashMap<>(); readMapFromFile(file, values -> { Map perms = (Map) values.get("perms"); @@ -235,6 +238,8 @@ public class YAMLBacking extends FlatfileBacking { if (files == null) return false; for (File file : files) { + registerFileAction("users", file); + UUID holder = UUID.fromString(file.getName().substring(0, file.getName().length() - 4)); Map nodes = new HashMap<>(); readMapFromFile(file, values -> { @@ -264,6 +269,7 @@ public class YAMLBacking extends FlatfileBacking { try { return call(() -> { File groupFile = new File(groupsDir, name + ".yml"); + registerFileAction("groups", groupFile); if (groupFile.exists()) { return readMapFromFile(groupFile, values -> { Map perms = (Map) values.get("perms"); @@ -296,6 +302,7 @@ public class YAMLBacking extends FlatfileBacking { try { return call(() -> { File groupFile = new File(groupsDir, name + ".yml"); + registerFileAction("groups", groupFile); return groupFile.exists() && readMapFromFile(groupFile, values -> { Map perms = (Map) values.get("perms"); group.setNodes(perms); @@ -331,6 +338,7 @@ public class YAMLBacking extends FlatfileBacking { try { return call(() -> { File groupFile = new File(groupsDir, group.getName() + ".yml"); + registerFileAction("groups", groupFile); if (!groupFile.exists()) { try { groupFile.createNewFile(); @@ -356,6 +364,7 @@ public class YAMLBacking extends FlatfileBacking { try { return call(() -> { File groupFile = new File(groupsDir, group.getName() + ".yml"); + registerFileAction("groups", groupFile); if (groupFile.exists()) { groupFile.delete(); } @@ -374,6 +383,8 @@ public class YAMLBacking extends FlatfileBacking { if (files == null) return false; for (File file : files) { + registerFileAction("groups", file); + String holder = file.getName().substring(0, file.getName().length() - 4); Map nodes = new HashMap<>(); readMapFromFile(file, values -> { @@ -403,6 +414,8 @@ public class YAMLBacking extends FlatfileBacking { try { return call(() -> { File trackFile = new File(tracksDir, name + ".yml"); + registerFileAction("tracks", trackFile); + if (trackFile.exists()) { return readMapFromFile(trackFile, values -> { track.setGroups((List) values.get("groups")); @@ -435,6 +448,8 @@ public class YAMLBacking extends FlatfileBacking { try { return call(() -> { File trackFile = new File(tracksDir, name + ".yml"); + registerFileAction("tracks", trackFile); + return trackFile.exists() && readMapFromFile(trackFile, values -> { track.setGroups((List) values.get("groups")); return true; @@ -468,6 +483,8 @@ public class YAMLBacking extends FlatfileBacking { try { return call(() -> { File trackFile = new File(tracksDir, track.getName() + ".yml"); + registerFileAction("tracks", trackFile); + if (!trackFile.exists()) { try { trackFile.createNewFile(); @@ -493,6 +510,8 @@ public class YAMLBacking extends FlatfileBacking { try { return call(() -> { File trackFile = new File(tracksDir, track.getName() + ".yml"); + registerFileAction("tracks", trackFile); + if (trackFile.exists()) { trackFile.delete(); } diff --git a/common/src/main/java/me/lucko/luckperms/common/utils/FileWatcher.java b/common/src/main/java/me/lucko/luckperms/common/utils/FileWatcher.java new file mode 100644 index 000000000..35fd2c264 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/utils/FileWatcher.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2016 Lucko (Luck) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.utils; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; + +import java.io.IOException; +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.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +public class FileWatcher implements Runnable { + private final LuckPermsPlugin plugin; + + private final Map keyMap; + private final Map internalChanges; + private WatchService watchService = null; + + public FileWatcher(LuckPermsPlugin plugin) { + this.plugin = plugin; + this.keyMap = Collections.synchronizedMap(new HashMap<>()); + this.internalChanges = Collections.synchronizedMap(new HashMap<>()); + try { + this.watchService = plugin.getDataDirectory().toPath().getFileSystem().newWatchService(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void subscribe(String id, Path path, Consumer consumer) { + if (watchService == null) { + return; + } + + // Register with a delay to ignore changes made at startup + plugin.getScheduler().doAsyncLater(() -> { + try { + // doesn't need to be atomic + if (keyMap.containsKey(id)) { + throw new IllegalArgumentException("id already registered"); + } + + WatchKey key = path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY); + keyMap.put(id, new WatchedLocation(path, key, consumer)); + } catch (IOException e) { + e.printStackTrace(); + } + }, 40L); + } + + public void registerChange(String id, String filename) { + internalChanges.put(id + "/" + filename, System.currentTimeMillis()); + } + + public void close() { + if (watchService == null) { + return; + } + + try { + watchService.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void run() { + long expireTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(4); + // was either processed last time, or recently modified by the system. + internalChanges.values().removeIf(lastChange -> lastChange < expireTime); + + List expired = new ArrayList<>(); + + for (Map.Entry ent : keyMap.entrySet()) { + String id = ent.getKey(); + Path path = ent.getValue().getPath(); + WatchKey key = ent.getValue().getKey(); + + List> watchEvents = key.pollEvents(); + + for (WatchEvent event : watchEvents) { + Path name = (Path) event.context(); + Path file = path.resolve(name); + + String fileName = name.toString(); + + if (internalChanges.containsKey(id + "/" + fileName)) { + // This file was modified by the system. + continue; + } + + registerChange(id, fileName); + + plugin.getLog().info("[FileWatcher] Detected change in file: " + file.toString()); + + // Process the change + ent.getValue().getFileConsumer().accept(fileName); + } + + boolean valid = key.reset(); + if (!valid) { + new RuntimeException("WatchKey no longer valid: " + key.toString()).printStackTrace(); + expired.add(id); + } + } + + expired.forEach(keyMap::remove); + } + + @Getter + @RequiredArgsConstructor + private static class WatchedLocation { + private final Path path; + private final WatchKey key; + private final Consumer fileConsumer; + } + +} diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java b/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java index 9c930e5a6..ffc550aa4 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java @@ -62,6 +62,7 @@ import me.lucko.luckperms.common.tasks.ExpireTemporaryTask; import me.lucko.luckperms.common.tasks.UpdateTask; import me.lucko.luckperms.common.treeview.PermissionVault; import me.lucko.luckperms.common.utils.BufferedRequest; +import me.lucko.luckperms.common.utils.FileWatcher; import me.lucko.luckperms.common.utils.LoggerImpl; import me.lucko.luckperms.sponge.commands.SpongeMainCommand; import me.lucko.luckperms.sponge.contexts.WorldCalculator; @@ -146,6 +147,7 @@ public class LPSpongePlugin implements LuckPermsPlugin { private SpongeGroupManager groupManager; private TrackManager trackManager; private Storage storage; + private FileWatcher fileWatcher = null; private InternalMessagingService messagingService = null; private UuidCache uuidCache; private ApiProvider apiProvider; @@ -183,6 +185,11 @@ public class LPSpongePlugin implements LuckPermsPlugin { // register events game.getEventManager().registerListeners(this, new SpongeListener(this)); + if (getConfiguration().get(ConfigKeys.WATCH_FILES)) { + fileWatcher = new FileWatcher(this); + getScheduler().doAsyncRepeating(fileWatcher, 30L); + } + // initialise datastore storage = StorageFactory.getInstance(this, StorageType.H2); @@ -309,6 +316,10 @@ public class LPSpongePlugin implements LuckPermsPlugin { getLog().info("Closing datastore..."); storage.shutdown(); + if (fileWatcher != null) { + fileWatcher.close(); + } + if (messagingService != null) { getLog().info("Closing messaging service..."); messagingService.close(); diff --git a/sponge/src/main/resources/luckperms.conf b/sponge/src/main/resources/luckperms.conf index c152feb49..19ef6dfb8 100644 --- a/sponge/src/main/resources/luckperms.conf +++ b/sponge/src/main/resources/luckperms.conf @@ -138,6 +138,12 @@ meta-formatting { # Fill out connection info below if you're using MySQL, PostgreSQL or MongoDB storage-method="h2" +# When using a file-based storage type, LuckPerms can monitor the data files for changes, and then schedule automatic +# updates when changes are detected. +# +# If you don't want this to happen, set this option to false. +watch-files=true + # This block enables support for split datastores. split-storage { enabled=false @@ -165,9 +171,14 @@ data { # This should *not* be set to "lp_" if you have previously ran LuckPerms v2.16.81 or earlier with this database. table_prefix="luckperms_" - # Set to -1 to disable. If this is the only instance accessing the datastore, you can disable syncing. - # e.g. if you're using sqlite or flatfile, this can be set to -1 to save resources. - sync-minutes=3 + # This option controls how frequently LuckPerms will perform a sync task. + # A sync task will refresh all data from the storage, and ensure that the most up-to-date data is being used by the plugin. + # + # This is disabled by default, as most users will not need it. However, if you're using a remote storage type + # without a messaging service setup, you may wish to set this value to something like 3. + # + # Set to -1 to disable the task completely. + sync-minutes=-1 } # Settings for the messaging service