Implement MiraiConsole bootstrap and plugin loading

This commit is contained in:
Him188 2020-06-28 10:44:10 +08:00
parent 442d7ee0ce
commit 01fd1b3b6e
10 changed files with 186 additions and 60 deletions

View File

@ -8,6 +8,7 @@
*/
@file:Suppress("WRONG_MODIFIER_CONTAINING_DECLARATION")
@file:OptIn(ConsoleInternalAPI::class)
package net.mamoe.mirai.console
@ -16,18 +17,22 @@ import kotlinx.coroutines.Job
import kotlinx.io.charsets.Charset
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.MiraiConsole.INSTANCE
import net.mamoe.mirai.console.command.ConsoleCommandOwner
import net.mamoe.mirai.console.command.ConsoleCommandSender
import net.mamoe.mirai.console.plugin.PluginLoader
import net.mamoe.mirai.console.plugin.PluginManager
import net.mamoe.mirai.console.plugin.center.CuiPluginCenter
import net.mamoe.mirai.console.plugin.center.PluginCenter
import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader
import net.mamoe.mirai.console.setting.SettingStorage
import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI
import net.mamoe.mirai.console.utils.ConsoleInternalAPI
import net.mamoe.mirai.utils.DefaultLogger
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.info
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.PrintStream
import java.text.SimpleDateFormat
import java.util.*
import kotlin.coroutines.CoroutineContext
@ -80,14 +85,14 @@ internal object MiraiConsoleInitializer {
/** 由前端调用 */
internal fun init(instance: IMiraiConsole) {
this.instance = instance
MiraiConsoleInternal.initialize()
MiraiConsoleInternal.doStart()
}
}
internal object MiraiConsoleBuildConstants { // auto-filled on build (task :mirai-console:fillBuildConstants)
@JvmStatic
val buildDate: Date = Date(1592799753404L) // 2020-06-22 12:22:33
const val version: String = "0.5.1"
const val version: String = "1.0-M1"
}
/**
@ -107,9 +112,10 @@ internal object MiraiConsoleInternal : CoroutineScope, IMiraiConsole, MiraiConso
get() = instance.mainLogger
override val coroutineContext: CoroutineContext get() = instance.coroutineContext
override val builtInPluginLoaders: List<PluginLoader<*, *>> get() = instance.builtInPluginLoaders
override val consoleCommandOwner: ConsoleCommandOwner get() = instance.consoleCommandOwner
override val consoleCommandSender: ConsoleCommandSender get() = instance.consoleCommandSender
override val settingStorage: SettingStorage get() = instance.settingStorage
init {
DefaultLogger = { identity -> this.newLogger(identity) }
}
@ -117,13 +123,23 @@ internal object MiraiConsoleInternal : CoroutineScope, IMiraiConsole, MiraiConso
@ConsoleExperimentalAPI
override fun newLogger(identity: String?): MiraiLogger = frontEnd.loggerFor(identity)
internal fun initialize() {
internal fun doStart() {
val buildDateFormatted = SimpleDateFormat("yyyy-MM-dd").format(buildDate)
mainLogger.info { "Starting mirai-console..." }
mainLogger.info { "Backend: version $version, built on $buildDateFormatted." }
mainLogger.info { "Frontend ${frontEnd.name}: version $version." }
if (coroutineContext[Job] == null) {
throw IllegalMiraiConsoleImplementationError("The coroutineContext given to MiraiConsole must have a Job in it.")
}
this.coroutineContext[Job]!!.invokeOnCompletion {
Bot.botInstances.forEach { kotlin.runCatching { it.close() }.exceptionOrNull()?.let(mainLogger::error) }
}
mainLogger.info { "Loading plugins..." }
PluginManager.loadEnablePlugins()
mainLogger.info { "${PluginManager.plugins.size} plugin(s) loaded." }
mainLogger.info { "mirai-console started successfully." }
// Only for initialize
}
}
@ -155,9 +171,9 @@ internal interface IMiraiConsole : CoroutineScope {
*/
val builtInPluginLoaders: List<PluginLoader<*, *>>
internal val consoleCommandOwner: ConsoleCommandOwner
val consoleCommandSender: ConsoleCommandSender
internal val consoleCommandSender: ConsoleCommandSender
val settingStorage: SettingStorage
}
/**

View File

@ -11,15 +11,23 @@ package net.mamoe.mirai.console
import net.mamoe.mirai.Bot
import net.mamoe.mirai.utils.LoginSolver
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.MiraiLogger
/**
* 只需要实现一个这个传入 MiraiConsole 就可以绑定 UI 层与 Console
* 需要保证线程安全
*/
@MiraiInternalAPI
interface MiraiConsoleFrontEnd {
/**
* 名称
*/
val name: String
/**
* 版本
*/
val version: String
fun loggerFor(identity: String?): MiraiLogger
/**

View File

@ -85,4 +85,18 @@ abstract class AbstractFilePluginLoader<P : Plugin, D : PluginDescription>(
protected abstract fun Sequence<File>.mapToDescription(): List<D>
final override fun listPlugins(): List<D> = pluginsFilesSequence().mapToDescription()
}
}
// Not yet decided to make public API
internal class DeferredPluginLoader<P : Plugin, D : PluginDescription>(
initializer: () -> PluginLoader<P, D>
) : PluginLoader<P, D> {
private val instance by lazy(initializer)
override fun listPlugins(): List<D> = instance.listPlugins()
override val P.description: D get() = instance.run { description }
override fun load(description: D): P = instance.load(description)
override fun enable(plugin: P) = instance.enable(plugin)
override fun disable(plugin: P) = instance.disable(plugin)
}

View File

@ -7,12 +7,13 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("NOTHING_TO_INLINE")
@file:Suppress("NOTHING_TO_INLINE", "unused")
package net.mamoe.mirai.console.plugin
import kotlinx.atomicfu.locks.withLock
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.utils.info
import java.io.File
import java.util.concurrent.locks.ReentrantLock
@ -26,8 +27,10 @@ object PluginManager {
val pluginsDir = File(MiraiConsole.rootDir, "plugins").apply { mkdir() }
val pluginsDataFolder = File(MiraiConsole.rootDir, "data").apply { mkdir() }
@Suppress("ObjectPropertyName")
private val _pluginLoaders: MutableList<PluginLoader<*, *>> = mutableListOf()
private val loadersLock: ReentrantLock = ReentrantLock()
private val logger = MiraiConsole.newLogger("PluginManager")
@JvmField
internal val resolvedPlugins: MutableList<Plugin> = mutableListOf()
@ -70,13 +73,33 @@ object PluginManager {
// region LOADING
private fun <P : Plugin, D : PluginDescription> PluginLoader<P, D>.loadPluginNoEnable(description: D): P {
// TODO: 2020/5/23 HANDLE INITIALIZATION EXCEPTION
return this.load(description).also { resolvedPlugins.add(it) }
return kotlin.runCatching {
this.load(description).also { resolvedPlugins.add(it) }
}.fold(
onSuccess = {
logger.info { "Successfully loaded plugin ${description.name}" }
it
},
onFailure = {
logger.info { "Cannot load plugin ${description.name}" }
throw it
}
)
}
private fun <P : Plugin, D : PluginDescription> PluginLoader<P, D>.loadPluginAndEnable(description: D) {
@Suppress("UNCHECKED_CAST")
return this.enable(loadPluginNoEnable(description.unwrap()))
private fun <P : Plugin, D : PluginDescription> PluginLoader<P, D>.enablePlugin(plugin: Plugin) {
kotlin.runCatching {
@Suppress("UNCHECKED_CAST")
this.enable(plugin as P)
}.fold(
onSuccess = {
logger.info { "Successfully enabled plugin ${plugin.description.name}" }
},
onFailure = {
logger.info { "Cannot enable plugin ${plugin.description.name}" }
throw it
}
)
}
/**
@ -93,10 +116,15 @@ object PluginManager {
@Suppress("UNCHECKED_CAST")
@Throws(PluginMissingDependencyException::class)
internal fun loadEnablePlugins() {
val all = loadAndEnableLoaderProviders() + _pluginLoaders.listAllPlugins().flatMap { it.second }
(loadAndEnableLoaderProviders() + _pluginLoaders.listAllPlugins().flatMap { it.second })
.sortByDependencies().loadAndEnableAllInOrder()
}
for ((loader, desc) in all.sortByDependencies()) {
loader.loadPluginAndEnable(desc)
private fun List<PluginDescriptionWithLoader>.loadAndEnableAllInOrder() {
return this.map { (loader, desc) ->
loader to loader.loadPluginNoEnable(desc)
}.forEach { (loader, plugin) ->
loader.enablePlugin(plugin)
}
}
@ -112,9 +140,7 @@ object PluginManager {
.onEach { (loader, descriptions) ->
loader as PluginLoader<Plugin, PluginDescription>
for (it in descriptions.filter { it.kind == PluginKind.LOADER }.sortByDependencies()) {
loader.loadPluginAndEnable(it)
}
descriptions.filter { it.kind == PluginKind.LOADER }.sortByDependencies().loadAndEnableAllInOrder()
}
.flatMap { it.second.asSequence() }
@ -161,12 +187,7 @@ object PluginManager {
// endregion
}
class PluginMissingDependencyException : PluginResolutionException {
constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
}
class PluginMissingDependencyException(message: String?) : PluginResolutionException(message)
open class PluginResolutionException : Exception {
constructor() : super()

View File

@ -14,10 +14,12 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.command.ConsoleCommandOwner
import net.mamoe.mirai.console.command.ConsoleCommandSender
import net.mamoe.mirai.console.plugin.PluginLoader
import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader
import net.mamoe.mirai.console.setting.MemorySettingStorage
import net.mamoe.mirai.console.setting.SettingStorage
import net.mamoe.mirai.console.utils.ConsoleInternalAPI
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.utils.DefaultLogger
import net.mamoe.mirai.utils.LoginSolver
@ -28,6 +30,7 @@ import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlin.test.assertNotNull
@OptIn(ConsoleInternalAPI::class)
fun initTestEnvironment() {
MiraiConsoleInitializer.init(object : IMiraiConsole {
override val rootDir: File = createTempDir()
@ -39,10 +42,10 @@ fun initTestEnvironment() {
}
override val mainLogger: MiraiLogger = DefaultLogger("main")
override val builtInPluginLoaders: List<PluginLoader<*, *>> = listOf(JarPluginLoader)
override val consoleCommandOwner: ConsoleCommandOwner = object : ConsoleCommandOwner() {}
override val consoleCommandSender: ConsoleCommandSender = object : ConsoleCommandSender() {
override suspend fun sendMessage(message: Message) = println(message)
}
override val settingStorage: SettingStorage get() = MemorySettingStorage
override val coroutineContext: CoroutineContext = SupervisorJob()
})
}

View File

@ -27,7 +27,7 @@ import org.junit.jupiter.api.Test
import kotlin.test.*
object TestCompositeCommand : CompositeCommand(
ConsoleCommandOwner.instance,
ConsoleCommandOwner,
"testComposite", "tsC"
) {
@SubCommand
@ -44,7 +44,7 @@ object TestSimpleCommand : RawCommand(owner, "testSimple", "tsS") {
}
internal val sender by lazy { ConsoleCommandSender.instance }
internal val owner by lazy { ConsoleCommandOwner.instance }
internal val owner by lazy { ConsoleCommandOwner }
internal class TestCommand {
companion object {
@ -61,7 +61,7 @@ internal class TestCommand {
assertTrue(TestCompositeCommand.register())
assertFalse(TestCompositeCommand.register())
assertEquals(1, ConsoleCommandOwner.instance.registeredCommands.size)
assertEquals(1, ConsoleCommandOwner.registeredCommands.size)
assertEquals(1, InternalCommandManager.registeredCommands.size)
assertEquals(2, InternalCommandManager.requiredPrefixCommandMap.size)
@ -131,14 +131,16 @@ internal class TestCommand {
fun `composite sub command resolution conflict`() {
runBlocking {
val composite = object : CompositeCommand(
ConsoleCommandOwner.instance,
ConsoleCommandOwner,
"tr"
) {
@Suppress("UNUSED_PARAMETER")
@SubCommand
fun mute(seconds: Int) {
Testing.ok(1)
}
@Suppress("UNUSED_PARAMETER")
@SubCommand
fun mute(seconds: Int, arg2: Int) {
Testing.ok(2)
@ -164,7 +166,7 @@ internal class TestCommand {
)
val composite = object : CompositeCommand(
ConsoleCommandOwner.instance,
ConsoleCommandOwner,
"test",
overrideContext = CommandParserContext {
add(object : CommandArgParser<MyClass> {

View File

@ -16,7 +16,7 @@ import org.jline.reader.impl.completer.NullCompleter
import org.jline.terminal.Terminal
import org.jline.terminal.TerminalBuilder
object ConsoleUtils {
internal object ConsoleUtils {
val lineReader: LineReader
val terminal: Terminal
@ -24,10 +24,10 @@ object ConsoleUtils {
init {
val dumb = System.getProperty("java.class.path")
.contains("idea_rt.jar") || System.getProperty("mirai.idea") !== null
.contains("idea_rt.jar") || System.getProperty("mirai.idea") !== null || System.getenv("mirai.idea") !== null
terminal = TerminalBuilder.builder()
.dumb(dumb)
.jansi(true)
.build()
lineReader = LineReaderBuilder.builder()
.terminal(terminal)

View File

@ -7,12 +7,26 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress(
"INVISIBLE_MEMBER",
"INVISIBLE_REFERENCE",
"CANNOT_OVERRIDE_INVISIBLE_MEMBER",
"INVISIBLE_SETTER",
"INVISIBLE_GETTER",
"INVISIBLE_ABSTRACT_MEMBER_FROM_SUPER",
"INVISIBLE_ABSTRACT_MEMBER_FROM_SUPER_WARNING",
"EXPOSED_SUPER_CLASS"
)
package net.mamoe.mirai.console.pure
//import net.mamoe.mirai.console.command.CommandManager
//import net.mamoe.mirai.console.utils.MiraiConsoleFrontEnd
import kotlinx.coroutines.suspendCancellableCoroutine
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.MiraiConsoleBuildConstants
import net.mamoe.mirai.console.MiraiConsoleFrontEnd
import net.mamoe.mirai.console.utils.ConsoleInternalAPI
import net.mamoe.mirai.utils.DefaultLoginSolver
import net.mamoe.mirai.utils.LoginSolver
import net.mamoe.mirai.utils.MiraiLogger
@ -21,6 +35,7 @@ import org.fusesource.jansi.Ansi
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.resume
private val ANSI_RESET = Ansi().reset().toString()
@ -30,6 +45,13 @@ internal val LoggerCreator: (identity: String?) -> MiraiLogger = {
})
}
/**
* mirai-console-pure 前端实现
*
* @see MiraiConsolePure 后端实现
* @see MiraiConsolePureLoader CLI 入口点
*/
@ConsoleInternalAPI
@Suppress("unused")
object MiraiConsoleFrontEndPure : MiraiConsoleFrontEnd {
private val globalLogger = LoggerCreator("Mirai")
@ -53,7 +75,10 @@ object MiraiConsoleFrontEndPure : MiraiConsoleFrontEnd {
val sdf by lazy {
SimpleDateFormat("HH:mm:ss")
}
override val name: String
get() = "Pure"
override val version: String
get() = MiraiConsoleBuildConstants.version
override fun loggerFor(identity: String?): MiraiLogger {
identity?.apply {
@ -74,7 +99,9 @@ object MiraiConsoleFrontEndPure : MiraiConsoleFrontEnd {
.toString()
)
}
return ConsoleUtils.lineReader.readLine("> ")
return suspendCancellableCoroutine {
it.resume(ConsoleUtils.lineReader.readLine("> "))
}
}
override fun createLoginSolver(): LoginSolver {

View File

@ -15,32 +15,58 @@
"INVISIBLE_SETTER",
"INVISIBLE_GETTER",
"INVISIBLE_ABSTRACT_MEMBER_FROM_SUPER",
"INVISIBLE_ABSTRACT_MEMBER_FROM_SUPE_WARNING"
"INVISIBLE_ABSTRACT_MEMBER_FROM_SUPER_WARNING",
"EXPOSED_SUPER_CLASS"
)
@file:OptIn(ConsoleInternalAPI::class)
package net.mamoe.mirai.console.pure
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import net.mamoe.mirai.console.IMiraiConsole
import net.mamoe.mirai.console.MiraiConsoleFrontEnd
import net.mamoe.mirai.console.MiraiConsoleInitializer
import net.mamoe.mirai.console.command.ConsoleCommandSender
import net.mamoe.mirai.console.plugin.DeferredPluginLoader
import net.mamoe.mirai.console.plugin.PluginLoader
import net.mamoe.mirai.utils.DefaultLogger
import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader
import net.mamoe.mirai.console.setting.MultiFileSettingStorage
import net.mamoe.mirai.console.setting.SettingStorage
import net.mamoe.mirai.console.utils.ConsoleInternalAPI
import net.mamoe.mirai.utils.MiraiLogger
import java.io.File
import java.util.*
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
private val delegateScope = CoroutineScope(EmptyCoroutineContext)
object MiraiConsolePure : IMiraiConsole {
override val builtInPluginLoaders: List<PluginLoader<*, *>> = LinkedList()
override val frontEnd: MiraiConsoleFrontEnd = MiraiConsoleFrontEndPure
override val mainLogger: MiraiLogger = DefaultLogger("Console")
override val rootDir: File = File("./test/console").also {
it.mkdirs()
/**
* mirai-console-pure 后端实现
*
* @see MiraiConsoleFrontEndPure 前端实现
* @see MiraiConsolePureLoader CLI 入口点
*/
class MiraiConsolePure @JvmOverloads constructor(
override val rootDir: File = File("."),
override val builtInPluginLoaders: List<PluginLoader<*, *>> = listOf(DeferredPluginLoader { JarPluginLoader }),
override val frontEnd: MiraiConsoleFrontEnd = MiraiConsoleFrontEndPure,
override val mainLogger: MiraiLogger = frontEnd.loggerFor("Console"),
override val consoleCommandSender: ConsoleCommandSender = ConsoleCommandSenderImpl,
override val settingStorage: SettingStorage = MultiFileSettingStorage(rootDir)
) : IMiraiConsole, CoroutineScope by CoroutineScope(SupervisorJob()) {
init {
rootDir.mkdir()
require(rootDir.isDirectory) { "rootDir ${rootDir.absolutePath} is not a directory" }
}
companion object {
@Volatile
@JvmStatic
private var started: Boolean = false
@JvmStatic
fun MiraiConsolePure.start() = synchronized(this) {
check(!started) { "mirai-console is already started and can't be restarted." }
MiraiConsoleInitializer.init(MiraiConsolePure())
started = true
}
}
override val coroutineContext: CoroutineContext
get() = delegateScope.coroutineContext
}

View File

@ -16,17 +16,23 @@
"INVISIBLE_ABSTRACT_MEMBER_FROM_SUPER",
"INVISIBLE_ABSTRACT_MEMBER_FROM_SUPE_WARNING"
)
@file:OptIn(ConsoleInternalAPI::class)
package net.mamoe.mirai.console.pure
import net.mamoe.mirai.console.MiraiConsoleInitializer
import kotlinx.coroutines.isActive
import net.mamoe.mirai.console.command.CommandExecuteStatus
import net.mamoe.mirai.console.command.ConsoleCommandSender
import net.mamoe.mirai.console.command.executeCommandDetailed
import net.mamoe.mirai.console.pure.MiraiConsolePure.Companion.start
import net.mamoe.mirai.console.utils.ConsoleInternalAPI
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.utils.DefaultLogger
import kotlin.concurrent.thread
/**
* mirai-console-pure CLI 入口点
*/
object MiraiConsolePureLoader {
@JvmStatic
fun main(args: Array<String>?) {
@ -36,18 +42,21 @@ object MiraiConsolePureLoader {
internal fun startup() {
MiraiConsoleInitializer.init(MiraiConsolePure)
startConsoleThread()
MiraiConsolePure().start()
}
internal fun startConsoleThread() {
thread(name = "Console", isDaemon = false) {
val consoleLogger = DefaultLogger("Console")
kotlinx.coroutines.runBlocking {
while (true) {
while (isActive) {
val next = MiraiConsoleFrontEndPure.requestInput("")
if (next.isBlank()) {
continue
}
consoleLogger.debug("INPUT> $next")
val result = ConsoleCS.executeCommandDetailed(next)
val result = ConsoleCommandSenderImpl.executeCommandDetailed(next)
when (result.status) {
CommandExecuteStatus.SUCCESSFUL -> {
}
@ -65,7 +74,7 @@ internal fun startConsoleThread() {
}
}
object ConsoleCS : ConsoleCommandSender() {
internal object ConsoleCommandSenderImpl : ConsoleCommandSender() {
override suspend fun sendMessage(message: Message) {
ConsoleUtils.lineReader.printAbove(message.contentToString())
}