diff --git a/backend/mirai-console/src/main/java/net/mamoe/mirai/console/command/JCommandManager.java b/backend/mirai-console/src/main/java/net/mamoe/mirai/console/command/JCommandManager.java index 58d922319..74ad92fa9 100644 --- a/backend/mirai-console/src/main/java/net/mamoe/mirai/console/command/JCommandManager.java +++ b/backend/mirai-console/src/main/java/net/mamoe/mirai/console/command/JCommandManager.java @@ -1,6 +1,7 @@ package net.mamoe.mirai.console.command; import kotlin.NotImplementedError; +import kotlin.coroutines.Continuation; import kotlin.coroutines.EmptyCoroutineContext; import kotlinx.coroutines.BuildersKt; import kotlinx.coroutines.CoroutineScope; @@ -18,7 +19,7 @@ import java.util.concurrent.CompletableFuture; /** * Java 适配的 {@link CommandManagerKt} */ -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "RedundantSuppression"}) public final class JCommandManager { private JCommandManager() { throw new NotImplementedError(); @@ -101,14 +102,17 @@ public final class JCommandManager { CommandManagerKt.unregisterAllCommands(owner); } + /** * 解析并执行一个指令 * * @param args 接受 {@link String} 或 {@link Message} , 其他对象将会被 {@link Object#toString()} - * @see CommandExecuteResult + * @return 成功执行的指令, 在无匹配指令时返回 null + * @throws CommandExecutionException 当 {@link Command#onCommand(CommandSender, Object[], Continuation)} 抛出异常时包装并附带相关指令信息抛出 * @see #executeCommandAsync(CoroutineScope, CommandSender, Object...) */ - public static CommandExecuteResult executeCommand(final @NotNull CommandSender sender, final @NotNull Object... args) throws InterruptedException { + @Nullable + public static Command executeCommand(final @NotNull CommandSender sender, final @NotNull Object... args) throws CommandExecutionException, InterruptedException { Objects.requireNonNull(sender, "sender"); Objects.requireNonNull(args, "args"); for (Object arg : args) { @@ -123,10 +127,11 @@ public final class JCommandManager { * * @param scope 协程作用域 (用于管理协程生命周期). 一般填入 {@link JavaPlugin} 实例. * @param args 接受 {@link String} 或 {@link Message} , 其他对象将会被 {@link Object#toString()} - * @see CommandExecuteResult + * @return 成功执行的指令, 在无匹配指令时返回 null * @see #executeCommand(CommandSender, Object...) */ - public static CompletableFuture executeCommandAsync(final @NotNull CoroutineScope scope, final @NotNull CommandSender sender, final @NotNull Object... args) { + @NotNull + public static CompletableFuture<@Nullable Command> executeCommandAsync(final @NotNull CoroutineScope scope, final @NotNull CommandSender sender, final @NotNull Object... args) { Objects.requireNonNull(sender, "sender"); Objects.requireNonNull(args, "args"); Objects.requireNonNull(scope, "scope"); @@ -136,4 +141,46 @@ public final class JCommandManager { return FutureKt.future(scope, EmptyCoroutineContext.INSTANCE, CoroutineStart.DEFAULT, (sc, completion) -> CommandManagerKt.executeCommand(sender, args, completion)); } + + + /** + * 解析并执行一个指令, 获取详细的指令参数等信息. + *
+ * 执行过程中产生的异常将不会直接抛出, 而会包装为 {@link CommandExecuteResult.ExecutionException} + * + * @param args 接受 {@link String} 或 {@link Message} , 其他对象将会被 {@link Object#toString()} + * @return 执行结果 + * @see #executeCommandDetailedAsync(CoroutineScope, CommandSender, Object...) + */ + @NotNull + public static CommandExecuteResult executeCommandDetailed(final @NotNull CommandSender sender, final @NotNull Object... args) throws InterruptedException { + Objects.requireNonNull(sender, "sender"); + Objects.requireNonNull(args, "args"); + for (Object arg : args) { + Objects.requireNonNull(arg, "element of args"); + } + + return BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, (scope, completion) -> CommandManagerKt.executeCommandDetailed(sender, args, completion)); + } + + /** + * 异步 (在 Kotlin 协程线程池) 解析并执行一个指令, 获取详细的指令参数等信息 + * + * @param scope 协程作用域 (用于管理协程生命周期). 一般填入 {@link JavaPlugin} 实例. + * @param args 接受 {@link String} 或 {@link Message} , 其他对象将会被 {@link Object#toString()} + * @return 执行结果 + * @see #executeCommandDetailed(CommandSender, Object...) + */ + @NotNull + public static CompletableFuture<@NotNull CommandExecuteResult> + executeCommandDetailedAsync(final @NotNull CoroutineScope scope, final @NotNull CommandSender sender, final @NotNull Object... args) { + Objects.requireNonNull(sender, "sender"); + Objects.requireNonNull(args, "args"); + Objects.requireNonNull(scope, "scope"); + for (Object arg : args) { + Objects.requireNonNull(arg, "element of args"); + } + + return FutureKt.future(scope, EmptyCoroutineContext.INSTANCE, CoroutineStart.DEFAULT, (sc, completion) -> CommandManagerKt.executeCommandDetailed(sender, args, completion)); + } } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecuteResult.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecuteResult.kt new file mode 100644 index 000000000..a9ebce45a --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecuteResult.kt @@ -0,0 +1,146 @@ +@file:Suppress("unused") + +package net.mamoe.mirai.console.command + +import net.mamoe.mirai.console.command.CommandExecuteResult.CommandExecuteStatus +import net.mamoe.mirai.message.data.Message +import kotlin.contracts.contract + +/** + * 指令的执行返回 + * + * @see CommandExecuteStatus + */ +sealed class CommandExecuteResult { + /** 指令最终执行状态 */ + abstract val status: CommandExecuteStatus + + /** 指令执行时发生的错误 (如果有) */ + abstract val exception: Throwable? + + /** 尝试执行的指令 (如果匹配到) */ + abstract val command: Command? + + /** 尝试执行的指令名 (如果匹配到) */ + abstract val commandName: String? + + /** 基础分割后的实际参数列表, 元素类型可能为 [Message] 或 [String] */ + abstract val args: Array? + + // abstract val to allow smart casting + + /** 指令执行成功 */ + class Success( + /** 尝试执行的指令 */ + override val command: Command, + /** 尝试执行的指令名 */ + override val commandName: String, + /** 基础分割后的实际参数列表, 元素类型可能为 [Message] 或 [String] */ + override val args: Array + ) : CommandExecuteResult() { + /** 指令执行时发生的错误, 总是 `null` */ + override val exception: Nothing? get() = null + + /** 指令最终执行状态, 总是 [CommandExecuteStatus.SUCCESSFUL] */ + override val status: CommandExecuteStatus get() = CommandExecuteStatus.SUCCESSFUL + } + + /** 指令执行过程出现了错误 */ + class ExecutionException( + /** 指令执行时发生的错误 */ + override val exception: Throwable, + /** 尝试执行的指令 */ + override val command: Command, + /** 尝试执行的指令名 */ + override val commandName: String, + /** 基础分割后的实际参数列表, 元素类型可能为 [Message] 或 [String] */ + override val args: Array + ) : CommandExecuteResult() { + /** 指令最终执行状态, 总是 [CommandExecuteStatus.EXECUTION_EXCEPTION] */ + override val status: CommandExecuteStatus get() = CommandExecuteStatus.EXECUTION_EXCEPTION + } + + /** 没有匹配的指令 */ + class CommandNotFound( + /** 尝试执行的指令名 */ + override val commandName: String + ) : CommandExecuteResult() { + /** 指令执行时发生的错误, 总是 `null` */ + override val exception: Nothing? get() = null + + /** 尝试执行的指令, 总是 `null` */ + override val command: Nothing? get() = null + + /** 基础分割后的实际参数列表, 元素类型可能为 [Message] 或 [String] */ + override val args: Nothing? get() = null + + /** 指令最终执行状态, 总是 [CommandExecuteStatus.COMMAND_NOT_FOUND] */ + override val status: CommandExecuteStatus get() = CommandExecuteStatus.COMMAND_NOT_FOUND + } + + /** + * 指令的执行状态 + */ + enum class CommandExecuteStatus { + /** 指令执行成功 */ + SUCCESSFUL, + + /** 指令执行过程出现了错误 */ + EXECUTION_EXCEPTION, + + /** 没有匹配的指令 */ + COMMAND_NOT_FOUND + } +} + + +@Suppress("RemoveRedundantQualifierName") +typealias CommandExecuteStatus = CommandExecuteResult.CommandExecuteStatus + +/** + * 当 [this] 为 [CommandExecuteResult.Success] 时返回 `true` + */ +@JvmSynthetic +fun CommandExecuteResult.isSuccess(): Boolean { + contract { + returns(true) implies (this@isSuccess is CommandExecuteResult.Success) + returns(false) implies (this@isSuccess !is CommandExecuteResult.Success) + } + return this is CommandExecuteResult.Success +} + +/** + * 当 [this] 为 [CommandExecuteResult.ExecutionException] 时返回 `true` + */ +@JvmSynthetic +fun CommandExecuteResult.isExecutionException(): Boolean { + contract { + returns(true) implies (this@isExecutionException is CommandExecuteResult.ExecutionException) + returns(false) implies (this@isExecutionException !is CommandExecuteResult.ExecutionException) + } + return this is CommandExecuteResult.ExecutionException +} + +/** + * 当 [this] 为 [CommandExecuteResult.ExecutionException] 时返回 `true` + */ +@JvmSynthetic +fun CommandExecuteResult.isCommandNotFound(): Boolean { + contract { + returns(true) implies (this@isCommandNotFound is CommandExecuteResult.CommandNotFound) + returns(false) implies (this@isCommandNotFound !is CommandExecuteResult.CommandNotFound) + } + return this is CommandExecuteResult.CommandNotFound +} + +/** + * 当 [this] 为 [CommandExecuteResult.ExecutionException] 或 [CommandExecuteResult.CommandNotFound] 时返回 `true` + */ +@JvmSynthetic +fun CommandExecuteResult.isFailure(): Boolean { + contract { + returns(true) implies (this@isFailure !is CommandExecuteResult.Success) + returns(false) implies (this@isFailure is CommandExecuteResult.Success) + } + return this !is CommandExecuteResult.Success +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt index 56d980cda..bda174d01 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt @@ -7,7 +7,10 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -@file:Suppress("NOTHING_TO_INLINE", "unused") +@file:Suppress( + "NOTHING_TO_INLINE", "unused", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE", + "MemberVisibilityCanBePrivate" +) @file:JvmName("CommandManagerKt") package net.mamoe.mirai.console.command @@ -18,7 +21,6 @@ import kotlinx.coroutines.Job import net.mamoe.mirai.console.plugin.Plugin import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.MessageChain -import net.mamoe.mirai.message.data.SingleMessage import net.mamoe.mirai.utils.MiraiInternalAPI /** @@ -126,104 +128,119 @@ fun Command.unregister(): Boolean = InternalCommandManager.modifyLock.withLock { InternalCommandManager.registeredCommands.remove(this) } -//// executing +//// executing without detailed result (faster) /** * 解析并执行一个指令 * - * Java 调用方式: ` CommandManager.executeCommand(Command)` - * * @param messages 接受 [String] 或 [Message], 其他对象将会被 [Any.toString] - * @see CommandExecuteResult + * + * @return 成功执行的指令, 在无匹配指令时返回 `null` + * @throws CommandExecutionException 当 [Command.onCommand] 抛出异常时包装并附带相关指令信息抛出 * * @see JCommandManager.executeCommand Java 方法 */ -suspend fun CommandSender.executeCommand(vararg messages: Any): CommandExecuteResult { - if (messages.isEmpty()) return CommandExecuteResult( - status = CommandExecuteStatus.EMPTY_COMMAND - ) - return executeCommandInternal( - messages, - messages[0].let { if (it is SingleMessage) it.toString() else it.toString().substringBefore(' ') }) +suspend fun CommandSender.executeCommand(vararg messages: Any): Command? { + if (messages.isEmpty()) return null + return executeCommandInternal(messages, messages[0].toString().substringBefore(' ')) } -@JvmSynthetic -internal inline fun List.dropToTypedArray(n: Int): Array = Array(size - n) { this[n + it] } - /** * 解析并执行一个指令 - * @see CommandExecuteResult + * + * @return 成功执行的指令, 在无匹配指令时返回 `null` + * @throws CommandExecutionException 当 [Command.onCommand] 抛出异常时包装并附带相关指令信息抛出 * * @see JCommandManager.executeCommand Java 方法 */ -suspend fun CommandSender.executeCommand(message: MessageChain): CommandExecuteResult { - if (message.isEmpty()) return CommandExecuteResult( - status = CommandExecuteStatus.EMPTY_COMMAND - ) +@Throws(CommandExecutionException::class) +suspend fun CommandSender.executeCommand(message: MessageChain): Command? { + if (message.isEmpty()) return null return executeCommandInternal(message, message[0].toString()) } -@JvmSynthetic -internal suspend inline fun CommandSender.executeCommandInternal( - messages: Any, - commandName: String -): CommandExecuteResult { - val command = InternalCommandManager.matchCommand(commandName) ?: return CommandExecuteResult( - status = CommandExecuteStatus.COMMAND_NOT_FOUND, - commandName = commandName - ) - val rawInput = messages.flattenCommandComponents() - kotlin.runCatching { - command.onCommand(this, rawInput.dropToTypedArray(1)) - }.onFailure { - return CommandExecuteResult( - status = CommandExecuteStatus.FAILED, - commandName = commandName, - command = command, - exception = it - ) - } - return CommandExecuteResult( - status = CommandExecuteStatus.SUCCESSFUL, - commandName = commandName, - command = command - ) + +/** + * 在 [executeCommand] 中, [Command.onCommand] 抛出异常时包装的异常. + */ +class CommandExecutionException( + /** + * 执行过程发生异常的指令 + */ + val command: Command, + /** + * 匹配到的指令名 + */ + val name: String, + /** + * 基础分割后的实际参数列表, 元素类型可能为 [Message] 或 [String] + */ + val args: Array, + cause: Throwable +) : RuntimeException( + "Exception while executing command '${command.primaryName}' with args ${args.joinToString { "'$it'" }}", + cause +) { + override fun toString(): String = + "CommandExecutionException(command=$command, name='$name', args=${args.contentToString()})" +} + + +//// execution with detailed result + +/** + * 解析并执行一个指令, 获取详细的指令参数等信息 + * + * @param messages 接受 [String] 或 [Message], 其他对象将会被 [Any.toString] + * + * @return 执行结果 + * + * @see JCommandManager.executeCommandDetailed Java 方法 + */ +suspend fun CommandSender.executeCommandDetailed(vararg messages: Any): CommandExecuteResult { + if (messages.isEmpty()) return CommandExecuteResult.CommandNotFound("") + return executeCommandDetailedInternal(messages, messages[0].toString().substringBefore(' ')) } /** - * 命令的执行返回 + * 解析并执行一个指令, 获取详细的指令参数等信息 * - * @param status 命令最终执行状态 - * @param exception 命令执行时发生的错误(如果有) - * @param command 尝试执行的命令 (status = SUCCESSFUL | FAILED) - * @param commandName 尝试执行的命令的名字 (status != EMPTY_COMMAND) + * 执行过程中产生的异常将不会直接抛出, 而会包装为 [CommandExecuteResult.ExecutionException] * + * @return 执行结果 * - * @see CommandExecuteStatus + * @see JCommandManager.executeCommandDetailed Java 方法 */ -class CommandExecuteResult( - val status: CommandExecuteStatus, - val exception: Throwable? = null, - val command: Command? = null, - val commandName: String? = null -) { - /** - * 命令的执行状态 - * - * 当为 [SUCCESSFUL] 的时候,代表命令执行成功 - * - * 当为 [FAILED] 的时候, 代表命令执行出现了错误 - * - * 当为 [COMMAND_NOT_FOUND] 的时候,代表没有匹配的命令 - * - * 当为 [EMPTY_COMMAND] 的时候, 代表尝试执行 "" - * - */ - enum class CommandExecuteStatus { - SUCCESSFUL, FAILED, COMMAND_NOT_FOUND, EMPTY_COMMAND - } - +suspend fun CommandSender.executeCommandDetailed(messages: MessageChain): CommandExecuteResult { + if (messages.isEmpty()) return CommandExecuteResult.CommandNotFound("") + return executeCommandDetailedInternal(messages, messages[0].toString()) } -@Suppress("RemoveRedundantQualifierName") -typealias CommandExecuteStatus = CommandExecuteResult.CommandExecuteStatus +@JvmSynthetic +internal suspend inline fun CommandSender.executeCommandDetailedInternal( + messages: Any, + commandName: String +): CommandExecuteResult { + val command = + InternalCommandManager.matchCommand(commandName) ?: return CommandExecuteResult.CommandNotFound(commandName) + val args = messages.flattenCommandComponents().dropToTypedArray(1) + kotlin.runCatching { + command.onCommand(this, args) + }.fold( + onSuccess = { + return CommandExecuteResult.Success( + commandName = commandName, + command = command, + args = args + ) + }, + onFailure = { + return CommandExecuteResult.ExecutionException( + commandName = commandName, + command = command, + exception = it, + args = args + ) + } + ) +} diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal.kt index e88f5a486..1552786d8 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal.kt @@ -156,4 +156,33 @@ internal fun Group.fuzzySearchMember(nameCardTarget: String): Member? { return this.members.fuzzySearchOnly(nameCardTarget) { it.nameCard } -} \ No newline at end of file +} + + +//// internal + +@JvmSynthetic +internal inline fun List.dropToTypedArray(n: Int): Array = Array(size - n) { this[n + it] } + +@JvmSynthetic +@Throws(CommandExecutionException::class) +internal suspend inline fun CommandSender.executeCommandInternal( + messages: Any, + commandName: String +): Command? { + val command = InternalCommandManager.matchCommand(commandName) ?: return null + val rawInput = messages.flattenCommandComponents() + + val loweredArgs = rawInput.dropToTypedArray(1) + + kotlin.runCatching { + command.onCommand(this, loweredArgs) + }.fold( + onSuccess = { + return command + }, + onFailure = { + throw CommandExecutionException(command, commandName, loweredArgs, it) + } + ) +} diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt index ad142caa5..77a55e1e9 100644 --- a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt +++ b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt @@ -22,9 +22,8 @@ package net.mamoe.mirai.console.pure import net.mamoe.mirai.console.MiraiConsoleInitializer import net.mamoe.mirai.console.command.CommandExecuteStatus import net.mamoe.mirai.console.command.ConsoleCommandSender -import net.mamoe.mirai.console.command.executeCommand +import net.mamoe.mirai.console.command.executeCommandDetailed import net.mamoe.mirai.message.data.Message -import net.mamoe.mirai.message.data.PlainText import net.mamoe.mirai.utils.DefaultLogger import kotlin.concurrent.thread @@ -48,14 +47,11 @@ internal fun startConsoleThread() { while (true) { val next = MiraiConsoleFrontEndPure.requestInput("") consoleLogger.debug("INPUT> $next") - val result = ConsoleCS.executeCommand(PlainText(next)) + val result = ConsoleCS.executeCommandDetailed(next) when (result.status) { CommandExecuteStatus.SUCCESSFUL -> { } - CommandExecuteStatus.EMPTY_COMMAND -> { - } - CommandExecuteStatus.FAILED -> { - consoleLogger.error("An error occurred while executing the command: $next", result.exception) + CommandExecuteStatus.EXECUTION_EXCEPTION -> { } CommandExecuteStatus.COMMAND_NOT_FOUND -> { consoleLogger.warning("Unknown command: ${result.commandName}")