mirror of
https://github.com/mamoe/mirai.git
synced 2025-02-11 02:14:45 +08:00
Plugin infrastructure
This commit is contained in:
parent
3efebfb627
commit
027286a226
@ -13,7 +13,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.io.charsets.Charset
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.console.command.CommandOwner
|
||||
import net.mamoe.mirai.console.utils.MiraiConsoleFrontEnd
|
||||
import net.mamoe.mirai.utils.DefaultLogger
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalAPI
|
||||
@ -43,7 +42,7 @@ interface IMiraiConsole : CoroutineScope {
|
||||
val mainLogger: MiraiLogger
|
||||
}
|
||||
|
||||
object MiraiConsole : CoroutineScope, IMiraiConsole, CommandOwner {
|
||||
object MiraiConsole : CoroutineScope, IMiraiConsole {
|
||||
private lateinit var instance: IMiraiConsole
|
||||
|
||||
/** 由前端调用 */
|
||||
|
@ -4,6 +4,7 @@
|
||||
package net.mamoe.mirai.console.command
|
||||
|
||||
import kotlinx.atomicfu.locks.withLock
|
||||
import net.mamoe.mirai.console.plugins.PluginBase
|
||||
import net.mamoe.mirai.message.data.Message
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import java.util.*
|
||||
@ -11,7 +12,12 @@ import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
typealias CommandFullName = Array<out Any>
|
||||
|
||||
interface CommandOwner
|
||||
sealed class CommandOwner
|
||||
|
||||
abstract class PluginCommandOwner(plugin: PluginBase) : CommandOwner()
|
||||
|
||||
// 由前端实现
|
||||
internal abstract class ConsoleCommandOwner : CommandOwner()
|
||||
|
||||
val CommandOwner.registeredCommands: List<Command> get() = InternalCommandManager.registeredCommands.filter { it.owner == this }
|
||||
|
||||
|
@ -0,0 +1,436 @@
|
||||
/*
|
||||
* Copyright 2020 Mamoe Technologies and contributors.
|
||||
*
|
||||
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
||||
*
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.console.plugins
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.mamoe.mirai.console.MiraiConsole
|
||||
import net.mamoe.mirai.console.command.Command
|
||||
import net.mamoe.mirai.console.command.CommandSender
|
||||
import net.mamoe.mirai.console.command.description
|
||||
import net.mamoe.mirai.console.encodeToString
|
||||
import net.mamoe.mirai.utils.LockFreeLinkedList
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.net.JarURLConnection
|
||||
import java.net.URL
|
||||
import java.util.jar.JarFile
|
||||
|
||||
|
||||
sealed class JarPlugin : Plugin(), CoroutineScope {
|
||||
internal lateinit var _description: JarPluginDescription
|
||||
|
||||
final override val description: PluginDescription get() = _description
|
||||
final override val loader: JarPluginLoader get() = JarPluginLoader
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal class JarPluginDescription(
|
||||
override val name: String,
|
||||
override val author: String,
|
||||
override val version: String,
|
||||
override val info: String,
|
||||
override val depends: List<String>
|
||||
) : PluginDescription
|
||||
|
||||
abstract class JavaPlugin : JarPlugin()
|
||||
|
||||
abstract class KotlinPlugin : JarPlugin()
|
||||
|
||||
|
||||
/**
|
||||
* 内建的 Jar (JVM) 插件加载器
|
||||
*/
|
||||
object JarPluginLoader : PluginLoader<JarPlugin> {
|
||||
override val list: List<JarPlugin>
|
||||
get() = TODO("Not yet implemented")
|
||||
|
||||
override fun load(plugin: JarPlugin) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun enable(plugin: JarPlugin) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
object PluginManagerOld {
|
||||
/**
|
||||
* 通过插件获取介绍
|
||||
* @see description
|
||||
*/
|
||||
fun getPluginDescription(base: PluginBase): PluginDescription {
|
||||
nameToPluginBaseMap.forEach { (s, pluginBase) ->
|
||||
if (pluginBase == base) {
|
||||
return pluginDescriptions[s]!!
|
||||
}
|
||||
}
|
||||
error("can not find plugin description")
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有插件摘要
|
||||
*/
|
||||
fun getAllPluginDescriptions(): Collection<PluginDescription> {
|
||||
return pluginDescriptions.values
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有插件
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun disablePlugins(throwable: CancellationException? = null) {
|
||||
pluginsSequence.forEach { plugin ->
|
||||
plugin.unregisterAllCommands()
|
||||
plugin.disable(throwable)
|
||||
}
|
||||
nameToPluginBaseMap.clear()
|
||||
pluginDescriptions.clear()
|
||||
pluginsLoader.clear()
|
||||
pluginsSequence.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 重载所有插件
|
||||
*/
|
||||
fun reloadPlugins() {
|
||||
pluginsSequence.forEach {
|
||||
it.disable()
|
||||
}
|
||||
loadPlugins(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试加载全部插件
|
||||
*/
|
||||
fun loadPlugins(clear: Boolean = true) = loadPluginsImpl(clear)
|
||||
|
||||
|
||||
//////////////////
|
||||
//// internal ////
|
||||
//////////////////
|
||||
|
||||
internal val pluginsPath = (MiraiConsole.path + "/plugins/").replace("//", "/").also {
|
||||
File(it).mkdirs()
|
||||
}
|
||||
|
||||
private val logger = MiraiConsole.newLogger("Plugin Manager")
|
||||
|
||||
/**
|
||||
* 加载成功的插件, 名字->插件
|
||||
*/
|
||||
internal val nameToPluginBaseMap: MutableMap<String, PluginBase> = mutableMapOf()
|
||||
|
||||
/**
|
||||
* 加载成功的插件, 名字->插件摘要
|
||||
*/
|
||||
private val pluginDescriptions: MutableMap<String, PluginDescription> = mutableMapOf()
|
||||
|
||||
/**
|
||||
* 加载插件的PluginsLoader
|
||||
*/
|
||||
private val pluginsLoader: PluginsLoader = PluginsLoader(this.javaClass.classLoader)
|
||||
|
||||
/**
|
||||
* 插件优先级队列
|
||||
* 任何操作应该按这个Sequence顺序进行
|
||||
* 他的优先级取决于依赖,
|
||||
* 在这个队列中, 被依赖的插件会在依赖的插件之前
|
||||
*/
|
||||
private val pluginsSequence: LockFreeLinkedList<PluginBase> = LockFreeLinkedList()
|
||||
|
||||
|
||||
/**
|
||||
* 广播Command方法
|
||||
*/
|
||||
internal fun onCommand(command: Command, sender: CommandSender, args: List<String>) {
|
||||
pluginsSequence.forEach {
|
||||
try {
|
||||
it.onCommand(command, sender, args)
|
||||
} catch (e: Throwable) {
|
||||
logger.info(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Volatile
|
||||
internal var lastPluginName: String = ""
|
||||
|
||||
/**
|
||||
* 判断文件名/插件名是否已加载
|
||||
*/
|
||||
private fun isPluginLoaded(file: File, name: String): Boolean {
|
||||
pluginDescriptions.forEach {
|
||||
if (it.key == name || it.value.file == file) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 寻找所有安装的插件(在文件夹), 并将它读取, 记录位置
|
||||
* 这个不等同于加载的插件, 可以理解为还没有加载的插件
|
||||
*/
|
||||
internal data class FindPluginsResult(
|
||||
val pluginsLocation: MutableMap<String, File>,
|
||||
val pluginsFound: MutableMap<String, PluginDescription>
|
||||
)
|
||||
|
||||
internal fun findPlugins(): FindPluginsResult {
|
||||
val pluginsLocation: MutableMap<String, File> = mutableMapOf()
|
||||
val pluginsFound: MutableMap<String, PluginDescription> = mutableMapOf()
|
||||
|
||||
File(pluginsPath).listFiles()?.forEach { file ->
|
||||
if (file != null && file.extension == "jar") {
|
||||
val jar = JarFile(file)
|
||||
val pluginYml =
|
||||
jar.entries().asSequence().filter { it.name.toLowerCase().contains("plugin.yml") }.firstOrNull()
|
||||
|
||||
if (pluginYml == null) {
|
||||
logger.info("plugin.yml not found in jar " + jar.name + ", it will not be consider as a Plugin")
|
||||
} else {
|
||||
try {
|
||||
val description = PluginDescription.readFromContent(
|
||||
URL("jar:file:" + file.absoluteFile + "!/" + pluginYml.name).openConnection().let {
|
||||
val res = it.inputStream.use { input ->
|
||||
input.readBytes().encodeToString()
|
||||
}
|
||||
|
||||
// 关闭jarFile,解决热更新插件问题
|
||||
(it as JarURLConnection).jarFile.close()
|
||||
res
|
||||
}, file
|
||||
)
|
||||
if (!isPluginLoaded(file, description.name)) {
|
||||
pluginsFound[description.name] = description
|
||||
pluginsLocation[description.name] = file
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.info(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return FindPluginsResult(pluginsLocation, pluginsFound)
|
||||
}
|
||||
|
||||
internal fun loadPluginsImpl(clear: Boolean = true) {
|
||||
logger.info("""开始加载${pluginsPath}下的插件""")
|
||||
val findPluginsResult = findPlugins()
|
||||
val pluginsFound = findPluginsResult.pluginsFound
|
||||
val pluginsLocation = findPluginsResult.pluginsLocation
|
||||
|
||||
//不仅要解决A->B->C->A, 还要解决A->B->A->A
|
||||
fun checkNoCircularDepends(
|
||||
target: PluginDescription,
|
||||
needDepends: List<String>,
|
||||
existDepends: MutableList<String>
|
||||
) {
|
||||
|
||||
if (!target.noCircularDepend) {
|
||||
return
|
||||
}
|
||||
|
||||
existDepends.add(target.name)
|
||||
|
||||
if (needDepends.any { existDepends.contains(it) }) {
|
||||
target.noCircularDepend = false
|
||||
}
|
||||
|
||||
existDepends.addAll(needDepends)
|
||||
|
||||
needDepends.forEach {
|
||||
if (pluginsFound.containsKey(it)) {
|
||||
checkNoCircularDepends(pluginsFound[it]!!, pluginsFound[it]!!.depends, existDepends)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pluginsFound.values.forEach {
|
||||
checkNoCircularDepends(it, it.depends, mutableListOf())
|
||||
}
|
||||
|
||||
//load plugin individually
|
||||
fun loadPlugin(description: PluginDescription): Boolean {
|
||||
if (!description.noCircularDepend) {
|
||||
logger.error("Failed to load plugin " + description.name + " because it has circular dependency")
|
||||
return false
|
||||
}
|
||||
|
||||
if (description.loaded || nameToPluginBaseMap.containsKey(description.name)) {
|
||||
return true
|
||||
}
|
||||
|
||||
description.depends.forEach { dependent ->
|
||||
if (!pluginsFound.containsKey(dependent)) {
|
||||
logger.error("Failed to load plugin " + description.name + " because it need " + dependent + " as dependency")
|
||||
return false
|
||||
}
|
||||
val depend = pluginsFound[dependent]!!
|
||||
|
||||
if (!loadPlugin(depend)) {//先加载depend
|
||||
logger.error("Failed to load plugin " + description.name + " because " + dependent + " as dependency failed to load")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("loading plugin " + description.name)
|
||||
|
||||
val jarFile = pluginsLocation[description.name]!!
|
||||
val pluginClass = try {
|
||||
pluginsLoader.loadPluginMainClassByJarFile(description.name, description.basePath, jarFile)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
pluginsLoader.loadPluginMainClassByJarFile(description.name, "${description.basePath}Kt", jarFile)
|
||||
}
|
||||
|
||||
val subClass = pluginClass.asSubclass(PluginBase::class.java)
|
||||
|
||||
lastPluginName = description.name
|
||||
val plugin: PluginBase =
|
||||
subClass.kotlin.objectInstance ?: subClass.getDeclaredConstructor().apply {
|
||||
kotlin.runCatching {
|
||||
this.isAccessible = true
|
||||
}
|
||||
}.newInstance()
|
||||
plugin.dataFolder // initialize right now
|
||||
|
||||
description.loaded = true
|
||||
logger.info("successfully loaded plugin " + description.name + " version " + description.version + " by " + description.author)
|
||||
logger.info(description.info)
|
||||
|
||||
nameToPluginBaseMap[description.name] = plugin
|
||||
pluginDescriptions[description.name] = description
|
||||
plugin.pluginName = description.name
|
||||
pluginsSequence.addLast(plugin)//按照实际加载顺序加入队列
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
if (clear) {
|
||||
//清掉优先级队列, 来重新填充
|
||||
pluginsSequence.clear()
|
||||
}
|
||||
|
||||
pluginsFound.values.forEach {
|
||||
try {
|
||||
// 尝试加载插件
|
||||
loadPlugin(it)
|
||||
} catch (e: Throwable) {
|
||||
pluginsLoader.remove(it.name)
|
||||
when (e) {
|
||||
is ClassCastException -> logger.error(
|
||||
"failed to load plugin " + it.name + " , Main class does not extends PluginBase",
|
||||
e
|
||||
)
|
||||
is ClassNotFoundException -> logger.error(
|
||||
"failed to load plugin " + it.name + " , Main class not found under " + it.basePath,
|
||||
e
|
||||
)
|
||||
is NoClassDefFoundError -> logger.error(
|
||||
"failed to load plugin " + it.name + " , dependent class not found.",
|
||||
e
|
||||
)
|
||||
else -> logger.error("failed to load plugin " + it.name, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pluginsSequence.forEach {
|
||||
try {
|
||||
it.load()
|
||||
} catch (ignored: Throwable) {
|
||||
logger.info(ignored)
|
||||
logger.info(it.pluginName + " failed to load, disabling it")
|
||||
logger.info(it.pluginName + " 推荐立即删除/替换并重启")
|
||||
if (ignored is CancellationException) {
|
||||
disablePlugin(it, ignored)
|
||||
} else {
|
||||
disablePlugin(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pluginsSequence.forEach {
|
||||
try {
|
||||
it.enable()
|
||||
} catch (ignored: Throwable) {
|
||||
logger.info(ignored)
|
||||
logger.info(it.pluginName + " failed to enable, disabling it")
|
||||
logger.info(it.pluginName + " 推荐立即删除/替换并重启")
|
||||
if (ignored is CancellationException) {
|
||||
disablePlugin(it, ignored)
|
||||
} else {
|
||||
disablePlugin(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("""加载了${nameToPluginBaseMap.size}个插件""")
|
||||
}
|
||||
|
||||
private fun disablePlugin(
|
||||
plugin: PluginBase,
|
||||
exception: CancellationException? = null
|
||||
) {
|
||||
plugin.unregisterAllCommands()
|
||||
plugin.disable(exception)
|
||||
nameToPluginBaseMap.remove(plugin.pluginName)
|
||||
pluginDescriptions.remove(plugin.pluginName)
|
||||
pluginsLoader.remove(plugin.pluginName)
|
||||
pluginsSequence.remove(plugin)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据插件名字找Jar的文件
|
||||
* null => 没找到
|
||||
* 这里的url的jarFile没关,热更新插件可能出事
|
||||
*/
|
||||
internal fun getJarFileByName(pluginName: String): File? {
|
||||
File(pluginsPath).listFiles()?.forEach { file ->
|
||||
if (file != null && file.extension == "jar") {
|
||||
val jar = JarFile(file)
|
||||
val pluginYml =
|
||||
jar.entries().asSequence().filter { it.name.toLowerCase().contains("plugin.yml") }.firstOrNull()
|
||||
if (pluginYml != null) {
|
||||
val description =
|
||||
PluginDescription.readFromContent(
|
||||
URL("jar:file:" + file.absoluteFile + "!/" + pluginYml.name).openConnection().inputStream.use {
|
||||
it.readBytes().encodeToString()
|
||||
}, file
|
||||
)
|
||||
if (description.name.toLowerCase() == pluginName.toLowerCase()) {
|
||||
return file
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据插件名字找Jar中的文件
|
||||
* null => 没找到
|
||||
* 这里的url的jarFile没关,热更新插件可能出事
|
||||
*/
|
||||
internal fun getFileInJarByName(pluginName: String, toFind: String): InputStream? {
|
||||
val jarFile = getJarFileByName(pluginName) ?: return null
|
||||
val jar = JarFile(jarFile)
|
||||
val toFindFile =
|
||||
jar.entries().asSequence().filter { it.name == toFind }.firstOrNull() ?: return null
|
||||
return URL("jar:file:" + jarFile.absoluteFile + "!/" + toFindFile.name).openConnection().inputStream
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2020 Mamoe Technologies and contributors.
|
||||
*
|
||||
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
||||
*
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.console.plugins
|
||||
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import net.mamoe.mirai.console.MiraiConsole
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalAPI
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* 插件信息
|
||||
*/
|
||||
interface PluginDescription {
|
||||
val name: String
|
||||
val author: String
|
||||
val version: String
|
||||
val info: String
|
||||
val depends: List<String>
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件基类.
|
||||
*
|
||||
* 内建的插件类型:
|
||||
* - [JarPlugin]
|
||||
*/
|
||||
abstract class Plugin : CoroutineScope {
|
||||
abstract val description: PluginDescription
|
||||
abstract val loader: PluginLoader<*>
|
||||
|
||||
@OptIn(MiraiExperimentalAPI::class)
|
||||
val logger: MiraiLogger by lazy { MiraiConsole.newLogger(description.name) }
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = SupervisorJob(MiraiConsole.coroutineContext[Job]) + CoroutineExceptionHandler { _, throwable ->
|
||||
logger.error(throwable)
|
||||
}
|
||||
|
||||
open fun onLoaded() {}
|
||||
open fun onDisabled() {}
|
||||
open fun onEnabled() {}
|
||||
}
|
@ -13,7 +13,6 @@ package net.mamoe.mirai.console.plugins
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import net.mamoe.mirai.console.command.Command
|
||||
import net.mamoe.mirai.console.command.CommandOwner
|
||||
import net.mamoe.mirai.console.command.CommandSender
|
||||
import net.mamoe.mirai.console.events.EventListener
|
||||
import net.mamoe.mirai.console.scheduler.PluginScheduler
|
||||
@ -27,7 +26,7 @@ import kotlin.coroutines.EmptyCoroutineContext
|
||||
* 所有插件的基类
|
||||
*/
|
||||
abstract class PluginBase
|
||||
@JvmOverloads constructor(coroutineContext: CoroutineContext = EmptyCoroutineContext) : CoroutineScope, CommandOwner {
|
||||
@JvmOverloads constructor(coroutineContext: CoroutineContext = EmptyCoroutineContext) : CoroutineScope {
|
||||
final override val coroutineContext: CoroutineContext = coroutineContext + SupervisorJob()
|
||||
|
||||
/**
|
||||
@ -166,64 +165,3 @@ abstract class PluginBase
|
||||
|
||||
internal var pluginName: String = ""
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件描述
|
||||
* @see PluginBase.description
|
||||
*/
|
||||
class PluginDescription(
|
||||
val file: File,
|
||||
val name: String,
|
||||
val author: String,
|
||||
val basePath: String,
|
||||
val version: String,
|
||||
val info: String,
|
||||
val depends: List<String>,//插件的依赖
|
||||
internal var loaded: Boolean = false,
|
||||
internal var noCircularDepend: Boolean = true
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "name: $name\nauthor: $author\npath: $basePath\nver: $version\ninfo: $info\ndepends: $depends"
|
||||
}
|
||||
|
||||
companion object {
|
||||
@OptIn(ToBeRemoved::class)
|
||||
fun readFromContent(content_: String, file: File): PluginDescription {
|
||||
with(Config.load(content_, "yml")) {
|
||||
try {
|
||||
return PluginDescription(
|
||||
file = file,
|
||||
name = this.getString("name"),
|
||||
author = kotlin.runCatching {
|
||||
this.getString("author")
|
||||
}.getOrElse {
|
||||
"unknown"
|
||||
},
|
||||
basePath = kotlin.runCatching {
|
||||
this.getString("path")
|
||||
}.getOrElse {
|
||||
this.getString("main")
|
||||
},
|
||||
version = kotlin.runCatching {
|
||||
this.getString("version")
|
||||
}.getOrElse {
|
||||
"unknown"
|
||||
},
|
||||
info = kotlin.runCatching {
|
||||
this.getString("info")
|
||||
}.getOrElse {
|
||||
"unknown"
|
||||
},
|
||||
depends = kotlin.runCatching {
|
||||
this.getStringList("depends")
|
||||
}.getOrElse {
|
||||
listOf()
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
error("Failed to read Plugin.YML")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
package net.mamoe.mirai.console.plugins
|
||||
|
||||
/**
|
||||
* 插件加载器
|
||||
*
|
||||
* @see JarPluginLoader 内建的 Jar (JVM) 插件加载器.
|
||||
*/
|
||||
interface PluginLoader<P : Plugin> {
|
||||
val list: List<P>
|
||||
|
||||
fun loadAll() = list.forEach(::load)
|
||||
fun enableAll() = list.forEach(::enable)
|
||||
fun unloadAll() = list.forEach(::unload)
|
||||
fun reloadAll() = list.forEach(::reload)
|
||||
|
||||
val isUnloadSupported: Boolean
|
||||
get() = false
|
||||
|
||||
fun load(plugin: P)
|
||||
fun enable(plugin: P)
|
||||
fun unload(plugin: P) {
|
||||
error("NotImplemented")
|
||||
}
|
||||
|
||||
fun reload(plugin: P) {
|
||||
unload(plugin)
|
||||
load(plugin)
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package net.mamoe.mirai.console.plugins
|
||||
|
||||
|
||||
object PluginManager {
|
||||
private val _loaders: MutableSet<PluginLoader<*>> = mutableSetOf()
|
||||
|
||||
val loaders: Set<PluginLoader<*>> get() = _loaders
|
||||
|
||||
fun registerPluginLoader(loader: PluginLoader<*>) {
|
||||
_loaders.add(loader)
|
||||
}
|
||||
|
||||
fun unregisterPluginLoader(loader: PluginLoader<*>) {
|
||||
_loaders.remove(loader)
|
||||
}
|
||||
|
||||
fun loadPlugins() {
|
||||
loaders.forEach(PluginLoader<*>::loadAll)
|
||||
}
|
||||
|
||||
fun enablePlugins() {
|
||||
loaders.forEach(PluginLoader<*>::enableAll)
|
||||
}
|
||||
}
|
@ -11,390 +11,5 @@
|
||||
|
||||
package net.mamoe.mirai.console.plugins
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import net.mamoe.mirai.console.MiraiConsole
|
||||
import net.mamoe.mirai.console.command.Command
|
||||
import net.mamoe.mirai.console.command.CommandSender
|
||||
import net.mamoe.mirai.console.command.description
|
||||
import net.mamoe.mirai.console.command.unregisterAllCommands
|
||||
import net.mamoe.mirai.console.encodeToString
|
||||
import net.mamoe.mirai.utils.LockFreeLinkedList
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.net.JarURLConnection
|
||||
import java.net.URL
|
||||
import java.util.jar.JarFile
|
||||
|
||||
val PluginBase.description: PluginDescription get() = TODO()
|
||||
|
||||
object PluginManagerOld {
|
||||
/**
|
||||
* 通过插件获取介绍
|
||||
* @see description
|
||||
*/
|
||||
fun getPluginDescription(base: PluginBase): PluginDescription {
|
||||
nameToPluginBaseMap.forEach { (s, pluginBase) ->
|
||||
if (pluginBase == base) {
|
||||
return pluginDescriptions[s]!!
|
||||
}
|
||||
}
|
||||
error("can not find plugin description")
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有插件摘要
|
||||
*/
|
||||
fun getAllPluginDescriptions(): Collection<PluginDescription> {
|
||||
return pluginDescriptions.values
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有插件
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun disablePlugins(throwable: CancellationException? = null) {
|
||||
pluginsSequence.forEach { plugin ->
|
||||
plugin.unregisterAllCommands()
|
||||
plugin.disable(throwable)
|
||||
}
|
||||
nameToPluginBaseMap.clear()
|
||||
pluginDescriptions.clear()
|
||||
pluginsLoader.clear()
|
||||
pluginsSequence.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 重载所有插件
|
||||
*/
|
||||
fun reloadPlugins() {
|
||||
pluginsSequence.forEach {
|
||||
it.disable()
|
||||
}
|
||||
loadPlugins(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试加载全部插件
|
||||
*/
|
||||
fun loadPlugins(clear: Boolean = true) = loadPluginsImpl(clear)
|
||||
|
||||
|
||||
//////////////////
|
||||
//// internal ////
|
||||
//////////////////
|
||||
|
||||
internal val pluginsPath = (MiraiConsole.path + "/plugins/").replace("//", "/").also {
|
||||
File(it).mkdirs()
|
||||
}
|
||||
|
||||
private val logger = MiraiConsole.newLogger("Plugin Manager")
|
||||
|
||||
/**
|
||||
* 加载成功的插件, 名字->插件
|
||||
*/
|
||||
internal val nameToPluginBaseMap: MutableMap<String, PluginBase> = mutableMapOf()
|
||||
|
||||
/**
|
||||
* 加载成功的插件, 名字->插件摘要
|
||||
*/
|
||||
private val pluginDescriptions: MutableMap<String, PluginDescription> = mutableMapOf()
|
||||
|
||||
/**
|
||||
* 加载插件的PluginsLoader
|
||||
*/
|
||||
private val pluginsLoader: PluginsLoader = PluginsLoader(this.javaClass.classLoader)
|
||||
|
||||
/**
|
||||
* 插件优先级队列
|
||||
* 任何操作应该按这个Sequence顺序进行
|
||||
* 他的优先级取决于依赖,
|
||||
* 在这个队列中, 被依赖的插件会在依赖的插件之前
|
||||
*/
|
||||
private val pluginsSequence: LockFreeLinkedList<PluginBase> = LockFreeLinkedList()
|
||||
|
||||
|
||||
/**
|
||||
* 广播Command方法
|
||||
*/
|
||||
internal fun onCommand(command: Command, sender: CommandSender, args: List<String>) {
|
||||
pluginsSequence.forEach {
|
||||
try {
|
||||
it.onCommand(command, sender, args)
|
||||
} catch (e: Throwable) {
|
||||
logger.info(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Volatile
|
||||
internal var lastPluginName: String = ""
|
||||
|
||||
/**
|
||||
* 判断文件名/插件名是否已加载
|
||||
*/
|
||||
private fun isPluginLoaded(file: File, name: String): Boolean {
|
||||
pluginDescriptions.forEach {
|
||||
if (it.key == name || it.value.file == file) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 寻找所有安装的插件(在文件夹), 并将它读取, 记录位置
|
||||
* 这个不等同于加载的插件, 可以理解为还没有加载的插件
|
||||
*/
|
||||
internal data class FindPluginsResult(
|
||||
val pluginsLocation: MutableMap<String, File>,
|
||||
val pluginsFound: MutableMap<String, PluginDescription>
|
||||
)
|
||||
|
||||
internal fun findPlugins(): FindPluginsResult {
|
||||
val pluginsLocation: MutableMap<String, File> = mutableMapOf()
|
||||
val pluginsFound: MutableMap<String, PluginDescription> = mutableMapOf()
|
||||
|
||||
File(pluginsPath).listFiles()?.forEach { file ->
|
||||
if (file != null && file.extension == "jar") {
|
||||
val jar = JarFile(file)
|
||||
val pluginYml =
|
||||
jar.entries().asSequence().filter { it.name.toLowerCase().contains("plugin.yml") }.firstOrNull()
|
||||
|
||||
if (pluginYml == null) {
|
||||
logger.info("plugin.yml not found in jar " + jar.name + ", it will not be consider as a Plugin")
|
||||
} else {
|
||||
try {
|
||||
val description = PluginDescription.readFromContent(
|
||||
URL("jar:file:" + file.absoluteFile + "!/" + pluginYml.name).openConnection().let {
|
||||
val res = it.inputStream.use { input ->
|
||||
input.readBytes().encodeToString()
|
||||
}
|
||||
|
||||
// 关闭jarFile,解决热更新插件问题
|
||||
(it as JarURLConnection).jarFile.close()
|
||||
res
|
||||
}, file
|
||||
)
|
||||
if (!isPluginLoaded(file, description.name)) {
|
||||
pluginsFound[description.name] = description
|
||||
pluginsLocation[description.name] = file
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.info(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return FindPluginsResult(pluginsLocation, pluginsFound)
|
||||
}
|
||||
|
||||
internal fun loadPluginsImpl(clear: Boolean = true) {
|
||||
logger.info("""开始加载${pluginsPath}下的插件""")
|
||||
val findPluginsResult = findPlugins()
|
||||
val pluginsFound = findPluginsResult.pluginsFound
|
||||
val pluginsLocation = findPluginsResult.pluginsLocation
|
||||
|
||||
//不仅要解决A->B->C->A, 还要解决A->B->A->A
|
||||
fun checkNoCircularDepends(
|
||||
target: PluginDescription,
|
||||
needDepends: List<String>,
|
||||
existDepends: MutableList<String>
|
||||
) {
|
||||
|
||||
if (!target.noCircularDepend) {
|
||||
return
|
||||
}
|
||||
|
||||
existDepends.add(target.name)
|
||||
|
||||
if (needDepends.any { existDepends.contains(it) }) {
|
||||
target.noCircularDepend = false
|
||||
}
|
||||
|
||||
existDepends.addAll(needDepends)
|
||||
|
||||
needDepends.forEach {
|
||||
if (pluginsFound.containsKey(it)) {
|
||||
checkNoCircularDepends(pluginsFound[it]!!, pluginsFound[it]!!.depends, existDepends)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pluginsFound.values.forEach {
|
||||
checkNoCircularDepends(it, it.depends, mutableListOf())
|
||||
}
|
||||
|
||||
//load plugin individually
|
||||
fun loadPlugin(description: PluginDescription): Boolean {
|
||||
if (!description.noCircularDepend) {
|
||||
logger.error("Failed to load plugin " + description.name + " because it has circular dependency")
|
||||
return false
|
||||
}
|
||||
|
||||
if (description.loaded || nameToPluginBaseMap.containsKey(description.name)) {
|
||||
return true
|
||||
}
|
||||
|
||||
description.depends.forEach { dependent ->
|
||||
if (!pluginsFound.containsKey(dependent)) {
|
||||
logger.error("Failed to load plugin " + description.name + " because it need " + dependent + " as dependency")
|
||||
return false
|
||||
}
|
||||
val depend = pluginsFound[dependent]!!
|
||||
|
||||
if (!loadPlugin(depend)) {//先加载depend
|
||||
logger.error("Failed to load plugin " + description.name + " because " + dependent + " as dependency failed to load")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("loading plugin " + description.name)
|
||||
|
||||
val jarFile = pluginsLocation[description.name]!!
|
||||
val pluginClass = try {
|
||||
pluginsLoader.loadPluginMainClassByJarFile(description.name, description.basePath, jarFile)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
pluginsLoader.loadPluginMainClassByJarFile(description.name, "${description.basePath}Kt", jarFile)
|
||||
}
|
||||
|
||||
val subClass = pluginClass.asSubclass(PluginBase::class.java)
|
||||
|
||||
lastPluginName = description.name
|
||||
val plugin: PluginBase =
|
||||
subClass.kotlin.objectInstance ?: subClass.getDeclaredConstructor().apply {
|
||||
kotlin.runCatching {
|
||||
this.isAccessible = true
|
||||
}
|
||||
}.newInstance()
|
||||
plugin.dataFolder // initialize right now
|
||||
|
||||
description.loaded = true
|
||||
logger.info("successfully loaded plugin " + description.name + " version " + description.version + " by " + description.author)
|
||||
logger.info(description.info)
|
||||
|
||||
nameToPluginBaseMap[description.name] = plugin
|
||||
pluginDescriptions[description.name] = description
|
||||
plugin.pluginName = description.name
|
||||
pluginsSequence.addLast(plugin)//按照实际加载顺序加入队列
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
if (clear) {
|
||||
//清掉优先级队列, 来重新填充
|
||||
pluginsSequence.clear()
|
||||
}
|
||||
|
||||
pluginsFound.values.forEach {
|
||||
try {
|
||||
// 尝试加载插件
|
||||
loadPlugin(it)
|
||||
} catch (e: Throwable) {
|
||||
pluginsLoader.remove(it.name)
|
||||
when (e) {
|
||||
is ClassCastException -> logger.error(
|
||||
"failed to load plugin " + it.name + " , Main class does not extends PluginBase",
|
||||
e
|
||||
)
|
||||
is ClassNotFoundException -> logger.error(
|
||||
"failed to load plugin " + it.name + " , Main class not found under " + it.basePath,
|
||||
e
|
||||
)
|
||||
is NoClassDefFoundError -> logger.error(
|
||||
"failed to load plugin " + it.name + " , dependent class not found.",
|
||||
e
|
||||
)
|
||||
else -> logger.error("failed to load plugin " + it.name, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pluginsSequence.forEach {
|
||||
try {
|
||||
it.load()
|
||||
} catch (ignored: Throwable) {
|
||||
logger.info(ignored)
|
||||
logger.info(it.pluginName + " failed to load, disabling it")
|
||||
logger.info(it.pluginName + " 推荐立即删除/替换并重启")
|
||||
if (ignored is CancellationException) {
|
||||
disablePlugin(it, ignored)
|
||||
} else {
|
||||
disablePlugin(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pluginsSequence.forEach {
|
||||
try {
|
||||
it.enable()
|
||||
} catch (ignored: Throwable) {
|
||||
logger.info(ignored)
|
||||
logger.info(it.pluginName + " failed to enable, disabling it")
|
||||
logger.info(it.pluginName + " 推荐立即删除/替换并重启")
|
||||
if (ignored is CancellationException) {
|
||||
disablePlugin(it, ignored)
|
||||
} else {
|
||||
disablePlugin(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("""加载了${nameToPluginBaseMap.size}个插件""")
|
||||
}
|
||||
|
||||
private fun disablePlugin(
|
||||
plugin: PluginBase,
|
||||
exception: CancellationException? = null
|
||||
) {
|
||||
plugin.unregisterAllCommands()
|
||||
plugin.disable(exception)
|
||||
nameToPluginBaseMap.remove(plugin.pluginName)
|
||||
pluginDescriptions.remove(plugin.pluginName)
|
||||
pluginsLoader.remove(plugin.pluginName)
|
||||
pluginsSequence.remove(plugin)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据插件名字找Jar的文件
|
||||
* null => 没找到
|
||||
* 这里的url的jarFile没关,热更新插件可能出事
|
||||
*/
|
||||
internal fun getJarFileByName(pluginName: String): File? {
|
||||
File(pluginsPath).listFiles()?.forEach { file ->
|
||||
if (file != null && file.extension == "jar") {
|
||||
val jar = JarFile(file)
|
||||
val pluginYml =
|
||||
jar.entries().asSequence().filter { it.name.toLowerCase().contains("plugin.yml") }.firstOrNull()
|
||||
if (pluginYml != null) {
|
||||
val description =
|
||||
PluginDescription.readFromContent(
|
||||
URL("jar:file:" + file.absoluteFile + "!/" + pluginYml.name).openConnection().inputStream.use {
|
||||
it.readBytes().encodeToString()
|
||||
}, file
|
||||
)
|
||||
if (description.name.toLowerCase() == pluginName.toLowerCase()) {
|
||||
return file
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据插件名字找Jar中的文件
|
||||
* null => 没找到
|
||||
* 这里的url的jarFile没关,热更新插件可能出事
|
||||
*/
|
||||
internal fun getFileInJarByName(pluginName: String, toFind: String): InputStream? {
|
||||
val jarFile = getJarFileByName(pluginName) ?: return null
|
||||
val jar = JarFile(jarFile)
|
||||
val toFindFile =
|
||||
jar.entries().asSequence().filter { it.name == toFind }.firstOrNull() ?: return null
|
||||
return URL("jar:file:" + jarFile.absoluteFile + "!/" + toFindFile.name).openConnection().inputStream
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user