Merge pull request #155 from Karlatemp/dependencies

Plugin dependencies loading.
This commit is contained in:
Him188 2020-10-27 19:42:46 +08:00 committed by GitHub
commit 75be7fec02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 349 additions and 5 deletions

View File

@ -23,7 +23,6 @@ import net.mamoe.mirai.console.plugin.loader.PluginLoadException
import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScope
import net.mamoe.mirai.utils.MiraiLogger
import java.io.File
import java.net.URLClassLoader
import java.util.concurrent.ConcurrentHashMap
internal object BuiltInJvmPluginLoaderImpl :
@ -52,8 +51,22 @@ internal object BuiltInJvmPluginLoaderImpl :
override fun Sequence<File>.extractPlugins(): List<JvmPlugin> {
ensureActive()
fun Sequence<Map.Entry<File, ClassLoader>>.findAllInstances(): Sequence<Map.Entry<File, JvmPlugin>> {
return map { (f, pluginClassLoader) ->
fun Sequence<Map.Entry<File, JvmPluginClassLoader>>.findAllInstances(): Sequence<Map.Entry<File, JvmPlugin>> {
return onEach { (_, pluginClassLoader) ->
val exportManagers = pluginClassLoader.findServices(
ExportManager::class
).loadAllServices()
if (exportManagers.isEmpty()) {
val rules = pluginClassLoader.getResourceAsStream("export-rules.txt")
if (rules == null)
pluginClassLoader.declaredFilter = StandardExportManagers.AllExported
else rules.bufferedReader(Charsets.UTF_8).useLines {
pluginClassLoader.declaredFilter = ExportManagerImpl.parse(it.iterator())
}
} else {
pluginClassLoader.declaredFilter = exportManagers[0]
}
}.map { (f, pluginClassLoader) ->
f to pluginClassLoader.findServices(
JvmPlugin::class,
KotlinPlugin::class,
@ -61,6 +74,7 @@ internal object BuiltInJvmPluginLoaderImpl :
JavaPlugin::class
).loadAllServices()
}.flatMap { (f, list) ->
list.associateBy { f }.asSequence()
}
}
@ -68,7 +82,7 @@ internal object BuiltInJvmPluginLoaderImpl :
val filePlugins = this.filterNot {
pluginFileToInstanceMap.containsKey(it)
}.associateWith {
JvmPluginClassLoader(it, MiraiConsole::class.java.classLoader)
JvmPluginClassLoader(it, MiraiConsole::class.java.classLoader, classLoaders)
}.onEach { (_, classLoader) ->
classLoaders.add(classLoader)
}.asSequence().findAllInstances().onEach {

View File

@ -0,0 +1,57 @@
/*
* Copyright (c) 2018-2020 Karlatemp. All rights reserved.
* @author Karlatemp <karlatemp@vip.qq.com> <https://github.com/Karlatemp>
*
* LuckPerms-Mirai/mirai-console.mirai-console.main/ExportManagerImpl.kt
*
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
*
* https://github.com/Karlatemp/LuckPerms-Mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.console.internal.plugin
import net.mamoe.mirai.console.plugin.jvm.ExportManager
internal class ExportManagerImpl(
private val rules: List<(String) -> Boolean?>
) : ExportManager {
override fun isExported(className: String): Boolean {
rules.forEach {
val result = it(className)
if (result != null) return@isExported result
}
return true
}
companion object {
@JvmStatic
fun parse(lines: Iterator<String>): ExportManagerImpl {
fun Boolean.without(value: Boolean) = if (this == value) null else this
val rules = ArrayList<(String) -> Boolean?>()
lines.asSequence().map { it.trim() }.filter { it.isNotBlank() }.filterNot {
it[0] == '#'
}.forEach { line ->
val command = line.substringBefore(' ')
val argument = line.substringAfter(' ', missingDelimiterValue = "").trim()
val argumentPackage = "$argument."
when (command) {
"exports" -> rules.add {
(it == argument || it.startsWith(argumentPackage)).without(false)
}
"protects" -> rules.add {
if (it == argument || it.startsWith(argumentPackage))
false
else null
}
"export-all", "export-plugin", "export-system" -> rules.add { true }
"protect-all", "protect-plugin", "protect-system" -> rules.add { false }
}
}
return ExportManagerImpl(rules)
}
}
}

View File

@ -7,21 +7,120 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*
*/
@file:Suppress("MemberVisibilityCanBePrivate")
package net.mamoe.mirai.console.internal.plugin
import net.mamoe.mirai.console.plugin.jvm.ExportManager
import java.io.File
import java.net.URL
import java.net.URLClassLoader
import java.util.*
import java.util.concurrent.ConcurrentHashMap
internal class JvmPluginClassLoader(
file: File,
val file: File,
parent: ClassLoader?,
val classLoaders: Collection<JvmPluginClassLoader>,
) : URLClassLoader(arrayOf(file.toURI().toURL()), parent) {
//// 只允许插件 getResource 时获取插件自身资源, #205
override fun getResources(name: String?): Enumeration<URL> = findResources(name)
override fun getResource(name: String?): URL? = findResource(name)
// getResourceAsStream 在 URLClassLoader 中通过 getResource 确定资源
// 因此无需 override getResourceAsStream
override fun toString(): String {
return "JvmPluginClassLoader{source=$file}"
}
private val cache = ConcurrentHashMap<String, Class<*>>()
internal var declaredFilter: ExportManager? = null
companion object {
val loadingLock = ConcurrentHashMap<String, Any>()
init {
ClassLoader.registerAsParallelCapable()
}
}
override fun findClass(name: String): Class<*> {
synchronized(kotlin.run {
val lock = Any()
loadingLock.putIfAbsent(name, lock) ?: lock
}) {
return findClass(name, false) ?: throw ClassNotFoundException(name)
}
}
internal fun findClass(name: String, disableGlobal: Boolean): Class<*>? {
// First. Try direct load in cache.
val cachedClass = cache[name]
if (cachedClass != null) {
if (disableGlobal) {
val filter = declaredFilter
if (filter != null && !filter.isExported(name)) {
throw LoadingDeniedException(name)
}
}
return cachedClass
}
if (disableGlobal) {
// ==== Process Loading Request From JvmPluginClassLoader ====
//
// If load from other classloader,
// means no other loaders are cached.
// direct load
return kotlin.runCatching {
super.findClass(name).also { cache[name] = it }
}.getOrElse {
if (it is ClassNotFoundException) null
else throw it
}?.also {
// This request is from other classloader,
// so we need to check the class is exported or not.
val filter = declaredFilter
if (filter != null && !filter.isExported(name)) {
throw LoadingDeniedException(name)
}
}
}
// ==== Process Loading Request From JDK ClassLoading System ====
// First. scan other classLoaders's caches
classLoaders.forEach { otherClassloader ->
if (otherClassloader === this) return@forEach
val filter = otherClassloader.declaredFilter
if (otherClassloader.cache.containsKey(name)) {
return if (filter == null || filter.isExported(name)) {
otherClassloader.cache[name]
} else throw LoadingDeniedException("$name was not exported by $otherClassloader")
}
}
// If no cache...
return kotlin.runCatching {
// Try load this class direct....
super.findClass(name).also { cache[name] = it }
}.getOrElse { exception ->
if (exception is ClassNotFoundException) {
// Cannot load the class from this, try others.
classLoaders.forEach { otherClassloader ->
if (otherClassloader === this) return@forEach
val other = kotlin.runCatching {
otherClassloader.findClass(name, true)
}.onFailure { err ->
if (err is LoadingDeniedException || err !is ClassNotFoundException)
throw err
}.getOrNull()
if (other != null) return other
}
}
// Great, nobody known what is the class.
throw exception
}
}
}
internal class LoadingDeniedException(name: String) : ClassNotFoundException(name)

View File

@ -0,0 +1,110 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.console.plugin.jvm
import net.mamoe.mirai.console.internal.plugin.ExportManagerImpl
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
/**
* 插件的类导出管理器
*
*
* 允许插件将一些内部实现保护起来 避免其他插件调用 要启动这个特性
* 只需要创建名为 `export-rules.txt` 的规则文件便可以控制插件的类的公开规则
*
* 如果正在使用 `Gradle` 项目, 该规则文件一般位于 `src/main/resources`
*
* Example:
* ```text
*
* # #开头的行全部识别为注释
*
* # exports, 允许其他插件直接使用某个类
*
* # 导出了一个internal包的一个类
* #
* exports org.example.miraiconsole.myplugin.internal.OpenInternal
*
* # 导出了整个 api
* #
* exports org.example.miraiconsole.myplugin.api
*
* # 保护 org.example.miraiconsole.myplugin.api2.Internal, 不允许其他插件直接使用
* #
* protects org.example.miraiconsole.myplugin.api2.Internal
*
* # 保护整个包
* #
* # 别名: protect-package
* protects org.example.miraiconsole.myplugin.internal
*
* # 此规则不会生效, 因为在此条规则之前,
* # org.example.miraiconsole.myplugin.internal 已经被加入到保护域中
* exports org.example.miraiconsole.myplugin.internal.NotOpenInternal
*
*
* # export-plugin, 允许其他插件使用除了已经被保护的全部类
* # 使用此规则会同时让此规则后的所有规则全部失效
* # 别名: export-all, export-system
* # export-plugin
*
*
* # 将整个插件放入保护域中
* # 除了此规则之前显式 export 的类, 其他插件将不允许直接使用被保护的插件的任何类
* # 别名: protect-all, protect-system
* protect-plugin
*
* ```
*
* 插件也可以通过 Service 来自定义导出控制
*
* Example:
* ```kotlin
* @AutoService(ExportManager::class)
* object MyExportManager: ExportManager {
* override fun isExported(className: String): Boolean {
* println(" <== $className")
* return true
* }
* }
* ```
*
*/
@ConsoleExperimentalApi
public interface ExportManager {
/**
* 如果 [className] 能够通过 [ExportManager] 的规则, 返回 true
*
* @param className [className] 是一个合法的满足 [ClassLoader] 的加载规则 的全限定名.
* [className] 不应该是数组的全限定名或者JVM基本类型的名字.
* See also: [ClassLoader.loadClass]
* [ClassLoader#name](https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html#name)
*/
public fun isExported(className: String): Boolean
}
@ConsoleExperimentalApi
public object StandardExportManagers {
@ConsoleExperimentalApi
public object AllExported : ExportManager {
override fun isExported(className: String): Boolean = true
}
@ConsoleExperimentalApi
public object AllDenied : ExportManager {
override fun isExported(className: String): Boolean = false
}
@ConsoleExperimentalApi
@JvmStatic
public fun parse(lines: Iterator<String>): ExportManager {
return ExportManagerImpl.parse(lines)
}
}

View File

@ -16,6 +16,8 @@
[`PluginConfig`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginConfig.kt
[`PluginDataStorage`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataStorage.kt
[`ExportManager`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/ExportManager.kt
[`MiraiConsole`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt
[`MiraiConsoleImplementation`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleImplementation.kt
<!--[MiraiConsoleFrontEnd]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleFrontEnd.kt-->
@ -160,6 +162,68 @@ public final class JExample extends JavaPlugin {
多个插件的加载是*顺序的*,意味着若一个插件的 `onLoad()` 等回调处理缓慢,后续插件的加载也会被延后,即使它们可能没有依赖关系。
因此请尽量让 `onLoad()``onEnable()``onDisable()`快速返回。
### API 导出管理
允许插件将一些内部实现保护起来, 避免其他插件调用, 要启动这个特性,
只需要创建名为 `export-rules.txt` 的规则文件,便可以控制插件的类的公开规则。
如果正在使用 `Gradle` 项目, 该规则文件一般位于 `src/main/resources`
Example:
```text
# #开头的行全部识别为注释
# exports, 允许其他插件直接使用某个类
# 导出了一个internal包的一个类
#
exports org.example.miraiconsole.myplugin.internal.OpenInternal
# 导出了整个 api 包
#
exports org.example.miraiconsole.myplugin.api
# 保护 org.example.miraiconsole.myplugin.api2.Internal, 不允许其他插件直接使用
#
protects org.example.miraiconsole.myplugin.api2.Internal
# 保护整个包
#
# 别名: protect-package
protects org.example.miraiconsole.myplugin.internal
# 此规则不会生效, 因为在此条规则之前,
# org.example.miraiconsole.myplugin.internal 已经被加入到保护域中
exports org.example.miraiconsole.myplugin.internal.NotOpenInternal
# export-plugin, 允许其他插件使用除了已经被保护的全部类
# 使用此规则会同时让此规则后的所有规则全部失效
# 别名: export-all, export-system
# export-plugin
# 将整个插件放入保护域中
# 除了此规则之前显式 export 的类, 其他插件将不允许直接使用被保护的插件的任何类
# 别名: protect-all, protect-system
protect-plugin
```
插件也可以通过 Service 来自定义导出控制
Example:
```kotlin
@AutoService(ExportManager::class)
object MyExportManager: ExportManager {
override fun isExported(className: String): Boolean {
println(" <== $className")
return true
}
}
```
### 插件生命周期
Mirai Console 不提供热加载和热卸载功能,所有插件只能在服务器启动前加载,在服务器结束时卸载。([为什么不支持热加载和卸载插件?]