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;
This commit is contained in:
Him188 2020-06-27 00:16:36 +08:00
parent 6beed81f40
commit e76cfe8c69
12 changed files with 273 additions and 27 deletions

View File

@ -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 : Plugin> P.safeLoader: PluginLoader<P, PluginDescription>
get() = this.loader as PluginLoader<P, PluginDescription>
/**
* 支持文件系统存储的扩展.
*
* @see JvmPlugin
*/
@MiraiExperimentalAPI("classname is subject to change")
interface PluginFileExtensions {
/**
* 插件数据目录
* 数据目录
*/
val dataFolder: File
/**
* 从数据目录获取一个文件, 若不存在则创建文件.
*/
@JvmDefault
fun file(relativePath: String): File = File(dataFolder, relativePath).apply { createNewFile() }
}

View File

@ -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<P : Plugin, D : PluginDescription> {
/**
@ -29,12 +31,15 @@ interface PluginLoader<P : Plugin, D : PluginDescription> {
/**
* 获取此插件的描述
*
* @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<P : Plugin, D : PluginDescription> : PluginLoader<P, D> {
@ -65,6 +70,9 @@ interface FilePluginLoader<P : Plugin, D : PluginDescription> : PluginLoader<P,
val fileSuffix: String
}
/**
* [FilePluginLoader] 的默认基础实现
*/
abstract class AbstractFilePluginLoader<P : Plugin, D : PluginDescription>(
override val fileSuffix: String
) : FilePluginLoader<P, D> {

View File

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

View File

@ -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 <T : Setting> getSetting(clazz: Class<T>): 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")
}

View File

@ -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<JvmPlugin, JvmPluginDescriptio
MiraiConsole.newLogger(JarPluginLoader::class.simpleName!!)
}
@MiraiExperimentalAPI
val settingStorage: SettingStorage = TODO()
override val coroutineContext: CoroutineContext by lazy {
MiraiConsole.coroutineContext + SupervisorJob(
MiraiConsole.coroutineContext[Job]
@ -48,7 +53,9 @@ object JarPluginLoader : AbstractFilePluginLoader<JvmPlugin, JvmPluginDescriptio
}
}
override fun getPluginDescription(plugin: JvmPlugin): JvmPluginDescription = plugin.description
@Suppress("EXTENSION_SHADOWED_BY_MEMBER") // doesn't matter
override val JvmPlugin.description: JvmPluginDescription
get() = this.description
override fun Sequence<File>.mapToDescription(): List<JvmPluginDescription> {
return this.associateWith { URL("jar:${it.absolutePath}!/plugin.yml") }.mapNotNull { (file, url) ->
@ -82,7 +89,7 @@ object JarPluginLoader : AbstractFilePluginLoader<JvmPlugin, JvmPluginDescriptio
check(main is JvmPlugin) { "The main class of Jar plugin must extend JvmPlugin, recommending JavaPlugin or KotlinPlugin" }
if (main is JvmPluginImpl) {
if (main is JvmPluginInternal) {
main._description = description
main.internalOnLoad()
} else main.onLoad()
@ -93,13 +100,13 @@ object JarPluginLoader : AbstractFilePluginLoader<JvmPlugin, JvmPluginDescriptio
override fun enable(plugin: JvmPlugin) {
ensureActive()
if (plugin is JvmPluginImpl) {
if (plugin is JvmPluginInternal) {
plugin.internalOnEnable()
} else plugin.onEnable()
}
override fun disable(plugin: JvmPlugin) {
if (plugin is JvmPluginImpl) {
if (plugin is JvmPluginInternal) {
plugin.internalOnDisable()
} else plugin.onDisable()
}

View File

@ -22,6 +22,62 @@ abstract class JavaPlugin @JvmOverloads constructor(
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
) : JvmPlugin, AbstractJvmPlugin(parentCoroutineContext) {
/*
@Volatile
internal var lastAutoSaveJob: Job? = null
@Volatile
internal var currentFirstStartTime = atomic(0L)
/**
* [PluginSetting] 每次自动保存时间间隔
*
* - 区间的左端点为最小间隔, 一个 [Value] 被修改后, 若此时间段后无其他修改, 将触发自动保存; 若有, 将重新开始计时.
* - 区间的右端点为最大间隔, 一个 [Value] 被修改后, 最多不超过这个时间段后就会被保存.
*
* 备注: 当插件被关闭时, 所有相关 [PluginSetting] 总是会被自动保存.
*/
open val autoSaveIntervalMillis: LongRange
get() = 30.secondsToMillis..10.minutesToSeconds
/**
* 链接自动保存的 [Setting].
* 当任一相关 [Value] 的值被修改时, 将在一段时间无其他修改时保存
*/
abstract inner class PluginSetting : AbstractSetting() {
init {
this@AbstractJvmPlugin.job.invokeOnCompletion {
doSave()
}
}
final override fun onValueChanged(value: Value<*>) {
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
*/

View File

@ -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 <T : Setting> getSetting(clazz: Class<T>): T
@JvmDefault
fun onLoad() {
}
@ -47,4 +60,5 @@ interface JvmPlugin : Plugin, CoroutineScope {
}
}
fun <T : Setting> JvmPlugin.getSetting(clazz: KClass<T>) = this.getSetting(clazz.java)
inline fun <reified T : Setting> JvmPlugin.getSetting() = this.getSetting(T::class)

View File

@ -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 <T> SerializerAwareValue<T>.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 <T> SerializerAwareValue<T>.provideDelegate(
thisRef: Any?,
property: KProperty<*>
): SerializerAwareValue<T> {
@ -28,7 +42,19 @@ abstract class Setting : SettingImpl() {
return this
}
public override val updaterSerializer: KSerializer<Unit> get() = super.updaterSerializer
final override val updaterSerializer: KSerializer<Unit> get() = super.updaterSerializer
}
// TODO: 2020/6/26 document
interface Setting {
// TODO: 2020/6/26 document
operator fun <T> SerializerAwareValue<T>.provideDelegate(
thisRef: Any?,
property: KProperty<*>
): SerializerAwareValue<T>
// TODO: 2020/6/26 document
val updaterSerializer: KSerializer<Unit>
}
//// region Setting_value_primitives CODEGEN ////

View File

@ -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 <T : Setting> load(holder: SettingHolder, settingClass: KClass<T>): T = this.load(holder, settingClass.java)
fun <T : Setting> load(holder: SettingHolder, settingClass: Class<T>): T
@MiraiExperimentalAPI
fun store(holder: SettingHolder, setting: Setting)
}
@MiraiExperimentalAPI
interface SettingHolder {
val name: String
}

View File

@ -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<Unit>
)
internal fun <T> SerializerAwareValue<T>.provideDelegateImpl(
property: KProperty<*>
): SerializerAwareValue<T> {
val name = property.serialName
valueNodes.add(Node(name, this, this.serializer))
return this
}
internal val valueNodes: MutableList<Node<*>> = mutableListOf()
internal open val updaterSerializer: KSerializer<Unit> = object : KSerializer<Unit> {

View File

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

View File

@ -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<MutableMap<String, String>>()
val map2 by value<MutableMap<String, MutableMap<String, String>>>()