Support plugin reloading

This commit is contained in:
Him188 2020-05-23 19:11:47 +08:00
parent c3120cf1ac
commit 633e333609
2 changed files with 49 additions and 385 deletions

View File

@ -1,9 +1,6 @@
package net.mamoe.mirai.console.plugins.builtin
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.*
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.plugins.AbstractFilePluginLoader
import net.mamoe.mirai.console.plugins.PluginLoadException
@ -16,8 +13,7 @@ import kotlin.reflect.full.createInstance
/**
* 内建的 Jar (JVM) 插件加载器
*/
object JarPluginLoader : AbstractFilePluginLoader<JvmPlugin, JvmPluginDescription>("jar"),
CoroutineScope {
object JarPluginLoader : AbstractFilePluginLoader<JvmPlugin, JvmPluginDescription>("jar"), CoroutineScope {
private val logger: MiraiLogger by lazy {
MiraiConsole.newLogger(JarPluginLoader::class.simpleName!!)
}
@ -29,9 +25,15 @@ object JarPluginLoader : AbstractFilePluginLoader<JvmPlugin, JvmPluginDescriptio
logger.error("Unhandled Jar plugin exception: ${throwable.message}", throwable)
}
}
private val supervisor: Job = coroutineContext[Job]!!
private val classLoader: PluginsLoader =
PluginsLoader(this.javaClass.classLoader)
private val classLoader: PluginsLoader = PluginsLoader(this.javaClass.classLoader)
init {
supervisor.invokeOnCompletion {
classLoader.clear()
}
}
override fun getPluginDescription(plugin: JvmPlugin): JvmPluginDescription = plugin.description
@ -48,6 +50,7 @@ object JarPluginLoader : AbstractFilePluginLoader<JvmPlugin, JvmPluginDescriptio
@Throws(PluginLoadException::class)
override fun load(description: JvmPluginDescription): JvmPlugin = description.runCatching {
ensureActive()
val main = classLoader.loadPluginMainClassByJarFile(name, mainClassName, file).kotlin.run {
objectInstance
?: kotlin.runCatching { createInstance() }.getOrNull()
@ -61,16 +64,9 @@ object JarPluginLoader : AbstractFilePluginLoader<JvmPlugin, JvmPluginDescriptio
if (main is JvmPluginImpl) {
main._description = description
}
TODO(
"""
FIND PLUGIN MAIN, THEN LOAD
SET JvmPluginImpl._description
SET JvmPluginImpl._intrinsicCoroutineContext
""".trimIndent()
)
// no need to check dependencies
main.internalOnLoad()
} else main.onLoad()
main
}.getOrElse {
throw PluginLoadException(
"Exception while loading ${description.name}",
@ -78,6 +74,16 @@ object JarPluginLoader : AbstractFilePluginLoader<JvmPlugin, JvmPluginDescriptio
)
}
override fun enable(plugin: JvmPlugin) = plugin.onEnable()
override fun disable(plugin: JvmPlugin) = plugin.onDisable()
override fun enable(plugin: JvmPlugin) {
ensureActive()
if (plugin is JvmPluginImpl) {
plugin.internalOnEnable()
} else plugin.onEnable()
}
override fun disable(plugin: JvmPlugin) {
if (plugin is JvmPluginImpl) {
plugin.internalOnDisable()
} else plugin.onDisable()
}
}

View File

@ -11,6 +11,7 @@
package net.mamoe.mirai.console.plugins.builtin
import kotlinx.atomicfu.locks.withLock
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@ -20,6 +21,7 @@ import net.mamoe.mirai.console.plugins.Plugin
import net.mamoe.mirai.console.plugins.PluginLoader
import net.mamoe.mirai.console.utils.JavaPluginScheduler
import net.mamoe.mirai.utils.MiraiLogger
import java.util.concurrent.locks.ReentrantLock
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@ -84,388 +86,44 @@ internal abstract class JvmPluginImpl(
// for future use
@Suppress("PropertyName")
@JvmField
internal var _intrinsicCoroutineContext: CoroutineContext = EmptyCoroutineContext
override val description: JvmPluginDescription get() = _description
final override val logger: MiraiLogger by lazy { MiraiConsole.newLogger(this._description.name) }
final override val coroutineContext: CoroutineContext by lazy {
@JvmField
internal val coroutineContextInitializer = {
CoroutineExceptionHandler { _, throwable -> logger.error(throwable) }
.plus(parentCoroutineContext)
.plus(SupervisorJob(parentCoroutineContext[Job])) + _intrinsicCoroutineContext
}
}
/*
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")
private var firstRun = true
internal fun internalOnDisable() {
firstRun = false
this.onDisable()
}
/**
* 获取所有插件摘要
*/
fun getAllPluginDescriptions(): Collection<PluginDescription> {
return pluginDescriptions.values
internal fun internalOnLoad() {
this.onLoad()
}
/**
* 关闭所有插件
*/
@JvmOverloads
fun disablePlugins(throwable: CancellationException? = null) {
pluginsSequence.forEach { plugin ->
plugin.unregisterAllCommands()
plugin.disable(throwable)
}
nameToPluginBaseMap.clear()
pluginDescriptions.clear()
pluginsLoader.clear()
pluginsSequence.clear()
internal fun internalOnEnable() {
if (!firstRun) refreshCoroutineContext()
this.onEnable()
}
/**
* 重载所有插件
*/
fun reloadPlugins() {
pluginsSequence.forEach {
it.disable()
}
loadPlugins(false)
private fun refreshCoroutineContext(): CoroutineContext {
return coroutineContextInitializer().also { _coroutineContext = it }
}
/**
* 尝试加载全部插件
*/
fun loadPlugins(clear: Boolean = true) = loadPluginsImpl(clear)
private val contextUpdateLock: ReentrantLock = ReentrantLock()
private var _coroutineContext: CoroutineContext? = null
final override val coroutineContext: CoroutineContext
get() = _coroutineContext
?: contextUpdateLock.withLock { _coroutineContext ?: refreshCoroutineContext() }
//////////////////
//// 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
}
}*/
}