Implement callback-based completion for Minecraft command execution and enhance timeout handling

This commit is contained in:
Xujiayao 2026-02-26 14:13:19 +08:00
parent 79a9aec67b
commit f1ee8b59e2
5 changed files with 67 additions and 17 deletions

View file

@ -47,6 +47,8 @@ import static com.xujiayao.discord_mc_chat.Constants.LOGGER;
*/
public class ClientHandler extends SimpleChannelInboundHandler<Packet> {
private static final int CONSOLE_COMMAND_TIMEOUT_SECONDS = 10;
private final NettyClient client;
private final CompletableFuture<Boolean> initialLoginFuture;
private boolean allowReconnect = true; // Default to true for network errors
@ -150,7 +152,7 @@ public class ClientHandler extends SimpleChannelInboundHandler<Packet> {
}
}
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<Packet> {
}
};
EventManager.post(new CoreEvents.MinecraftCommandExecutionEvent(captureSender, p.commandLine));
CompletableFuture<Void> 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

View file

@ -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<String, CompletableFuture<ConsoleResponsePacket>> 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<Void> 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.
}
}
/**

View file

@ -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.
* <p>
@ -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<Void> 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.
}
}
}

View file

@ -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 {
* <p>
* The handler should construct a virtual CommandSourceStack with the sender's OP level
* and dispatch the command to the Minecraft command dispatcher.
* <p>
* 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<Void> completionFuture
) {
}

View file

@ -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 -> {