Implement SettingStorage

This commit is contained in:
Him188 2020-06-28 10:43:25 +08:00
parent e25a818942
commit 442d7ee0ce
7 changed files with 248 additions and 80 deletions

View File

@ -47,7 +47,7 @@ internal abstract class JvmPluginInternal(
* Initialized immediately after construction of [JvmPluginInternal] instance
*/
@Suppress("PropertyName")
internal lateinit var _description: JvmPluginDescription
internal open lateinit var _description: JvmPluginDescription
override val description: JvmPluginDescription get() = _description

View File

@ -11,6 +11,7 @@ package net.mamoe.mirai.console.plugin.jvm
import kotlinx.coroutines.*
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.MiraiConsoleInternal
import net.mamoe.mirai.console.plugin.AbstractFilePluginLoader
import net.mamoe.mirai.console.plugin.PluginLoadException
import net.mamoe.mirai.console.plugin.internal.JvmPluginInternal
@ -28,24 +29,21 @@ import kotlin.reflect.full.createInstance
* 内建的 Jar (JVM) 插件加载器
*/
object JarPluginLoader : AbstractFilePluginLoader<JvmPlugin, JvmPluginDescription>(".jar"), CoroutineScope {
private val logger: MiraiLogger by lazy {
MiraiConsole.newLogger(JarPluginLoader::class.simpleName!!)
}
private val logger: MiraiLogger = MiraiConsole.newLogger(JarPluginLoader::class.simpleName!!)
@ConsoleExperimentalAPI
val settingStorage: SettingStorage by lazy { TODO() }
val settingStorage: SettingStorage = MiraiConsoleInternal.settingStorage
override val coroutineContext: CoroutineContext by lazy {
MiraiConsole.coroutineContext + SupervisorJob(
MiraiConsole.coroutineContext[Job]
) + CoroutineExceptionHandler { _, throwable ->
logger.error("Unhandled Jar plugin exception: ${throwable.message}", throwable)
}.also { init() }
}
override val coroutineContext: CoroutineContext =
MiraiConsole.coroutineContext +
SupervisorJob(MiraiConsole.coroutineContext[Job]) +
CoroutineExceptionHandler { _, throwable ->
logger.error("Unhandled Jar plugin exception: ${throwable.message}", throwable)
}
private val classLoader: PluginsLoader by lazy { PluginsLoader(this.javaClass.classLoader) }
private val classLoader: PluginsLoader = PluginsLoader(this.javaClass.classLoader)
private fun init() { // delayed
init { // delayed
coroutineContext[Job]!!.invokeOnCompletion {
classLoader.clear()
}

View File

@ -22,61 +22,6 @@ 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

@ -14,8 +14,8 @@ 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.AutoSaveSettingHolder
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
@ -32,7 +32,7 @@ import kotlin.reflect.KClass
* @see JvmPlugin 支持文件系统扩展
* @see ResourceContainer 支持资源获取 ( Jar 中的资源文件)
*/
interface JvmPlugin : Plugin, CoroutineScope, PluginFileExtensions, ResourceContainer, SettingHolder {
interface JvmPlugin : Plugin, CoroutineScope, PluginFileExtensions, ResourceContainer, AutoSaveSettingHolder {
/** 日志 */
val logger: MiraiLogger
@ -47,6 +47,7 @@ interface JvmPlugin : Plugin, CoroutineScope, PluginFileExtensions, ResourceCont
*/
fun <T : Setting> getSetting(clazz: Class<T>): T
@JvmDefault
fun onLoad() {
}

View File

@ -11,12 +11,44 @@
package net.mamoe.mirai.console.plugin.jvm
import net.mamoe.mirai.console.setting.Setting
import net.mamoe.mirai.console.setting.getValue
import net.mamoe.mirai.console.setting.value
import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
/**
* Kotlin 插件的父类
* Kotlin 插件的父类.
*
* 必须通过 "plugin.yml" 指定主类并由 [JarPluginLoader] 加载.
*/
abstract class KotlinPlugin @JvmOverloads constructor(
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
) : JvmPlugin, AbstractJvmPlugin(parentCoroutineContext)
) : JvmPlugin, AbstractJvmPlugin(parentCoroutineContext)
/**
* 在内存动态加载的插件.
*/
@ConsoleExperimentalAPI
abstract class KotlinMemoryPlugin @JvmOverloads constructor(
description: JvmPluginDescription,
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
) : JvmPlugin, AbstractJvmPlugin(parentCoroutineContext) {
final override var _description: JvmPluginDescription
get() = super._description
set(value) {
super._description = value
}
init {
_description = description
}
}
object MyPlugin : KotlinPlugin()
object AccountSetting : Setting by MyPlugin.getSetting() {
val s by value(1)
}

View File

@ -1,19 +1,211 @@
package net.mamoe.mirai.console.setting
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import net.mamoe.mirai.console.command.internal.qualifiedNameOrTip
import net.mamoe.mirai.console.plugin.internal.updateWhen
import net.mamoe.mirai.console.plugin.jvm.getSetting
import net.mamoe.mirai.console.setting.AutoSaveSettingHolder.AutoSaveSetting
import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI
import net.mamoe.mirai.utils.currentTimeMillis
import net.mamoe.mirai.utils.minutesToSeconds
import net.mamoe.mirai.utils.secondsToMillis
import net.mamoe.yamlkt.Yaml
import java.io.File
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.full.findAnnotation
/**
* [Setting] 存储容器
*/
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
@ConsoleExperimentalAPI
/**
* 保存一个实例
*/
fun store(holder: SettingHolder, setting: Setting)
}
@ConsoleExperimentalAPI
fun <T : Setting> SettingStorage.load(holder: SettingHolder, settingClass: KClass<T>): T =
this.load(holder, settingClass.java)
/**
* 可以持有相关 [Setting] 的对象.
*
* @see SettingStorage.load
* @see SettingStorage.store
*
* @see AutoSaveSettingHolder 自动保存
*/
interface SettingHolder {
/**
* 保存时使用的分类名
*/
val name: String
}
}
/**
* 可以持有相关 [AutoSaveSetting] 的对象.
*/
interface AutoSaveSettingHolder : SettingHolder, CoroutineScope {
/**
* [AutoSaveSetting] 每次自动保存时间间隔
*
* - 区间的左端点为最小间隔, 一个 [Value] 被修改后, 若此时间段后无其他修改, 将触发自动保存; 若有, 将重新开始计时.
* - 区间的右端点为最大间隔, 一个 [Value] 被修改后, 最多不超过这个时间段后就会被保存.
*
* [coroutineContext] 含有 [Job], [AutoSaveSetting] 会通过 [Job.invokeOnCompletion] Job 完结时触发自动保存.
*/
val autoSaveIntervalMillis: LongRange
get() = 30.secondsToMillis..10.minutesToSeconds
/**
* 链接自动保存的 [Setting].
* 当任一相关 [Value] 的值被修改时, 将在一段时间无其他修改时保存
*
* [AutoSaveSettingHolder.coroutineContext] 含有 [Job], [AutoSaveSetting] 会通过 [Job.invokeOnCompletion] Job 完结时触发自动保存.
*
* @see getSetting
*/
open class AutoSaveSetting(private val owner: AutoSaveSettingHolder, private val storage: SettingStorage) :
AbstractSetting() {
@Volatile
internal var lastAutoSaveJob: Job? = null
@Volatile
internal var currentFirstStartTime = atomic(0L)
init {
owner.coroutineContext[Job]?.invokeOnCompletion { doSave() }
}
private val updaterBlock: suspend CoroutineScope.() -> Unit = {
currentFirstStartTime.updateWhen({ it == 0L }, { currentTimeMillis })
delay(owner.autoSaveIntervalMillis.first.coerceAtLeast(1000)) // for safety
if (lastAutoSaveJob == this.coroutineContext[Job]) {
doSave()
} else {
if (currentFirstStartTime.updateWhen(
{ currentTimeMillis - it >= owner.autoSaveIntervalMillis.last },
{ 0 })
) doSave()
}
}
final override fun onValueChanged(value: Value<*>) {
lastAutoSaveJob = owner.launch(block = updaterBlock)
}
private fun doSave() = storage.store(owner, this)
}
}
object MemorySettingStorage : SettingStorage {
private val list = mutableMapOf<Class<out Setting>, Setting>()
internal class MemorySettingImpl : AbstractSetting() {
override fun onValueChanged(value: Value<*>) {
// nothing to do
}
}
@Suppress("UNCHECKED_CAST")
override fun <T : Setting> load(holder: SettingHolder, settingClass: Class<T>): T {
return synchronized(list) {
list.getOrPut(settingClass) {
settingClass.kotlin.run {
objectInstance ?: createInstanceOrNull() ?: kotlin.run {
if (settingClass != Setting::class.java) {
throw IllegalArgumentException(
"Cannot create Setting instance. Make sure settingClass is Setting::class.java or a Kotlin's object, " +
"or has a constructor which either has no parameters or all parameters of which are optional"
)
}
MemorySettingImpl()
}
}
}
} as T
}
override fun store(holder: SettingHolder, setting: Setting) {
synchronized(list) {
list[setting::class.java] = setting
}
}
}
class MultiFileSettingStorage(
private val directory: File
) : SettingStorage {
override fun <T : Setting> load(holder: SettingHolder, settingClass: Class<T>): T = with(settingClass.kotlin) {
val file = settingFile(holder, settingClass::class)
@Suppress("UNCHECKED_CAST")
val instance = objectInstance ?: this.createInstanceOrNull() ?: kotlin.run {
if (settingClass != Setting::class.java) {
throw IllegalArgumentException(
"Cannot create Setting instance. Make sure settingClass is Setting::class.java or a Kotlin's object, " +
"or has a constructor which either has no parameters or all parameters of which are optional"
)
}
if (holder is AutoSaveSettingHolder) {
AutoSaveSetting(holder, this@MultiFileSettingStorage) as T?
} else null
} ?: throw IllegalArgumentException(
"Cannot create Setting instance. Make sure 'holder' is a AutoSaveSettingHolder, " +
"or 'setting' is an object or has a constructor which either has no parameters or all parameters of which are optional"
)
if (file.exists() && file.isFile && file.canRead()) {
Yaml.default.parse(instance.updaterSerializer, file.readText())
}
instance
}
private fun settingFile(holder: SettingHolder, clazz: KClass<*>): File = with(clazz) {
val name = findASerialName()
val dir = File(directory, holder.name)
if (dir.isFile) {
error("Target directory ${dir.path} for holder $holder is occupied by a file therefore setting $qualifiedNameOrTip can't be saved.")
}
val file = File(directory, name)
if (file.isDirectory) {
error("Target file $file is occupied by a directory therefore setting $qualifiedNameOrTip can't be saved.")
}
return file
}
@ConsoleExperimentalAPI
override fun store(holder: SettingHolder, setting: Setting) = with(setting::class) {
val file = settingFile(holder, this)
if (file.exists() && file.isFile && file.canRead()) {
file.writeText(Yaml.default.stringify(setting.updaterSerializer, Unit))
}
}
}
private fun <T : Any> KClass<T>.createInstanceOrNull(): T? {
val noArgsConstructor = constructors.singleOrNull { it.parameters.all(KParameter::isOptional) }
?: return null
return noArgsConstructor.callBy(emptyMap())
}
private fun KClass<*>.findASerialName(): String =
findAnnotation<SerialName>()?.value
?: qualifiedName
?: throw IllegalArgumentException("Cannot find a serial name for $this")

View File

@ -9,7 +9,7 @@
object Versions {
const val core = "1.1-EA"
const val console = "0.5.1"
const val console = "1.1-dev-1"
const val consoleGraphical = "0.0.7"
const val consoleTerminal = "0.1.0"
const val consolePure = "0.1.0"