Add multi plugin watcher

This commit is contained in:
Frank van der Heijden 2021-07-31 21:05:03 +02:00
parent 1b4997869d
commit 8d3a85d472
No known key found for this signature in database
GPG key ID: B808721C2DD5B5B8
8 changed files with 300 additions and 103 deletions

View file

@ -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<U extends ServerUtilsPlugin<P, ?, C, ?,
))
.handler(this::handleReloadPlugin));
manager.command(buildSubcommand(builder, "watchplugin")
.argument(getArgument("plugin"))
.argument(new PluginsArgument<>(
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<U extends ServerUtilsPlugin<P, ?, C, ?,
private void handleWatchPlugin(CommandContext<C> context) {
C sender = context.getSender();
P pluginArg = context.get("plugin");
List<P> plugins = Arrays.asList(context.get("plugins"));
AbstractPluginManager<P, ?> 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<C> context) {
C sender = context.getSender();
P pluginArg = context.get("plugin");
AbstractPluginManager<P, ?> 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<C> context) {

View file

@ -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<P, T, C extends ServerCommandSender<S>, S, D extends ServerUtilsPluginDescription> {
private final UpdateManager updateManager = new UpdateManager();
private final WatchManager<P, T> watchManager = new WatchManager<>(this);
private CommandsResource commandsResource;
private ConfigResource configResource;
private MessagesResource messagesResource;
@ -57,6 +59,10 @@ public abstract class ServerUtilsPlugin<P, T, C extends ServerCommandSender<S>,
return updateManager;
}
public WatchManager<P, T> getWatchManager() {
return watchManager;
}
public abstract Logger getLogger();
public abstract File getDataFolder();

View file

@ -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<String> args = null;
public WatchResult arg(String arg) {
return args(Collections.singletonList(arg));
}
public WatchResult args(List<String> 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<String[]> 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);
}
}
}
}

View file

@ -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<P, D extends ServerUtilsPluginDescription> implements PluginProvider<P, D> {
@ -294,23 +289,4 @@ public abstract class AbstractPluginManager<P, D extends ServerUtilsPluginDescri
return DependencyUtils.determineOrder(dependencyMap);
}
/**
* Starts watching the specified plugin for changes.
* Reloads the plugin if a change is detected.
*/
public AbstractResult watchPlugin(ServerCommandSender<?> 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;
}
}

View file

@ -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<P, T> {
private final ServerUtilsPlugin<P, T, ?, ?, ?> plugin;
private final Map<String, WatchTask> watchTasks;
public WatchManager(ServerUtilsPlugin<P, T, ?, ?, ?> 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<P> plugins) {
List<String> 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<String> pluginIds;
private WatchTask(UUID key, List<String> pluginIds) {
this.key = key;
this.pluginIds = pluginIds;
}
}
}

View file

@ -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<P, T> 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<P, T, ?, ?, ?> 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<String, WatchEntry> fileNameToWatchEntryMap;
private final Map<String, WatchEntry> 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<P, T, ?, ?, ?> plugin, ServerCommandSender<?> sender, List<P> 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<P, ?> 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<P, ?> 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<? extends ServerUtilsPluginDescription> 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<P, ?> pluginManager = plugin.getPluginManager();
Optional<File> 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<P> plugins = new ArrayList<>(fileNameToWatchEntryMap.size());
Map<String, WatchEntry> retainedWatchEntries = new HashMap<>();
for (WatchEntry oldEntry : fileNameToWatchEntryMap.values()) {
Optional<P> pluginOptional = pluginManager.getPlugin(oldEntry.pluginId);
if (!pluginOptional.isPresent()) continue;
plugins.add(pluginOptional.get());
retainedWatchEntries.put(oldEntry.pluginId, oldEntry);
}
fileNameToWatchEntryMap.clear();
PluginResults<P> reloadResults = pluginManager.reloadPlugins(plugins);
reloadResults.sendTo(sender, "reload");
for (PluginResult<P> 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();
}
}
}