API stabilization: rearrange implementations

This commit is contained in:
Him188 2020-07-24 20:11:10 +08:00
parent ead891e223
commit 4d7826f3d9
28 changed files with 726 additions and 420 deletions

View File

@ -42,6 +42,7 @@ kotlin {
useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiInternalAPI")
useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiExperimentalAPI")
useExperimentalAnnotation("net.mamoe.mirai.console.ConsoleFrontEndImplementation")
useExperimentalAnnotation("net.mamoe.mirai.console.utils.ConsoleExperimentalAPI")
useExperimentalAnnotation("kotlin.ExperimentalUnsignedTypes")
useExperimentalAnnotation("kotlin.experimental.ExperimentalTypeInference")

View File

@ -7,7 +7,7 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("WRONG_MODIFIER_CONTAINING_DECLARATION")
@file:Suppress("WRONG_MODIFIER_CONTAINING_DECLARATION", "unused")
@file:OptIn(ConsoleInternalAPI::class)
package net.mamoe.mirai.console
@ -77,32 +77,22 @@ public interface MiraiConsole : CoroutineScope {
@ConsoleExperimentalAPI
public fun newLogger(identity: String?): MiraiLogger
public companion object INSTANCE : MiraiConsole by MiraiConsoleInternal
}
public class IllegalMiraiConsoleImplementationError(
override val message: String?
) : Error()
/**
* 获取 [MiraiConsole] [Job]
*/
public val MiraiConsole.job: Job
get() = this.coroutineContext[Job] ?: error("Internal error: Job not found in MiraiConsole.coroutineContext")
//// internal
internal object MiraiConsoleInitializer {
internal lateinit var instance: IMiraiConsole
/** 由前端调用 */
internal fun init(instance: IMiraiConsole) {
this.instance = instance
MiraiConsoleInternal.doStart()
public companion object INSTANCE : MiraiConsole by MiraiConsoleImplementationBridge {
/**
* 获取 [MiraiConsole] [Job]
*/ // MiraiConsole.INSTANCE.getJob()
public val job: Job
get() = MiraiConsole.coroutineContext[Job]
?: error("Internal error: Job not found in MiraiConsole.coroutineContext")
}
}
public class IllegalMiraiConsoleImplementationError @JvmOverloads constructor(
public override val message: String? = null,
public override val cause: Throwable? = null
) : Error()
internal object MiraiConsoleBuildConstants { // auto-filled on build (task :mirai-console:fillBuildConstants)
@JvmStatic
val buildDate: Date = Date(1595136353901L) // 2020-07-19 13:25:53
@ -110,12 +100,13 @@ internal object MiraiConsoleBuildConstants { // auto-filled on build (task :mira
}
/**
* mirai 控制台实例.
* [MiraiConsole] 公开 API 与前端实现的连接桥.
*/
internal object MiraiConsoleInternal : CoroutineScope, IMiraiConsole, MiraiConsole {
internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleImplementation,
MiraiConsole {
override val pluginCenter: PluginCenter get() = CuiPluginCenter
private val instance: IMiraiConsole get() = MiraiConsoleInitializer.instance
private val instance: MiraiConsoleImplementation get() = MiraiConsoleImplementation.instance
override val buildDate: Date get() = MiraiConsoleBuildConstants.buildDate
override val version: String get() = MiraiConsoleBuildConstants.version
override val rootDir: File get() = instance.rootDir
@ -147,7 +138,7 @@ internal object MiraiConsoleInternal : CoroutineScope, IMiraiConsole, MiraiConso
if (coroutineContext[Job] == null) {
throw IllegalMiraiConsoleImplementationError("The coroutineContext given to MiraiConsole must have a Job in it.")
}
job.invokeOnCompletion {
MiraiConsole.job.invokeOnCompletion {
Bot.botInstances.forEach { kotlin.runCatching { it.close() }.exceptionOrNull()?.let(mainLogger::error) }
}
@ -165,35 +156,6 @@ internal object MiraiConsoleInternal : CoroutineScope, IMiraiConsole, MiraiConso
}
}
// 前端使用
internal interface IMiraiConsole : CoroutineScope {
/**
* Console 运行路径
*/
val rootDir: File
/**
* Console 前端接口
*/
val frontEnd: MiraiConsoleFrontEnd
/**
* 与前端交互所使用的 Logger
*/
val mainLogger: MiraiLogger
/**
* 内建加载器列表, 一般需要包含 [JarPluginLoader]
*/
val builtInPluginLoaders: List<PluginLoader<*, *>>
val consoleCommandSender: ConsoleCommandSender
val settingStorageForJarPluginLoader: SettingStorage
val settingStorageForBuiltIns: SettingStorage
}
/**
* Included in kotlin stdlib 1.4
*/

View File

@ -10,13 +10,17 @@
package net.mamoe.mirai.console
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI
import net.mamoe.mirai.utils.LoginSolver
import net.mamoe.mirai.utils.MiraiLogger
/**
* 只需要实现一个这个传入 MiraiConsole 就可以绑定 UI 层与 Console
*
* 需要保证线程安全
*/
@ConsoleExperimentalAPI
@ConsoleFrontEndImplementation
public interface MiraiConsoleFrontEnd {
/**
* 名称

View File

@ -0,0 +1,78 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused")
package net.mamoe.mirai.console
import kotlinx.atomicfu.locks.withLock
import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.console.command.ConsoleCommandSender
import net.mamoe.mirai.console.plugin.PluginLoader
import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader
import net.mamoe.mirai.console.setting.SettingStorage
import net.mamoe.mirai.utils.MiraiLogger
import java.io.File
import java.util.concurrent.locks.ReentrantLock
import kotlin.annotation.AnnotationTarget.*
/**
* 标记一个仅用于 [MiraiConsole] 前端实现的 API. 这些 API 只应由前端实现者使用, 而不应该被插件或其他调用者使用.
*
* 前端实现时
*/
@Retention(AnnotationRetention.SOURCE)
@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
@Target(CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR)
@MustBeDocumented
public annotation class ConsoleFrontEndImplementation
/**
* [MiraiConsole] 前端实现, 需低啊用
*/
@ConsoleFrontEndImplementation
public interface MiraiConsoleImplementation : CoroutineScope {
/**
* Console 运行路径
*/
public val rootDir: File
/**
* Console 前端接口
*/
public val frontEnd: MiraiConsoleFrontEnd
/**
* 与前端交互所使用的 Logger
*/
public val mainLogger: MiraiLogger
/**
* 内建加载器列表, 一般需要包含 [JarPluginLoader]
*/
public val builtInPluginLoaders: List<PluginLoader<*, *>>
public val consoleCommandSender: ConsoleCommandSender
public val settingStorageForJarPluginLoader: SettingStorage
public val settingStorageForBuiltIns: SettingStorage
public companion object {
internal lateinit var instance: MiraiConsoleImplementation
private val initLock = ReentrantLock()
/** 由前端调用, 初始化 [MiraiConsole] 实例, 并 */
@JvmStatic
public fun MiraiConsoleImplementation.start(): Unit = initLock.withLock {
this@Companion.instance = this
MiraiConsoleImplementationBridge.doStart()
}
}
}

View File

@ -17,7 +17,6 @@ import kotlinx.coroutines.sync.withLock
import net.mamoe.mirai.Bot
import net.mamoe.mirai.alsoLogin
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.job
import net.mamoe.mirai.console.stacktraceString
import net.mamoe.mirai.event.selectMessagesUnit
import net.mamoe.mirai.utils.DirectoryLogger

View File

@ -13,7 +13,7 @@ package net.mamoe.mirai.console.command
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.MiraiConsoleInternal
import net.mamoe.mirai.console.MiraiConsoleImplementationBridge
import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI
import net.mamoe.mirai.console.utils.JavaFriendlyAPI
import net.mamoe.mirai.contact.*
@ -68,7 +68,7 @@ public abstract class ConsoleCommandSender internal constructor() : CommandSende
public final override val bot: Nothing? get() = null
public companion object {
internal val instance get() = MiraiConsoleInternal.consoleCommandSender
internal val instance get() = MiraiConsoleImplementationBridge.consoleCommandSender
}
}

View File

@ -12,7 +12,6 @@ package net.mamoe.mirai.console.command.internal
import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.console.job
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.event.Listener

View File

@ -7,6 +7,8 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused")
package net.mamoe.mirai.console.plugin
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
@ -29,6 +31,23 @@ public interface Plugin {
public val loader: PluginLoader<*, *>
}
/**
* 禁用这个插件
*
* @see PluginLoader.disable
*/
public fun Plugin.disable(): Unit = safeLoader.disable(this)
/**
* 启用这个插件
*
* @see PluginLoader.enable
*/
public fun Plugin.enable(): Unit = safeLoader.enable(this)
/**
* 经过泛型类型转换的 [PluginLoader]
*/
@get:JvmSynthetic
@Suppress("UNCHECKED_CAST")
public inline val <P : Plugin> P.safeLoader: PluginLoader<P, PluginDescription>

View File

@ -0,0 +1,109 @@
package net.mamoe.mirai.console.plugin.internal
import kotlinx.coroutines.*
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.MiraiConsoleImplementationBridge
import net.mamoe.mirai.console.plugin.AbstractFilePluginLoader
import net.mamoe.mirai.console.plugin.PluginLoadException
import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
import net.mamoe.mirai.console.setting.SettingStorage
import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.yamlkt.Yaml
import java.io.File
import java.net.URI
import kotlin.coroutines.CoroutineContext
import kotlin.reflect.full.createInstance
internal object JarPluginLoaderImpl :
AbstractFilePluginLoader<JvmPlugin, JvmPluginDescription>(".jar"),
CoroutineScope,
JarPluginLoader {
private val logger: MiraiLogger = MiraiConsole.newLogger(JarPluginLoader::class.simpleName!!)
@ConsoleExperimentalAPI
override val settingStorage: SettingStorage
get() = MiraiConsoleImplementationBridge.settingStorageForJarPluginLoader
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 = PluginsLoader(this.javaClass.classLoader)
init { // delayed
coroutineContext[Job]!!.invokeOnCompletion {
classLoader.clear()
}
}
@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 { URI("jar:file:${it.absolutePath.replace('\\', '/')}!/plugin.yml").toURL() }
.mapNotNull { (file, url) ->
kotlin.runCatching {
url.readText()
}.fold(
onSuccess = { yaml ->
Yaml.nonStrict.parse(JvmPluginDescription.serializer(), yaml)
},
onFailure = {
logger.error("Cannot load plugin file ${file.name}", it)
null
}
)?.also { it._file = file }
}
}
@Suppress("RemoveExplicitTypeArguments") // until Kotlin 1.4 NI
@Throws(PluginLoadException::class)
override fun load(description: JvmPluginDescription): JvmPlugin =
description.runCatching<JvmPluginDescription, JvmPlugin> {
ensureActive()
val main = classLoader.loadPluginMainClassByJarFile(
pluginName = name,
mainClass = mainClassName,
jarFile = file
).kotlin.run {
objectInstance
?: kotlin.runCatching { createInstance() }.getOrNull()
?: (java.constructors + java.declaredConstructors)
.firstOrNull { it.parameterCount == 0 }
?.apply { kotlin.runCatching { isAccessible = true } }
?.newInstance()
} ?: error("No Kotlin object or public no-arg constructor found")
check(main is JvmPlugin) { "The main class of Jar plugin must extend JvmPlugin, recommending JavaPlugin or KotlinPlugin" }
if (main is JvmPluginInternal) {
main._description = description
main.internalOnLoad()
} else main.onLoad()
main
}.getOrElse<JvmPlugin, JvmPlugin> {
throw PluginLoadException("Exception while loading ${description.name}", it)
}
override fun enable(plugin: JvmPlugin) {
ensureActive()
if (plugin is JvmPluginInternal) {
plugin.internalOnEnable()
} else plugin.onEnable()
}
override fun disable(plugin: JvmPlugin) {
if (plugin is JvmPluginInternal) {
plugin.internalOnDisable()
} else plugin.onDisable()
}
}

View File

@ -20,7 +20,7 @@ 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.console.utils.ResourceContainer.Companion.asResourceContainer
import net.mamoe.mirai.utils.MiraiLogger
import java.io.File
import java.io.InputStream

View File

@ -12,7 +12,8 @@
package net.mamoe.mirai.console.plugin.jvm
import net.mamoe.mirai.console.plugin.internal.JvmPluginInternal
import net.mamoe.mirai.console.setting.Setting
import net.mamoe.mirai.utils.minutesToSeconds
import net.mamoe.mirai.utils.secondsToMillis
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@ -27,5 +28,5 @@ public abstract class AbstractJvmPlugin @JvmOverloads constructor(
) : JvmPlugin, JvmPluginInternal(parentCoroutineContext) {
public final override val name: String get() = this.description.name
public override fun <T : Setting> getSetting(clazz: Class<T>): T = loader.settingStorage.load(this, clazz)
public override val autoSaveIntervalMillis: LongRange = 30.secondsToMillis..10.minutesToSeconds
}

View File

@ -9,120 +9,21 @@
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 kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.console.plugin.FilePluginLoader
import net.mamoe.mirai.console.plugin.PluginLoadException
import net.mamoe.mirai.console.plugin.internal.JvmPluginInternal
import net.mamoe.mirai.console.plugin.internal.PluginsLoader
import net.mamoe.mirai.console.plugin.internal.JarPluginLoaderImpl
import net.mamoe.mirai.console.setting.SettingStorage
import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.yamlkt.Yaml
import java.io.File
import java.net.URI
import kotlin.coroutines.CoroutineContext
import kotlin.reflect.full.createInstance
/**
* 内建的 Jar (JVM) 插件加载器
*/
public interface JarPluginLoader : CoroutineScope, FilePluginLoader<JvmPlugin, JvmPluginDescription> {
/**
* [JvmPlugin.loadSetting] 默认使用的实例
*/
@ConsoleExperimentalAPI
public val settingStorage: SettingStorage
public companion object INSTANCE : JarPluginLoader by JarPluginLoaderImpl
}
internal object JarPluginLoaderImpl :
AbstractFilePluginLoader<JvmPlugin, JvmPluginDescription>(".jar"),
CoroutineScope,
JarPluginLoader {
private val logger: MiraiLogger = MiraiConsole.newLogger(JarPluginLoader::class.simpleName!!)
@ConsoleExperimentalAPI
override val settingStorage: SettingStorage
get() = MiraiConsoleInternal.settingStorageForJarPluginLoader
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 = PluginsLoader(this.javaClass.classLoader)
init { // delayed
coroutineContext[Job]!!.invokeOnCompletion {
classLoader.clear()
}
}
@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 { URI("jar:file:${it.absolutePath.replace('\\', '/')}!/plugin.yml").toURL() }
.mapNotNull { (file, url) ->
kotlin.runCatching {
url.readText()
}.fold(
onSuccess = { yaml ->
Yaml.nonStrict.parse(JvmPluginDescription.serializer(), yaml)
},
onFailure = {
logger.error("Cannot load plugin file ${file.name}", it)
null
}
)?.also { it._file = file }
}
}
@Suppress("RemoveExplicitTypeArguments") // until Kotlin 1.4 NI
@Throws(PluginLoadException::class)
override fun load(description: JvmPluginDescription): JvmPlugin =
description.runCatching<JvmPluginDescription, JvmPlugin> {
ensureActive()
val main = classLoader.loadPluginMainClassByJarFile(
pluginName = name,
mainClass = mainClassName,
jarFile = file
).kotlin.run {
objectInstance
?: kotlin.runCatching { createInstance() }.getOrNull()
?: (java.constructors + java.declaredConstructors)
.firstOrNull { it.parameterCount == 0 }
?.apply { kotlin.runCatching { isAccessible = true } }
?.newInstance()
} ?: error("No Kotlin object or public no-arg constructor found")
check(main is JvmPlugin) { "The main class of Jar plugin must extend JvmPlugin, recommending JavaPlugin or KotlinPlugin" }
if (main is JvmPluginInternal) {
main._description = description
main.internalOnLoad()
} else main.onLoad()
main
}.getOrElse<JvmPlugin, JvmPlugin> {
throw PluginLoadException("Exception while loading ${description.name}", it)
}
override fun enable(plugin: JvmPlugin) {
ensureActive()
if (plugin is JvmPluginInternal) {
plugin.internalOnEnable()
} else plugin.onEnable()
}
override fun disable(plugin: JvmPlugin) {
if (plugin is JvmPluginInternal) {
plugin.internalOnDisable()
} else plugin.onDisable()
}
}

View File

@ -41,13 +41,15 @@ public interface JvmPlugin : Plugin, CoroutineScope,
public val description: JvmPluginDescription
/** 所属插件加载器实例 */
public override val loader: JarPluginLoader get() = JarPluginLoader
@JvmDefault
public override val loader: JarPluginLoader
get() = JarPluginLoader
/**
* 获取一个 [Setting] 实例
*/
public fun <T : Setting> getSetting(clazz: Class<T>): T
@JvmDefault
public fun <T : Setting> loadSetting(clazz: Class<T>): T = loader.settingStorage.load(this, clazz)
// TODO: 2020/7/11 document onLoad, onEnable, onDisable
@JvmDefault
@ -64,7 +66,7 @@ public interface JvmPlugin : Plugin, CoroutineScope,
}
@JvmSynthetic
public inline fun <T : Setting> JvmPlugin.getSetting(clazz: KClass<T>): T = this.getSetting(clazz.java)
public inline fun <T : Setting> JvmPlugin.loadSetting(clazz: KClass<T>): T = this.loadSetting(clazz.java)
@JvmSynthetic
public inline fun <reified T : Setting> JvmPlugin.getSetting(): T = this.getSetting(T::class)
public inline fun <reified T : Setting> JvmPlugin.loadSetting(): T = this.loadSetting(T::class)

View File

@ -12,7 +12,11 @@
package net.mamoe.mirai.console.setting
import kotlinx.serialization.KSerializer
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
import net.mamoe.mirai.console.plugin.jvm.loadSetting
import net.mamoe.mirai.console.setting.internal.*
import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI
import net.mamoe.mirai.console.utils.ConsoleInternalAPI
import kotlin.internal.LowPriorityInOverloadResolution
import kotlin.reflect.KProperty
import kotlin.reflect.KType
@ -23,16 +27,31 @@ import kotlin.reflect.KType
*
* :
* ```
* class MySetting : Setting() {
*
* @SerialName("accounts")
* object AccountSettings : Setting by ... {
* @SerialName("info")
* val map: Map<String, String> by value("a" to "b")
* }
* ```
*
* 将被保存为配置 (YAML 作为示例):
* ```yaml
* accounts:
* info:
* a: b
* ```
*/
// TODO: 2020/6/26 document
public typealias SerialName = kotlinx.serialization.SerialName
// TODO: 2020/6/26 document
/**
* [Setting] 的默认实现. 支持使用 `by value()` 等委托方法创建 [Value] 并跟踪其改动.
*
* @see Setting
*/
public abstract class AbstractSetting : Setting, SettingImpl() {
/**
* 使用 `by` 时自动调用此方法, 添加对 [Value] 的值修改的跟踪.
*/
public final override operator fun <T> SerializerAwareValue<T>.provideDelegate(
thisRef: Any?,
property: KProperty<*>
@ -42,21 +61,56 @@ public abstract class AbstractSetting : Setting, SettingImpl() {
return this
}
public final override val updaterSerializer: KSerializer<Unit> get() = super.updaterSerializer
/**
* 值更新序列化器. 仅供内部使用
*/
@ConsoleInternalAPI
public final override val updaterSerializer: KSerializer<Unit>
get() = super.updaterSerializer
/**
* 当所属于这个 [Setting] [Value] [][Value.value] 被修改时被调用.
*/
public abstract override fun onValueChanged(value: Value<*>)
}
// TODO: 2020/6/26 document
/**
* 一个配置对象. 可包含对多个 [Value] 的值变更的跟踪.
*
* [JvmPlugin] 的实现方式:
* ```
* object PluginMain : KotlinPlugin()
*
* object AccountSettings : Setting by PluginMain.getSetting() {
* val map: Map<String, String> by value("a" to "b")
* }
* ```
*
* @see JvmPlugin.loadSetting 通过 [JvmPlugin] 获取指定 [Setting] 实例.
*/
public interface Setting {
// TODO: 2020/6/26 document
/**
* 使用 `by` 时自动调用此方法, 添加对 [Value] 的值修改的跟踪.
*/
public operator fun <T> SerializerAwareValue<T>.provideDelegate(
thisRef: Any?,
property: KProperty<*>
): SerializerAwareValue<T>
// TODO: 2020/6/26 document
/**
* 值更新序列化器. 仅供内部使用
*/
public val updaterSerializer: KSerializer<Unit>
/**
* 当所属于这个 [Setting] [Value] [][Value.value] 被修改时被调用.
*/
public fun onValueChanged(value: Value<*>)
/**
* 当这个 [Setting] 被放入一个 [SettingStorage] 时调用
*/
public fun setStorage(storage: SettingStorage)
}
//// region Setting_value_primitives CODEGEN ////
@ -101,6 +155,7 @@ public inline fun <reified T> Setting.value(): SerializerAwareValue<T> = value(T
* Creates a [Value] with specified [KType], and set default value.
*/
@Suppress("UNCHECKED_CAST")
@ConsoleExperimentalAPI
public fun <T> Setting.valueFromKType(type: KType, default: T): SerializerAwareValue<T> =
(valueFromKTypeImpl(type) as SerializerAwareValue<Any?>).apply { this.value = default } as SerializerAwareValue<T>

View File

@ -1,34 +1,31 @@
@file:Suppress("NOTHING_TO_INLINE")
@file:Suppress("NOTHING_TO_INLINE", "UNCHECKED_CAST", "unused")
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 net.mamoe.mirai.console.plugin.jvm.JarPluginLoader
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
import net.mamoe.mirai.console.setting.SettingStorage.Companion.load
import net.mamoe.mirai.console.setting.internal.*
import java.io.File
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.KType
import kotlin.reflect.full.createType
/**
* [Setting] 存储容器
* [Setting] 存储容器.
*
* 此为较低层的 API, 一般插件开发者不会接触.
*
* [JarPluginLoader] 实现一个 [SettingStorage], 用于管理所有 [JvmPlugin] [Setting] 实例.
*
* @see SettingHolder
* @see JarPluginLoader.settingStorage
*/
public interface SettingStorage {
/**
* 读取一个实例
* 读取一个实例. [T] 实例创建后 [设置 [SettingStorage]][Setting.setStorage]
*/
public fun <T : Setting> load(holder: SettingHolder, settingClass: Class<T>): T
@ -36,39 +33,82 @@ public interface SettingStorage {
* 保存一个实例
*/
public fun store(holder: SettingHolder, setting: Setting)
}
// TODO: 2020/7/11 document
public interface MemorySettingStorage : SettingStorage {
public companion object {
/**
* 读取一个实例. [T] 实例创建后 [设置 [SettingStorage]][Setting.setStorage]
*/
@JvmStatic
@JvmName("create")
public operator fun invoke(): MemorySettingStorage = MemorySettingStorageImpl()
public fun <T : Setting> SettingStorage.load(holder: SettingHolder, settingClass: KClass<T>): T =
this.load(holder, settingClass.java)
/**
* 读取一个实例. [T] 实例创建后 [设置 [SettingStorage]][Setting.setStorage]
*/
@JvmSynthetic
public inline fun <reified T : Setting> SettingStorage.load(holder: SettingHolder): T =
this.load(holder, T::class)
}
}
// TODO: 2020/7/11 document
/**
* 在内存存储所有 [Setting] 实例的 [SettingStorage]. 在内存数据丢失后相关 [Setting] 实例也会丢失.
*/
public interface MemorySettingStorage : SettingStorage, Map<Class<out Setting>, Setting> {
/**
* 当任一 [Setting] 实例拥有的 [Value] 的值被改变后调用的回调函数.
*/
public /* fun */ interface OnChangedCallback { // TODO: 2020/7/24 make `fun` in 1.4
public fun onChanged(storage: MemorySettingStorage, value: Value<*>)
/**
* 无任何操作的 [OnChangedCallback]
* @see OnChangedCallback
*/
public object NoOp : OnChangedCallback {
public override fun onChanged(storage: MemorySettingStorage, value: Value<*>) {
// no-op
}
}
}
public companion object {
/**
* 创建一个 [MemorySettingStorage] 实例.
*
* @param onChanged 当任一 [Setting] 实例拥有的 [Value] 的值被改变后调用的回调函数.
*/
@JvmStatic
@JvmName("create")
@JvmOverloads
public operator fun invoke(onChanged: OnChangedCallback = OnChangedCallback.NoOp): MemorySettingStorage =
MemorySettingStorageImpl(onChanged)
}
}
/**
* 在内存存储所有 [Setting] 实例的 [SettingStorage].
*/
public interface MultiFileSettingStorage : SettingStorage {
/**
* 存放 [Setting] 的目录.
*/
public val directory: File
public companion object {
/**
* 创建一个 [MultiFileSettingStorage] 实例.
*
* @see directory 存放 [Setting] 的目录.
*/
@JvmStatic
@JvmName("create")
public operator fun invoke(directory: File): MultiFileSettingStorage = MultiFileSettingStorageImpl(directory)
}
}
// TODO: 2020/7/11 here or companion?
public inline fun <T : Setting> SettingStorage.load(holder: SettingHolder, settingClass: KClass<T>): T =
this.load(holder, settingClass.java)
// TODO: 2020/7/11 here or companion?
public inline fun <reified T : Setting> SettingStorage.load(holder: SettingHolder): T =
this.load(holder, T::class)
/**
* 可以持有相关 [Setting] 的对象.
* 可以持有相关 [Setting] 实例的对象, 作为 [Setting] 实例的拥有者.
*
* @see SettingStorage.load
* @see SettingStorage.store
@ -80,6 +120,28 @@ public interface SettingHolder {
* 保存时使用的分类名
*/
public val name: String
/**
* 创建一个 [Setting] 实例.
*
* @see Companion.newSettingInstance
* @see KClass.createType
*/
@JvmDefault
public fun <T : Setting> newSettingInstance(type: KType): T =
newSettingInstanceUsingReflection<Setting>(type) as T
public companion object {
/**
* 创建一个 [Setting] 实例.
*
* @see SettingHolder.newSettingInstance
*/
@JvmSynthetic
public inline fun <reified T : Setting> SettingHolder.newSettingInstance(): T {
return this.newSettingInstance(typeOf0<T>())
}
}
}
/**
@ -94,158 +156,23 @@ public interface AutoSaveSettingHolder : SettingHolder, CoroutineScope {
* - 区间的左端点为最小间隔, 一个 [Value] 被修改后, 若此时间段后无其他修改, 将触发自动保存; 若有, 将重新开始计时.
* - 区间的右端点为最大间隔, 一个 [Value] 被修改后, 最多不超过这个时间段后就会被保存.
*
* [coroutineContext] 含有 [Job], [AutoSaveSetting] 会通过 [Job.invokeOnCompletion] Job 完结时触发自动保存.
* [AutoSaveSettingHolder.coroutineContext] 含有 [Job],
* [AutoSaveSetting] 会通过 [Job.invokeOnCompletion] Job 完结时触发自动保存.
*
* @see LongRange Java 用户使用 [LongRange] 的构造器创建
* @see Long.rangeTo Kotlin 用户使用 [Long.rangeTo] 创建, `3000..50000`
*/
public val autoSaveIntervalMillis: LongRange
get() = 30.secondsToMillis..10.minutesToSeconds
/**
* 链接自动保存的 [Setting].
* 当任一相关 [Value] 的值被修改时, 将在一段时间无其他修改时保存
*
* [AutoSaveSettingHolder.coroutineContext] 含有 [Job], [AutoSaveSetting] 会通过 [Job.invokeOnCompletion] Job 完结时触发自动保存.
*
* @see getSetting
* 仅支持确切的 [Setting] 类型
*/
public open class AutoSaveSetting(private val owner: AutoSaveSettingHolder, private val storage: SettingStorage) :
AbstractSetting() {
@JvmField
@Volatile
internal var lastAutoSaveJob: Job? = null
@JvmField
@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()
}
}
public final override fun onValueChanged(value: Value<*>) {
lastAutoSaveJob = owner.launch(block = updaterBlock)
}
private fun doSave() = storage.store(owner, this)
}
}
// internal
internal class MemorySettingStorageImpl : SettingStorage, MemorySettingStorage {
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
@JvmDefault
public override fun <T : Setting> newSettingInstance(type: KType): T {
val classifier = type.classifier?.cast<KClass<*>>()?.java
require(classifier == Setting::class.java) {
"Cannot create Setting instance. AutoSaveSettingHolder supports only Setting type."
}
return AutoSaveSetting(this) as T // T is always Setting
}
}
public open class MultiFileSettingStorageImpl(
public final override val directory: File
) : SettingStorage, MultiFileSettingStorage {
public override fun <T : Setting> load(holder: SettingHolder, settingClass: Class<T>): T =
with(settingClass.kotlin) {
val file = getSettingFile(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@MultiFileSettingStorageImpl) 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
}
protected open fun getSettingFile(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
public override fun store(holder: SettingHolder, setting: Setting): Unit = with(setting::class) {
val file = getSettingFile(holder, this)
if (file.exists() && file.isFile && file.canRead()) {
file.writeText(Yaml.default.stringify(setting.updaterSerializer, Unit))
}
}
}
internal fun <T : Any> KClass<T>.createInstanceOrNull(): T? {
val noArgsConstructor = constructors.singleOrNull { it.parameters.all(KParameter::isOptional) }
?: return null
return noArgsConstructor.callBy(emptyMap())
}
internal fun KClass<*>.findASerialName(): String =
findAnnotation<SerialName>()?.value
?: qualifiedName
?: throw IllegalArgumentException("Cannot find a serial name for $this")

View File

@ -41,18 +41,22 @@ public class SerializableValue<T>(
/**
* The serializer used to update and dump [delegate]
*/
override val serializer: KSerializer<Unit>
public override val serializer: KSerializer<Unit>
) : Value<T> by delegate, SerializerAwareValue<T> {
override fun toString(): String = delegate.toString()
}
public override fun toString(): String = delegate.toString()
public fun <T> Value<T>.serializableValueWith(
serializer: KSerializer<T>
): SerializableValue<T> {
return SerializableValue(
this,
serializer.map(serializer = { this.value }, deserializer = { this.setValueBySerializer(it) })
)
public companion object {
@JvmStatic
@JvmName("create")
public fun <T> Value<T>.serializableValueWith(
serializer: KSerializer<T>
): SerializableValue<T> {
return SerializableValue(
this,
serializer.map(serializer = { this.value }, deserializer = { this.setValueBySerializer(it) })
)
}
}
}
/**
@ -60,14 +64,32 @@ public fun <T> Value<T>.serializableValueWith(
*/
public interface SerializerAwareValue<T> : Value<T> {
public val serializer: KSerializer<Unit>
}
public fun <T> SerializerAwareValue<T>.serialize(format: StringFormat): String {
return format.stringify(this.serializer, Unit)
}
public companion object {
@JvmStatic
@ConsoleExperimentalAPI("will be changed due to reconstruction of kotlinx.serialization")
public fun <T> SerializerAwareValue<T>.serialize(format: StringFormat): String {
return format.stringify(this.serializer, Unit)
}
public fun <T> SerializerAwareValue<T>.serialize(format: BinaryFormat): ByteArray {
return format.dump(this.serializer, Unit)
@JvmStatic
@ConsoleExperimentalAPI("will be changed due to reconstruction of kotlinx.serialization")
public fun <T> SerializerAwareValue<T>.serialize(format: BinaryFormat): ByteArray {
return format.dump(this.serializer, Unit)
}
@JvmStatic
@ConsoleExperimentalAPI("will be changed due to reconstruction of kotlinx.serialization")
public fun <T> SerializerAwareValue<T>.deserialize(format: StringFormat, value: String) {
format.parse(this.serializer, value)
}
@JvmStatic
@ConsoleExperimentalAPI("will be changed due to reconstruction of kotlinx.serialization")
public fun <T> SerializerAwareValue<T>.deserialize(format: BinaryFormat, value: ByteArray) {
format.load(this.serializer, value)
}
}
}
@JvmSynthetic

View File

@ -13,9 +13,9 @@ package net.mamoe.mirai.console.setting.internal
import kotlinx.serialization.*
import kotlinx.serialization.builtins.*
import net.mamoe.mirai.console.setting.SerializableValue.Companion.serializableValueWith
import net.mamoe.mirai.console.setting.SerializerAwareValue
import net.mamoe.mirai.console.setting.Setting
import net.mamoe.mirai.console.setting.serializableValueWith
import net.mamoe.mirai.console.setting.valueFromKType
import net.mamoe.yamlkt.YamlDynamicSerializer
import net.mamoe.yamlkt.YamlNullableDynamicSerializer

View File

@ -0,0 +1,190 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.console.setting.internal
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.loadSetting
import net.mamoe.mirai.console.setting.*
import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI
import net.mamoe.mirai.console.utils.ConsoleInternalAPI
import net.mamoe.mirai.utils.currentTimeMillis
import net.mamoe.yamlkt.Yaml
import java.io.File
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.full.findAnnotation
/**
* 链接自动保存的 [Setting].
* 当任一相关 [Value] 的值被修改时, 将在一段时间无其他修改时保存
*
* [AutoSaveSettingHolder.coroutineContext] 含有 [Job], [AutoSaveSetting] 会通过 [Job.invokeOnCompletion] Job 完结时触发自动保存.
*
* @see loadSetting
*/
internal open class AutoSaveSetting(private val owner: AutoSaveSettingHolder) :
AbstractSetting() {
private lateinit var storage: SettingStorage
override fun setStorage(storage: SettingStorage) {
check(!this::storage.isInitialized) { "storage is already initialized" }
this.storage = storage
}
@JvmField
@Volatile
internal var lastAutoSaveJob: Job? = null
@JvmField
@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()
}
}
@Suppress("RedundantVisibilityModifier")
@ConsoleInternalAPI
public final override fun onValueChanged(value: Value<*>) {
lastAutoSaveJob = owner.launch(block = updaterBlock)
}
private fun doSave() = storage.store(owner, this)
}
internal class MemorySettingStorageImpl(
private val onChanged: MemorySettingStorage.OnChangedCallback
) : SettingStorage, MemorySettingStorage,
MutableMap<Class<out Setting>, Setting> by mutableMapOf() {
internal inner class MemorySettingImpl : AbstractSetting() {
@ConsoleInternalAPI
override fun onValueChanged(value: Value<*>) {
onChanged.onChanged(this@MemorySettingStorageImpl, value)
}
override fun setStorage(storage: SettingStorage) {
check(storage is MemorySettingStorageImpl) { "storage is not MemorySettingStorageImpl" }
}
}
@Suppress("UNCHECKED_CAST")
override fun <T : Setting> load(holder: SettingHolder, settingClass: Class<T>): T = (synchronized(this) {
this.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).also { it.setStorage(this) }
override fun store(holder: SettingHolder, setting: Setting) {
synchronized(this) {
this[setting::class.java] = setting
}
}
}
@Suppress("RedundantVisibilityModifier") // might be public in the future
internal open class MultiFileSettingStorageImpl(
public final override val directory: File
) : SettingStorage, MultiFileSettingStorage {
public override fun <T : Setting> load(holder: SettingHolder, settingClass: Class<T>): T =
with(settingClass.kotlin) {
val file = getSettingFile(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) 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
}
protected open fun getSettingFile(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
public override fun store(holder: SettingHolder, setting: Setting): Unit = with(setting::class) {
val file = getSettingFile(holder, this)
if (file.exists() && file.isFile && file.canRead()) {
file.writeText(Yaml.default.stringify(setting.updaterSerializer, Unit))
}
}
}
@JvmSynthetic
internal fun <T : Any> KClass<T>.createInstanceOrNull(): T? {
val noArgsConstructor = constructors.singleOrNull { it.parameters.all(KParameter::isOptional) }
?: return null
return noArgsConstructor.callBy(emptyMap())
}
@JvmSynthetic
internal fun KClass<*>.findASerialName(): String =
findAnnotation<SerialName>()?.value
?: qualifiedName
?: throw IllegalArgumentException("Cannot find a serial name for $this")

View File

@ -0,0 +1,35 @@
package net.mamoe.mirai.console.setting.internal
import net.mamoe.mirai.console.command.internal.qualifiedNameOrTip
import net.mamoe.mirai.console.setting.Setting
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.full.isSubclassOf
@Suppress("UNCHECKED_CAST")
internal inline fun <reified T : Any> KType.asKClass(): KClass<out T> {
val clazz = requireNotNull(classifier as? KClass<T>) { "Unsupported classifier: $classifier" }
val fromClass = arguments[0].type?.classifier as? KClass<*> ?: Any::class
val toClass = T::class
require(toClass.isSubclassOf(fromClass)) {
"Cannot cast KClass<${fromClass.qualifiedNameOrTip}> to KClass<${toClass.qualifiedNameOrTip}>"
}
return clazz
}
internal inline fun <reified T : Setting> newSettingInstanceUsingReflection(type: KType): T {
val classifier = type.asKClass<T>()
return with(classifier) {
objectInstance
?: createInstanceOrNull()
?: throw IllegalArgumentException(
"Cannot create Setting instance. " +
"SettingHolder supports Settings implemented as an object " +
"or the ones with a constructor which either has no parameters or all parameters of which are optional, by default newSettingInstance implementation."
)
}
}

View File

@ -15,9 +15,11 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.MiraiConsoleInternal
import net.mamoe.mirai.console.MiraiConsoleImplementationBridge
import net.mamoe.mirai.console.setting.*
import net.mamoe.mirai.console.setting.SettingStorage.Companion.load
import net.mamoe.mirai.contact.User
import net.mamoe.mirai.utils.minutesToMillis
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@ -52,7 +54,10 @@ internal fun CoroutineScope.childScope(context: CoroutineContext = EmptyCoroutin
internal object ConsoleBuiltInSettingHolder : AutoSaveSettingHolder,
CoroutineScope by MiraiConsole.childScope() {
override val autoSaveIntervalMillis: LongRange
get() = 30.minutesToMillis..60.minutesToMillis
override val name: String get() = "ConsoleBuiltIns"
}
internal object ConsoleBuiltInSettingStorage : SettingStorage by MiraiConsoleInternal.settingStorageForJarPluginLoader
internal object ConsoleBuiltInSettingStorage :
SettingStorage by MiraiConsoleImplementationBridge.settingStorageForJarPluginLoader

View File

@ -5,6 +5,7 @@ import kotlin.annotation.AnnotationTarget.*
/**
* 表明这个 API 是为了让 Java 使用者调用更方便. Kotlin 使用者不应该使用这些 API.
*/
@Retention(AnnotationRetention.SOURCE)
@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
@Target(PROPERTY, FUNCTION, TYPE, CLASS)
internal annotation class JavaFriendlyAPI
@ -15,7 +16,7 @@ internal annotation class JavaFriendlyAPI
* 这些 API 可能会在任意时刻更改, 且不会发布任何预警.
* 非常不建议在发行版本中使用这些 API.
*/
@Retention(AnnotationRetention.SOURCE)
@Retention(AnnotationRetention.BINARY)
@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
@Target(CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR, CLASS, FUNCTION, PROPERTY)
@MustBeDocumented
@ -29,7 +30,7 @@ public annotation class ConsoleInternalAPI(
* 这些 API 不具有稳定性, 且可能会在任意时刻更改.
* 不建议在发行版本中使用这些 API.
*/
@Retention(AnnotationRetention.SOURCE)
@Retention(AnnotationRetention.BINARY)
@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
@Target(CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR)
@MustBeDocumented

View File

@ -3,12 +3,18 @@
package net.mamoe.mirai.console.utils
import net.mamoe.mirai.console.encodeToString
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
import net.mamoe.mirai.console.utils.ResourceContainer.Companion.asResourceContainer
import java.io.InputStream
import java.nio.charset.Charset
import kotlin.reflect.KClass
/**
* 资源容器.
*
* 资源容器可能使用 [Class.getResourceAsStream], 也可能使用其他方式, 取决于实现方式.
*
* @see JvmPlugin [JvmPlugin] 实现 [ResourceContainer], 使用 [ResourceContainer.asResourceContainer]
*/
public interface ResourceContainer {
/**
@ -32,27 +38,21 @@ public interface ResourceContainer {
public companion object {
/**
* 使用 [Class.getResourceAsStream] 读取资源文件
*
* @see asResourceContainer Kotlin 使用
*/
@JvmStatic
@JavaFriendlyAPI
public fun byClass(clazz: Class<*>): ResourceContainer = clazz.asResourceContainer()
@JvmName("byClass")
public fun KClass<*>.asResourceContainer(): ResourceContainer = this.java.asResourceContainer()
/**
* 使用 [Class.getResourceAsStream] 读取资源文件
*/
@JvmStatic
@JvmName("byClass")
public fun Class<*>.asResourceContainer(): ResourceContainer = ClassAsResourceContainer(this)
}
}
/**
* 使用 [Class.getResourceAsStream] 读取资源文件
*/
public fun KClass<*>.asResourceContainer(): ResourceContainer = ClassAsResourceContainer(this.java)
/**
* 使用 [Class.getResourceAsStream] 读取资源文件
*/
public fun Class<*>.asResourceContainer(): ResourceContainer = ClassAsResourceContainer(this)
internal class ClassAsResourceContainer(
private class ClassAsResourceContainer(
private val clazz: Class<*>
) : ResourceContainer {
override fun getResourceAsStream(name: String): InputStream = clazz.getResourceAsStream(name)

View File

@ -14,6 +14,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start
import net.mamoe.mirai.console.command.ConsoleCommandSender
import net.mamoe.mirai.console.plugin.DeferredPluginLoader
import net.mamoe.mirai.console.plugin.PluginLoader
@ -34,7 +35,7 @@ import kotlin.test.assertNotNull
@OptIn(ConsoleInternalAPI::class)
fun initTestEnvironment() {
MiraiConsoleInitializer.init(object : IMiraiConsole {
object : MiraiConsoleImplementation {
override val rootDir: File = createTempDir()
override val frontEnd: MiraiConsoleFrontEnd = object : MiraiConsoleFrontEnd {
override val name: String get() = "Test"
@ -52,7 +53,7 @@ fun initTestEnvironment() {
override val settingStorageForJarPluginLoader: SettingStorage get() = MemorySettingStorage()
override val settingStorageForBuiltIns: SettingStorage get() = MemorySettingStorage()
override val coroutineContext: CoroutineContext = SupervisorJob()
})
}.start()
}
internal object Testing {

View File

@ -12,10 +12,12 @@ package net.mamoe.mirai.console.setting
import kotlinx.serialization.UnstableDefault
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import net.mamoe.mirai.console.utils.ConsoleInternalAPI
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertSame
@OptIn(ConsoleInternalAPI::class)
internal class SettingTest {
class MySetting : AbstractSetting() {
@ -23,9 +25,13 @@ internal class SettingTest {
val map by value<MutableMap<String, String>>()
val map2 by value<MutableMap<String, MutableMap<String, String>>>()
@ConsoleInternalAPI
override fun onValueChanged(value: Value<*>) {
}
override fun setStorage(storage: SettingStorage) {
}
}
@OptIn(UnstableDefault::class)

View File

@ -17,6 +17,7 @@ kotlin {
languageSettings.useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiInternalAPI")
languageSettings.useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiExperimentalAPI")
languageSettings.useExperimentalAnnotation("net.mamoe.mirai.console.utils.ConsoleExperimentalAPI")
languageSettings.useExperimentalAnnotation("net.mamoe.mirai.console.ConsoleFrontEndImplementation")
languageSettings.useExperimentalAnnotation("kotlin.ExperimentalUnsignedTypes")
languageSettings.useExperimentalAnnotation("kotlin.experimental.ExperimentalTypeInference")
languageSettings.useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts")

View File

@ -48,7 +48,7 @@ internal val LoggerCreator: (identity: String?) -> MiraiLogger = {
/**
* mirai-console-pure 前端实现
*
* @see MiraiConsolePure 后端实现
* @see MiraiConsoleImplementationPure 后端实现
* @see MiraiConsolePureLoader CLI 入口点
*/
@ConsoleInternalAPI

View File

@ -18,16 +18,16 @@
"INVISIBLE_ABSTRACT_MEMBER_FROM_SUPER_WARNING",
"EXPOSED_SUPER_CLASS"
)
@file:OptIn(ConsoleInternalAPI::class)
@file:OptIn(ConsoleInternalAPI::class, ConsoleFrontEndImplementation::class)
package net.mamoe.mirai.console.pure
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import net.mamoe.mirai.console.IMiraiConsole
import net.mamoe.mirai.console.ConsoleFrontEndImplementation
import net.mamoe.mirai.console.MiraiConsoleFrontEnd
import net.mamoe.mirai.console.MiraiConsoleInitializer
import net.mamoe.mirai.console.MiraiConsoleImplementation
import net.mamoe.mirai.console.command.ConsoleCommandSender
import net.mamoe.mirai.console.plugin.DeferredPluginLoader
import net.mamoe.mirai.console.plugin.PluginLoader
@ -44,7 +44,8 @@ import java.io.File
* @see MiraiConsoleFrontEndPure 前端实现
* @see MiraiConsolePureLoader CLI 入口点
*/
class MiraiConsolePure @JvmOverloads constructor(
class MiraiConsoleImplementationPure
@JvmOverloads constructor(
override val rootDir: File = File("."),
override val builtInPluginLoaders: List<PluginLoader<*, *>> = listOf(DeferredPluginLoader { JarPluginLoader }),
override val frontEnd: MiraiConsoleFrontEnd = MiraiConsoleFrontEndPure,
@ -52,21 +53,9 @@ class MiraiConsolePure @JvmOverloads constructor(
override val consoleCommandSender: ConsoleCommandSender = ConsoleCommandSenderImpl,
override val settingStorageForJarPluginLoader: SettingStorage = MultiFileSettingStorage(rootDir),
override val settingStorageForBuiltIns: SettingStorage = MultiFileSettingStorage(rootDir)
) : IMiraiConsole, CoroutineScope by CoroutineScope(SupervisorJob()) {
) : MiraiConsoleImplementation, CoroutineScope by CoroutineScope(SupervisorJob()) {
init {
rootDir.mkdir()
require(rootDir.isDirectory) { "rootDir ${rootDir.absolutePath} is not a directory" }
}
@JvmField
internal var started: Boolean = false
companion object {
@JvmStatic
fun MiraiConsolePure.start() = synchronized(this) {
check(!started) { "mirai-console is already started and can't be restarted." }
MiraiConsoleInitializer.init(this)
started = true
}
}
}

View File

@ -22,9 +22,8 @@ package net.mamoe.mirai.console.pure
import kotlinx.coroutines.isActive
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start
import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.console.job
import net.mamoe.mirai.console.pure.MiraiConsolePure.Companion.start
import net.mamoe.mirai.console.utils.ConsoleInternalAPI
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.content
@ -46,7 +45,7 @@ object MiraiConsolePureLoader {
internal fun startup() {
DefaultLogger = { MiraiConsoleFrontEndPure.loggerFor(it) }
overrideSTD()
MiraiConsolePure().start()
MiraiConsoleImplementationPure().start()
startConsoleThread()
}