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 b88c327f7..be7ea9010 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java @@ -38,6 +38,7 @@ import me.lucko.luckperms.bukkit.inject.server.InjectorSubscriptionMap; import me.lucko.luckperms.bukkit.inject.server.LuckPermsDefaultsMap; import me.lucko.luckperms.bukkit.inject.server.LuckPermsPermissionMap; import me.lucko.luckperms.bukkit.inject.server.LuckPermsSubscriptionMap; +import me.lucko.luckperms.bukkit.listeners.BukkitCommandListUpdater; import me.lucko.luckperms.bukkit.listeners.BukkitConnectionListener; import me.lucko.luckperms.bukkit.listeners.BukkitPlatformListener; import me.lucko.luckperms.bukkit.messaging.BukkitMessagingFactory; @@ -273,6 +274,12 @@ public class LPBukkitPlugin extends AbstractLuckPermsPlugin { }); } + // register bukkit command list updater + if (getConfiguration().get(ConfigKeys.UPDATE_CLIENT_COMMAND_LIST) && BukkitCommandListUpdater.isSupported()) { + BukkitCommandListUpdater commandListUpdater = new BukkitCommandListUpdater(this); + getApiProvider().getEventBus().subscribe(UserDataRecalculateEvent.class, commandListUpdater::onUserDataRecalculate); + } + // Load any online users (in the case of a reload) for (Player player : this.bootstrap.getServer().getOnlinePlayers()) { this.bootstrap.getScheduler().executeAsync(() -> { diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitCommandListUpdater.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitCommandListUpdater.java new file mode 100644 index 000000000..aa9a0f7ec --- /dev/null +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitCommandListUpdater.java @@ -0,0 +1,99 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * 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.bukkit.listeners; + +import com.github.benmanes.caffeine.cache.LoadingCache; + +import me.lucko.luckperms.bukkit.LPBukkitPlugin; +import me.lucko.luckperms.common.cache.BufferedRequest; +import me.lucko.luckperms.common.util.CaffeineFactory; + +import net.luckperms.api.event.user.UserDataRecalculateEvent; + +import org.bukkit.entity.Player; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * Calls {@link Player#updateCommands()} when a players permissions change. + */ +public class BukkitCommandListUpdater { + + public static boolean isSupported() { + try { + Player.class.getMethod("updateCommands"); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + private final LPBukkitPlugin plugin; + private final LoadingCache sendingBuffers = CaffeineFactory.newBuilder() + .expireAfterAccess(10, TimeUnit.SECONDS) + .build(SendBuffer::new); + + public BukkitCommandListUpdater(LPBukkitPlugin plugin) { + this.plugin = plugin; + } + + // Called when a user's data is recalculated. + public void onUserDataRecalculate(UserDataRecalculateEvent e) { + UUID uniqueId = e.getUser().getUniqueId(); + if (!this.plugin.getBootstrap().isPlayerOnline(uniqueId)) { + return; + } + + // Buffer the request to send a commands update. + this.sendingBuffers.get(uniqueId).request(); + } + + // Called when the buffer times out. + private void sendUpdate(UUID uniqueId) { + this.plugin.getBootstrap().getScheduler().sync().execute(() -> { + Player player = this.plugin.getBootstrap().getPlayer(uniqueId).orElse(null); + if (player != null) { + player.updateCommands(); + } + }); + } + + private final class SendBuffer extends BufferedRequest { + private final UUID uniqueId; + + SendBuffer(UUID uniqueId) { + super(500, TimeUnit.MILLISECONDS, BukkitCommandListUpdater.this.plugin.getBootstrap().getScheduler()); + this.uniqueId = uniqueId; + } + + @Override + protected Void perform() { + sendUpdate(this.uniqueId); + return null; + } + } +} diff --git a/bukkit/src/main/resources/config.yml b/bukkit/src/main/resources/config.yml index 737e72c71..db590a40d 100644 --- a/bukkit/src/main/resources/config.yml +++ b/bukkit/src/main/resources/config.yml @@ -608,6 +608,9 @@ allow-invalid-usernames: false # - When this happens, the plugin will set their primary group back to default. prevent-primary-group-removal: false +# If LuckPerms should update the list of commands sent to the client when permissions are changed. +update-client-command-list: true + # If LuckPerms should attempt to resolve Vanilla command target selectors for LP commands. # See here for more info: https://minecraft.gamepedia.com/Commands#Target_selectors resolve-command-selectors: false diff --git a/common/src/main/java/me/lucko/luckperms/common/cache/BufferedRequest.java b/common/src/main/java/me/lucko/luckperms/common/cache/BufferedRequest.java index 4f7687ed1..fc7a53838 100644 --- a/common/src/main/java/me/lucko/luckperms/common/cache/BufferedRequest.java +++ b/common/src/main/java/me/lucko/luckperms/common/cache/BufferedRequest.java @@ -75,7 +75,7 @@ public abstract class BufferedRequest { if (this.processor != null) { try { return this.processor.extendAndGetFuture(); - } catch (IllegalStateException e) { + } catch (ProcessorAlreadyRanException e) { // ignore } } @@ -101,8 +101,8 @@ public abstract class BufferedRequest { */ protected abstract T perform(); - private static class Processor { - private Supplier supplier; + private static final class Processor { + private final Supplier supplier; private final long delay; private final TimeUnit unit; @@ -110,11 +110,11 @@ public abstract class BufferedRequest { private final SchedulerAdapter schedulerAdapter; private final Object[] mutex = new Object[0]; - private CompletableFuture future = new CompletableFuture<>(); + private final CompletableFuture future = new CompletableFuture<>(); private boolean usable = true; private SchedulerTask scheduledTask; - private BoundTask boundTask = null; + private CompletionTask boundTask = null; Processor(Supplier supplier, long delay, TimeUnit unit, SchedulerAdapter schedulerAdapter) { this.supplier = supplier; @@ -122,32 +122,36 @@ public abstract class BufferedRequest { this.unit = unit; this.schedulerAdapter = schedulerAdapter; - rescheduleTask(); + scheduleTask(); } - private void rescheduleTask() { + private void rescheduleTask() throws ProcessorAlreadyRanException { synchronized (this.mutex) { if (!this.usable) { - throw new IllegalStateException("Processor not usable"); + throw new ProcessorAlreadyRanException(); } if (this.scheduledTask != null) { this.scheduledTask.cancel(); } - this.boundTask = new BoundTask(); - this.scheduledTask = this.schedulerAdapter.asyncLater(this.boundTask, this.delay, this.unit); + scheduleTask(); } } + private void scheduleTask() { + this.boundTask = new CompletionTask(); + this.scheduledTask = this.schedulerAdapter.asyncLater(this.boundTask, this.delay, this.unit); + } + CompletableFuture getFuture() { return this.future; } - CompletableFuture extendAndGetFuture() { + CompletableFuture extendAndGetFuture() throws ProcessorAlreadyRanException { rescheduleTask(); return this.future; } - private final class BoundTask implements Runnable { + private final class CompletionTask implements Runnable { @Override public void run() { synchronized (Processor.this.mutex) { @@ -172,14 +176,11 @@ public abstract class BufferedRequest { new RuntimeException("Processor " + Processor.this.supplier + " threw an exception whilst computing a result", e).printStackTrace(); Processor.this.future.completeExceptionally(e); } - - // allow supplier and future to be GCed - Processor.this.supplier = null; - Processor.this.future = null; - Processor.this.scheduledTask = null; - Processor.this.boundTask = null; } } + } + + private static final class ProcessorAlreadyRanException extends Exception { } diff --git a/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MetaCache.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MetaCache.java index 48df4f517..f0e8b6040 100644 --- a/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MetaCache.java +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MetaCache.java @@ -90,6 +90,13 @@ public class MetaCache extends SimpleMetaCache implements CachedMetaData { return new MonitoredMetaMap(super.getMeta(origin), origin); } + @Override + public int getWeight(MetaCheckEvent.Origin origin) { + int value = super.getWeight(origin); + this.plugin.getVerboseHandler().offerMetaCheckEvent(origin, this.verboseCheckTarget, this.metadata.getQueryOptions(), "weight", String.valueOf(value)); + return value; + } + public @Nullable String getPrimaryGroup(MetaCheckEvent.Origin origin) { String value = super.getPrimaryGroup(origin); this.plugin.getVerboseHandler().offerMetaCheckEvent(origin, this.verboseCheckTarget, this.metadata.getQueryOptions(), "primarygroup", String.valueOf(value)); 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 cfab38354..86bcc25d3 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 @@ -134,6 +134,11 @@ public final class ConfigKeys { */ public static final ConfigKey CANCEL_FAILED_LOGINS = booleanKey("cancel-failed-logins", false); + /** + * If LuckPerms should update the list of commands sent to the client when permissions are changed. + */ + public static final ConfigKey UPDATE_CLIENT_COMMAND_LIST = enduringKey(booleanKey("update-client-command-list", true)); + /** * If LuckPerms should attempt to resolve Vanilla command target selectors for LP commands. */ diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/scheduler/AbstractJavaScheduler.java b/common/src/main/java/me/lucko/luckperms/common/plugin/scheduler/AbstractJavaScheduler.java index 767a80e6e..2e57b9e11 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/scheduler/AbstractJavaScheduler.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/scheduler/AbstractJavaScheduler.java @@ -35,25 +35,31 @@ import java.util.concurrent.Executors; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * Abstract implementation of {@link SchedulerAdapter} using a {@link ScheduledExecutorService}. */ public abstract class AbstractJavaScheduler implements SchedulerAdapter { - private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat("luckperms-scheduler") - .build() - ); + private final ScheduledThreadPoolExecutor scheduler; + private final ErrorReportingExecutor schedulerWorkerPool; + private final ForkJoinPool worker; - private final ErrorReportingExecutor schedulerWorkerPool = new ErrorReportingExecutor(Executors.newCachedThreadPool(new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat("luckperms-scheduler-worker-%d") - .build() - )); - - private final ForkJoinPool worker = new ForkJoinPool(32, ForkJoinPool.defaultForkJoinWorkerThreadFactory, (t, e) -> e.printStackTrace(), false); + public AbstractJavaScheduler() { + this.scheduler = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("luckperms-scheduler") + .build() + ); + this.scheduler.setRemoveOnCancelPolicy(true); + this.schedulerWorkerPool = new ErrorReportingExecutor(Executors.newCachedThreadPool(new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("luckperms-scheduler-worker-%d") + .build() + )); + this.worker = new ForkJoinPool(32, ForkJoinPool.defaultForkJoinWorkerThreadFactory, (t, e) -> e.printStackTrace(), false); + } @Override public Executor async() {