mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-25 15:40:28 +08:00
Support PluginLoader with ServiceLoader
This commit is contained in:
parent
1bd1b5a4fd
commit
316a40e4bc
@ -9,6 +9,7 @@ import java.util.TimeZone
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
kotlin("plugin.serialization")
|
||||
kotlin("kapt")
|
||||
id("java")
|
||||
`maven-publish`
|
||||
id("com.jfrog.bintray")
|
||||
@ -82,6 +83,11 @@ dependencies {
|
||||
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api:5.2.0")
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.2.0")
|
||||
|
||||
|
||||
val autoService = "1.0-rc7"
|
||||
kapt("com.google.auto.service", "auto-service", autoService)
|
||||
compileOnly("com.google.auto.service", "auto-service-annotations", autoService)
|
||||
}
|
||||
|
||||
ext.apply {
|
||||
|
@ -19,13 +19,13 @@ import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.console.MiraiConsole.INSTANCE
|
||||
import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start
|
||||
import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge
|
||||
import net.mamoe.mirai.console.internal.data.builtin.childScopeContext
|
||||
import net.mamoe.mirai.console.plugin.PluginLoader
|
||||
import net.mamoe.mirai.console.plugin.PluginManager
|
||||
import net.mamoe.mirai.console.plugin.center.PluginCenter
|
||||
import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader
|
||||
import net.mamoe.mirai.console.util.ConsoleExperimentalAPI
|
||||
import net.mamoe.mirai.console.util.ConsoleInternalAPI
|
||||
import net.mamoe.mirai.console.util.childScopeContext
|
||||
import net.mamoe.mirai.utils.BotConfiguration
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import java.io.File
|
||||
|
@ -1,31 +0,0 @@
|
||||
/*
|
||||
* 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.internal.data
|
||||
|
||||
import com.vdurmont.semver4j.Semver
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
|
||||
@Serializer(forClass = Semver::class)
|
||||
internal object SemverAsStringSerializerLoose : KSerializer<Semver> by String.serializer().map(
|
||||
serializer = { it.toString() },
|
||||
deserializer = {
|
||||
Semver(it.removePrefix("v").removePrefix("V"), Semver.SemverType.LOOSE)
|
||||
}
|
||||
)
|
||||
|
||||
@Serializer(forClass = Semver::class)
|
||||
internal object SemverAsStringSerializerIvy : KSerializer<Semver> by String.serializer().map(
|
||||
serializer = { it.toString() },
|
||||
deserializer = {
|
||||
Semver(it.removePrefix("v").removePrefix("V"), Semver.SemverType.IVY)
|
||||
}
|
||||
)
|
@ -16,6 +16,7 @@ import net.mamoe.mirai.console.data.PluginConfig
|
||||
import net.mamoe.mirai.console.data.PluginData
|
||||
import net.mamoe.mirai.console.data.PluginDataStorage
|
||||
import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge
|
||||
import net.mamoe.mirai.console.util.childScope
|
||||
import net.mamoe.mirai.utils.minutesToMillis
|
||||
|
||||
|
||||
|
@ -11,23 +11,23 @@ package net.mamoe.mirai.console.internal.plugin
|
||||
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import net.mamoe.mirai.console.MiraiConsole
|
||||
import net.mamoe.mirai.console.data.PluginDataStorage
|
||||
import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge
|
||||
import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip
|
||||
import net.mamoe.mirai.console.internal.data.createInstanceOrNull
|
||||
import net.mamoe.mirai.console.plugin.AbstractFilePluginLoader
|
||||
import net.mamoe.mirai.console.plugin.PluginLoadException
|
||||
import net.mamoe.mirai.console.plugin.jvm.*
|
||||
import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader
|
||||
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
|
||||
import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
|
||||
import net.mamoe.mirai.console.util.ConsoleExperimentalAPI
|
||||
import net.mamoe.mirai.console.util.childScopeContext
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import net.mamoe.yamlkt.Yaml
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.net.URLClassLoader
|
||||
import java.util.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.streams.asSequence
|
||||
|
||||
internal object JarPluginLoaderImpl :
|
||||
AbstractFilePluginLoader<JvmPlugin, JvmPluginDescription>(".jar"),
|
||||
@ -48,71 +48,53 @@ internal object JarPluginLoaderImpl :
|
||||
logger.error("Unhandled Jar plugin exception: ${throwable.message}", throwable)
|
||||
})
|
||||
|
||||
internal val classLoader: PluginsLoader = PluginsLoader(this.javaClass.classLoader)
|
||||
|
||||
init { // delayed
|
||||
coroutineContext[Job]!!.invokeOnCompletion {
|
||||
classLoader.clear()
|
||||
}
|
||||
}
|
||||
internal val classLoaders: MutableList<ClassLoader> = mutableListOf()
|
||||
|
||||
@Suppress("EXTENSION_SHADOWED_BY_MEMBER") // doesn't matter
|
||||
override val JvmPlugin.description: JvmPluginDescription
|
||||
get() = this.description
|
||||
|
||||
override fun Sequence<File>.mapToDescription(): List<JvmPluginDescriptionImpl> {
|
||||
return this.associateWith { URI("jar:file:${it.absolutePath.replace('\\', '/')}!/plugin.yml").toURL() }
|
||||
.mapNotNull { (file, url) ->
|
||||
override fun Sequence<File>.extractPlugins(): List<JvmPlugin> {
|
||||
ensureActive()
|
||||
|
||||
fun <T> ServiceLoader<T>.loadAll(file: File?): Sequence<T> {
|
||||
return stream().asSequence().mapNotNull {
|
||||
kotlin.runCatching {
|
||||
url.readText()
|
||||
}.fold(
|
||||
onSuccess = { yaml ->
|
||||
Yaml.nonStrict.decodeFromString(JvmPluginDescriptionImpl.serializer(), yaml)
|
||||
},
|
||||
onFailure = {
|
||||
logger.error("Cannot load plugin file ${file.name}", it)
|
||||
null
|
||||
}
|
||||
)?.also { it._file = file }
|
||||
it.type().kotlin.objectInstance ?: it.get()
|
||||
}.onFailure {
|
||||
logger.error("Cannot load plugin ${file ?: "<no-file>"}", it)
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
val inMemoryPlugins =
|
||||
ServiceLoader.load(
|
||||
JvmPlugin::class.java,
|
||||
generateSequence(MiraiConsole::class.java.classLoader) { it.parent }.last()
|
||||
).loadAll(null)
|
||||
|
||||
val filePlugins = this.associateWith {
|
||||
URLClassLoader(arrayOf(it.toURI().toURL()), MiraiConsole::class.java.classLoader)
|
||||
}.onEach { (_, classLoader) ->
|
||||
classLoaders.add(classLoader)
|
||||
}.mapValues {
|
||||
ServiceLoader.load(JvmPlugin::class.java, it.value)
|
||||
}.flatMap { (file, loader) ->
|
||||
loader.loadAll(file)
|
||||
}
|
||||
|
||||
return (inMemoryPlugins + filePlugins).toSet().toList()
|
||||
}
|
||||
|
||||
@Throws(PluginLoadException::class)
|
||||
override fun load(description: JvmPluginDescription): JvmPlugin {
|
||||
val main = when (description) {
|
||||
is JvmMemoryPluginDescription -> {
|
||||
description.instance
|
||||
}
|
||||
is JvmPluginDescriptionImpl -> with(description) {
|
||||
classLoader.loadPluginMainClassByJarFile(
|
||||
pluginName = name,
|
||||
mainClass = mainClassName,
|
||||
jarFile = file
|
||||
).kotlin.run {
|
||||
objectInstance
|
||||
?: createInstanceOrNull()
|
||||
?: (java.constructors + java.declaredConstructors)
|
||||
.firstOrNull { it.parameterCount == 0 }
|
||||
?.apply { kotlin.runCatching { isAccessible = true } }
|
||||
?.newInstance()
|
||||
} ?: error("No Kotlin object or public no-arg constructor found for $mainClassName")
|
||||
}
|
||||
else -> error("Illegal description: ${description::class.qualifiedName}")
|
||||
}
|
||||
|
||||
description.runCatching {
|
||||
ensureActive()
|
||||
|
||||
check(main is JvmPlugin) { "Main class ${main::class.qualifiedNameOrTip} from plugin ${description.name} does not extend JvmPlugin." }
|
||||
|
||||
if (main is JvmPluginInternal) {
|
||||
main._description = description
|
||||
main.internalOnLoad()
|
||||
} else main.onLoad()
|
||||
|
||||
return main
|
||||
override fun load(plugin: JvmPlugin) {
|
||||
ensureActive()
|
||||
runCatching {
|
||||
if (plugin is JvmPluginInternal) {
|
||||
plugin.internalOnLoad()
|
||||
} else plugin.onLoad()
|
||||
}.getOrElse {
|
||||
throw PluginLoadException("Exception while loading ${description.name}", it)
|
||||
throw PluginLoadException("Exception while loading ${plugin.description.name}", it)
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,6 +108,7 @@ internal object JarPluginLoaderImpl :
|
||||
|
||||
override fun disable(plugin: JvmPlugin) {
|
||||
if (!plugin.isEnabled) return
|
||||
ensureActive()
|
||||
|
||||
if (plugin is JvmPluginInternal) {
|
||||
plugin.internalOnDisable()
|
||||
|
@ -19,7 +19,6 @@ import net.mamoe.mirai.console.plugin.PluginManager
|
||||
import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.safeLoader
|
||||
import net.mamoe.mirai.console.plugin.ResourceContainer.Companion.asResourceContainer
|
||||
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
|
||||
@ -35,8 +34,7 @@ internal val <T> T.job: Job where T : CoroutineScope, T : Plugin get() = this.co
|
||||
@PublishedApi
|
||||
internal abstract class JvmPluginInternal(
|
||||
parentCoroutineContext: CoroutineContext
|
||||
) : JvmPlugin,
|
||||
CoroutineScope {
|
||||
) : JvmPlugin, CoroutineScope {
|
||||
|
||||
final override var isEnabled: Boolean = false
|
||||
|
||||
@ -44,17 +42,9 @@ internal abstract class JvmPluginInternal(
|
||||
override fun getResourceAsStream(path: String): InputStream? = resourceContainerDelegate.getResourceAsStream(path)
|
||||
|
||||
// region JvmPlugin
|
||||
/**
|
||||
* Initialized immediately after construction of [JvmPluginInternal] instance
|
||||
*/
|
||||
@Suppress("PropertyName")
|
||||
internal open lateinit var _description: JvmPluginDescription
|
||||
|
||||
final override val description: JvmPluginDescription get() = _description
|
||||
|
||||
final override val logger: MiraiLogger by lazy {
|
||||
MiraiConsole.newLogger(
|
||||
"Plugin ${this._description.name}"
|
||||
"Plugin ${this.description.name}"
|
||||
)
|
||||
}
|
||||
|
||||
@ -121,11 +111,13 @@ internal abstract class JvmPluginInternal(
|
||||
internal val _intrinsicCoroutineContext: CoroutineContext by lazy {
|
||||
CoroutineName("Plugin $name")
|
||||
}
|
||||
|
||||
@JvmField
|
||||
internal val coroutineContextInitializer = {
|
||||
CoroutineExceptionHandler { _, throwable ->
|
||||
if (throwable !is CancellationException) logger.error(throwable)
|
||||
CoroutineExceptionHandler { context, throwable ->
|
||||
if (throwable.rootCauseOrSelf !is CancellationException) logger.error(
|
||||
"Exception in coroutine ${context[CoroutineName]?.name ?: "<unnamed>"} of ${description.name}",
|
||||
throwable
|
||||
)
|
||||
}
|
||||
.plus(parentCoroutineContext)
|
||||
.plus(
|
||||
@ -183,4 +175,6 @@ internal inline fun AtomicLong.updateWhen(condition: (Long) -> Boolean, update:
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal val Throwable.rootCauseOrSelf: Throwable get() = generateSequence(this) { it.cause }.lastOrNull() ?: this
|
@ -15,7 +15,6 @@ import kotlinx.atomicfu.locks.withLock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import net.mamoe.mirai.console.MiraiConsole
|
||||
import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip
|
||||
import net.mamoe.mirai.console.internal.data.cast
|
||||
import net.mamoe.mirai.console.internal.data.mkdir
|
||||
import net.mamoe.mirai.console.plugin.*
|
||||
@ -84,16 +83,16 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol
|
||||
|
||||
// region LOADING
|
||||
|
||||
private fun <P : Plugin, D : PluginDescription> PluginLoader<P, D>.loadPluginNoEnable(description: D): P {
|
||||
return kotlin.runCatching {
|
||||
this.load(description).also { resolvedPlugins.add(it) }
|
||||
private fun <P : Plugin, D : PluginDescription> PluginLoader<P, D>.loadPluginNoEnable(plugin: P) {
|
||||
kotlin.runCatching {
|
||||
this.load(plugin)
|
||||
resolvedPlugins.add(plugin)
|
||||
}.fold(
|
||||
onSuccess = {
|
||||
logger.info { "Successfully loaded plugin ${description.name}" }
|
||||
it
|
||||
logger.info { "Successfully loaded plugin ${plugin.description.name}" }
|
||||
},
|
||||
onFailure = {
|
||||
logger.info { "Cannot load plugin ${description.name}" }
|
||||
logger.info { "Cannot load plugin ${plugin.description.name}" }
|
||||
throw it
|
||||
}
|
||||
)
|
||||
@ -138,33 +137,30 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol
|
||||
|
||||
private fun loadPluginLoaderProvidedByPlugins() {
|
||||
loadersLock.withLock {
|
||||
JarPluginLoaderImpl.classLoader.pluginLoaders.asSequence()
|
||||
.flatMap { (name, pluginClassLoader) ->
|
||||
JarPluginLoaderImpl.classLoaders.asSequence()
|
||||
.flatMap { pluginClassLoader ->
|
||||
ServiceLoader.load(PluginLoader::class.java, pluginClassLoader)
|
||||
.stream().asSequence()
|
||||
.associateBy { name }
|
||||
.asSequence()
|
||||
}
|
||||
.forEach { (name, provider) ->
|
||||
.forEach { provider ->
|
||||
val pluginLoader = kotlin.runCatching {
|
||||
provider.get()
|
||||
}.getOrElse {
|
||||
logger.error(
|
||||
{ "Could not load PluginLoader ${it::class.qualifiedNameOrTip} from plugin $name" },
|
||||
{ "Could not load PluginLoader ${provider.type().canonicalName}." },
|
||||
it
|
||||
)
|
||||
return@forEach
|
||||
}
|
||||
_pluginLoaders.add(pluginLoader)
|
||||
logger.info { "Successfully loaded PluginLoader ${pluginLoader::class.qualifiedNameOrTip} from plugin $name" }
|
||||
logger.info { "Successfully loaded PluginLoader ${provider.type().canonicalName}." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<PluginDescriptionWithLoader>.loadAndEnableAllInOrder() {
|
||||
return this.map { (loader, desc) ->
|
||||
loader to loader.loadPluginNoEnable(desc)
|
||||
}.forEach { (loader, plugin) ->
|
||||
return this.forEach { (loader, _, plugin) ->
|
||||
loader.enablePlugin(plugin)
|
||||
}
|
||||
}
|
||||
@ -189,7 +185,9 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol
|
||||
}
|
||||
|
||||
private fun List<PluginLoader<*, *>>.listAllPlugins(): List<Pair<PluginLoader<*, *>, List<PluginDescriptionWithLoader>>> {
|
||||
return associateWith { loader -> loader.listPlugins().map { desc -> desc.wrapWith(loader) } }.toList()
|
||||
return associateWith { loader ->
|
||||
loader.listPlugins().map { plugin -> plugin.description.wrapWith(loader, plugin) }
|
||||
}.toList()
|
||||
}
|
||||
|
||||
@Throws(PluginMissingDependencyException::class)
|
||||
@ -231,8 +229,9 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol
|
||||
}
|
||||
|
||||
internal data class PluginDescriptionWithLoader(
|
||||
@JvmField val loader: PluginLoader<*, PluginDescription>, // easier type
|
||||
@JvmField val delegate: PluginDescription
|
||||
@JvmField val loader: PluginLoader<Plugin, PluginDescription>, // easier type
|
||||
@JvmField val delegate: PluginDescription,
|
||||
@JvmField val plugin: Plugin
|
||||
) : PluginDescription by delegate
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@ -240,9 +239,9 @@ internal fun <D : PluginDescription> PluginDescription.unwrap(): D =
|
||||
if (this is PluginDescriptionWithLoader) this.delegate as D else this as D
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal fun PluginDescription.wrapWith(loader: PluginLoader<*, *>): PluginDescriptionWithLoader =
|
||||
internal fun PluginDescription.wrapWith(loader: PluginLoader<*, *>, plugin: Plugin): PluginDescriptionWithLoader =
|
||||
PluginDescriptionWithLoader(
|
||||
loader as PluginLoader<*, PluginDescription>, this
|
||||
loader as PluginLoader<Plugin, PluginDescription>, this, plugin
|
||||
)
|
||||
|
||||
internal operator fun List<PluginDescription>.contains(dependency: PluginDependency): Boolean =
|
||||
|
@ -1,159 +0,0 @@
|
||||
/*
|
||||
* 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.internal.plugin
|
||||
|
||||
import net.mamoe.mirai.console.MiraiConsole
|
||||
import java.io.File
|
||||
import java.net.URLClassLoader
|
||||
|
||||
internal class PluginsLoader(private val parentClassLoader: ClassLoader) {
|
||||
private val loggerName = "PluginsLoader"
|
||||
internal val pluginLoaders = linkedMapOf<String, PluginClassLoader>()
|
||||
private val classesCache = mutableMapOf<String, Class<*>>()
|
||||
private val logger = MiraiConsole.newLogger(loggerName)
|
||||
|
||||
/**
|
||||
* 清除所有插件加载器
|
||||
*/
|
||||
fun clear() {
|
||||
val iterator = pluginLoaders.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val plugin = iterator.next()
|
||||
var cl = ""
|
||||
try {
|
||||
cl = plugin.value.toString()
|
||||
plugin.value.close()
|
||||
iterator.remove()
|
||||
} catch (e: Throwable) {
|
||||
logger.error("Plugin(${plugin.key}) can't not close its ClassLoader(${cl})", e)
|
||||
}
|
||||
}
|
||||
classesCache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除单个插件加载器
|
||||
*/
|
||||
fun remove(pluginName: String): Boolean {
|
||||
pluginLoaders[pluginName]?.close() ?: return false
|
||||
pluginLoaders.remove(pluginName)
|
||||
return true
|
||||
}
|
||||
|
||||
fun loadPluginMainClassByJarFile(pluginName: String, mainClass: String, jarFile: File): Class<*> {
|
||||
try {
|
||||
if (!pluginLoaders.containsKey(pluginName)) {
|
||||
pluginLoaders[pluginName] =
|
||||
PluginClassLoader(
|
||||
jarFile,
|
||||
this,
|
||||
parentClassLoader
|
||||
)
|
||||
}
|
||||
return pluginLoaders[pluginName]!!.loadClass(mainClass)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
throw ClassNotFoundException(
|
||||
"PluginsClassLoader(${pluginName}) can't load this pluginMainClass:${mainClass}",
|
||||
e
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
throw Throwable("init or load class error", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试加载插件的依赖,无则返回null
|
||||
*/
|
||||
fun findClassByName(name: String): Class<*>? {
|
||||
return classesCache[name] ?: pluginLoaders.values.asSequence().mapNotNull {
|
||||
kotlin.runCatching {
|
||||
it.findClass(name, false)
|
||||
}.getOrNull()
|
||||
}.firstOrNull()
|
||||
}
|
||||
|
||||
fun addClassCache(name: String, clz: Class<*>) {
|
||||
synchronized(classesCache) {
|
||||
if (!classesCache.containsKey(name)) {
|
||||
classesCache[name] = clz
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A Adapted URL Class Loader that supports Android and JVM for single URL(File) Class Load
|
||||
*/
|
||||
|
||||
internal open class AdaptiveURLClassLoader(file: File, parent: ClassLoader) : ClassLoader() {
|
||||
|
||||
private val internalClassLoader: ClassLoader by lazy {
|
||||
kotlin.runCatching {
|
||||
val loaderClass = Class.forName("dalvik.system.PathClassLoader")
|
||||
loaderClass.getConstructor(String::class.java, ClassLoader::class.java)
|
||||
.newInstance(file.absolutePath, parent) as ClassLoader
|
||||
}.getOrElse {
|
||||
URLClassLoader(arrayOf((file.toURI().toURL())), parent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadClass(name: String?): Class<*> {
|
||||
return internalClassLoader.loadClass(name)
|
||||
}
|
||||
|
||||
|
||||
private val internalClassCache = mutableMapOf<String, Class<*>>()
|
||||
|
||||
internal val classesCache: Map<String, Class<*>>
|
||||
get() = internalClassCache
|
||||
|
||||
internal fun addClassCache(string: String, clazz: Class<*>) {
|
||||
synchronized(internalClassCache) {
|
||||
internalClassCache[string] = clazz
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun close() {
|
||||
if (internalClassLoader is URLClassLoader) {
|
||||
(internalClassLoader as URLClassLoader).close()
|
||||
}
|
||||
internalClassCache.clear()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class PluginClassLoader(
|
||||
file: File,
|
||||
private val pluginsLoader: PluginsLoader,
|
||||
parent: ClassLoader
|
||||
) : AdaptiveURLClassLoader(file, parent) {
|
||||
|
||||
override fun findClass(name: String): Class<*> {
|
||||
return findClass(name, true)
|
||||
}
|
||||
|
||||
fun findClass(name: String, global: Boolean = true): Class<*> {
|
||||
return classesCache[name] ?: kotlin.run {
|
||||
var clazz: Class<*>? = null
|
||||
if (global) {
|
||||
clazz = pluginsLoader.findClassByName(name)
|
||||
}
|
||||
if (clazz == null) {
|
||||
clazz = loadClass(name)//这里应该是find, 如果不行就要改
|
||||
}
|
||||
pluginsLoader.addClassCache(name, clazz)
|
||||
this.addClassCache(name, clazz)
|
||||
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
|
||||
clazz!! // compiler bug
|
||||
}
|
||||
}
|
||||
}
|
@ -42,13 +42,15 @@ import java.util.*
|
||||
*/
|
||||
public interface PluginLoader<P : Plugin, D : PluginDescription> {
|
||||
/**
|
||||
* 扫描并返回可以被加载的插件的 [描述][PluginDescription] 列表.
|
||||
* 扫描并返回可以被加载的插件的列表.
|
||||
*
|
||||
* 这些插件都应处于还未被加载的状态.
|
||||
*
|
||||
* 在 console 启动时, [PluginManager] 会获取所有 [PluginDescription], 分析依赖关系, 确认插件加载顺序.
|
||||
*
|
||||
* **实现细节:** 此函数*只应该*在 console 启动时被调用一次. 但取决于前端实现不同, 或由于被一些插件需要, 此函数也可能会被多次调用.
|
||||
*/
|
||||
public fun listPlugins(): List<D>
|
||||
public fun listPlugins(): List<P>
|
||||
|
||||
/**
|
||||
* 获取此插件的描述.
|
||||
@ -75,7 +77,7 @@ public interface PluginLoader<P : Plugin, D : PluginDescription> {
|
||||
* @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如找不到主类等).
|
||||
*/
|
||||
@Throws(PluginLoadException::class)
|
||||
public fun load(description: D): P
|
||||
public fun load(plugin: P)
|
||||
|
||||
/**
|
||||
* 启用这个插件.
|
||||
@ -165,11 +167,11 @@ public abstract class AbstractFilePluginLoader<P : Plugin, D : PluginDescription
|
||||
.filter { it.isFile && it.name.endsWith(fileSuffix, ignoreCase = true) }
|
||||
|
||||
/**
|
||||
* 读取扫描到的后缀与 [fileSuffix] 相同的文件中的 [PluginDescription]
|
||||
* 读取扫描到的后缀与 [fileSuffix] 相同的文件中的插件实例, 但不 [加载][PluginLoader.load]
|
||||
*/
|
||||
protected abstract fun Sequence<File>.mapToDescription(): List<D>
|
||||
protected abstract fun Sequence<File>.extractPlugins(): List<P>
|
||||
|
||||
public final override fun listPlugins(): List<D> = pluginsFilesSequence().mapToDescription()
|
||||
public final override fun listPlugins(): List<P> = pluginsFilesSequence().extractPlugins()
|
||||
}
|
||||
|
||||
|
||||
@ -179,9 +181,9 @@ internal class DeferredPluginLoader<P : Plugin, D : PluginDescription>(
|
||||
) : PluginLoader<P, D> {
|
||||
private val instance by lazy(initializer)
|
||||
|
||||
override fun listPlugins(): List<D> = instance.listPlugins()
|
||||
override fun listPlugins(): List<P> = instance.run { listPlugins() }
|
||||
override val P.description: D get() = instance.run { description }
|
||||
override fun load(description: D): P = instance.load(description)
|
||||
override fun load(plugin: P) = instance.load(plugin)
|
||||
override fun enable(plugin: P) = instance.enable(plugin)
|
||||
override fun disable(plugin: P) = instance.disable(plugin)
|
||||
}
|
||||
|
@ -131,6 +131,13 @@ public interface PluginManager {
|
||||
*/
|
||||
public fun Plugin.disable(): Unit = safeLoader.disable(this)
|
||||
|
||||
/**
|
||||
* 加载这个插件
|
||||
*
|
||||
* @see PluginLoader.load
|
||||
*/
|
||||
public fun Plugin.load(): Unit = safeLoader.load(this)
|
||||
|
||||
/**
|
||||
* 启用这个插件
|
||||
*
|
||||
@ -155,6 +162,7 @@ public interface PluginManager {
|
||||
public override fun PluginLoader<*, *>.unregister(): Boolean = PluginManagerImpl.run { unregister() }
|
||||
public override fun Plugin.disable(): Unit = PluginManagerImpl.run { disable() }
|
||||
public override fun Plugin.enable(): Unit = PluginManagerImpl.run { enable() }
|
||||
public override fun Plugin.load(): Unit = PluginManagerImpl.run { load() }
|
||||
public override val <P : Plugin> P.safeLoader: PluginLoader<P, PluginDescription> get() = PluginManagerImpl.run { safeLoader }
|
||||
}
|
||||
}
|
@ -7,22 +7,17 @@
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package net.mamoe.mirai.console.plugin.description
|
||||
|
||||
import com.vdurmont.semver4j.Semver
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import net.mamoe.mirai.console.internal.data.map
|
||||
import net.mamoe.yamlkt.Yaml
|
||||
import net.mamoe.yamlkt.YamlDynamicSerializer
|
||||
|
||||
/**
|
||||
* 插件的一个依赖的信息.
|
||||
*
|
||||
* @see PluginDescription.dependencies
|
||||
*/
|
||||
@Serializable(with = PluginDependency.SmartSerializer::class)
|
||||
public data class PluginDependency(
|
||||
/** 依赖插件名 */
|
||||
public val name: String,
|
||||
@ -33,47 +28,15 @@ public data class PluginDependency(
|
||||
*
|
||||
* 允许 [Apache Ivy 风格版本号表示](http://ant.apache.org/ivy/history/latest-milestone/settings/version-matchers.html)
|
||||
*/
|
||||
public val version: @Serializable(net.mamoe.mirai.console.internal.data.SemverAsStringSerializerIvy::class) Semver? = null,
|
||||
public val version: Semver? = null,
|
||||
/**
|
||||
* 若为 `false`, 插件在找不到此依赖时也能正常加载.
|
||||
*/
|
||||
public val isOptional: Boolean = false
|
||||
) {
|
||||
public override fun toString(): String {
|
||||
return "$name v$version${if (isOptional) "?" else ""}"
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 可支持解析 [String] 作为 [PluginDependency.version] 或单个 [PluginDependency]
|
||||
*/
|
||||
public object SmartSerializer : KSerializer<PluginDependency> by YamlDynamicSerializer.map(
|
||||
serializer = { it },
|
||||
deserializer = { any ->
|
||||
when (any) {
|
||||
is Map<*, *> -> Yaml.nonStrict.decodeFromString(
|
||||
serializer(),
|
||||
Yaml.nonStrict.encodeToString<Map<*, *>>(any)
|
||||
)
|
||||
else -> {
|
||||
var value = any.toString()
|
||||
val isOptional = value.endsWith('?')
|
||||
if (isOptional) {
|
||||
value = value.removeSuffix("?")
|
||||
}
|
||||
|
||||
val components = value.split(':')
|
||||
when (components.size) {
|
||||
1 -> PluginDependency(value, isOptional = isOptional)
|
||||
2 -> PluginDependency(
|
||||
components[0],
|
||||
Semver(components[1], Semver.SemverType.IVY),
|
||||
isOptional = isOptional
|
||||
)
|
||||
else -> error("Illegal plugin dependency statement: $value")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public constructor(name: String, version: String, isOptional: Boolean) : this(
|
||||
name,
|
||||
Semver(version, Semver.SemverType.IVY),
|
||||
isOptional
|
||||
)
|
||||
}
|
@ -10,7 +10,6 @@
|
||||
package net.mamoe.mirai.console.plugin.description
|
||||
|
||||
import com.vdurmont.semver4j.Semver
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.mamoe.mirai.console.plugin.Plugin
|
||||
|
||||
|
||||
@ -56,6 +55,6 @@ public interface PluginDescription {
|
||||
*
|
||||
* @see PluginDependency
|
||||
*/
|
||||
public val dependencies: List<@Serializable(with = PluginDependency.SmartSerializer::class) PluginDependency>
|
||||
public val dependencies: List<PluginDependency>
|
||||
}
|
||||
|
||||
|
@ -31,9 +31,12 @@ import net.mamoe.mirai.utils.MiraiLogger
|
||||
/**
|
||||
* Java, Kotlin 或其他 JVM 平台插件
|
||||
*
|
||||
* ### ResourceContainer
|
||||
* ## ResourceContainer
|
||||
* 实现为 [ClassLoader.getResourceAsStream]
|
||||
*
|
||||
* ## 实现 [JvmPlugin]
|
||||
* j
|
||||
*
|
||||
* @see AbstractJvmPlugin 默认实现
|
||||
*
|
||||
* @see JavaPlugin Java 插件
|
||||
|
@ -7,119 +7,45 @@
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package net.mamoe.mirai.console.plugin.jvm
|
||||
|
||||
import com.vdurmont.semver4j.Semver
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import net.mamoe.mirai.console.internal.data.SemverAsStringSerializerLoose
|
||||
import net.mamoe.mirai.console.plugin.description.PluginDependency
|
||||
import net.mamoe.mirai.console.plugin.description.PluginDescription
|
||||
import net.mamoe.mirai.console.plugin.description.PluginKind
|
||||
import net.mamoe.mirai.console.util.ConsoleExperimentalAPI
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalAPI
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* @see KotlinMemoryPlugin 不需要 "plugin.yml", 不需要相关资源的在内存中加载的插件.
|
||||
*/
|
||||
@ConsoleExperimentalAPI
|
||||
public data class JvmMemoryPluginDescription(
|
||||
public override val kind: PluginKind,
|
||||
public override val name: String,
|
||||
public override val author: String,
|
||||
public override val version: Semver,
|
||||
public override val info: String,
|
||||
public override val dependencies: List<PluginDependency>,
|
||||
val instance: JvmPlugin
|
||||
) : JvmPluginDescription {
|
||||
init {
|
||||
require(!name.contains(':')) { "':' is forbidden in plugin name" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JVM 插件的描述. 通常作为 `plugin.yml`
|
||||
*
|
||||
*
|
||||
* ```yaml
|
||||
* # 必须. 插件名称, 允许空格, 允许中文, 不允许 ':'
|
||||
* name: "MyTestPlugin"
|
||||
*
|
||||
* # 必须. 插件主类, 即继承 KotlinPlugin 或 JavaPlugin 的类
|
||||
* main: org.example.MyPluginMain
|
||||
*
|
||||
* # 必须. 插件版本. 遵循《语义化版本 2.0.0》规范
|
||||
* version: 0.1.0
|
||||
*
|
||||
* # 可选. 插件种类.
|
||||
* # 'NORMAL': 表示普通插件
|
||||
* # 'LOADER': 表示提供扩展插件加载器的插件
|
||||
* kind: NORMAL
|
||||
*
|
||||
* # 可选. 插件描述
|
||||
* info: "这是一个测试插件"
|
||||
*
|
||||
* # 可选. 插件作者
|
||||
* author: "Mirai Example"
|
||||
*
|
||||
* # 可选. 插件依赖列表. 两种指定方式均可.
|
||||
* dependencies:
|
||||
* - name: "the" # 依赖的插件名
|
||||
* version: null # 依赖的版本号, 支持 Apache Ivy 格式. 为 null 或不指定时不限制版本
|
||||
* isOptional: true # `true` 表示插件在找不到此依赖时也能正常加载
|
||||
* - "SamplePlugin" # 名称为 SamplePlugin 的插件, 不限制版本, isOptional=false
|
||||
* - "TestPlugin:1.0.0+" # 名称为 ExamplePlugin 的插件, 版本至少为 1.0.0, isOptional=false
|
||||
* - "ExamplePlugin:1.5.0+?" # 名称为 ExamplePlugin 的插件, 版本至少为 1.5.0, 末尾 `?` 表示 isOptional=true
|
||||
* - "Another test plugin:[1.0.0, 2.0.0)" # 名称为 Another test plugin 的插件, 版本要求大于等于 1.0.0, 小于 2.0.0, isOptional=false
|
||||
* ```
|
||||
* @see SimpleJvmPluginDescription
|
||||
*/
|
||||
public interface JvmPluginDescription : PluginDescription
|
||||
|
||||
/**
|
||||
* @see JvmPluginDescriptionImpl
|
||||
* @see JvmPluginDescription
|
||||
*/
|
||||
@MiraiExperimentalAPI
|
||||
@Serializable
|
||||
public class JvmPluginDescriptionImpl internal constructor(
|
||||
public override val kind: PluginKind = PluginKind.NORMAL,
|
||||
public data class SimpleJvmPluginDescription
|
||||
@JvmOverloads public constructor(
|
||||
public override val name: String,
|
||||
@SerialName("main")
|
||||
public val mainClassName: String,
|
||||
public override val version: Semver,
|
||||
public override val author: String = "",
|
||||
public override val version: @Serializable(with = SemverAsStringSerializerLoose::class) Semver,
|
||||
public override val info: String = "",
|
||||
@SerialName("depends")
|
||||
public override val dependencies: List<@Serializable(with = PluginDependency.SmartSerializer::class) PluginDependency> = listOf()
|
||||
public override val dependencies: List<PluginDependency> = listOf(),
|
||||
public override val kind: PluginKind = PluginKind.NORMAL,
|
||||
) : JvmPluginDescription {
|
||||
|
||||
@JvmOverloads
|
||||
public constructor(
|
||||
name: String,
|
||||
version: String,
|
||||
author: String = "",
|
||||
info: String = "",
|
||||
dependencies: List<PluginDependency> = listOf(),
|
||||
kind: PluginKind = PluginKind.NORMAL,
|
||||
) : this(name, Semver(version, Semver.SemverType.LOOSE), author, info, dependencies, kind)
|
||||
|
||||
init {
|
||||
require(!name.contains(':')) { "':' is forbidden in plugin name" }
|
||||
}
|
||||
|
||||
/**
|
||||
* 在手动实现时使用这个构造器.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
public constructor(
|
||||
kind: PluginKind, name: String, mainClassName: String, author: String,
|
||||
version: Semver, info: String, depends: List<PluginDependency>,
|
||||
file: File
|
||||
) : this(kind, name, mainClassName, author, version, info, depends) {
|
||||
this._file = file
|
||||
}
|
||||
|
||||
public val file: File
|
||||
get() = _file ?: error("Internal error: JvmPluginDescription(name=$name)._file == null")
|
||||
|
||||
|
||||
@Suppress("PropertyName")
|
||||
@Transient
|
||||
@JvmField
|
||||
internal var _file: File? = null
|
||||
|
||||
public override fun toString(): String {
|
||||
return "JvmPluginDescription(kind=$kind, name='$name', mainClassName='$mainClassName', author='$author', version='$version', info='$info', dependencies=$dependencies, _file=$_file)"
|
||||
}
|
||||
}
|
@ -20,27 +20,10 @@ import kotlin.coroutines.EmptyCoroutineContext
|
||||
* 必须通过 "plugin.yml" 指定主类并由 [JarPluginLoader] 加载.
|
||||
*/
|
||||
public abstract class KotlinPlugin @JvmOverloads constructor(
|
||||
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
|
||||
public final override val description: JvmPluginDescription,
|
||||
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext,
|
||||
) : JvmPlugin, AbstractJvmPlugin(parentCoroutineContext)
|
||||
|
||||
/**
|
||||
* 在内存动态加载的插件. 此为预览版本 API.
|
||||
*/
|
||||
public abstract class KotlinMemoryPlugin @JvmOverloads constructor(
|
||||
description: JvmPluginDescription,
|
||||
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
|
||||
) : JvmPlugin, AbstractJvmPlugin(parentCoroutineContext) {
|
||||
internal final override var _description: JvmPluginDescription
|
||||
get() = super._description
|
||||
set(value) {
|
||||
super._description = value
|
||||
}
|
||||
|
||||
init {
|
||||
_description = description
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
public object MyPlugin : KotlinPlugin()
|
||||
|
@ -14,7 +14,7 @@
|
||||
package net.mamoe.mirai.console.util
|
||||
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.console.internal.util.BotManagerImpl
|
||||
import net.mamoe.mirai.console.internal.data.builtin.BotManagerImpl
|
||||
import net.mamoe.mirai.contact.User
|
||||
|
||||
public interface BotManager {
|
||||
|
@ -11,6 +11,7 @@ package net.mamoe.mirai.console.data
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
|
||||
import net.mamoe.mirai.console.plugin.jvm.SimpleJvmPluginDescription
|
||||
import net.mamoe.mirai.console.util.ConsoleInternalAPI
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
@ -19,7 +20,11 @@ import kotlin.test.assertSame
|
||||
@OptIn(ConsoleInternalAPI::class)
|
||||
internal class PluginDataTest {
|
||||
|
||||
object MyPlugin : KotlinPlugin()
|
||||
object MyPlugin : KotlinPlugin(
|
||||
SimpleJvmPluginDescription(
|
||||
"1", "2"
|
||||
)
|
||||
)
|
||||
|
||||
class MyPluginData : AutoSavePluginData() {
|
||||
var int by value(1)
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
object Versions {
|
||||
const val core = "1.2.2"
|
||||
const val console = "1.0-M3-1"
|
||||
const val console = "1.0-RC-dev-1"
|
||||
const val consoleGraphical = "0.0.7"
|
||||
const val consoleTerminal = "0.1.0"
|
||||
const val consolePure = console
|
||||
|
@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
kotlin("plugin.serialization")
|
||||
kotlin("kapt")
|
||||
id("java")
|
||||
`maven-publish`
|
||||
id("com.jfrog.bintray")
|
||||
@ -43,6 +44,12 @@ dependencies {
|
||||
runtimeOnly("net.mamoe:mirai-core-qqandroid:${Versions.core}")
|
||||
testApi("net.mamoe:mirai-core-qqandroid:${Versions.core}")
|
||||
testApi(project(":mirai-console"))
|
||||
|
||||
|
||||
val autoService = "1.0-rc7"
|
||||
kapt("com.google.auto.service", "auto-service", autoService)
|
||||
compileOnly("com.google.auto.service", "auto-service-annotations", autoService)
|
||||
testCompileOnly("com.google.auto.service", "auto-service-annotations", autoService)
|
||||
}
|
||||
|
||||
ext.apply {
|
||||
|
Loading…
Reference in New Issue
Block a user