mirror of
https://github.com/LuckPerms/LuckPerms.git
synced 2024-11-28 05:35:26 +01:00
Refactor file watcher
This commit is contained in:
parent
2ed45c92a6
commit
5ce8217cd5
@ -50,7 +50,7 @@ import me.lucko.luckperms.common.sender.Sender;
|
|||||||
import me.lucko.luckperms.common.storage.Storage;
|
import me.lucko.luckperms.common.storage.Storage;
|
||||||
import me.lucko.luckperms.common.storage.StorageFactory;
|
import me.lucko.luckperms.common.storage.StorageFactory;
|
||||||
import me.lucko.luckperms.common.storage.StorageType;
|
import me.lucko.luckperms.common.storage.StorageType;
|
||||||
import me.lucko.luckperms.common.storage.implementation.file.FileWatcher;
|
import me.lucko.luckperms.common.storage.implementation.file.watcher.FileWatcher;
|
||||||
import me.lucko.luckperms.common.tasks.SyncTask;
|
import me.lucko.luckperms.common.tasks.SyncTask;
|
||||||
import me.lucko.luckperms.common.treeview.PermissionRegistry;
|
import me.lucko.luckperms.common.treeview.PermissionRegistry;
|
||||||
import me.lucko.luckperms.common.verbose.VerboseHandler;
|
import me.lucko.luckperms.common.verbose.VerboseHandler;
|
||||||
@ -232,7 +232,7 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin {
|
|||||||
|
|
||||||
// close file watcher
|
// close file watcher
|
||||||
if (this.fileWatcher != null) {
|
if (this.fileWatcher != null) {
|
||||||
this.fileWatcher.close();
|
//this.fileWatcher.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// unregister api
|
// unregister api
|
||||||
|
@ -49,7 +49,7 @@ import me.lucko.luckperms.common.plugin.logging.PluginLogger;
|
|||||||
import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener;
|
import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener;
|
||||||
import me.lucko.luckperms.common.sender.Sender;
|
import me.lucko.luckperms.common.sender.Sender;
|
||||||
import me.lucko.luckperms.common.storage.Storage;
|
import me.lucko.luckperms.common.storage.Storage;
|
||||||
import me.lucko.luckperms.common.storage.implementation.file.FileWatcher;
|
import me.lucko.luckperms.common.storage.implementation.file.watcher.FileWatcher;
|
||||||
import me.lucko.luckperms.common.tasks.SyncTask;
|
import me.lucko.luckperms.common.tasks.SyncTask;
|
||||||
import me.lucko.luckperms.common.treeview.PermissionRegistry;
|
import me.lucko.luckperms.common.treeview.PermissionRegistry;
|
||||||
import me.lucko.luckperms.common.verbose.VerboseHandler;
|
import me.lucko.luckperms.common.verbose.VerboseHandler;
|
||||||
|
@ -30,6 +30,7 @@ import me.lucko.luckperms.common.bulkupdate.comparison.Constraint;
|
|||||||
import me.lucko.luckperms.common.node.model.HeldNodeImpl;
|
import me.lucko.luckperms.common.node.model.HeldNodeImpl;
|
||||||
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
|
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
|
||||||
import me.lucko.luckperms.common.storage.implementation.file.loader.ConfigurateLoader;
|
import me.lucko.luckperms.common.storage.implementation.file.loader.ConfigurateLoader;
|
||||||
|
import me.lucko.luckperms.common.storage.implementation.file.watcher.FileWatcher;
|
||||||
import me.lucko.luckperms.common.util.Iterators;
|
import me.lucko.luckperms.common.util.Iterators;
|
||||||
|
|
||||||
import net.luckperms.api.node.HeldNode;
|
import net.luckperms.api.node.HeldNode;
|
||||||
|
@ -1,209 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of LuckPerms, licensed under the MIT License.
|
|
||||||
*
|
|
||||||
* Copyright (c) lucko (Luck) <luck@lucko.me>
|
|
||||||
* Copyright (c) contributors
|
|
||||||
*
|
|
||||||
* 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.storage.implementation.file;
|
|
||||||
|
|
||||||
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
|
|
||||||
import me.lucko.luckperms.common.util.Iterators;
|
|
||||||
|
|
||||||
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.CopyOnWriteArrayList;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
public class FileWatcher {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static final WatchEvent.Kind<Path>[] KINDS = new WatchEvent.Kind[]{
|
|
||||||
StandardWatchEventKinds.ENTRY_CREATE,
|
|
||||||
StandardWatchEventKinds.ENTRY_DELETE,
|
|
||||||
StandardWatchEventKinds.ENTRY_MODIFY
|
|
||||||
};
|
|
||||||
|
|
||||||
private final Path basePath;
|
|
||||||
private final Map<Path, WatchedLocation> watchedLocations;
|
|
||||||
|
|
||||||
// the watchservice instance
|
|
||||||
private final WatchService watchService;
|
|
||||||
|
|
||||||
private boolean initialised = false;
|
|
||||||
|
|
||||||
public FileWatcher(LuckPermsPlugin plugin, Path basePath) throws IOException {
|
|
||||||
this.watchedLocations = Collections.synchronizedMap(new HashMap<>());
|
|
||||||
this.basePath = basePath;
|
|
||||||
this.watchService = basePath.getFileSystem().newWatchService();
|
|
||||||
|
|
||||||
plugin.getBootstrap().getScheduler().asyncLater(this::initLocations, 5, TimeUnit.SECONDS);
|
|
||||||
plugin.getBootstrap().getScheduler().asyncRepeating(this::tick, 1, TimeUnit.SECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
public WatchedLocation getWatcher(Path path) {
|
|
||||||
Path relativePath = this.basePath.relativize(path);
|
|
||||||
return this.watchedLocations.computeIfAbsent(relativePath, p -> new WatchedLocation(this, p));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
if (this.watchService == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.watchService.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initLocations() {
|
|
||||||
for (WatchedLocation loc : this.watchedLocations.values()) {
|
|
||||||
try {
|
|
||||||
loc.setup();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.initialised = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void tick() {
|
|
||||||
List<Path> expired = new ArrayList<>();
|
|
||||||
for (Map.Entry<Path, WatchedLocation> ent : this.watchedLocations.entrySet()) {
|
|
||||||
boolean valid = ent.getValue().tick();
|
|
||||||
if (!valid) {
|
|
||||||
new RuntimeException("WatchKey no longer valid: " + ent.getKey().toString()).printStackTrace();
|
|
||||||
expired.add(ent.getKey());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expired.forEach(this.watchedLocations::remove);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isFileTemporary(String fileName) {
|
|
||||||
return fileName.endsWith(".tmp") || fileName.endsWith(".swp") || fileName.endsWith(".swx") || fileName.endsWith(".swpz");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encapsulates a "watcher" in a specific directory.
|
|
||||||
*/
|
|
||||||
public final class WatchedLocation {
|
|
||||||
// the parent watcher
|
|
||||||
private final FileWatcher watcher;
|
|
||||||
|
|
||||||
// the absolute path to the directory being watched
|
|
||||||
private final Path absolutePath;
|
|
||||||
|
|
||||||
// the times of recent changes
|
|
||||||
private final Map<String, Long> lastChange = Collections.synchronizedMap(new HashMap<>());
|
|
||||||
|
|
||||||
// if the key is registered
|
|
||||||
private boolean ready = false;
|
|
||||||
|
|
||||||
// the watch key
|
|
||||||
private WatchKey key = null;
|
|
||||||
|
|
||||||
// the callback functions
|
|
||||||
private final List<Consumer<Path>> callbacks = new CopyOnWriteArrayList<>();
|
|
||||||
|
|
||||||
private WatchedLocation(FileWatcher watcher, Path relativePath) {
|
|
||||||
this.watcher = watcher;
|
|
||||||
this.absolutePath = this.watcher.basePath.resolve(relativePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void setup() throws IOException {
|
|
||||||
if (this.ready) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.key = this.absolutePath.register(this.watcher.watchService, KINDS);
|
|
||||||
this.ready = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean tick() {
|
|
||||||
if (!this.ready) {
|
|
||||||
// await init
|
|
||||||
if (!FileWatcher.this.initialised) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setup();
|
|
||||||
return true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove old change entries.
|
|
||||||
long expireTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(4);
|
|
||||||
this.lastChange.values().removeIf(lastChange -> lastChange < expireTime);
|
|
||||||
|
|
||||||
List<WatchEvent<?>> watchEvents = this.key.pollEvents();
|
|
||||||
for (WatchEvent<?> event : watchEvents) {
|
|
||||||
Path context = (Path) event.context();
|
|
||||||
|
|
||||||
if (context == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String fileName = context.toString();
|
|
||||||
|
|
||||||
// ignore temporary changes
|
|
||||||
if (isFileTemporary(fileName)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore changes already registered to the system
|
|
||||||
if (this.lastChange.containsKey(fileName)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
this.lastChange.put(fileName, System.currentTimeMillis());
|
|
||||||
|
|
||||||
// process the change
|
|
||||||
Iterators.tryIterate(this.callbacks, cb -> cb.accept(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset the watch key.
|
|
||||||
return this.key.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void recordChange(String fileName) {
|
|
||||||
this.lastChange.put(fileName, System.currentTimeMillis());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addListener(Consumer<Path> updateConsumer) {
|
|
||||||
this.callbacks.add(updateConsumer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -31,6 +31,7 @@ import me.lucko.luckperms.common.model.User;
|
|||||||
import me.lucko.luckperms.common.node.model.HeldNodeImpl;
|
import me.lucko.luckperms.common.node.model.HeldNodeImpl;
|
||||||
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
|
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
|
||||||
import me.lucko.luckperms.common.storage.implementation.file.loader.ConfigurateLoader;
|
import me.lucko.luckperms.common.storage.implementation.file.loader.ConfigurateLoader;
|
||||||
|
import me.lucko.luckperms.common.storage.implementation.file.watcher.FileWatcher;
|
||||||
import me.lucko.luckperms.common.util.Iterators;
|
import me.lucko.luckperms.common.util.Iterators;
|
||||||
import me.lucko.luckperms.common.util.MoreFiles;
|
import me.lucko.luckperms.common.util.MoreFiles;
|
||||||
import me.lucko.luckperms.common.util.Uuids;
|
import me.lucko.luckperms.common.util.Uuids;
|
||||||
|
@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of LuckPerms, licensed under the MIT License.
|
||||||
|
*
|
||||||
|
* Copyright (c) lucko (Luck) <luck@lucko.me>
|
||||||
|
* Copyright (c) contributors
|
||||||
|
*
|
||||||
|
* 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.storage.implementation.file.watcher;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.ClosedWatchServiceException;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
|
import java.nio.file.FileVisitResult;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.LinkOption;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.SimpleFileVisitor;
|
||||||
|
import java.nio.file.StandardWatchEventKinds;
|
||||||
|
import java.nio.file.WatchEvent;
|
||||||
|
import java.nio.file.WatchKey;
|
||||||
|
import java.nio.file.WatchService;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for "watching" for file changes using a {@link WatchService}.
|
||||||
|
*/
|
||||||
|
public abstract class AbstractFileWatcher implements AutoCloseable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a {@link WatchKey} from the given {@link WatchService} in the given {@link Path directory}.
|
||||||
|
*
|
||||||
|
* @param watchService the watch service
|
||||||
|
* @param directory the directory
|
||||||
|
* @return the watch key
|
||||||
|
* @throws IOException if unable to register
|
||||||
|
*/
|
||||||
|
private static WatchKey register(WatchService watchService, Path directory) throws IOException {
|
||||||
|
return directory.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The watch service */
|
||||||
|
private final WatchService service;
|
||||||
|
|
||||||
|
/** A map of all registered watch keys */
|
||||||
|
private final Map<WatchKey, Path> keys = Collections.synchronizedMap(new HashMap<>());
|
||||||
|
|
||||||
|
/** If this file watcher should discover directories */
|
||||||
|
private final boolean autoRegisterNewSubDirectories;
|
||||||
|
|
||||||
|
/** The thread currently being used to wait for & process watch events */
|
||||||
|
private AtomicReference<Thread> processingThread = new AtomicReference<>();
|
||||||
|
|
||||||
|
public AbstractFileWatcher(FileSystem fileSystem, boolean autoRegisterNewSubDirectories) throws IOException {
|
||||||
|
this.service = fileSystem.newWatchService();
|
||||||
|
this.autoRegisterNewSubDirectories = autoRegisterNewSubDirectories;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a watch key in the given directory.
|
||||||
|
*
|
||||||
|
* @param directory the directory
|
||||||
|
* @throws IOException if unable to register a key
|
||||||
|
*/
|
||||||
|
public void register(Path directory) throws IOException {
|
||||||
|
final WatchKey key = register(this.service, directory);
|
||||||
|
this.keys.put(key, directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a watch key recursively in the given directory.
|
||||||
|
*
|
||||||
|
* @param root the root directory
|
||||||
|
* @throws IOException if unable to register a key
|
||||||
|
*/
|
||||||
|
public void registerRecursively(Path root) throws IOException {
|
||||||
|
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
|
||||||
|
@Override
|
||||||
|
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||||
|
register(dir);
|
||||||
|
return super.preVisitDirectory(dir, attrs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process an observed watch event.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
* @param path the resolved event context
|
||||||
|
*/
|
||||||
|
protected abstract void processEvent(WatchEvent<Path> event, Path path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes {@link WatchEvent}s from the watch service until it is closed, or until
|
||||||
|
* the thread is interrupted.
|
||||||
|
*/
|
||||||
|
public final void runEventProcessingLoop() {
|
||||||
|
if (!this.processingThread.compareAndSet(null, Thread.currentThread())) {
|
||||||
|
throw new IllegalStateException("A thread is already processing events for this watcher.");
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// poll for a key from the watch service
|
||||||
|
WatchKey key;
|
||||||
|
try {
|
||||||
|
key = this.service.take();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
break;
|
||||||
|
} catch (ClosedWatchServiceException e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the directory the key is watching
|
||||||
|
Path directory = this.keys.get(key);
|
||||||
|
if (directory == null) {
|
||||||
|
key.cancel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// process each watch event the key has
|
||||||
|
for (WatchEvent<?> ev : key.pollEvents()) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
WatchEvent<Path> event = (WatchEvent<Path>) ev;
|
||||||
|
|
||||||
|
Path context = event.context();
|
||||||
|
|
||||||
|
// ignore contexts with a name count of zero
|
||||||
|
if (context.getNameCount() == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve the context of the event against the directory being watched
|
||||||
|
Path file = directory.resolve(context);
|
||||||
|
|
||||||
|
// if the file is a regular file, send the event on to be processed
|
||||||
|
if (Files.isRegularFile(file)) {
|
||||||
|
processEvent(event, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle recursive directory creation
|
||||||
|
if (this.autoRegisterNewSubDirectories && event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
|
||||||
|
try {
|
||||||
|
if (Files.isDirectory(file, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
registerRecursively(file);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset the key
|
||||||
|
boolean valid = key.reset();
|
||||||
|
if (!valid) {
|
||||||
|
this.keys.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processingThread.compareAndSet(Thread.currentThread(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
this.service.close();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of LuckPerms, licensed under the MIT License.
|
||||||
|
*
|
||||||
|
* Copyright (c) lucko (Luck) <luck@lucko.me>
|
||||||
|
* Copyright (c) contributors
|
||||||
|
*
|
||||||
|
* 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.storage.implementation.file.watcher;
|
||||||
|
|
||||||
|
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
|
||||||
|
import me.lucko.luckperms.common.util.ExpiringSet;
|
||||||
|
import me.lucko.luckperms.common.util.Iterators;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.WatchEvent;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple implementation of {@link AbstractFileWatcher} for LuckPerms data files.
|
||||||
|
*/
|
||||||
|
public class FileWatcher extends AbstractFileWatcher {
|
||||||
|
|
||||||
|
/** The base watched path */
|
||||||
|
private final Path basePath;
|
||||||
|
|
||||||
|
/** A map of watched locations with corresponding listeners */
|
||||||
|
private final Map<Path, WatchedLocation> watchedLocations;
|
||||||
|
|
||||||
|
public FileWatcher(LuckPermsPlugin plugin, Path basePath) throws IOException {
|
||||||
|
super(basePath.getFileSystem(), true);
|
||||||
|
this.watchedLocations = Collections.synchronizedMap(new HashMap<>());
|
||||||
|
this.basePath = basePath;
|
||||||
|
|
||||||
|
super.registerRecursively(basePath);
|
||||||
|
plugin.getBootstrap().getScheduler().executeAsync(super::runEventProcessingLoop);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a {@link WatchedLocation} instance for a given path.
|
||||||
|
*
|
||||||
|
* @param path the path to get a watcher for
|
||||||
|
* @return the watched location
|
||||||
|
*/
|
||||||
|
public WatchedLocation getWatcher(Path path) {
|
||||||
|
if (path.isAbsolute()) {
|
||||||
|
path = this.basePath.relativize(path);
|
||||||
|
}
|
||||||
|
return this.watchedLocations.computeIfAbsent(path, WatchedLocation::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void processEvent(WatchEvent<Path> event, Path path) {
|
||||||
|
// get the relative path of the event
|
||||||
|
Path relativePath = this.basePath.relativize(path);
|
||||||
|
if (relativePath.getNameCount() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass the event onto all watched locations that match
|
||||||
|
for (Map.Entry<Path, WatchedLocation> entry : this.watchedLocations.entrySet()) {
|
||||||
|
if (relativePath.startsWith(entry.getKey())) {
|
||||||
|
entry.getValue().onEvent(event, relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulates a "watcher" in a specific directory.
|
||||||
|
*/
|
||||||
|
public static final class WatchedLocation {
|
||||||
|
/** The directory being watched by this instance. */
|
||||||
|
private final Path path;
|
||||||
|
|
||||||
|
/** A set of files which have been modified recently */
|
||||||
|
private final Set<String> recentlyModifiedFiles = new ExpiringSet<>(4, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
/** The listener callback functions */
|
||||||
|
private final List<Consumer<Path>> callbacks = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
WatchedLocation(Path path) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onEvent(WatchEvent<Path> event, Path path) {
|
||||||
|
// get the relative path of the modified file
|
||||||
|
Path relativePath = this.path.relativize(path);
|
||||||
|
|
||||||
|
// check if the file has been modified recently
|
||||||
|
String fileName = relativePath.toString();
|
||||||
|
if (!this.recentlyModifiedFiles.add(fileName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass the event onto registered listeners
|
||||||
|
Iterators.tryIterate(this.callbacks, cb -> cb.accept(relativePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record that a file has been changed recently.
|
||||||
|
*
|
||||||
|
* @param fileName the name of the file
|
||||||
|
*/
|
||||||
|
public void recordChange(String fileName) {
|
||||||
|
this.recentlyModifiedFiles.add(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a listener.
|
||||||
|
*
|
||||||
|
* @param listener the listener
|
||||||
|
*/
|
||||||
|
public void addListener(Consumer<Path> listener) {
|
||||||
|
this.callbacks.add(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -28,9 +28,7 @@ package me.lucko.luckperms.common.util;
|
|||||||
import com.github.benmanes.caffeine.cache.Cache;
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
import com.google.common.collect.ForwardingSet;
|
import com.google.common.collect.ForwardingSet;
|
||||||
|
|
||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
import java.util.Collections;
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@ -40,46 +38,11 @@ import java.util.concurrent.TimeUnit;
|
|||||||
* @param <E> element type
|
* @param <E> element type
|
||||||
*/
|
*/
|
||||||
public class ExpiringSet<E> extends ForwardingSet<E> {
|
public class ExpiringSet<E> extends ForwardingSet<E> {
|
||||||
private final Cache<E, Boolean> cache;
|
|
||||||
private final Set<E> setView;
|
private final Set<E> setView;
|
||||||
|
|
||||||
public ExpiringSet(long duration, TimeUnit unit) {
|
public ExpiringSet(long duration, TimeUnit unit) {
|
||||||
this.cache = CaffeineFactory.newBuilder().expireAfterAccess(duration, unit).build();
|
Cache<E, Boolean> cache = CaffeineFactory.newBuilder().expireAfterAccess(duration, unit).build();
|
||||||
this.setView = this.cache.asMap().keySet();
|
this.setView = Collections.newSetFromMap(cache.asMap());
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean add(@NonNull E element) {
|
|
||||||
this.cache.put(element, Boolean.TRUE);
|
|
||||||
|
|
||||||
// we don't care about the return value
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean addAll(@NonNull Collection<? extends E> collection) {
|
|
||||||
for (E element : collection) {
|
|
||||||
add(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
// we don't care about the return value
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean remove(@NonNull Object key) {
|
|
||||||
this.cache.invalidate(key);
|
|
||||||
|
|
||||||
// we don't care about the return value
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean removeAll(@NonNull Collection<?> keys) {
|
|
||||||
this.cache.invalidateAll(keys);
|
|
||||||
|
|
||||||
// we don't care about the return value
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
Loading…
Reference in New Issue
Block a user