From e76cfe8c6958af59ad8efd0d4bd38b26ad8dc2c7 Mon Sep 17 00:00:00 2001 From: Him188 Date: Sat, 27 Jun 2020 00:16:36 +0800 Subject: [PATCH] Implement PluginSetting and its auto-saving; Introduce PluginFileExtensions; Introduce ResourceContainer; Introduce SettingHolder; Add extensions to JvmPlugin for getSetting; Add Plugin.safeLoader extension to eliminate unchecked casting; Various documentation improvements; --- .../net/mamoe/mirai/console/plugin/Plugin.kt | 28 ++++++++- .../mirai/console/plugin/PluginLoader.kt | 18 ++++-- ...{JvmPluginImpl.kt => JvmPluginInternal.kt} | 23 +++++++- .../console/plugin/jvm/AbstractJvmPlugin.kt | 19 ++++-- .../console/plugin/jvm/JarPluginLoader.kt | 17 ++++-- .../mirai/console/plugin/jvm/JavaPlugin.kt | 56 ++++++++++++++++++ .../mirai/console/plugin/jvm/JvmPlugin.kt | 18 +++++- .../mamoe/mirai/console/setting/Setting.kt | 34 +++++++++-- .../mirai/console/setting/SettingStorage.kt | 18 +++++- .../console/setting/internal/SettingImpl.kt | 9 +++ .../mirai/console/utils/ResourceContainer.kt | 58 +++++++++++++++++++ .../mirai/console/setting/SettingTest.kt | 2 +- 12 files changed, 273 insertions(+), 27 deletions(-) rename backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/{JvmPluginImpl.kt => JvmPluginInternal.kt} (81%) create mode 100644 backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/ResourceContainer.kt diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt index f5a8c5142..c5e5e3144 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt @@ -10,22 +10,44 @@ package net.mamoe.mirai.console.plugin import net.mamoe.mirai.console.plugin.jvm.JvmPlugin +import net.mamoe.mirai.utils.MiraiExperimentalAPI import java.io.File /** * 表示一个 mirai-console 插件. * - * @see JvmPlugin * @see PluginDescription 插件描述 + * @see JvmPlugin Java, Kotlin 或其他 JVM 平台插件 + * @see PluginFileExtensions 支持文件系统存储的扩展 + * + * @see PluginLoader 插件加载器 */ interface Plugin { /** - * 所属插件加载器实例 + * 所属插件加载器实例, 此加载器必须能加载这个 [Plugin]. */ val loader: PluginLoader<*, *> +} +@Suppress("UNCHECKED_CAST") +inline val

P.safeLoader: PluginLoader + get() = this.loader as PluginLoader + +/** + * 支持文件系统存储的扩展. + * + * @see JvmPlugin + */ +@MiraiExperimentalAPI("classname is subject to change") +interface PluginFileExtensions { /** - * 插件数据目录 + * 数据目录 */ val dataFolder: File + + /** + * 从数据目录获取一个文件, 若不存在则创建文件. + */ + @JvmDefault + fun file(relativePath: String): File = File(dataFolder, relativePath).apply { createNewFile() } } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginLoader.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginLoader.kt index d0e3fdb8b..2ccc8500c 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginLoader.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginLoader.kt @@ -7,7 +7,7 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -@file:Suppress("unused") +@file:Suppress("unused", "INAPPLICABLE_JVM_NAME") package net.mamoe.mirai.console.plugin @@ -20,6 +20,8 @@ import java.io.File * 插件加载器只实现寻找插件列表, 加载插件, 启用插件, 关闭插件这四个功能. * * 有关插件的依赖和已加载的插件列表由 [PluginManager] 维护. + * + * @see JarPluginLoader Jar 插件加载器 */ interface PluginLoader

{ /** @@ -29,12 +31,15 @@ interface PluginLoader

{ /** * 获取此插件的描述 + * + * @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如无法读取插件信息等). */ - @Throws(PluginLoadException::class) - fun getPluginDescription(plugin: P): D + @get:JvmName("getPluginDescription") + @get:Throws(PluginLoadException::class) + val P.description: D /** - * 加载一个插件 (实例), 但不 [启用][enable] 它. 返回加载成功的实例 + * 加载一个插件 (实例), 但不 [启用][enable] 它. 返回加载成功的主类实例 * * @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如找不到主类等). */ @@ -55,7 +60,7 @@ open class PluginLoadException : RuntimeException { /** * '/plugins' 目录中的插件的加载器. 每个加载器需绑定一个后缀. * - * @see AbstractFilePluginLoader + * @see AbstractFilePluginLoader 默认基础实现 * @see JarPluginLoader 内建的 Jar (JVM) 插件加载器. */ interface FilePluginLoader

: PluginLoader { @@ -65,6 +70,9 @@ interface FilePluginLoader

: PluginLoader( override val fileSuffix: String ) : FilePluginLoader { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/JvmPluginImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/JvmPluginInternal.kt similarity index 81% rename from backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/JvmPluginImpl.kt rename to backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/JvmPluginInternal.kt index 48fa96095..e658e7baa 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/JvmPluginImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/JvmPluginInternal.kt @@ -9,6 +9,7 @@ package net.mamoe.mirai.console.plugin.internal +import kotlinx.atomicfu.AtomicLong import kotlinx.atomicfu.locks.withLock import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope @@ -19,8 +20,10 @@ import net.mamoe.mirai.console.plugin.Plugin import net.mamoe.mirai.console.plugin.PluginManager import net.mamoe.mirai.console.plugin.jvm.JvmPlugin import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription +import net.mamoe.mirai.console.utils.asResourceContainer import net.mamoe.mirai.utils.MiraiLogger import java.io.File +import java.io.InputStream import java.util.concurrent.locks.ReentrantLock import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -31,13 +34,17 @@ internal val T.job: Job where T : CoroutineScope, T : Plugin get() = this.co * Hides implementations from [JvmPlugin] */ @PublishedApi -internal abstract class JvmPluginImpl( +internal abstract class JvmPluginInternal( parentCoroutineContext: CoroutineContext ) : JvmPlugin, CoroutineScope { + + private val resourceConsoleDelegate by lazy { this::class.asResourceContainer() } + override fun getResourceAsStream(name: String): InputStream = resourceConsoleDelegate.getResourceAsStream(name) + // region JvmPlugin /** - * Initialized immediately after construction of [JvmPluginImpl] instance + * Initialized immediately after construction of [JvmPluginInternal] instance */ @Suppress("PropertyName") internal lateinit var _description: JvmPluginDescription @@ -102,4 +109,16 @@ internal abstract class JvmPluginImpl( ?: contextUpdateLock.withLock { _coroutineContext ?: refreshCoroutineContext() } // endregion +} + +internal inline fun AtomicLong.updateWhen(condition: (Long) -> Boolean, update: (Long) -> Long): Boolean { + while (true) { + val current = value + if (condition(current)) { + if (compareAndSet(0, update(current))) { + return true + } else continue + } + return false + } } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt index 8f5a75ad9..ad910d908 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt @@ -11,7 +11,10 @@ package net.mamoe.mirai.console.plugin.jvm -import net.mamoe.mirai.console.plugin.internal.JvmPluginImpl +import net.mamoe.mirai.console.plugin.internal.JvmPluginInternal +import net.mamoe.mirai.console.setting.Setting +import net.mamoe.mirai.console.setting.getValue +import net.mamoe.mirai.console.setting.value import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -23,8 +26,16 @@ import kotlin.coroutines.EmptyCoroutineContext */ abstract class AbstractJvmPlugin @JvmOverloads constructor( parentCoroutineContext: CoroutineContext = EmptyCoroutineContext -) : JvmPlugin, JvmPluginImpl(parentCoroutineContext) { - // TODO: 2020/6/24 添加 PluginSetting 继承 Setting, 实现 onValueChanged 并绑定自动保存. +) : JvmPlugin, JvmPluginInternal(parentCoroutineContext) { + final override val name: String get() = this.description.name - abstract class PluginSetting + override fun getSetting(clazz: Class): T = loader.settingStorage.load(this, clazz) +} + + +object MyPlugin : KotlinPlugin() + +object TestSetting : Setting by MyPlugin.getSetting() { + val account by value("123456") + val password by value("123") } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JarPluginLoader.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JarPluginLoader.kt index 9c7eca155..27633a20f 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JarPluginLoader.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JarPluginLoader.kt @@ -13,8 +13,10 @@ import kotlinx.coroutines.* import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.plugin.AbstractFilePluginLoader import net.mamoe.mirai.console.plugin.PluginLoadException -import net.mamoe.mirai.console.plugin.internal.JvmPluginImpl +import net.mamoe.mirai.console.plugin.internal.JvmPluginInternal import net.mamoe.mirai.console.plugin.internal.PluginsLoader +import net.mamoe.mirai.console.setting.SettingStorage +import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.yamlkt.Yaml import java.io.File @@ -30,6 +32,9 @@ object JarPluginLoader : AbstractFilePluginLoader.mapToDescription(): List { return this.associateWith { URL("jar:${it.absolutePath}!/plugin.yml") }.mapNotNull { (file, url) -> @@ -82,7 +89,7 @@ object JarPluginLoader : AbstractFilePluginLoader) { + lastAutoSaveJob = launch { + currentFirstStartTime.updateWhen({ it == 0L }, { currentTimeMillis }) + + delay(autoSaveIntervalMillis.first.coerceAtLeast(1000)) // for safety + + if (lastAutoSaveJob == job) { + doSave() + } else { + if (currentFirstStartTime.updateWhen( + { currentTimeMillis - it >= autoSaveIntervalMillis.last }, + { 0 }) + ) { + doSave() + } + } + } + } + + private fun doSave() { + loader.settingStorage.store(this@AbstractJvmPlugin, this@PluginSetting) + } + } + */ + /** * Java API Scheduler */ diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt index 243300905..8a629e0c9 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt @@ -13,7 +13,12 @@ package net.mamoe.mirai.console.plugin.jvm import kotlinx.coroutines.CoroutineScope import net.mamoe.mirai.console.plugin.Plugin +import net.mamoe.mirai.console.plugin.PluginFileExtensions +import net.mamoe.mirai.console.setting.Setting +import net.mamoe.mirai.console.setting.SettingHolder +import net.mamoe.mirai.console.utils.ResourceContainer import net.mamoe.mirai.utils.MiraiLogger +import kotlin.reflect.KClass /** @@ -23,8 +28,11 @@ import net.mamoe.mirai.utils.MiraiLogger * * @see JavaPlugin Java 插件 * @see KotlinPlugin Kotlin 插件 + * + * @see JvmPlugin 支持文件系统扩展 + * @see ResourceContainer 支持资源获取 (如 Jar 中的资源文件) */ -interface JvmPlugin : Plugin, CoroutineScope { +interface JvmPlugin : Plugin, CoroutineScope, PluginFileExtensions, ResourceContainer, SettingHolder { /** 日志 */ val logger: MiraiLogger @@ -34,6 +42,11 @@ interface JvmPlugin : Plugin, CoroutineScope { /** 所属插件加载器实例 */ override val loader: JarPluginLoader get() = JarPluginLoader + /** + * 获取一个 [Setting] 实例 + */ + fun getSetting(clazz: Class): T + @JvmDefault fun onLoad() { } @@ -47,4 +60,5 @@ interface JvmPlugin : Plugin, CoroutineScope { } } - +fun JvmPlugin.getSetting(clazz: KClass) = this.getSetting(clazz.java) +inline fun JvmPlugin.getSetting() = this.getSetting(T::class) \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/Setting.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/Setting.kt index c79a836b6..01f8ecb86 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/Setting.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/Setting.kt @@ -17,9 +17,23 @@ import kotlin.internal.LowPriorityInOverloadResolution import kotlin.reflect.KProperty import kotlin.reflect.KType -// Shows public APIs such as deciding when to auto-save. -abstract class Setting : SettingImpl() { - operator fun SerializerAwareValue.provideDelegate( + +/** + * 序列化之后的名称. + * + * 例: + * ``` + * class MySetting : Setting() { + * + * } + * ``` + */ +// TODO: 2020/6/26 document +typealias SerialName = kotlinx.serialization.SerialName + +// TODO: 2020/6/26 document +abstract class AbstractSetting : Setting, SettingImpl() { + final override operator fun SerializerAwareValue.provideDelegate( thisRef: Any?, property: KProperty<*> ): SerializerAwareValue { @@ -28,7 +42,19 @@ abstract class Setting : SettingImpl() { return this } - public override val updaterSerializer: KSerializer get() = super.updaterSerializer + final override val updaterSerializer: KSerializer get() = super.updaterSerializer +} + +// TODO: 2020/6/26 document +interface Setting { + // TODO: 2020/6/26 document + operator fun SerializerAwareValue.provideDelegate( + thisRef: Any?, + property: KProperty<*> + ): SerializerAwareValue + + // TODO: 2020/6/26 document + val updaterSerializer: KSerializer } //// region Setting_value_primitives CODEGEN //// diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/SettingStorage.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/SettingStorage.kt index e578269b3..abe7424e3 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/SettingStorage.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/SettingStorage.kt @@ -1,3 +1,19 @@ package net.mamoe.mirai.console.setting -interface SettingStorage +import net.mamoe.mirai.utils.MiraiExperimentalAPI +import kotlin.reflect.KClass + +interface SettingStorage { + @JvmDefault + fun load(holder: SettingHolder, settingClass: KClass): T = this.load(holder, settingClass.java) + + fun load(holder: SettingHolder, settingClass: Class): T + + @MiraiExperimentalAPI + fun store(holder: SettingHolder, setting: Setting) +} + +@MiraiExperimentalAPI +interface SettingHolder { + val name: String +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/SettingImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/SettingImpl.kt index 8569ec27e..44e4a10b4 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/SettingImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/SettingImpl.kt @@ -14,6 +14,7 @@ package net.mamoe.mirai.console.setting.internal import kotlinx.serialization.* import kotlinx.serialization.builtins.MapSerializer import kotlinx.serialization.builtins.serializer +import net.mamoe.mirai.console.setting.SerializerAwareValue import net.mamoe.mirai.console.setting.Setting import net.mamoe.mirai.console.setting.Value import net.mamoe.yamlkt.YamlNullableDynamicSerializer @@ -36,6 +37,14 @@ internal abstract class SettingImpl { val updaterSerializer: KSerializer ) + internal fun SerializerAwareValue.provideDelegateImpl( + property: KProperty<*> + ): SerializerAwareValue { + val name = property.serialName + valueNodes.add(Node(name, this, this.serializer)) + return this + } + internal val valueNodes: MutableList> = mutableListOf() internal open val updaterSerializer: KSerializer = object : KSerializer { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/ResourceContainer.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/ResourceContainer.kt new file mode 100644 index 000000000..6f8e8b4c5 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/ResourceContainer.kt @@ -0,0 +1,58 @@ +@file:Suppress("unused") + +package net.mamoe.mirai.console.utils + +import net.mamoe.mirai.console.encodeToString +import java.io.InputStream +import java.nio.charset.Charset +import kotlin.reflect.KClass + +/** + * 资源容器. + */ +interface ResourceContainer { + /** + * 获取一个资源文件 + */ + fun getResourceAsStream(name: String): InputStream + + /** + * 读取一个资源文件并以 [Charsets.UTF_8] 编码为 [String] + */ + @JvmDefault + fun getResource(name: String): String = getResource(name, Charsets.UTF_8) + + /** + * 读取一个资源文件并以 [charset] 编码为 [String] + */ + @JvmDefault + fun getResource(name: String, charset: Charset): String = + this.getResourceAsStream(name).use { it.readBytes() }.encodeToString() + + companion object { + /** + * 使用 [Class.getResourceAsStream] 读取资源文件 + * + * @see asResourceContainer Kotlin 使用 + */ + @JvmStatic + @JavaFriendlyAPI + fun byClass(clazz: Class<*>): ResourceContainer = clazz.asResourceContainer() + } +} + +internal class ClassAsResourceContainer( + private val clazz: Class<*> +) : ResourceContainer { + override fun getResourceAsStream(name: String): InputStream = clazz.getResourceAsStream(name) +} + +/** + * 使用 [Class.getResourceAsStream] 读取资源文件 + */ +fun KClass<*>.asResourceContainer(): ResourceContainer = ClassAsResourceContainer(this.java) + +/** + * 使用 [Class.getResourceAsStream] 读取资源文件 + */ +fun Class<*>.asResourceContainer(): ResourceContainer = ClassAsResourceContainer(this) \ No newline at end of file diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/setting/SettingTest.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/setting/SettingTest.kt index b3b2e51ed..26fd38504 100644 --- a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/setting/SettingTest.kt +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/setting/SettingTest.kt @@ -18,7 +18,7 @@ import kotlin.test.assertSame internal class SettingTest { - class MySetting : Setting() { + class MySetting : AbstractSetting() { var int by value(1) val map by value>() val map2 by value>>()