diff --git a/common/src/main/java/me/lucko/luckperms/common/command/CommandManager.java b/common/src/main/java/me/lucko/luckperms/common/command/CommandManager.java index c35a8c57d..22195db7e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/CommandManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/CommandManager.java @@ -60,6 +60,7 @@ import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.plugin.AbstractLuckPermsPlugin; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.ImmutableCollectors; @@ -67,11 +68,14 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.format.NamedTextColor; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Function; import java.util.stream.Collectors; @@ -129,8 +133,17 @@ public class CommandManager { } public CompletableFuture executeCommand(Sender sender, String label, List args) { - return CompletableFuture.supplyAsync(() -> { - this.lock.lock(); + SchedulerAdapter scheduler = this.plugin.getBootstrap().getScheduler(); + + // schedule a future to execute the command + AtomicReference thread = new AtomicReference<>(); + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + thread.set(Thread.currentThread()); + if (!this.lock.tryLock()) { + Message.ALREADY_EXECUTING_COMMAND.send(sender); + this.lock.lock(); + } + try { return execute(sender, label, args); } catch (Throwable e) { @@ -138,8 +151,26 @@ public class CommandManager { return null; } finally { this.lock.unlock(); + thread.set(null); } - }, this.plugin.getBootstrap().getScheduler().async()); + }, scheduler.async()); + + // catch if the command doesn't complete within a given time + scheduler.awaitTimeout(future, 10, TimeUnit.SECONDS, () -> handleCommandTimeout(thread, args)); + + return future; + } + + private void handleCommandTimeout(AtomicReference thread, List args) { + Thread executorThread = thread.get(); + if (executorThread == null) { + this.plugin.getLogger().warn("Command execution " + args + " has not completed - but executor thread is null!"); + } else { + String stackTrace = Arrays.stream(executorThread.getStackTrace()) + .map(el -> " " + el.toString()) + .collect(Collectors.joining("\n")); + this.plugin.getLogger().warn("Command execution " + args + " has not completed. Trace: \n" + stackTrace); + } } public boolean hasPermissionForAny(Sender sender) { diff --git a/common/src/main/java/me/lucko/luckperms/common/locale/Message.java b/common/src/main/java/me/lucko/luckperms/common/locale/Message.java index c52d9a11e..29338bf94 100644 --- a/common/src/main/java/me/lucko/luckperms/common/locale/Message.java +++ b/common/src/main/java/me/lucko/luckperms/common/locale/Message.java @@ -165,6 +165,12 @@ public interface Message { .append(FULL_STOP) ); + Args0 ALREADY_EXECUTING_COMMAND = () -> prefixed(translatable() + // "&7Another command is being executed, waiting for it to finish..." + .key("luckperms.commandsystem.already-executing-command") + .color(GRAY) + ); + Args2 FIRST_TIME_SETUP = (label, username) -> join(newline(), // "&3It seems that no permissions have been setup yet!" // "&3Before you can use any of the LuckPerms commands in-game, you need to use the console to give yourself access." diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/scheduler/SchedulerAdapter.java b/common/src/main/java/me/lucko/luckperms/common/plugin/scheduler/SchedulerAdapter.java index 1be23bfbc..b9de790d9 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/scheduler/SchedulerAdapter.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/scheduler/SchedulerAdapter.java @@ -25,8 +25,11 @@ package me.lucko.luckperms.common.plugin.scheduler; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; /** * A scheduler for running tasks using the systems provided by the platform @@ -85,6 +88,27 @@ public interface SchedulerAdapter { */ SchedulerTask asyncRepeating(Runnable task, long interval, TimeUnit unit); + /** + * Waits for the given time for {@code future} to complete. If the future isn't completed, + * {@code onTimeout} is executed. + * + * @param future the future to wait for + * @param timeout the time to wait + * @param unit the unit of timeout + * @param onTimeout the function to execute when the timeout expires + */ + default void awaitTimeout(CompletableFuture future, long timeout, TimeUnit unit, Runnable onTimeout) { + executeAsync(() -> { + try { + future.get(timeout, unit); + } catch (InterruptedException | ExecutionException e) { + // ignore + } catch (TimeoutException e) { + onTimeout.run(); + } + }); + } + /** * Shuts down the scheduler instance. * diff --git a/common/src/main/resources/luckperms_en.properties b/common/src/main/resources/luckperms_en.properties index efe8dfb72..cb7a85e72 100644 --- a/common/src/main/resources/luckperms_en.properties +++ b/common/src/main/resources/luckperms_en.properties @@ -5,6 +5,7 @@ luckperms.commandsystem.available-commands=Use {0} to view available commands luckperms.commandsystem.command-not-recognised=Command not recognised luckperms.commandsystem.no-permission=You do not have permission to use this command! luckperms.commandsystem.no-permission-subcommands=You do not have permission to use any sub commands +luckperms.commandsystem.already-executing-command=Another command is being executed, waiting for it to finish... luckperms.commandsystem.usage.sub-commands-header=Sub Commands luckperms.commandsystem.usage.usage-header=Command Usage luckperms.commandsystem.usage.arguments-header=Arguments