diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/client/ClientHandler.java b/core/src/main/java/com/xujiayao/discord_mc_chat/client/ClientHandler.java index 09d13842..a1b6d1f8 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/client/ClientHandler.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/client/ClientHandler.java @@ -47,6 +47,8 @@ import static com.xujiayao.discord_mc_chat.Constants.LOGGER; */ public class ClientHandler extends SimpleChannelInboundHandler { + private static final int CONSOLE_COMMAND_TIMEOUT_SECONDS = 10; + private final NettyClient client; private final CompletableFuture initialLoginFuture; private boolean allowReconnect = true; // Default to true for network errors @@ -150,7 +152,7 @@ public class ClientHandler extends SimpleChannelInboundHandler { } } case ConsoleRequestPacket p -> { - // Handle Minecraft command execution via CoreEvents + // Handle Minecraft command execution via CoreEvents with callback-based completion StringBuilder responseBuilder = new StringBuilder(); CommandSender captureSender = new CommandSender() { @@ -168,14 +170,16 @@ public class ClientHandler extends SimpleChannelInboundHandler { } }; - EventManager.post(new CoreEvents.MinecraftCommandExecutionEvent(captureSender, p.commandLine)); + CompletableFuture completionFuture = new CompletableFuture<>(); - // Note: Minecraft command execution is dispatched to the server thread. - // We schedule the response to be sent after a short delay to allow for execution. - // TODO: Consider a callback-based approach for more reliable response timing. - ctx.channel().eventLoop().schedule(() -> { - ctx.writeAndFlush(new ConsoleResponsePacket(p.requestId, responseBuilder.toString())); - }, 1, TimeUnit.SECONDS); + EventManager.post(new CoreEvents.MinecraftCommandExecutionEvent(captureSender, p.commandLine, completionFuture)); + + // Use the completion future with a timeout to send the response reliably + completionFuture + .orTimeout(CONSOLE_COMMAND_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .whenComplete((v, ex) -> { + ctx.writeAndFlush(new ConsoleResponsePacket(p.requestId, responseBuilder.toString())); + }); } case ExecuteAutoCompleteRequestPacket p -> { // Handle DMCC command auto-complete with OP level filtering diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/ConsoleCommand.java b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/ConsoleCommand.java index 20849fd3..5d9ddd5e 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/ConsoleCommand.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/ConsoleCommand.java @@ -37,6 +37,7 @@ import java.util.concurrent.TimeUnit; public class ConsoleCommand implements Command { private static final int CONSOLE_TIMEOUT_SECONDS = 30; + private static final int LOCAL_COMMAND_TIMEOUT_SECONDS = 10; private static final Map> pendingRequests = new ConcurrentHashMap<>(); /** @@ -125,7 +126,7 @@ public class ConsoleCommand implements Command { /** * Executes a Minecraft command on the local server (single_server mode). - * Dispatches via CoreEvents.MinecraftCommandExecutionEvent. + * Dispatches via CoreEvents.MinecraftCommandExecutionEvent with callback-based completion. * * @param sender The command sender. * @param args args[0] = command (may contain spaces from acceptsExtraArgs). @@ -144,7 +145,17 @@ public class ConsoleCommand implements Command { sender.reply(I18nManager.getDmccTranslation("commands.console.executing_local", commandLine)); - EventManager.post(new CoreEvents.MinecraftCommandExecutionEvent(sender, commandLine)); + CompletableFuture completionFuture = new CompletableFuture<>(); + + EventManager.post(new CoreEvents.MinecraftCommandExecutionEvent(sender, commandLine, completionFuture)); + + // Wait for the command to complete with a timeout + try { + completionFuture.get(LOCAL_COMMAND_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (Exception ignored) { + // Timeout or interruption - the command may still be running, + // but we've already sent all output that was produced so far via the sender. + } } /** diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/WhitelistCommand.java b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/WhitelistCommand.java index 020f9ac5..de11946d 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/WhitelistCommand.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/WhitelistCommand.java @@ -6,6 +6,9 @@ import com.xujiayao.discord_mc_chat.utils.events.CoreEvents; import com.xujiayao.discord_mc_chat.utils.events.EventManager; import com.xujiayao.discord_mc_chat.utils.i18n.I18nManager; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + /** * Proxy command for managing the server whitelist via DMCC. *

@@ -25,6 +28,8 @@ import com.xujiayao.discord_mc_chat.utils.i18n.I18nManager; */ public class WhitelistCommand implements Command { + private static final int WHITELIST_COMMAND_TIMEOUT_SECONDS = 10; + @Override public String name() { return "whitelist"; @@ -76,7 +81,18 @@ public class WhitelistCommand implements Command { } }; - // Delegate to Minecraft's native /whitelist add command - EventManager.post(new CoreEvents.MinecraftCommandExecutionEvent(elevatedSender, "whitelist add " + player)); + // Delegate to Minecraft's native /whitelist add command with callback-based completion + CompletableFuture completionFuture = new CompletableFuture<>(); + + EventManager.post(new CoreEvents.MinecraftCommandExecutionEvent(elevatedSender, "whitelist add " + player, completionFuture)); + + // Wait for the command to complete so the response is available before this method returns. + // This is critical for remote execution (execute command) where the response is collected + // from the sender's reply buffer after this method returns. + try { + completionFuture.get(WHITELIST_COMMAND_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (Exception ignored) { + // Timeout or interruption - output produced so far will still be in the sender's buffer. + } } } diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/utils/events/CoreEvents.java b/core/src/main/java/com/xujiayao/discord_mc_chat/utils/events/CoreEvents.java index 91ce59e3..5bcef60d 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/utils/events/CoreEvents.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/utils/events/CoreEvents.java @@ -3,6 +3,7 @@ package com.xujiayao.discord_mc_chat.utils.events; import com.xujiayao.discord_mc_chat.commands.CommandSender; import java.util.List; +import java.util.concurrent.CompletableFuture; /** * Core events for communication between DMCC Core and Minecraft-specific implementations. @@ -19,13 +20,20 @@ public class CoreEvents { *

* The handler should construct a virtual CommandSourceStack with the sender's OP level * and dispatch the command to the Minecraft command dispatcher. + *

+ * The handler MUST complete the {@code completionFuture} after the command has finished + * executing and all output has been sent to the sender. This enables reliable response + * timing for remote command execution (console/execute commands). * - * @param sender The command sender bridging the execution, used for replying results. - * @param commandLine The raw Minecraft command line to be executed (without leading slash). + * @param sender The command sender bridging the execution, used for replying results. + * @param commandLine The raw Minecraft command line to be executed (without leading slash). + * @param completionFuture A future that the handler MUST complete when command execution is done. + * Complete with {@code null} on success, or exceptionally on failure. */ public record MinecraftCommandExecutionEvent( CommandSender sender, - String commandLine + String commandLine, + CompletableFuture completionFuture ) { } diff --git a/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/events/MinecraftEventHandler.java b/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/events/MinecraftEventHandler.java index 02842e1e..08e6864b 100644 --- a/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/events/MinecraftEventHandler.java +++ b/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/events/MinecraftEventHandler.java @@ -190,6 +190,7 @@ public class MinecraftEventHandler { EventManager.register(CoreEvents.MinecraftCommandExecutionEvent.class, event -> { if (serverInstance == null) { event.sender().reply("Minecraft server is not ready yet."); + event.completionFuture().complete(null); return; } @@ -229,8 +230,18 @@ public class MinecraftEventHandler { null ); - // Must be dispatched to the main server thread to avoid concurrent modification - serverInstance.execute(() -> serverInstance.getCommands().performPrefixedCommand(source, event.commandLine())); + // Must be dispatched to the main server thread to avoid concurrent modification. + // The completion future is completed after the command has been executed on the server thread, + // ensuring all output has been sent to the sender before the response is collected. + serverInstance.execute(() -> { + try { + serverInstance.getCommands().performPrefixedCommand(source, event.commandLine()); + } catch (Exception e) { + event.sender().reply("Error executing command: " + e.getMessage()); + } finally { + event.completionFuture().complete(null); + } + }); }); EventManager.register(CoreEvents.MinecraftCommandAutoCompleteEvent.class, event -> {