Fix watched plugins reloading without ensuring the file has fully uploaded first (#8)

* build: add Apache codec library

Utilise the Apache commons codec library, which has file hashing and checking functions. This will be useful for a fix on hotreloading plugins.

* feat: run tasks later on taskmanagers

Add the ability to run tasks at a later date on task managers.
This is expressed in ticks for bungeecord and bukkit, and is converted to milliseconds for bungeecord.

* feat: add method to get hash of file

Add a method to get the MD5 hash of a file.

* fix: watching plugins waits until the plugin has finished uploading

If a watched plugin is changed, it will no longer immediately reload the plugin. Instead, it will check every 10 ticks (500 milliseconds for BungeeCord) to see if the file MD5 checksum has changed.

If it hasn't, then we can presume the file upload has completed and reload the plugin.

* build: bump version to 2.5.1

* build: relocate apache codec

Relocate the Apache Codec to avoid dependency clashes

* perf: remove unnecessary task call
This commit is contained in:
Phil Gibson 2021-02-03 19:36:31 +00:00 committed by GitHub
parent 5d227ac06e
commit b51ff445dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 75 additions and 19 deletions

View file

@ -1,11 +1,11 @@
package net.frankheijden.serverutils.common.managers;
import net.frankheijden.serverutils.common.entities.AbstractTask;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import net.frankheijden.serverutils.common.entities.AbstractTask;
public abstract class AbstractTaskManager<T> {
@ -15,6 +15,7 @@ public abstract class AbstractTaskManager<T> {
/**
* Constructs a new TaskManager with a consumer which closes a task.
*
* @param taskCloser The consumer which will close tasks.
*/
public AbstractTaskManager(Consumer<T> taskCloser) {
@ -25,13 +26,23 @@ public abstract class AbstractTaskManager<T> {
protected abstract T runTaskImpl(Runnable runnable);
/**
* Run a task later after a certain delay (synchronously).
*
* @param runnable The Runnable
* @param delay The delay in ticks (for BungeeCord, this is automatically converted to milliseconds).
* @return The scheduled task
*/
public abstract T runTaskLater(Runnable runnable, long delay);
public T runTask(Runnable runnable) {
return addTask(runTaskImpl(runnable));
}
/**
* Associates a synchronous task with a key which can be cancelled later by that key.
* @param key The key of the task.
*
* @param key The key of the task.
* @param abstractTask The AbstractTask.
* @return The implementation-specific scheduled task.
*/
@ -49,7 +60,8 @@ public abstract class AbstractTaskManager<T> {
/**
* Associates an asynchronous task with a key which can be cancelled later by that key.
* @param key The key of the task.
*
* @param key The key of the task.
* @param abstractTask The AbstractTask.
* @return The implementation-specific scheduled task.
*/
@ -68,6 +80,7 @@ public abstract class AbstractTaskManager<T> {
/**
* Cancels a single task by key.
*
* @param key The key of the task.
* @return Whether or not the task existed.
*/

View file

@ -1,6 +1,15 @@
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.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;
@ -10,18 +19,10 @@ import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.concurrent.atomic.AtomicBoolean;
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.WatchResult;
import net.frankheijden.serverutils.common.managers.AbstractPluginManager;
import net.frankheijden.serverutils.common.managers.AbstractTaskManager;
import net.frankheijden.serverutils.common.providers.ChatProvider;
public class PluginWatcherTask extends AbstractTask {
private static final WatchEvent.Kind<?>[] EVENTS = new WatchEvent.Kind[] {
private static final WatchEvent.Kind<?>[] EVENTS = new WatchEvent.Kind[]{
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE
@ -37,13 +38,16 @@ public class PluginWatcherTask extends AbstractTask {
private final ServerCommandSender sender;
private final String pluginName;
private File file;
private final AtomicBoolean run;
private File file;
private String hash;
private WatchService watchService;
private Object task = null;
/**
* Constructs a new PluginWatcherTask for the specified plugin.
*
* @param pluginName The name of the plugin.
*/
public PluginWatcherTask(ServerCommandSender sender, String pluginName) {
@ -65,12 +69,20 @@ public class PluginWatcherTask extends AbstractTask {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
if (file.getName().equals(event.context().toString())) {
send(WatchResult.CHANGE);
String previousHash = hash;
hash = FileUtils.getHash(file);
if (task != null) {
//noinspection unchecked
taskManager.cancelTask(task);
}
task = ServerUtilsApp.getPlugin().getTaskManager().runTaskLater(() -> {
if (hash.equals(previousHash)) {
send(WatchResult.CHANGE);
taskManager.runTask(() -> {
pluginManager.reloadPlugin(pluginName);
file = pluginManager.getPluginFile(pluginName);
});
pluginManager.reloadPlugin(pluginName);
file = pluginManager.getPluginFile(pluginName);
}
}, 10L);
}
}

View file

@ -2,8 +2,10 @@ package net.frankheijden.serverutils.common.utils;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import org.apache.commons.codec.digest.DigestUtils;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@ -32,4 +34,19 @@ public class FileUtils {
Files.copy(in, target.toPath());
return true;
}
/**
* Get the Hash of a file.
*
* @param file The file
* @return The file's hash
*/
public static String getHash(File file) {
try {
return DigestUtils.md5Hex(new FileInputStream(file));
} catch (IOException e) {
// Shouldn't happen
return null;
}
}
}