Support PluginLoader with ServiceLoader

This commit is contained in:
Him188 2020-08-28 20:07:02 +08:00
parent 1bd1b5a4fd
commit 316a40e4bc
19 changed files with 147 additions and 458 deletions

View File

@ -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 {

View File

@ -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

View 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)
}
)

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.data.PluginDataStorage
import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge
import net.mamoe.mirai.console.util.childScope
import net.mamoe.mirai.utils.minutesToMillis

View File

@ -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()

View File

@ -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

View File

@ -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 =

View File

@ -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
}
}
}

View File

@ -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)
}

View File

@ -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 }
}
}

View File

@ -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
)
}

View File

@ -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>
}

View File

@ -31,9 +31,12 @@ import net.mamoe.mirai.utils.MiraiLogger
/**
* Java, Kotlin 或其他 JVM 平台插件
*
* ### ResourceContainer
* ## ResourceContainer
* 实现为 [ClassLoader.getResourceAsStream]
*
* ## 实现 [JvmPlugin]
* j
*
* @see AbstractJvmPlugin 默认实现
*
* @see JavaPlugin Java 插件

View File

@ -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)"
}
}

View 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()

View File

@ -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 {

View File

@ -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)

View File

@ -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

View File

@ -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 {