mirror of
https://github.com/mamoe/mirai.git
synced 2025-03-13 14:50:43 +08:00
Implement SettingStorage
This commit is contained in:
parent
e25a818942
commit
442d7ee0ce
@ -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
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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")
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user