/* * 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 through the following link. * * https://github.com/mamoe/mirai/blob/master/LICENSE */ @file:Suppress("unused", "PropertyName", "PrivatePropertyName") package net.mamoe.mirai.console.data import kotlinx.atomicfu.atomic import kotlinx.coroutines.* import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.internal.data.qualifiedNameOrTip import net.mamoe.mirai.console.internal.plugin.updateWhen import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.utils.* /** * 链接自动保存的 [PluginData]. * * 当任一相关 [Value] 的值被修改时, 将在一段时间无其他修改时保存 * * 若 [AutoSavePluginDataHolder.coroutineContext] 含有 [Job], 则 [AutoSavePluginData] 会通过 [Job.invokeOnCompletion] 在 Job 完结时触发自动保存. * * @see PluginData */ public open class AutoSavePluginData private constructor( // KEEP THIS PRIMARY CONSTRUCTOR FOR FUTURE USE: WE'LL SUPPORT SERIALIZERS_MODULE FOR POLYMORPHISM @Suppress("UNUSED_PARAMETER") primaryConstructorMark: Any?, ) : AbstractPluginData() { private lateinit var owner_: AutoSavePluginDataHolder private val autoSaveIntervalMillis_: LongRange get() = owner_.autoSaveIntervalMillis private lateinit var storage_: PluginDataStorage public final override val saveName: String get() = _saveName private lateinit var _saveName: String public constructor(saveName: String) : this(null) { _saveName = saveName } @ConsoleExperimentalApi override fun onInit(owner: PluginDataHolder, storage: PluginDataStorage) { check(owner is AutoSavePluginDataHolder) { "owner must be AutoSavePluginDataHolder for AutoSavePluginData" } if (this::storage_.isInitialized) { check(storage == this.storage_) { "AutoSavePluginData is already initialized with one storage and cannot be reinitialized with another." } } this.storage_ = storage this.owner_ = owner owner_.coroutineContext[Job]?.invokeOnCompletion { kotlin.runCatching { doSave() }.onFailure { e -> owner_.coroutineContext[CoroutineExceptionHandler]?.handleException(owner_.coroutineContext, e) ?.let { return@invokeOnCompletion } MiraiConsole.mainLogger.error( "An exception occurred when saving config ${this@AutoSavePluginData::class.qualifiedNameOrTip} " + "but CoroutineExceptionHandler not found in PluginDataHolder.coroutineContext for ${owner::class.qualifiedNameOrTip}", e ) } } if (shouldPerformAutoSaveWheneverChanged()) { owner_.launch(CoroutineName("AutoSavePluginData.timedAutoSave: ${this::class.qualifiedNameOrTip}")) { while (isActive) { try { delay(autoSaveIntervalMillis_.last) // 定时自动保存一次, 用于 kts 序列化的对象 } catch (e: CancellationException) { return@launch } withContext(owner_.coroutineContext) { doSave() } } } } } @JvmField @Volatile internal var lastAutoSaveJob_: Job? = null @JvmField internal val currentFirstStartTime_ = atomic(MAGIC_NUMBER_CFST_INIT) /** * @return `true` 时, 一段时间后, 即使无属性改变, 也会进行保存. */ @ConsoleExperimentalApi protected open fun shouldPerformAutoSaveWheneverChanged(): Boolean { return true } private val updaterBlock: suspend CoroutineScope.() -> Unit = l@{ if (::storage_.isInitialized) { currentFirstStartTime_.updateWhen({ it == MAGIC_NUMBER_CFST_INIT }, { currentTimeMillis }) try { delay(autoSaveIntervalMillis_.first.coerceAtLeast(1000)) // for safety } catch (e: CancellationException) { return@l } if (lastAutoSaveJob_ == this.coroutineContext[Job]) { withContext(owner_.coroutineContext) { doSave() } } else { if (currentFirstStartTime_.updateWhen( { it != MAGIC_NUMBER_CFST_INIT && currentTimeMillis - it >= autoSaveIntervalMillis_.last }, { MAGIC_NUMBER_CFST_INIT }) ) { withContext(owner_.coroutineContext) { doSave() } } } } } @ConsoleExperimentalApi public final override fun onValueChanged(value: Value<*>) { debuggingLogger1.error { "onValueChanged: $value" } if (::owner_.isInitialized) { lastAutoSaveJob_ = owner_.launch( block = updaterBlock, context = CoroutineName("AutoSavePluginData.passiveAutoSave: ${this::class.qualifiedNameOrTip}") ) } } private fun doSave() { debuggingLogger1.error { "doSave: ${this::class.qualifiedName}" } storage_.store(owner_, this) } } internal val debuggingLogger1 by lazy { DefaultLogger("console.debug").withSwitch(false) } @Suppress("RESULT_CLASS_IN_RETURN_TYPE") internal inline fun MiraiLogger.runCatchingLog(message: String? = null, block: () -> R): Result { return kotlin.runCatching { block() }.onFailure { if (message != null) { error(message, it) } else error(it) } } @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") @kotlin.internal.LowPriorityInOverloadResolution internal inline fun MiraiLogger.runCatchingLog(message: (Throwable) -> String, block: () -> R): R? { return kotlin.runCatching { block() }.onFailure { error(message(it), it) }.getOrNull() } @Suppress("SpellCheckingInspection") private const val MAGIC_NUMBER_CFST_INIT: Long = Long.MAX_VALUE