diff --git a/Common/src/main/java/net/frankheijden/serverutils/common/commands/CommandServerUtils.java b/Common/src/main/java/net/frankheijden/serverutils/common/commands/CommandServerUtils.java index 4e28500..b2ec42d 100644 --- a/Common/src/main/java/net/frankheijden/serverutils/common/commands/CommandServerUtils.java +++ b/Common/src/main/java/net/frankheijden/serverutils/common/commands/CommandServerUtils.java @@ -17,13 +17,13 @@ import net.frankheijden.serverutils.common.commands.arguments.PluginArgument; import net.frankheijden.serverutils.common.commands.arguments.PluginsArgument; import net.frankheijden.serverutils.common.config.MessagesResource; import net.frankheijden.serverutils.common.config.ServerUtilsConfig; -import net.frankheijden.serverutils.common.entities.results.AbstractResult; import net.frankheijden.serverutils.common.entities.results.CloseablePluginResults; import net.frankheijden.serverutils.common.entities.results.PluginResult; import net.frankheijden.serverutils.common.entities.results.PluginResults; import net.frankheijden.serverutils.common.entities.results.Result; import net.frankheijden.serverutils.common.entities.ServerCommandSender; import net.frankheijden.serverutils.common.entities.ServerUtilsPlugin; +import net.frankheijden.serverutils.common.entities.results.WatchResult; import net.frankheijden.serverutils.common.managers.AbstractPluginManager; import net.frankheijden.serverutils.common.utils.FormatBuilder; import net.frankheijden.serverutils.common.utils.ListBuilder; @@ -73,7 +73,11 @@ public abstract class CommandServerUtils( + true, + "plugins", + new PluginsArgument.PluginsParser<>(plugin, arrayCreator, getRawPath("watchplugin")) + )) .handler(this::handleWatchPlugin)); manager.command(buildSubcommand(builder, "unwatchplugin") .argument(getArgument("plugin")) @@ -256,24 +260,23 @@ public abstract class CommandServerUtils context) { C sender = context.getSender(); - P pluginArg = context.get("plugin"); + List

plugins = Arrays.asList(context.get("plugins")); - AbstractPluginManager pluginManager = plugin.getPluginManager(); - String pluginId = pluginManager.getPluginId(pluginArg); + if (checkDependingPlugins(context, sender, plugins, "watchplugin")) { + return; + } - AbstractResult result = pluginManager.watchPlugin(sender, pluginId); - result.sendTo(sender, "watch", pluginId); + WatchResult result = plugin.getWatchManager().watchPlugins(sender, plugins); + result.sendTo(sender); } private void handleUnwatchPlugin(CommandContext context) { C sender = context.getSender(); P pluginArg = context.get("plugin"); - AbstractPluginManager pluginManager = plugin.getPluginManager(); - String pluginId = pluginManager.getPluginId(pluginArg); - - AbstractResult result = pluginManager.unwatchPlugin(pluginId); - result.sendTo(sender, "unwatch", pluginId); + String pluginId = plugin.getPluginManager().getPluginId(pluginArg); + WatchResult result = plugin.getWatchManager().unwatchPluginsAssociatedWith(pluginId); + result.sendTo(sender); } private void handlePluginInfo(CommandContext context) { diff --git a/Common/src/main/java/net/frankheijden/serverutils/common/entities/ServerUtilsPlugin.java b/Common/src/main/java/net/frankheijden/serverutils/common/entities/ServerUtilsPlugin.java index 8965299..81ffffe 100644 --- a/Common/src/main/java/net/frankheijden/serverutils/common/entities/ServerUtilsPlugin.java +++ b/Common/src/main/java/net/frankheijden/serverutils/common/entities/ServerUtilsPlugin.java @@ -17,6 +17,7 @@ import net.frankheijden.serverutils.common.config.MessagesResource; import net.frankheijden.serverutils.common.managers.AbstractPluginManager; import net.frankheijden.serverutils.common.managers.AbstractTaskManager; import net.frankheijden.serverutils.common.managers.UpdateManager; +import net.frankheijden.serverutils.common.managers.WatchManager; import net.frankheijden.serverutils.common.providers.ChatProvider; import net.frankheijden.serverutils.common.providers.ResourceProvider; import net.frankheijden.serverutils.common.utils.FileUtils; @@ -24,6 +25,7 @@ import net.frankheijden.serverutils.common.utils.FileUtils; public abstract class ServerUtilsPlugin, S, D extends ServerUtilsPluginDescription> { private final UpdateManager updateManager = new UpdateManager(); + private final WatchManager watchManager = new WatchManager<>(this); private CommandsResource commandsResource; private ConfigResource configResource; private MessagesResource messagesResource; @@ -57,6 +59,10 @@ public abstract class ServerUtilsPlugin, return updateManager; } + public WatchManager getWatchManager() { + return watchManager; + } + public abstract Logger getLogger(); public abstract File getDataFolder(); diff --git a/Common/src/main/java/net/frankheijden/serverutils/common/entities/results/WatchResult.java b/Common/src/main/java/net/frankheijden/serverutils/common/entities/results/WatchResult.java index 787378d..091981d 100644 --- a/Common/src/main/java/net/frankheijden/serverutils/common/entities/results/WatchResult.java +++ b/Common/src/main/java/net/frankheijden/serverutils/common/entities/results/WatchResult.java @@ -1,28 +1,60 @@ package net.frankheijden.serverutils.common.entities.results; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import net.frankheijden.serverutils.common.ServerUtilsApp; +import net.frankheijden.serverutils.common.config.MessagesResource; import net.frankheijden.serverutils.common.entities.ServerCommandSender; public enum WatchResult implements AbstractResult { START, CHANGE, + ALREADY_WATCHING, NOT_WATCHING, + FILE_DELETED, + DELETED_FILE_IS_CREATED, STOPPED; - /** - * Retrieves the associated message of the result - * and sends it to a CommandSender. - * @param sender The receiver. - * @param action The action which let to the result. - * @param what An associated variable. - */ + private List args = null; + + public WatchResult arg(String arg) { + return args(Collections.singletonList(arg)); + } + + public WatchResult args(List args) { + this.args = args; + return this; + } + @Override public void sendTo(ServerCommandSender sender, String action, String what) { - ServerUtilsApp.getPlugin().getMessagesResource().sendMessage( - sender, - "serverutils.watcher." + this.name().toLowerCase(), - "%what%", what - ); + arg(what); + sendTo(sender); + } + + /** + * Sends the result(s) to the console and specified sender. + */ + public void sendTo(ServerCommandSender sender) { + String path = "serverutils.watchplugin." + this.name().toLowerCase(); + List sendArguments = new ArrayList<>(); + if (args == null || args.isEmpty()) { + sendArguments.add(new String[0]); + } else { + for (String what : args) { + sendArguments.add(new String[] { "%what%", what }); + } + } + + MessagesResource messages = ServerUtilsApp.getPlugin().getMessagesResource(); + ServerCommandSender console = ServerUtilsApp.getPlugin().getChatProvider().getConsoleSender(); + for (String[] replacements : sendArguments) { + messages.sendMessage(sender, path, replacements); + if (sender.isPlayer()) { + messages.sendMessage(console, path, replacements); + } + } } } diff --git a/Common/src/main/java/net/frankheijden/serverutils/common/managers/AbstractPluginManager.java b/Common/src/main/java/net/frankheijden/serverutils/common/managers/AbstractPluginManager.java index 3468956..c5ef500 100644 --- a/Common/src/main/java/net/frankheijden/serverutils/common/managers/AbstractPluginManager.java +++ b/Common/src/main/java/net/frankheijden/serverutils/common/managers/AbstractPluginManager.java @@ -10,19 +10,14 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import net.frankheijden.serverutils.common.ServerUtilsApp; -import net.frankheijden.serverutils.common.entities.results.AbstractResult; import net.frankheijden.serverutils.common.entities.results.CloseablePluginResult; import net.frankheijden.serverutils.common.entities.results.CloseablePluginResults; import net.frankheijden.serverutils.common.entities.results.PluginResult; import net.frankheijden.serverutils.common.entities.results.PluginResults; import net.frankheijden.serverutils.common.entities.results.Result; -import net.frankheijden.serverutils.common.entities.ServerCommandSender; import net.frankheijden.serverutils.common.entities.ServerUtilsPluginDescription; -import net.frankheijden.serverutils.common.entities.results.WatchResult; import net.frankheijden.serverutils.common.entities.exceptions.InvalidPluginDescriptionException; import net.frankheijden.serverutils.common.providers.PluginProvider; -import net.frankheijden.serverutils.common.tasks.PluginWatcherTask; import net.frankheijden.serverutils.common.utils.DependencyUtils; public abstract class AbstractPluginManager implements PluginProvider { @@ -294,23 +289,4 @@ public abstract class AbstractPluginManager sender, String pluginId) { - if (getPlugin(pluginId).isPresent()) return Result.NOT_EXISTS; - ServerUtilsApp.getPlugin().getTaskManager() - .runTaskAsynchronously(pluginId, new PluginWatcherTask(sender, pluginId)); - return WatchResult.START; - } - - /** - * Stops watching the plugin for changes. - */ - public AbstractResult unwatchPlugin(String pluginId) { - if (ServerUtilsApp.getPlugin().getTaskManager().cancelTask(pluginId)) return WatchResult.STOPPED; - return WatchResult.NOT_WATCHING; - } } diff --git a/Common/src/main/java/net/frankheijden/serverutils/common/managers/WatchManager.java b/Common/src/main/java/net/frankheijden/serverutils/common/managers/WatchManager.java new file mode 100644 index 0000000..d9e53f3 --- /dev/null +++ b/Common/src/main/java/net/frankheijden/serverutils/common/managers/WatchManager.java @@ -0,0 +1,73 @@ +package net.frankheijden.serverutils.common.managers; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import net.frankheijden.serverutils.common.entities.ServerCommandSender; +import net.frankheijden.serverutils.common.entities.ServerUtilsPlugin; +import net.frankheijden.serverutils.common.entities.results.WatchResult; +import net.frankheijden.serverutils.common.tasks.PluginWatcherTask; + +public class WatchManager { + + private final ServerUtilsPlugin plugin; + private final Map watchTasks; + + public WatchManager(ServerUtilsPlugin plugin) { + this.plugin = plugin; + this.watchTasks = new HashMap<>(); + } + + /** + * Starts watching the specified plugin and reloads it when a change is detected. + */ + public WatchResult watchPlugins(ServerCommandSender sender, List

plugins) { + List pluginIds = new ArrayList<>(plugins.size()); + for (P watchPlugin : plugins) { + String pluginId = plugin.getPluginManager().getPluginId(watchPlugin); + if (watchTasks.containsKey(pluginId)) { + return WatchResult.ALREADY_WATCHING.arg(pluginId); + } + + pluginIds.add(plugin.getPluginManager().getPluginId(watchPlugin)); + } + + UUID key = UUID.randomUUID(); + plugin.getTaskManager().runTaskAsynchronously( + key.toString(), + new PluginWatcherTask<>(plugin, sender, plugins) + ); + + WatchTask watchTask = new WatchTask(key, pluginIds); + for (String pluginId : pluginIds) { + watchTasks.put(pluginId, watchTask); + } + + return WatchResult.START.args(pluginIds); + } + + /** + * Stops watching plugins for changes. + */ + public WatchResult unwatchPluginsAssociatedWith(String pluginId) { + WatchTask task = watchTasks.get(pluginId); + if (task != null && plugin.getTaskManager().cancelTask(task.key.toString())) { + task.pluginIds.forEach(watchTasks::remove); + return WatchResult.STOPPED.args(task.pluginIds); + } + return WatchResult.NOT_WATCHING.arg(pluginId); + } + + private static final class WatchTask { + + private final UUID key; + private final List pluginIds; + + private WatchTask(UUID key, List pluginIds) { + this.key = key; + this.pluginIds = pluginIds; + } + } +} diff --git a/Common/src/main/java/net/frankheijden/serverutils/common/tasks/PluginWatcherTask.java b/Common/src/main/java/net/frankheijden/serverutils/common/tasks/PluginWatcherTask.java index 3401f3e..70e705f 100644 --- a/Common/src/main/java/net/frankheijden/serverutils/common/tasks/PluginWatcherTask.java +++ b/Common/src/main/java/net/frankheijden/serverutils/common/tasks/PluginWatcherTask.java @@ -1,26 +1,34 @@ package net.frankheijden.serverutils.common.tasks; import com.sun.nio.file.SensitivityWatchEventModifier; -import net.frankheijden.serverutils.common.ServerUtilsApp; -import net.frankheijden.serverutils.common.entities.AbstractTask; -import net.frankheijden.serverutils.common.entities.ServerCommandSender; -import net.frankheijden.serverutils.common.entities.ServerUtilsPlugin; -import net.frankheijden.serverutils.common.entities.results.WatchResult; -import net.frankheijden.serverutils.common.managers.AbstractPluginManager; -import net.frankheijden.serverutils.common.managers.AbstractTaskManager; -import net.frankheijden.serverutils.common.providers.ChatProvider; -import net.frankheijden.serverutils.common.utils.FileUtils; import java.io.File; import java.io.IOException; import java.nio.file.ClosedWatchServiceException; 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.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; +import net.frankheijden.serverutils.common.entities.AbstractTask; +import net.frankheijden.serverutils.common.entities.ServerCommandSender; +import net.frankheijden.serverutils.common.entities.ServerUtilsPlugin; +import net.frankheijden.serverutils.common.entities.ServerUtilsPluginDescription; +import net.frankheijden.serverutils.common.entities.exceptions.InvalidPluginDescriptionException; +import net.frankheijden.serverutils.common.entities.results.PluginResult; +import net.frankheijden.serverutils.common.entities.results.PluginResults; +import net.frankheijden.serverutils.common.entities.results.WatchResult; +import net.frankheijden.serverutils.common.managers.AbstractPluginManager; +import net.frankheijden.serverutils.common.utils.FileUtils; -public class PluginWatcherTask extends AbstractTask { +public class PluginWatcherTask extends AbstractTask { private static final WatchEvent.Kind[] EVENTS = new WatchEvent.Kind[]{ StandardWatchEventKinds.ENTRY_CREATE, @@ -28,32 +36,33 @@ public class PluginWatcherTask extends AbstractTask { StandardWatchEventKinds.ENTRY_DELETE }; - private final ServerUtilsPlugin plugin = ServerUtilsApp.getPlugin(); - private final AbstractPluginManager pluginManager = plugin.getPluginManager(); - private final ChatProvider chatProvider = plugin.getChatProvider(); - @SuppressWarnings("rawtypes") - private final AbstractTaskManager taskManager = plugin.getTaskManager(); - + private final ServerUtilsPlugin plugin; private final ServerCommandSender sender; - private final String pluginName; - private final AtomicBoolean run; - private File file; - private String hash; - private long hashTimestamp = 0; + private final Map fileNameToWatchEntryMap; + private final Map pluginIdToWatchEntryMap; + private final AtomicBoolean run = new AtomicBoolean(true); private WatchService watchService; - private Object task = null; + private T task = null; /** * Constructs a new PluginWatcherTask for the specified plugin. - * - * @param pluginName The name of the plugin. */ - public PluginWatcherTask(ServerCommandSender sender, String pluginName) { + public PluginWatcherTask(ServerUtilsPlugin plugin, ServerCommandSender sender, List

plugins) { + this.plugin = plugin; this.sender = sender; - this.pluginName = pluginName; - this.file = pluginManager.getPluginFile(pluginName).orElse(null); - this.run = new AtomicBoolean(true); + this.fileNameToWatchEntryMap = new HashMap<>(); + this.pluginIdToWatchEntryMap = new HashMap<>(); + + AbstractPluginManager pluginManager = plugin.getPluginManager(); + for (P watchPlugin : plugins) { + File file = pluginManager.getPluginFile(watchPlugin); + + WatchEntry entry = new WatchEntry(pluginManager.getPluginId(watchPlugin)); + entry.update(file); + + this.fileNameToWatchEntryMap.put(file.getName(), entry); + } } @Override @@ -61,34 +70,21 @@ public class PluginWatcherTask extends AbstractTask { try (WatchService watchService = FileSystems.getDefault().newWatchService()) { this.watchService = watchService; - File folder = pluginManager.getPluginsFolder(); - folder.toPath().register(watchService, EVENTS, SensitivityWatchEventModifier.HIGH); + AbstractPluginManager pluginManager = plugin.getPluginManager(); + Path basePath = pluginManager.getPluginsFolder().toPath(); + basePath.register(watchService, EVENTS, SensitivityWatchEventModifier.HIGH); while (run.get()) { WatchKey key = watchService.take(); for (WatchEvent event : key.pollEvents()) { - if (file.getName().equals(event.context().toString())) { - if (task != null) { - taskManager.cancelTask(task); - } + Path path = basePath.resolve((Path) event.context()); - String previousHash = hash; - long previousHashTimestamp = hashTimestamp; - - hash = FileUtils.getHash(file.toPath()); - hashTimestamp = System.currentTimeMillis(); - task = ServerUtilsApp.getPlugin().getTaskManager().runTaskLater(() -> { - if (hash.equals(previousHash) || previousHashTimestamp < hashTimestamp - 1000L) { - send(WatchResult.CHANGE); - - pluginManager.reloadPlugin(pluginName); - file = pluginManager.getPluginFile(pluginName).orElse(null); - } - }, 10L); + if (!Files.isDirectory(path)) { + handleWatchEvent(path); } } - if (file == null || !key.reset()) { + if ((fileNameToWatchEntryMap.isEmpty() && pluginIdToWatchEntryMap.isEmpty()) || !key.reset()) { send(WatchResult.STOPPED); break; } @@ -102,10 +98,93 @@ public class PluginWatcherTask extends AbstractTask { } } + private void handleWatchEvent(Path path) { + String fileName = path.getFileName().toString(); + WatchEntry entry = fileNameToWatchEntryMap.get(fileName); + + if (entry == null && Files.exists(path)) { + Optional descriptionOptional; + try { + descriptionOptional = plugin.getPluginManager().getPluginDescription(path.toFile()); + } catch (InvalidPluginDescriptionException ignored) { + return; + } + + if (descriptionOptional.isPresent()) { + ServerUtilsPluginDescription description = descriptionOptional.get(); + WatchEntry foundEntry = pluginIdToWatchEntryMap.remove(description.getId()); + if (foundEntry != null) { + send(WatchResult.DELETED_FILE_IS_CREATED.arg(foundEntry.pluginId)); + fileNameToWatchEntryMap.put(fileName, foundEntry); + + if (pluginIdToWatchEntryMap.isEmpty()) { + entry = foundEntry; + } + } + } + } + + if (entry != null) { + checkWatchEntry(entry, fileName); + } + } + + private void checkWatchEntry(WatchEntry entry, String fileName) { + if (task != null) { + plugin.getTaskManager().cancelTask(task); + } + + AbstractPluginManager pluginManager = plugin.getPluginManager(); + Optional fileOptional = pluginManager.getPluginFile(entry.pluginId); + if (!fileOptional.isPresent()) { + send(WatchResult.FILE_DELETED.arg(entry.pluginId)); + + fileNameToWatchEntryMap.remove(fileName); + pluginIdToWatchEntryMap.put(entry.pluginId, entry); + return; + } + + String previousHash = entry.hash; + long previousTimestamp = entry.timestamp; + entry.update(fileOptional.get()); + + task = plugin.getTaskManager().runTaskLater(() -> { + if (entry.hash.equals(previousHash) || previousTimestamp < entry.timestamp - 1000L) { + send(WatchResult.CHANGE); + + List

plugins = new ArrayList<>(fileNameToWatchEntryMap.size()); + Map retainedWatchEntries = new HashMap<>(); + for (WatchEntry oldEntry : fileNameToWatchEntryMap.values()) { + Optional

pluginOptional = pluginManager.getPlugin(oldEntry.pluginId); + if (!pluginOptional.isPresent()) continue; + + plugins.add(pluginOptional.get()); + retainedWatchEntries.put(oldEntry.pluginId, oldEntry); + } + + fileNameToWatchEntryMap.clear(); + + PluginResults

reloadResults = pluginManager.reloadPlugins(plugins); + reloadResults.sendTo(sender, "reload"); + + for (PluginResult

reloadResult : reloadResults) { + if (!reloadResult.isSuccess()) continue; + + P reloadedPlugin = reloadResult.getPlugin(); + String pluginId = pluginManager.getPluginId(reloadedPlugin); + + WatchEntry retainedEntry = retainedWatchEntries.get(pluginId); + String pluginFileName = pluginManager.getPluginFile(reloadedPlugin).getName(); + fileNameToWatchEntryMap.put(pluginFileName, retainedEntry); + } + } + }, 10L); + } + private void send(WatchResult result) { - result.sendTo(sender, null, pluginName); + result.sendTo(sender); if (sender.isPlayer()) { - result.sendTo(chatProvider.getConsoleSender(), null, pluginName); + result.sendTo(plugin.getChatProvider().getConsoleSender()); } } @@ -118,4 +197,20 @@ public class PluginWatcherTask extends AbstractTask { ex.printStackTrace(); } } + + private static final class WatchEntry { + + private final String pluginId; + private String hash = null; + private long timestamp = 0L; + + public WatchEntry(String pluginId) { + this.pluginId = pluginId; + } + + public void update(File file) { + this.hash = FileUtils.getHash(file.toPath()); + this.timestamp = System.currentTimeMillis(); + } + } } diff --git a/Common/src/main/resources/commands.json b/Common/src/main/resources/commands.json index 7357d8d..ffecd21 100644 --- a/Common/src/main/resources/commands.json +++ b/Common/src/main/resources/commands.json @@ -80,8 +80,17 @@ "main": "watchplugin", "aliases": ["wp"], "permission": "serverutils.watchplugin", - "description": "Watches the specified plugin for changes.", - "display-in-help": true + "description": "Watches the specified plugin(s) for changes.", + "display-in-help": true, + "flags": { + "force": { + "main": "force", + "aliases": ["f"], + "permission": "serverutils.watchplugin", + "description": "Force watches the specified plugin(s) for changes.", + "display-in-help": false + } + } }, "unwatchplugin": { "main": "unwatchplugin", diff --git a/Common/src/main/resources/messages.json b/Common/src/main/resources/messages.json index b2a02ce..1cf76d5 100644 --- a/Common/src/main/resources/messages.json +++ b/Common/src/main/resources/messages.json @@ -20,11 +20,14 @@ "last_separator": " &cand ", "override": "&cUse \"&4/%command%&c\" to force command execution." }, - "watcher": { + "watchplugin": { "start": "&3Started watching &b%what%&3!", - "change": "&3Change detected for plugin &b%what%&3, reloading now...", + "change": "&3Changes detected, reloading plugins now...", "stopped": "&3Stopped watching &b%what%&3!", - "not_watching": "&cWe aren't watching that plugin!" + "file_deleted": "&cPlugin file &4%what%&c has been deleted! Waiting for plugin to show up...", + "deleted_file_is_created": "&3Plugin file &b%what%&3 has shown up again!", + "already_watching": "&cYou are already watching &4%what%&c!", + "not_watching": "&cYou are not watching &4%what%&c!" }, "update": { "available": "&8&m------------=&r&8[ &b&lServerUtils Update&r &8]&m=--------------\n &3Current version: &b%old%\n &3New version: &b%new%\n &3Release info: &b%info%\n&8&m-------------------------------------------------",