1
0
mirror of https://github.com/mamoe/mirai.git synced 2025-04-24 20:43:33 +08:00

[console] Load plugins with plugin.yml

This commit is contained in:
Karlatemp 2023-07-15 19:34:03 +08:00
parent bd3f50f848
commit f14f1a01b6
No known key found for this signature in database
GPG Key ID: BA173CA2B9956C59
19 changed files with 355 additions and 24 deletions

View File

@ -0,0 +1 @@
net.mamoe.consoleit.plugin-with-yml:plugin-library:0.0.0

View File

@ -0,0 +1,17 @@
/*
* Copyright 2019-2023 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/dev/LICENSE
*/
package net.mamoe.console.itest.pluginwithpluginyml.library
public object PluginLibrary {
@JvmStatic
public fun ok() {
println("Plugin with plugin.yml using libraries under clinit ok")
}
}

View File

@ -0,0 +1,10 @@
#
# Copyright 2019-2023 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/dev/LICENSE
#
net.mamoe.console.itest.pluginwithpluginyml.clinit.PluginWithPluginYmlClinitTest

View File

@ -0,0 +1,5 @@
id: net.mamoe.console.itest.plugin-with-yml-can-use-library-while-clinit
version: 0.0.0
dependencies:
- net.mamoe.console.itest.plugin-with-yml

View File

@ -0,0 +1,25 @@
/*
* Copyright 2019-2023 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/dev/LICENSE
*/
package net.mamoe.console.itest.pluginwithpluginyml.clinit
import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
internal class PluginWithPluginYmlClinitTest : KotlinPlugin() {
companion object {
init {
// this is <clinit>
Thread.dumpStack()
Class.forName("net.mamoe.console.itest.pluginwithpluginyml.library.PluginLibrary")
.getMethod("ok").invoke(null)
}
}
}

View File

@ -0,0 +1,10 @@
#
# Copyright 2019-2023 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/dev/LICENSE
#
net.mamoe.console.itest.pluginwithpluginyml.PluginWithPluginYml

View File

@ -0,0 +1,5 @@
id: net.mamoe.console.itest.plugin-with-yml
version: 0.0.0
dependencies:
- net.mamoe.console.itest.serviceloader

View File

@ -0,0 +1,27 @@
package net.mamoe.console.itest.pluginwithpluginyml
import net.mamoe.mirai.console.plugin.PluginManager
import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.description
import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
import kotlin.test.assertTrue
/*
* Copyright 2019-2023 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/dev/LICENSE
*/
internal object PluginWithPluginYml : KotlinPlugin() {
override fun onEnable() {
println(description)
println(description.id)
val pluginId = description.id
assertTrue {
PluginManager.plugins.first { it.description.id == pluginId } === PluginWithPluginYml
}
}
}

View File

@ -29,6 +29,7 @@ import net.mamoe.mirai.console.plugin.loader.PluginLoadException
import net.mamoe.mirai.console.plugin.name
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.utils.*
import net.mamoe.yamlkt.Yaml
import java.io.File
import java.nio.file.Path
import java.util.concurrent.ConcurrentHashMap
@ -177,7 +178,7 @@ internal class BuiltInJvmPluginLoaderImpl(
override fun Sequence<File>.extractPlugins(): List<JvmPlugin> {
ensureActive()
fun Sequence<Map.Entry<File, JvmPluginClassLoaderN>>.findAllInstances(): Sequence<Map.Entry<File, JvmPlugin>> {
fun Sequence<Map.Entry<File, JvmPluginClassLoaderN>>.initialize(): Sequence<Map.Entry<File, JvmPluginClassLoaderN>> {
return onEach { (_, pluginClassLoader) ->
val exportManagers = pluginClassLoader.findServices(
ExportManager::class
@ -192,7 +193,11 @@ internal class BuiltInJvmPluginLoaderImpl(
} else {
pluginClassLoader.declaredFilter = exportManagers[0]
}
}.map { (f, pluginClassLoader) ->
}
}
fun Sequence<Map.Entry<File, JvmPluginClassLoaderN>>.findAllInstances(): Sequence<Map.Entry<File, JvmPlugin>> {
return map { (f, pluginClassLoader) ->
f to pluginClassLoader.findServices(
JvmPlugin::class,
KotlinPlugin::class,
@ -206,19 +211,80 @@ internal class BuiltInJvmPluginLoaderImpl(
}
}
fun Map.Entry<File, JvmPluginClassLoaderN>.loadWithoutPluginDescription(): Sequence<Pair<File, JvmPlugin>> {
return sequenceOf(this).initialize().findAllInstances().map { (k, v) -> k to v }
}
fun Map.Entry<File, JvmPluginClassLoaderN>.loadWithPluginDescription(description: JvmPluginDescription): Sequence<Pair<File, JvmPlugin>> {
val pluginClassLoader = this.value
val pluginFile = this.key
pluginClassLoader.pluginDescriptionFromPluginResource = description
val pendingPlugin = object : NotYetLoadedJvmPlugin(
description = description,
classLoaderN = pluginClassLoader,
) {
private val plugin by lazy {
val services = pluginClassLoader.findServices(
JvmPlugin::class,
KotlinPlugin::class,
JavaPlugin::class
).loadAllServices()
if (services.isEmpty()) {
error("No plugin instance found in $pluginFile")
}
if (services.size > 1) {
error(
"Only one plugin can exist at the same time when using plugin.yml:\n\nPlugins found:\n" + services.joinToString(
separator = "\n"
) { it.javaClass.name + " (from " + it.javaClass.classLoader + ")" }
)
}
return@lazy services[0]
}
override fun resolve(): JvmPlugin = plugin
}
pluginClassLoader.linkedLogger = pendingPlugin.logger
return sequenceOf(pluginFile to pendingPlugin)
}
val filePlugins = this.filterNot {
pluginFileToInstanceMap.containsKey(it)
}.associateWith {
JvmPluginClassLoaderN.newLoader(it, jvmPluginLoadingCtx)
}.onEach { (_, classLoader) ->
classLoaders.add(classLoader)
}.asSequence().findAllInstances().onEach {
//logger.verbose { "Successfully initialized JvmPlugin ${loaded}." }
}.asSequence().flatMap { entry ->
val (file, pluginClassLoader) = entry
val pluginDescriptionDefine = pluginClassLoader.getResourceAsStream("plugin.yml")
if (pluginDescriptionDefine == null) {
entry.loadWithoutPluginDescription()
} else {
val desc = kotlin.runCatching {
pluginDescriptionDefine.bufferedReader().use { resource ->
Yaml.decodeFromString(
SimpleJvmPluginDescription.SerialData.serializer(),
resource.readText()
).toJvmPluginDescription()
}
}.onFailure { err ->
throw PluginLoadException("Invalid plugin.yml in " + file.absolutePath, err)
}.getOrThrow()
entry.loadWithPluginDescription(desc)
}
}.onEach {
logger.verbose { "Successfully initialized JvmPlugin ${it.second}." }
}.onEach { (file, plugin) ->
pluginFileToInstanceMap[file] = plugin
} + pluginFileToInstanceMap.asSequence()
}
return filePlugins.toSet().map { it.value }
return filePlugins.toSet().map { it.second }
}
private val loadedPlugins = ConcurrentHashMap<JvmPlugin, Unit>()
@ -266,9 +332,17 @@ internal class BuiltInJvmPluginLoaderImpl(
// move nameFolder in config and data to idFolder
PluginManager.pluginsDataPath.moveNameFolder(plugin)
PluginManager.pluginsConfigPath.moveNameFolder(plugin)
check(plugin is JvmPluginInternal) { "A JvmPlugin must extend AbstractJvmPlugin to be loaded by JvmPluginLoader.BuiltIn" }
check(plugin is JvmPluginInternal || plugin is NotYetLoadedJvmPlugin) {
"A JvmPlugin must extend AbstractJvmPlugin to be loaded by JvmPluginLoader.BuiltIn"
}
// region Link dependencies
plugin.javaClass.classLoader.safeCast<JvmPluginClassLoaderN>()?.let { jvmPluginClassLoaderN ->
when (plugin) {
is NotYetLoadedJvmPlugin -> plugin.classLoaderN
else -> plugin.javaClass.classLoader
}.safeCast<JvmPluginClassLoaderN>()?.let { jvmPluginClassLoaderN ->
// Link plugin dependencies
plugin.description.dependencies.asSequence().mapNotNull { dependency ->
plugin.logger.verbose { "Linking dependency: ${dependency.id}" }
@ -282,8 +356,19 @@ internal class BuiltInJvmPluginLoaderImpl(
}
jvmPluginClassLoaderN.linkPluginLibraries(plugin.logger)
}
val realPlugin = when (plugin) {
is NotYetLoadedJvmPlugin -> plugin.resolve().also { realPlugin ->
check(plugin.description === realPlugin.description) {
"A JvmPlugin loaded by plugin.yml must has same description reference"
}
}
else -> plugin
}
check(realPlugin is JvmPluginInternal) { "A JvmPlugin must extend AbstractJvmPlugin to be loaded by JvmPluginLoader.BuiltIn" }
// endregion
plugin.internalOnLoad()
realPlugin.internalOnLoad()
}.getOrElse {
throw PluginLoadException("Exception while loading ${plugin.description.smartToString()}", it)
}

View File

@ -13,6 +13,7 @@ package net.mamoe.mirai.console.internal.plugin
import net.mamoe.mirai.console.plugin.jvm.ExportManager
import net.mamoe.mirai.console.plugin.jvm.JvmPluginClasspath
import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.utils.*
import org.eclipse.aether.artifact.Artifact
@ -245,6 +246,8 @@ internal class DynLibClassLoader : DynamicClasspathClassLoader {
}
internal class JvmPluginClassLoaderN : URLClassLoader {
var pluginDescriptionFromPluginResource: JvmPluginDescription? = null
val openaccess: JvmPluginClasspath = OpenAccess()
val file: File
val ctx: JvmPluginsLoadingCtx

View File

@ -33,6 +33,7 @@ import net.mamoe.mirai.console.plugin.id
import net.mamoe.mirai.console.plugin.jvm.AbstractJvmPlugin
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin.Companion.onLoad
import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
import net.mamoe.mirai.console.plugin.jvm.JvmPluginLoader
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.utils.MiraiInternalApi
@ -106,7 +107,13 @@ internal abstract class JvmPluginInternal(
}
error("Failed to switch plugin '$id' status from $nowStatus to $update, current status = ${pluginStatus.value}")
}
error("Failed to switch plugin '$id' status to $update because current status $nowStatus doesn't contain flag ${Integer.toBinaryString(expectFlag)}")
error(
"Failed to switch plugin '$id' status to $update because current status $nowStatus doesn't contain flag ${
Integer.toBinaryString(
expectFlag
)
}"
)
}
@JvmSynthetic
@ -364,3 +371,11 @@ internal inline fun AtomicLong.updateWhen(condition: (Long) -> Boolean, update:
}
internal val Throwable.rootCauseOrSelf: Throwable get() = generateSequence(this) { it.cause }.lastOrNull() ?: this
internal fun Class<out JvmPluginInternal>.loadPluginDescriptionFromClassLoader(): JvmPluginDescription {
val classLoader =
this.classLoader as? JvmPluginClassLoaderN ?: error("Plugin $this is not loaded by JvmPluginClassLoader")
return classLoader.pluginDescriptionFromPluginResource ?: error("Missing `plugin.yml`")
}

View File

@ -0,0 +1,59 @@
/*
* Copyright 2019-2023 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/dev/LICENSE
*/
package net.mamoe.mirai.console.internal.plugin
import net.mamoe.mirai.console.data.runCatchingLog
import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.console.permission.PermissionId
import net.mamoe.mirai.console.permission.PermissionService
import net.mamoe.mirai.console.plugin.NotYetLoadedPlugin
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
import net.mamoe.mirai.utils.MiraiLogger
import java.io.File
import java.io.InputStream
import java.nio.file.Path
import kotlin.coroutines.CoroutineContext
internal abstract class NotYetLoadedJvmPlugin(
override val description: JvmPluginDescription,
val classLoaderN: JvmPluginClassLoaderN,
) : JvmPlugin, NotYetLoadedPlugin<JvmPlugin> {
abstract override fun resolve(): JvmPlugin
override val logger: MiraiLogger by lazy {
BuiltInJvmPluginLoaderImpl.logger.runCatchingLog {
MiraiLogger.Factory.create(NotYetLoadedJvmPlugin::class, this.description.name)
}.getOrThrow()
}
override val isEnabled: Boolean get() = false
override val parentPermission: Permission
get() = error("Not yet loaded")
override fun permissionId(name: String): PermissionId {
return PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, name)
}
override val coroutineContext: CoroutineContext
get() = error("Not yet loaded")
override val dataFolderPath: Path
get() = error("Not yet loaded")
override val dataFolder: File
get() = error("Not yet loaded")
override val configFolderPath: Path
get() = error("Not yet loaded")
override val configFolder: File
get() = error("Not yet loaded")
override fun getResourceAsStream(path: String): InputStream? {
return classLoaderN.getResourceAsStream(path)
}
}

View File

@ -17,6 +17,7 @@ import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.extensions.PluginLoaderProvider
import net.mamoe.mirai.console.internal.data.mkdir
import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage
import net.mamoe.mirai.console.plugin.NotYetLoadedPlugin
import net.mamoe.mirai.console.plugin.Plugin
import net.mamoe.mirai.console.plugin.PluginManager
import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.safeLoader
@ -94,7 +95,15 @@ internal class PluginManagerImpl(
private fun <P : Plugin, D : PluginDescription> PluginLoader<P, D>.loadPluginNoEnable(plugin: P) {
kotlin.runCatching {
this.load(plugin)
resolvedPlugins.add(plugin)
resolvedPlugins.add(
when (plugin) {
is NotYetLoadedPlugin<*> -> plugin.resolve()
else -> plugin
}
)
}.fold(
onSuccess = {
logger.info { "Successfully loaded plugin ${getPluginDescription(plugin).smartToString()}" }

View File

@ -0,0 +1,26 @@
/*
* Copyright 2019-2023 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/dev/LICENSE
*/
package net.mamoe.mirai.console.plugin
import net.mamoe.mirai.console.plugin.loader.PluginLoader
/**
* 代表一个 未完成加载/延迟加载 的插件
*
* 此实例仅用于插件加载系统, [PluginManager] 加载插件时会自动调用 [resolve] 解析真正的插件实例
*
* @see PluginLoader.listPlugins
* @see PluginManager
*
* @since 2.16.0
*/
public interface NotYetLoadedPlugin<T : Plugin> : Plugin {
public fun resolve(): T
}

View File

@ -16,6 +16,7 @@ import net.mamoe.mirai.console.data.PluginConfig
import net.mamoe.mirai.console.data.PluginData
import net.mamoe.mirai.console.internal.plugin.JvmPluginClassLoaderN
import net.mamoe.mirai.console.internal.plugin.JvmPluginInternal
import net.mamoe.mirai.console.internal.plugin.loadPluginDescriptionFromClassLoader
import net.mamoe.mirai.console.internal.util.PluginServiceHelper
import net.mamoe.mirai.console.permission.PermissionId
import net.mamoe.mirai.console.permission.PermissionService
@ -33,9 +34,22 @@ import kotlin.reflect.KClass
* @see KotlinPlugin
*/
@OptIn(ConsoleExperimentalApi::class)
public abstract class AbstractJvmPlugin @JvmOverloads constructor(
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext,
) : JvmPlugin, JvmPluginInternal(parentCoroutineContext), AutoSavePluginDataHolder {
public abstract class AbstractJvmPlugin : JvmPluginInternal, JvmPlugin, AutoSavePluginDataHolder {
@JvmOverloads
public constructor(
description: JvmPluginDescription,
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext,
) : super(parentCoroutineContext) {
this.description = description
}
public constructor(parentCoroutineContext: CoroutineContext = EmptyCoroutineContext) : super(parentCoroutineContext) {
this.description = javaClass.loadPluginDescriptionFromClassLoader()
}
final override val description: JvmPluginDescription
@ConsoleExperimentalApi
public final override val dataHolderName: String
get() = this.description.id
@ -106,7 +120,7 @@ public abstract class AbstractJvmPlugin @JvmOverloads constructor(
* : 仅包括当前插件 JAR Service
*/
@JvmSynthetic
protected fun <T: Any> services(kClass: KClass<out T>): Lazy<List<T>> = lazy {
protected fun <T : Any> services(kClass: KClass<out T>): Lazy<List<T>> = lazy {
val classLoader = try {
jvmPluginClasspath.pluginClassLoader
} catch (_: IllegalStateException) {
@ -127,7 +141,7 @@ public abstract class AbstractJvmPlugin @JvmOverloads constructor(
*
* : 仅包括当前插件 JAR Service
*/
protected fun <T: Any> services(clazz: Class<out T>): Lazy<List<T>> = services(kClass = clazz.kotlin)
protected fun <T : Any> services(clazz: Class<out T>): Lazy<List<T>> = services(kClass = clazz.kotlin)
/**
* 获取 指定类的 SPI Service

View File

@ -17,10 +17,20 @@ import kotlin.coroutines.EmptyCoroutineContext
/**
* Java 插件的父类
*/
public abstract class JavaPlugin @JvmOverloads constructor(
public final override val description: JvmPluginDescription,
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
) : JvmPlugin, AbstractJvmPlugin(parentCoroutineContext) {
public abstract class JavaPlugin : JvmPlugin, AbstractJvmPlugin {
@JvmOverloads
public constructor(
description: JvmPluginDescription,
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext,
) : super(description, parentCoroutineContext)
@JvmOverloads
public constructor(
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext,
) : super(parentCoroutineContext)
init {
__jpi_try_to_init_dependencies()
}

View File

@ -17,10 +17,19 @@ import kotlin.coroutines.EmptyCoroutineContext
/**
* Kotlin 插件的父类.
*/
public abstract class KotlinPlugin @JvmOverloads constructor(
public final override val description: JvmPluginDescription,
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext,
) : JvmPlugin, AbstractJvmPlugin(parentCoroutineContext) {
public abstract class KotlinPlugin : JvmPlugin, AbstractJvmPlugin {
@JvmOverloads
public constructor(
description: JvmPluginDescription,
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext,
) : super(description, parentCoroutineContext)
@JvmOverloads
public constructor(
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext,
) : super(parentCoroutineContext)
init {
__jpi_try_to_init_dependencies()
}