diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt index 4de54b1fb..a69a4eb6a 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt @@ -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 /** 由前端调用 */ 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 40fa99ca5..4d15ef759 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 @@ -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 -interface CommandOwner +sealed class CommandOwner + +abstract class PluginCommandOwner(plugin: PluginBase) : CommandOwner() + +// 由前端实现 +internal abstract class ConsoleCommandOwner : CommandOwner() val CommandOwner.registeredCommands: List get() = InternalCommandManager.registeredCommands.filter { it.owner == this } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/JarPlugin.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/JarPlugin.kt new file mode 100644 index 000000000..263e294ce --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/JarPlugin.kt @@ -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 +) : PluginDescription + +abstract class JavaPlugin : JarPlugin() + +abstract class KotlinPlugin : JarPlugin() + + +/** + * 内建的 Jar (JVM) 插件加载器 + */ +object JarPluginLoader : PluginLoader { + override val list: List + 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 { + 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 = mutableMapOf() + + /** + * 加载成功的插件, 名字->插件摘要 + */ + private val pluginDescriptions: MutableMap = mutableMapOf() + + /** + * 加载插件的PluginsLoader + */ + private val pluginsLoader: PluginsLoader = PluginsLoader(this.javaClass.classLoader) + + /** + * 插件优先级队列 + * 任何操作应该按这个Sequence顺序进行 + * 他的优先级取决于依赖, + * 在这个队列中, 被依赖的插件会在依赖的插件之前 + */ + private val pluginsSequence: LockFreeLinkedList = LockFreeLinkedList() + + + /** + * 广播Command方法 + */ + internal fun onCommand(command: Command, sender: CommandSender, args: List) { + 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, + val pluginsFound: MutableMap + ) + + internal fun findPlugins(): FindPluginsResult { + val pluginsLocation: MutableMap = mutableMapOf() + val pluginsFound: MutableMap = 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, + existDepends: MutableList + ) { + + 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 + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/Plugin.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/Plugin.kt new file mode 100644 index 000000000..f967bff79 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/Plugin.kt @@ -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 +} + +/** + * 插件基类. + * + * 内建的插件类型: + * - [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() {} +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginBase.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginBase.kt index bfc59ab3d..f97eb092b 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginBase.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginBase.kt @@ -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,//插件的依赖 - 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") - } - } - } - } -} diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginLoader.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginLoader.kt new file mode 100644 index 000000000..144b9b21d --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginLoader.kt @@ -0,0 +1,29 @@ +package net.mamoe.mirai.console.plugins + +/** + * 插件加载器 + * + * @see JarPluginLoader 内建的 Jar (JVM) 插件加载器. + */ +interface PluginLoader

{ + val list: List

+ + 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) + } +} diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginManager.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginManager.kt new file mode 100644 index 000000000..5b01f905c --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginManager.kt @@ -0,0 +1,24 @@ +package net.mamoe.mirai.console.plugins + + +object PluginManager { + private val _loaders: MutableSet> = mutableSetOf() + + val loaders: Set> 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) + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginManagerOld.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginManagerOld.kt index a8c79d636..ade4a15c6 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginManagerOld.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginManagerOld.kt @@ -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 { - 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 = mutableMapOf() - - /** - * 加载成功的插件, 名字->插件摘要 - */ - private val pluginDescriptions: MutableMap = mutableMapOf() - - /** - * 加载插件的PluginsLoader - */ - private val pluginsLoader: PluginsLoader = PluginsLoader(this.javaClass.classLoader) - - /** - * 插件优先级队列 - * 任何操作应该按这个Sequence顺序进行 - * 他的优先级取决于依赖, - * 在这个队列中, 被依赖的插件会在依赖的插件之前 - */ - private val pluginsSequence: LockFreeLinkedList = LockFreeLinkedList() - - - /** - * 广播Command方法 - */ - internal fun onCommand(command: Command, sender: CommandSender, args: List) { - 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, - val pluginsFound: MutableMap - ) - - internal fun findPlugins(): FindPluginsResult { - val pluginsLocation: MutableMap = mutableMapOf() - val pluginsFound: MutableMap = 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, - existDepends: MutableList - ) { - - 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 - } -} \ No newline at end of file