Public API stabilization:

- Redesign MiraiConsoleFrontEndDescription
- Redesign MiraiConsoleImplementation
- Rework path-relevant properties
- Add docs
This commit is contained in:
Him188 2020-08-22 19:43:07 +08:00
parent bcd93ab34e
commit 218fb2bdcc
23 changed files with 442 additions and 282 deletions

View File

@ -12,6 +12,7 @@
package net.mamoe.mirai.console
import com.vdurmont.semver4j.Semver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import net.mamoe.mirai.Bot
@ -24,8 +25,8 @@ import net.mamoe.mirai.console.util.ConsoleExperimentalAPI
import net.mamoe.mirai.console.util.ConsoleInternalAPI
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.MiraiLogger
import java.io.File
import java.util.*
import java.nio.file.Path
import java.time.Instant
/**
@ -36,19 +37,20 @@ import java.util.*
*/
public interface MiraiConsole : CoroutineScope {
/**
* Console 运行路径
* Console 运行根目录, 由前端决定确切路径.
*
* 所有子模块都会在这个目录之下创建子目录.
*/
public val rootDir: File
public val rootPath: Path
/**
* Console 前端接口
*/
@ConsoleExperimentalAPI
public val frontEnd: MiraiConsoleFrontEnd
/**
* 与前端交互所使用的 Logger
* Console 主日志.
*
* **实现细节**: 这个 [MiraiLogger] [MiraiLogger.identity] 通常为 `main`
*
* **注意**: 插件不应该在任何时刻使用它.
*/
@ConsoleInternalAPI
public val mainLogger: MiraiLogger
/**
@ -58,13 +60,22 @@ public interface MiraiConsole : CoroutineScope {
*/
public val builtInPluginLoaders: List<PluginLoader<*, *>>
public val buildDate: Date
/**
* Console 后端构建时间
*/
public val buildDate: Instant
public val version: String
/**
* Console 后端版本号
*/
public val version: Semver
@ConsoleExperimentalAPI
public val pluginCenter: PluginCenter
/**
* 创建一个 logger
*/
@ConsoleExperimentalAPI
public fun newLogger(identity: String?): MiraiLogger
@ -88,8 +99,9 @@ public interface MiraiConsole : CoroutineScope {
public fun addBot(id: Long, password: String, configuration: BotConfiguration.() -> Unit = {}): Bot =
Bot(id, password) {
fileBasedDeviceInfo()
this.loginSolver = frontEnd.createLoginSolver()
redirectNetworkLogToDirectory()
this.loginSolver = MiraiConsoleImplementationBridge.createLoginSolver(id, this)
configuration()
}
}

View File

@ -1,53 +0,0 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 with Mamoe Exceptions 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 with Mamoe Exceptions license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.console
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.util.ConsoleExperimentalAPI
import net.mamoe.mirai.utils.LoginSolver
import net.mamoe.mirai.utils.MiraiLogger
/**
* 只需要实现一个这个传入 MiraiConsole 就可以绑定 UI 层与 Console
*
* 需要保证线程安全
*/
@ConsoleExperimentalAPI
@ConsoleFrontEndImplementation
public interface MiraiConsoleFrontEnd {
/**
* 名称
*/
public val name: String
/**
* 版本
*/
public val version: String
public fun loggerFor(identity: String?): MiraiLogger
/**
* UI 层接受一个新的bot
* */
public fun pushBot(
bot: Bot
)
/**
* UI 层提供一个输入, 相当于 [readLine]
*/
public suspend fun requestInput(hint: String): String
/**
* UI 层创建一个 [LoginSolver]
*/
public fun createLoginSolver(): LoginSolver
}

View File

@ -0,0 +1,37 @@
/*
* 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
import com.vdurmont.semver4j.Semver
/**
* 有关前端实现的信息
*/
public interface MiraiConsoleFrontEndDescription {
/**
* 此前端实现的名称
*/
public val name: String
/**
* 此前端实现的提供者
*/
public val vendor: String
/**
* 此前端实现的名称
*/
public val version: Semver
/**
* 返回显示在 [MiraiConsole] 启动时的信息
*/
public fun render(): String = "Frontend ${name}: version ${version}, provided by $vendor"
}

View File

@ -13,6 +13,7 @@ package net.mamoe.mirai.console
import kotlinx.atomicfu.locks.withLock
import kotlinx.coroutines.CoroutineScope
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.data.PluginDataStorage
@ -20,8 +21,11 @@ import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge
import net.mamoe.mirai.console.plugin.PluginLoader
import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader
import net.mamoe.mirai.console.util.ConsoleExperimentalAPI
import net.mamoe.mirai.console.util.ConsoleInput
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.LoginSolver
import net.mamoe.mirai.utils.MiraiLogger
import java.io.File
import java.nio.file.Path
import java.util.concurrent.locks.ReentrantLock
import kotlin.annotation.AnnotationTarget.*
import kotlin.coroutines.CoroutineContext
@ -51,15 +55,16 @@ public interface MiraiConsoleImplementation : CoroutineScope {
public override val coroutineContext: CoroutineContext
/**
* Console 运行路径
* Console 运行根目录
* @see MiraiConsole.rootPath
*/
public val rootDir: File
public val rootPath: Path
/**
* Console 前端接口
*/
@ConsoleExperimentalAPI
public val frontEnd: MiraiConsoleFrontEnd
public val frontEndDescription: MiraiConsoleFrontEndDescription
/**
* 与前端交互所使用的 Logger
@ -79,6 +84,28 @@ public interface MiraiConsoleImplementation : CoroutineScope {
public val configStorageForJarPluginLoader: PluginDataStorage
public val dataStorageForBuiltIns: PluginDataStorage
/**
* @see ConsoleInput 的实现
*/
public val consoleInput: ConsoleInput
/**
* 创建一个 [LoginSolver]
*
* **调用备注**: 此函数通常在构造 [Bot] 实例时调用.
*
* @param requesterBot 请求者 [Bot.id]
* @param configuration 请求者 [Bot.configuration]
*
* @see LoginSolver.Default
*/
public fun createLoginSolver(requesterBot: Long, configuration: BotConfiguration): LoginSolver
/**
* 创建一个 logger
*/
public fun newLogger(identity: String?): MiraiLogger
public companion object {
internal lateinit var instance: MiraiConsoleImplementation
private val initLock = ReentrantLock()

View File

@ -19,6 +19,7 @@ import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.command.CommandManagerImpl.allRegisteredCommands
import net.mamoe.mirai.console.command.CommandManagerImpl.register
import net.mamoe.mirai.console.util.ConsoleExperimentalAPI
import net.mamoe.mirai.console.util.ConsoleInternalAPI
import net.mamoe.mirai.message.nextMessageOrNull
import net.mamoe.mirai.utils.secondsToMillis
import kotlin.concurrent.thread
@ -79,6 +80,7 @@ public object BuiltInCommands {
}.fold(
onSuccess = { sendMessage("mirai-console stopped successfully.") },
onFailure = {
@OptIn(ConsoleInternalAPI::class)
MiraiConsole.mainLogger.error(it)
sendMessage(it.localizedMessage ?: it.message ?: it.toString())
}

View File

@ -16,6 +16,7 @@ import net.mamoe.mirai.console.internal.data.MultiFilePluginDataStorageImpl
import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
import java.io.File
import java.nio.file.Path
import kotlin.reflect.KClass
/**
@ -99,7 +100,7 @@ public interface MultiFilePluginDataStorage : PluginDataStorage {
/**
* 存放 [PluginData] 的目录.
*/
public val directory: File
public val directoryPath: Path
public companion object {
/**
@ -109,7 +110,11 @@ public interface MultiFilePluginDataStorage : PluginDataStorage {
*/
@JvmStatic
@JvmName("create")
public operator fun invoke(directory: File): MultiFilePluginDataStorage =
public operator fun invoke(directory: Path): MultiFilePluginDataStorage =
MultiFilePluginDataStorageImpl(directory)
}
}
@get:JvmSynthetic
public inline val MultiFilePluginDataStorage.directory: File
get() = this.directoryPath.toFile()

View File

@ -9,10 +9,13 @@
package net.mamoe.mirai.console.internal
import java.util.*
import com.vdurmont.semver4j.Semver
import java.time.Instant
internal object MiraiConsoleBuildConstants { // auto-filled on build (task :mirai-console:fillBuildConstants)
@JvmStatic
val buildDate: Date = Date(1597935352287L) // 2020-08-20 22:55:52
const val version: String = "1.0-M2-1"
val buildDate: Instant = Instant.ofEpochMilli(1597935352287L) // 2020-08-20 22:55:52
@JvmStatic
val version: Semver = Semver("1.0-M2-1")
}

View File

@ -11,12 +11,13 @@
package net.mamoe.mirai.console.internal
import com.vdurmont.semver4j.Semver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.IllegalMiraiConsoleImplementationError
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.MiraiConsoleFrontEnd
import net.mamoe.mirai.console.MiraiConsoleFrontEndDescription
import net.mamoe.mirai.console.MiraiConsoleImplementation
import net.mamoe.mirai.console.command.BuiltInCommands
import net.mamoe.mirai.console.command.Command.Companion.primaryName
@ -30,12 +31,12 @@ import net.mamoe.mirai.console.plugin.PluginLoader
import net.mamoe.mirai.console.plugin.PluginManager
import net.mamoe.mirai.console.plugin.center.PluginCenter
import net.mamoe.mirai.console.util.ConsoleExperimentalAPI
import net.mamoe.mirai.utils.DefaultLogger
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.info
import java.io.File
import net.mamoe.mirai.console.util.ConsoleInput
import net.mamoe.mirai.console.util.ConsoleInternalAPI
import net.mamoe.mirai.utils.*
import java.nio.file.Path
import java.text.SimpleDateFormat
import java.util.*
import java.time.Instant
import kotlin.coroutines.CoroutineContext
/**
@ -45,36 +46,39 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI
MiraiConsole {
override val pluginCenter: PluginCenter get() = CuiPluginCenter
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
override val frontEnd: MiraiConsoleFrontEnd get() = instance.frontEnd
private val instance: MiraiConsoleImplementation by MiraiConsoleImplementation.Companion::instance
override val buildDate: Instant by MiraiConsoleBuildConstants::buildDate
override val version: Semver by MiraiConsoleBuildConstants::version
override val rootPath: Path by instance::rootPath
override val frontEndDescription: MiraiConsoleFrontEndDescription by instance::frontEndDescription
@ConsoleExperimentalAPI
override val mainLogger: MiraiLogger
get() = instance.mainLogger
override val coroutineContext: CoroutineContext get() = instance.coroutineContext
override val builtInPluginLoaders: List<PluginLoader<*, *>> get() = instance.builtInPluginLoaders
override val consoleCommandSender: ConsoleCommandSender get() = instance.consoleCommandSender
@OptIn(ConsoleInternalAPI::class)
override val mainLogger: MiraiLogger by instance::mainLogger
override val coroutineContext: CoroutineContext by instance::coroutineContext
override val builtInPluginLoaders: List<PluginLoader<*, *>> by instance::builtInPluginLoaders
override val consoleCommandSender: ConsoleCommandSender by instance::consoleCommandSender
override val dataStorageForJarPluginLoader: PluginDataStorage get() = instance.dataStorageForJarPluginLoader
override val configStorageForJarPluginLoader: PluginDataStorage get() = instance.configStorageForJarPluginLoader
override val dataStorageForBuiltIns: PluginDataStorage get() = instance.dataStorageForBuiltIns
override val dataStorageForJarPluginLoader: PluginDataStorage by instance::dataStorageForJarPluginLoader
override val configStorageForJarPluginLoader: PluginDataStorage by instance::configStorageForJarPluginLoader
override val dataStorageForBuiltIns: PluginDataStorage by instance::dataStorageForBuiltIns
override val consoleInput: ConsoleInput by instance::consoleInput
override fun createLoginSolver(requesterBot: Long, configuration: BotConfiguration): LoginSolver =
instance.createLoginSolver(requesterBot, configuration)
init {
DefaultLogger = { identity -> this.newLogger(identity) }
DefaultLogger = this::newLogger
}
@ConsoleExperimentalAPI
override fun newLogger(identity: String?): MiraiLogger = frontEnd.loggerFor(identity)
override fun newLogger(identity: String?): MiraiLogger = instance.newLogger(identity)
@OptIn(ConsoleExperimentalAPI::class)
internal fun doStart() {
val buildDateFormatted = SimpleDateFormat("yyyy-MM-dd").format(buildDate)
mainLogger.info { "Starting mirai-console..." }
mainLogger.info { "Backend: version $version, built on $buildDateFormatted." }
mainLogger.info { "Frontend ${frontEnd.name}: version $version." }
mainLogger.info { frontEndDescription.render() }
if (coroutineContext[Job] == null) {
throw IllegalMiraiConsoleImplementationError("The coroutineContext given to MiraiConsole must have a Job in it.")

View File

@ -14,14 +14,15 @@ import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip
import net.mamoe.mirai.console.util.ConsoleExperimentalAPI
import net.mamoe.yamlkt.Yaml
import java.io.File
import java.nio.file.Path
import kotlin.reflect.KClass
@Suppress("RedundantVisibilityModifier") // might be public in the future
internal open class MultiFilePluginDataStorageImpl(
public final override val directory: File
public final override val directoryPath: Path
) : PluginDataStorage, MultiFilePluginDataStorage {
init {
directory.mkdir()
directoryPath.mkdir()
}
public override fun <T : PluginData> load(holder: PluginDataHolder, dataClass: Class<T>): T =
@ -53,17 +54,17 @@ internal open class MultiFilePluginDataStorageImpl(
protected open fun getPluginDataFile(holder: PluginDataHolder, clazz: KClass<*>): File = with(clazz) {
val name = findASerialName()
val dir = File(directory, holder.name)
val dir = directoryPath.resolve(holder.name)
if (dir.isFile) {
error("Target directory ${dir.path} for holder $holder is occupied by a file therefore data $qualifiedNameOrTip can't be saved.")
error("Target directory $dir for holder $holder is occupied by a file therefore data $qualifiedNameOrTip can't be saved.")
}
dir.mkdir()
val file = File(directory, name)
val file = directoryPath.resolve(name)
if (file.isDirectory) {
error("Target file $file is occupied by a directory therefore data $qualifiedNameOrTip can't be saved.")
}
return file
return file.toFile()
}
@ConsoleExperimentalAPI
@ -78,4 +79,8 @@ internal open class MultiFilePluginDataStorageImpl(
file.writeText(Yaml.default.encodeToString(pluginData.updaterSerializer, Unit))
}
}
}
}
internal fun Path.mkdir(): Boolean = this.toFile().mkdir()
internal val Path.isFile: Boolean get() = this.toFile().isFile
internal val Path.isDirectory: Boolean get() = this.toFile().isDirectory

View File

@ -13,6 +13,7 @@ import kotlinx.atomicfu.AtomicLong
import kotlinx.atomicfu.locks.withLock
import kotlinx.coroutines.*
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.internal.data.mkdir
import net.mamoe.mirai.console.plugin.Plugin
import net.mamoe.mirai.console.plugin.PluginManager
import net.mamoe.mirai.console.plugin.ResourceContainer.Companion.asResourceContainer
@ -20,8 +21,8 @@ import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
import net.mamoe.mirai.console.plugin.safeLoader
import net.mamoe.mirai.utils.MiraiLogger
import java.io.File
import java.io.InputStream
import java.nio.file.Path
import java.util.concurrent.locks.ReentrantLock
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@ -60,11 +61,8 @@ internal abstract class JvmPluginInternal(
private var firstRun = true
final override val dataFolder: File by lazy {
File(
PluginManager.pluginsDataFolder,
description.name
).apply { mkdir() }
final override val dataFolderPath: Path by lazy {
PluginManager.pluginsPath.resolve(description.name).apply { mkdir() }
}
internal fun internalOnDisable() {

View File

@ -16,14 +16,15 @@ import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.Job
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.internal.data.cast
import net.mamoe.mirai.console.internal.data.mkdir
import net.mamoe.mirai.console.plugin.*
import net.mamoe.mirai.utils.info
import java.io.File
import java.nio.file.Path
import java.util.concurrent.locks.ReentrantLock
internal object PluginManagerImpl : PluginManager {
override val pluginsDir = File(MiraiConsole.rootDir, "plugins").apply { mkdir() }
override val pluginsDataFolder = File(MiraiConsole.rootDir, "data").apply { mkdir() }
override val pluginsPath: Path = MiraiConsole.rootPath.resolve("plugins").apply { mkdir() }
override val pluginsDataPath = MiraiConsole.rootPath.resolve("data").apply { mkdir() }
@Suppress("ObjectPropertyName")
private val _pluginLoaders: MutableList<PluginLoader<*, *>> = mutableListOf()
@ -45,15 +46,15 @@ internal object PluginManagerImpl : PluginManager {
?.getDescription(this)
?: error("Plugin is unloaded")
override fun registerPluginLoader(loader: PluginLoader<*, *>): Boolean = loadersLock.withLock {
if (_pluginLoaders.any { it::class == loader }) {
override fun PluginLoader<*, *>.register(): Boolean = loadersLock.withLock {
if (_pluginLoaders.any { it::class == this }) {
return false
}
_pluginLoaders.add(loader)
_pluginLoaders.add(this)
}
override fun unregisterPluginLoader(loader: PluginLoader<*, *>) = loadersLock.withLock {
_pluginLoaders.remove(loader)
override fun PluginLoader<*, *>.unregister() = loadersLock.withLock {
_pluginLoaders.remove(this)
}
init {

View File

@ -0,0 +1,15 @@
package net.mamoe.mirai.console.internal.util
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.util.ConsoleInput
import net.mamoe.mirai.console.util.requestInput
@Suppress("unused")
internal object ConsoleInputImpl : ConsoleInput {
private val inputLock = Mutex()
override suspend fun requestInput(hint: String): String =
inputLock.withLock { MiraiConsole.requestInput(hint) }
}

View File

@ -0,0 +1,28 @@
/*
* 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.plugin
// TODO: 2020/8/22 Document PluginMissingDependencyException
public class PluginMissingDependencyException : PluginResolutionException {
public constructor() : super()
public constructor(message: String?) : super(message)
public constructor(message: String?, cause: Throwable?) : super(message, cause)
public constructor(cause: Throwable?) : super(cause)
}
// TODO: 2020/8/22 Document PluginResolutionException
public open class PluginResolutionException : Exception {
public constructor() : super()
public constructor(message: String?) : super(message)
public constructor(message: String?, cause: Throwable?) : super(message, cause)
public constructor(cause: Throwable?) : super(cause)
}

View File

@ -14,11 +14,12 @@ package net.mamoe.mirai.console.plugin
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
import net.mamoe.mirai.console.util.ConsoleExperimentalAPI
import java.io.File
import java.nio.file.Path
/**
* 表示一个 mirai-console 插件.
*
* @see PluginDescription 插件描述
* @see PluginDescription 插件描述 需由 [PluginLoader] 帮助提供[PluginLoader.description]
* @see JvmPlugin Java, Kotlin 或其他 JVM 平台插件
* @see PluginFileExtensions 支持文件系统存储的扩展
*
@ -71,13 +72,33 @@ public interface PluginFileExtensions {
/**
* 数据目录
*/
public val dataFolder: File
public val dataFolderPath: Path
/**
* 从数据目录获取一个文件, 若不存在则创建文件.
* 从数据目录获取一个文件.
* @see dataFolderPath
*/
@JvmDefault
public fun file(relativePath: String): File = File(dataFolder, relativePath).apply { createNewFile() }
public fun resolveDataFile(relativePath: String): File = dataFolderPath.resolve(relativePath).toFile()
// TODO: 2020/7/11 add `fun path(...): Path` ?
/**
* 从数据目录获取一个文件.
* @see dataFolderPath
*/
@JvmDefault
public fun resolveDataPath(relativePath: String): Path = dataFolderPath.resolve(relativePath)
/**
* 从数据目录获取一个文件.
* @see dataFolderPath
*/
@JvmDefault
public fun resolveDataFile(relativePath: Path): File = dataFolderPath.resolve(relativePath).toFile()
/**
* 从数据目录获取一个文件路径.
* @see dataFolderPath
*/
@JvmDefault
public fun resolveDataPath(relativePath: Path): Path = dataFolderPath.resolve(relativePath)
}

View File

@ -21,18 +21,35 @@ import java.io.File
*
* 有关插件的依赖和已加载的插件列表由 [PluginManager] 维护.
*
* ### 内建加载器
* - [JarPluginLoader] Jar 插件加载器
*
* ### 扩展加载器
* 插件被允许扩展一个加载器 可通过 [PluginManager.registerPluginLoader]
*
* @see JarPluginLoader Jar 插件加载器
* @see PluginManager.registerPluginLoader 注册一个扩展的插件加载器
*/
public interface PluginLoader<P : Plugin, D : PluginDescription> {
/**
* 扫描并返回可以被加载的插件的 [描述][PluginDescription] 列表. 此函数只会被调用一次
* 扫描并返回可以被加载的插件的 [描述][PluginDescription] 列表.
*
* console 启动时, [PluginManager] 会获取所有 [PluginDescription], 分析依赖关系, 确认插件加载顺序.
*
* **实现细节:** 此函数只*应该* console 启动时被调用一次. 但取决于前端实现不同, 或可能由于被一些插件需要, 此函数也可能会被多次调用.
*/
public fun listPlugins(): List<D>
/**
* 获取此插件的描述
* 获取此插件的描述.
*
* **实现细节**: 此函数只允许抛出 [PluginLoadException] 作为正常失败原因, 其他任意异常都属于意外错误.
*
* 若在 console 启动并加载所有插件的过程中, 本函数抛出异常, 则会放弃此插件的加载, 并影响依赖它的其他插件.
*
* @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如无法读取插件信息等).
*
* @see PluginDescription 插件描述
*/
@get:JvmName("getPluginDescription")
@get:Throws(PluginLoadException::class)
@ -41,11 +58,19 @@ public interface PluginLoader<P : Plugin, D : PluginDescription> {
/**
* 加载一个插件 (实例), 但不 [启用][enable] . 返回加载成功的主类实例
*
* **实现细节**: 此函数只允许抛出 [PluginLoadException] 作为正常失败原因, 其他任意异常都属于意外错误.
* 当异常发生时, 插件将会直接被放弃加载, 并影响依赖它的其他插件.
*
* @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如找不到主类等).
*/
@Throws(PluginLoadException::class)
public fun load(description: D): P
/**
* 启用这个插件.
*
* **实现约定**: 若插件已经启用, 抛出
*/
public fun enable(plugin: P)
public fun disable(plugin: P)
}
@ -62,7 +87,7 @@ public open class PluginLoadException : RuntimeException {
}
/**
* '/plugins' 目录中的插件的加载器. 每个加载器需绑定一个后缀.
* ['/plugins'][PluginManager.pluginsPath] 目录中的插件的加载器. 每个加载器需绑定一个后缀.
*
* @see AbstractFilePluginLoader 默认基础实现
* @see JarPluginLoader 内建的 Jar (JVM) 插件加载器.
@ -75,13 +100,16 @@ public interface FilePluginLoader<P : Plugin, D : PluginDescription> : PluginLoa
}
/**
* [FilePluginLoader] 的默认基础实现
* [FilePluginLoader] 的默认基础实现.
*
* @see FilePluginLoader
*/
public abstract class AbstractFilePluginLoader<P : Plugin, D : PluginDescription>(
public override val fileSuffix: String
) : FilePluginLoader<P, D> {
private fun pluginsFilesSequence(): Sequence<File> =
PluginManager.pluginsDir.walk().filter { it.isFile && it.name.endsWith(fileSuffix, ignoreCase = true) }
PluginManager.pluginsPath.toFile().walk()
.filter { it.isFile && it.name.endsWith(fileSuffix, ignoreCase = true) }
/**
* 读取扫描到的后缀与 [fileSuffix] 相同的文件中的 [PluginDescription]

View File

@ -14,41 +14,74 @@ package net.mamoe.mirai.console.plugin
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.internal.plugin.PluginManagerImpl
import java.io.File
import java.nio.file.Path
/**
* 插件管理器.
*
* [PluginManager] 管理所有 [插件加载器][PluginLoader], 储存对所有插件的引用 ([plugins]), 但不直接与 [插件实例][Plugin] 交互.
*
* 有关 [插件加载][PluginLoader.load], [插件启用][PluginLoader.enable] 等操作都由 [PluginLoader] 完成.
* [PluginManager] 仅作为一个联系所有 [插件加载器][PluginLoader], 使它们互相合作的桥梁.
*
* 若要主动加载一个插件, 请获取相应插件的 [PluginLoader], 然后使用 [PluginLoader.enable]
*
* @see Plugin 插件
* @see PluginLoader 插件加载器
*/
public interface PluginManager {
/**
* `$rootDir/plugins`
* 插件自身存放路径. 由前端决定具体路径.
*
* **实现细节**: terminal 前端实现为 `$rootPath/plugins`
*
* @see pluginsFolder [File] 类型
*/
public val pluginsDir: File
public val pluginsPath: Path
/**
* `$rootDir/data`
* 插件数据存放路径
*
* **实现细节**: terminal 前端实现为 `$rootPath/data`
*
* @see pluginsDataFolder [File] 类型
*/
public val pluginsDataFolder: File
public val pluginsDataPath: Path
/**
* 已加载的插件列表
*
* @return 只读列表
*/
public val plugins: List<Plugin>
/**
* 内建的插件加载器列表. [MiraiConsole] 初始化.
*
* @return 不可变的 list.
* @return 只读列表
*/
public val builtInLoaders: List<PluginLoader<*, *>>
/**
* 由插件创建的 [PluginLoader]
*
* @return 只读列表
*/
public val pluginLoaders: List<PluginLoader<*, *>>
public fun registerPluginLoader(loader: PluginLoader<*, *>): Boolean
/**
* 注册一个扩展的插件加载器
*
* @see PluginLoader 插件加载器
*/
public fun PluginLoader<*, *>.register(): Boolean
public fun unregisterPluginLoader(loader: PluginLoader<*, *>): Boolean
/**
* 取消注册一个扩展的插件加载器
*
* @see PluginLoader 插件加载器
*/
public fun PluginLoader<*, *>.unregister(): Boolean
/**
* 获取插件的 [描述][PluginDescription], 通过 [PluginLoader.getDescription]
@ -56,26 +89,23 @@ public interface PluginManager {
public val Plugin.description: PluginDescription
public companion object INSTANCE : PluginManager by PluginManagerImpl {
override val Plugin.description: PluginDescription get() = PluginManagerImpl.run { description }
// due to Kotlin's bug
public override val Plugin.description: PluginDescription get() = PluginManagerImpl.run { description }
public override fun PluginLoader<*, *>.register(): Boolean = PluginManagerImpl.run { register() }
public override fun PluginLoader<*, *>.unregister(): Boolean = PluginManagerImpl.run { unregister() }
}
}
@JvmSynthetic
public inline fun PluginLoader<*, *>.register(): Boolean = PluginManager.registerPluginLoader(this)
/**
* @see PluginManager.pluginsPath
*/
@get:JvmSynthetic
public inline val PluginManager.pluginsFolder: File
get() = pluginsPath.toFile()
@JvmSynthetic
public inline fun PluginLoader<*, *>.unregister(): Boolean = PluginManager.unregisterPluginLoader(this)
public class PluginMissingDependencyException : PluginResolutionException {
public constructor() : super()
public constructor(message: String?) : super(message)
public constructor(message: String?, cause: Throwable?) : super(message, cause)
public constructor(cause: Throwable?) : super(cause)
}
public open class PluginResolutionException : Exception {
public constructor() : super()
public constructor(message: String?) : super(message)
public constructor(message: String?, cause: Throwable?) : super(message, cause)
public constructor(cause: Throwable?) : super(cause)
}
/**
* @see PluginManager.pluginsDataPath
*/
@get:JvmSynthetic
public inline val PluginManager.pluginsDataFolder: File
get() = pluginsDataPath.toFile()

View File

@ -22,21 +22,53 @@ import java.io.File
/**
* 插件描述
* 插件描述.
*
* @see Plugin
*/
public interface PluginDescription {
/**
* 插件类型. 将会决定加载顺序
*
* @see PluginKind
*/
public val kind: PluginKind
/**
* 插件名称.
*/
public val name: String
/**
* 插件作者, 允许为空
*/
public val author: String
/**
* 插件版本.
*
* 语法参考: ([语义化版本 2.0.0](https://semver.org/lang/zh-CN/))
*
* @see Semver 语义化版本
*/
public val version: Semver
/**
* 插件信息, 允许为空
*/
public val info: String
/** 此插件依赖的其他插件, 将会在这些插件加载之后加载此插件 */
/**
* 此插件依赖的其他插件, 将会在这些插件加载之后加载此插件
*
* @see PluginDependency
*/
public val dependencies: List<@Serializable(with = PluginDependency.SmartSerializer::class) PluginDependency>
}
/** 插件类型 */
/**
* 插件类型
*/
@Serializable(with = PluginKind.AsStringSerializer::class)
public enum class PluginKind {
/** 表示此插件提供一个 [PluginLoader], 应在加载其他 [NORMAL] 类型插件前加载 */
@ -55,7 +87,11 @@ public enum class PluginKind {
)
}
/** 插件的一个依赖的信息 */
/**
* 插件的一个依赖的信息
*
* @see PluginDescription.dependencies
*/
@Serializable(with = PluginDependency.SmartSerializer::class)
public data class PluginDependency(
/** 依赖插件名 */

View File

@ -13,10 +13,9 @@
package net.mamoe.mirai.console.util
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.mamoe.kjbb.JvmBlockingBridge
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.internal.util.ConsoleInputImpl
/**
* Console 输入. 由于 console 接管了 [stdin][System. in], [readLine] 等操作需要在这里进行.
@ -28,16 +27,9 @@ public interface ConsoleInput {
@JvmBlockingBridge
public suspend fun requestInput(hint: String): String
public companion object INSTANCE : ConsoleInput by ConsoleInputImpl {
@JvmSynthetic
public suspend inline fun MiraiConsole.requestInput(hint: String): String = ConsoleInput.requestInput(hint)
}
public companion object INSTANCE : ConsoleInput by ConsoleInputImpl
}
@Suppress("unused")
internal object ConsoleInputImpl : ConsoleInput {
private val inputLock = Mutex()
override suspend fun requestInput(hint: String): String =
inputLock.withLock { MiraiConsole.frontEnd.requestInput(hint) }
}
// don't move into INSTANCE, Compilation error
@JvmSynthetic
public suspend inline fun MiraiConsole.requestInput(hint: String): String = ConsoleInput.requestInput(hint)

View File

@ -13,7 +13,6 @@ import kotlinx.coroutines.SupervisorJob
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.CommandManager
import net.mamoe.mirai.console.command.ConsoleCommandSender
@ -22,13 +21,15 @@ import net.mamoe.mirai.console.data.PluginDataStorage
import net.mamoe.mirai.console.plugin.DeferredPluginLoader
import net.mamoe.mirai.console.plugin.PluginLoader
import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader
import net.mamoe.mirai.console.util.ConsoleExperimentalAPI
import net.mamoe.mirai.console.util.ConsoleInput
import net.mamoe.mirai.console.util.ConsoleInternalAPI
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.DefaultLogger
import net.mamoe.mirai.utils.LoginSolver
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.PlatformLogger
import java.io.File
import java.nio.file.Path
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
@ -37,22 +38,34 @@ import kotlin.test.assertNotNull
@OptIn(ConsoleInternalAPI::class)
fun initTestEnvironment() {
object : MiraiConsoleImplementation {
override val rootDir: File = createTempDir()
override val frontEnd: MiraiConsoleFrontEnd = object : MiraiConsoleFrontEnd {
override val name: String get() = "Test"
override val version: String get() = "1.0.0"
override fun loggerFor(identity: String?): MiraiLogger = PlatformLogger(identity)
override fun pushBot(bot: Bot) = println("pushBot: $bot")
override suspend fun requestInput(hint: String): String = readLine()!!
override fun createLoginSolver(): LoginSolver = LoginSolver.Default
}
override val rootPath: Path = createTempDir().toPath()
@ConsoleExperimentalAPI
override val frontEndDescription: MiraiConsoleFrontEndDescription
get() = TODO("Not yet implemented")
override val mainLogger: MiraiLogger = DefaultLogger("main")
override val builtInPluginLoaders: List<PluginLoader<*, *>> = listOf(DeferredPluginLoader { JarPluginLoader })
override val consoleCommandSender: ConsoleCommandSender = object : ConsoleCommandSender() {
override suspend fun sendMessage(message: Message) = println(message)
}
override val dataStorageForJarPluginLoader: PluginDataStorage get() = MemoryPluginDataStorage()
override val configStorageForJarPluginLoader: PluginDataStorage
get() = TODO("Not yet implemented")
override val dataStorageForBuiltIns: PluginDataStorage get() = MemoryPluginDataStorage()
override val consoleInput: ConsoleInput = object : ConsoleInput {
override suspend fun requestInput(hint: String): String {
println(hint)
return readLine() ?: error("No stdin")
}
}
override fun createLoginSolver(requesterBot: Long, configuration: BotConfiguration): LoginSolver =
LoginSolver.Default
override fun newLogger(identity: String?): MiraiLogger {
return DefaultLogger(identity)
}
override val coroutineContext: CoroutineContext = SupervisorJob()
}.start()
CommandManager

View File

@ -17,6 +17,7 @@ import net.mamoe.mirai.console.command.CommandExecuteStatus
import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommandDetailed
import net.mamoe.mirai.console.util.ConsoleInternalAPI
import net.mamoe.mirai.console.util.requestInput
import net.mamoe.mirai.utils.DefaultLogger
import org.fusesource.jansi.Ansi
import java.util.*
@ -50,11 +51,9 @@ internal fun startupConsoleThread() {
val consoleLogger = DefaultLogger("console")
while (isActive) {
try {
val next = MiraiConsoleFrontEndPure.requestInput("").let {
val next = MiraiConsole.requestInput("").let {
when {
it.startsWith(CommandManager.commandPrefix) -> {
it
}
it.startsWith(CommandManager.commandPrefix) -> it
it == "?" -> CommandManager.commandPrefix + BuiltInCommands.Help.primaryName
else -> CommandManager.commandPrefix + it
}
@ -71,7 +70,7 @@ internal fun startupConsoleThread() {
result.exception?.printStackTrace()
}
CommandExecuteStatus.COMMAND_NOT_FOUND -> {
consoleLogger.warning("Unknown command: ${result.commandName}")
consoleLogger.warning("未知指令: ${result.commandName}, 输入 ? 获取帮助")
}
CommandExecuteStatus.PERMISSION_DENIED -> {
consoleLogger.warning("Permission denied.")

View File

@ -22,20 +22,13 @@ package net.mamoe.mirai.console.pure
//import net.mamoe.mirai.console.command.CommandManager
//import net.mamoe.mirai.console.utils.MiraiConsoleFrontEnd
import io.ktor.utils.io.concurrent.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.MiraiConsoleFrontEnd
import com.vdurmont.semver4j.Semver
import net.mamoe.mirai.console.MiraiConsoleFrontEndDescription
import net.mamoe.mirai.console.util.ConsoleInternalAPI
import net.mamoe.mirai.utils.DefaultLoginSolver
import net.mamoe.mirai.utils.LoginSolver
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.PlatformLogger
import org.fusesource.jansi.Ansi
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ConcurrentHashMap
private val ANSI_RESET = Ansi().reset().toString()
@ -53,25 +46,7 @@ internal val LoggerCreator: (identity: String?) -> MiraiLogger = {
*/
@ConsoleInternalAPI
@Suppress("unused")
object MiraiConsoleFrontEndPure : MiraiConsoleFrontEnd {
private val globalLogger = LoggerCreator("Mirai")
private val cachedLoggers = ConcurrentHashMap<String, MiraiLogger>()
// companion object {
// ANSI color codes
const val COLOR_RED = "\u001b[38;5;196m"
const val COLOR_CYAN = "\u001b[38;5;87m"
const val COLOR_GREEN = "\u001b[38;5;82m"
// use a dark yellow(more like orange) instead of light one to save Solarized-light users
const val COLOR_YELLOW = "\u001b[38;5;220m"
const val COLOR_GREY = "\u001b[38;5;244m"
const val COLOR_BLUE = "\u001b[38;5;27m"
const val COLOR_NAVY = "\u001b[38;5;24m" // navy uniform blue
const val COLOR_PINK = "\u001b[38;5;207m"
const val COLOR_RESET = "\u001b[39;49m"
// }
object MiraiConsoleFrontEndPure : MiraiConsoleFrontEndDescription {
internal val sdf by ThreadLocal.withInitial {
// SimpleDateFormat not thread safe.
SimpleDateFormat("HH:mm:ss")
@ -81,32 +56,9 @@ object MiraiConsoleFrontEndPure : MiraiConsoleFrontEnd {
return this.get()
}
override val name: String
get() = "Pure"
override val version: String
get() = net.mamoe.mirai.console.internal.MiraiConsoleBuildConstants.version
override fun loggerFor(identity: String?): MiraiLogger {
identity?.apply {
return cachedLoggers.computeIfAbsent(this, LoggerCreator)
}
return globalLogger
}
override fun pushBot(bot: Bot) {
}
override suspend fun requestInput(hint: String): String {
return ConsoleUtils.miraiLineReader(hint)
}
override fun createLoginSolver(): LoginSolver {
return DefaultLoginSolver(
input = suspend {
requestInput("LOGIN> ")
}
)
}
override val name: String get() = "Pure"
override val version: Semver get() = net.mamoe.mirai.console.internal.MiraiConsoleBuildConstants.version
override val vendor: String get() = "Mamoe Technologies"
}

View File

@ -22,10 +22,12 @@
package net.mamoe.mirai.console.pure
import com.vdurmont.semver4j.Semver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import net.mamoe.mirai.console.ConsoleFrontEndImplementation
import net.mamoe.mirai.console.MiraiConsoleFrontEnd
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.MiraiConsoleFrontEndDescription
import net.mamoe.mirai.console.MiraiConsoleImplementation
import net.mamoe.mirai.console.command.ConsoleCommandSender
import net.mamoe.mirai.console.data.MultiFilePluginDataStorage
@ -33,9 +35,15 @@ import net.mamoe.mirai.console.data.PluginDataStorage
import net.mamoe.mirai.console.plugin.DeferredPluginLoader
import net.mamoe.mirai.console.plugin.PluginLoader
import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader
import net.mamoe.mirai.console.pure.ConsoleInputImpl.requestInput
import net.mamoe.mirai.console.util.ConsoleInput
import net.mamoe.mirai.console.util.ConsoleInternalAPI
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.DefaultLoginSolver
import net.mamoe.mirai.utils.LoginSolver
import net.mamoe.mirai.utils.MiraiLogger
import java.io.File
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
/**
@ -44,20 +52,43 @@ import java.util.*
* @see MiraiConsoleFrontEndPure 前端实现
* @see MiraiConsolePureLoader CLI 入口点
*/
class MiraiConsoleImplementationPure
internal class MiraiConsoleImplementationPure
@JvmOverloads constructor(
override val rootDir: File = File("."),
override val rootPath: Path = Paths.get("."),
override val builtInPluginLoaders: List<PluginLoader<*, *>> = Collections.unmodifiableList(
listOf(DeferredPluginLoader { JarPluginLoader })
),
override val frontEnd: MiraiConsoleFrontEnd = MiraiConsoleFrontEndPure,
override val mainLogger: MiraiLogger = frontEnd.loggerFor("main"),
override val frontEndDescription: MiraiConsoleFrontEndDescription = ConsoleFrontEndDescImpl,
override val consoleCommandSender: ConsoleCommandSender = ConsoleCommandSenderImpl,
override val dataStorageForJarPluginLoader: PluginDataStorage = MultiFilePluginDataStorage(File(rootDir, "data")),
override val dataStorageForBuiltIns: PluginDataStorage = MultiFilePluginDataStorage(File(rootDir, "data"))
override val dataStorageForJarPluginLoader: PluginDataStorage = MultiFilePluginDataStorage(rootPath.resolve("data")),
override val dataStorageForBuiltIns: PluginDataStorage = MultiFilePluginDataStorage(rootPath.resolve("data")),
override val configStorageForJarPluginLoader: PluginDataStorage = MultiFilePluginDataStorage(rootPath.resolve("config"))
) : MiraiConsoleImplementation, CoroutineScope by CoroutineScope(SupervisorJob()) {
init {
rootDir.mkdir()
require(rootDir.isDirectory) { "rootDir ${rootDir.absolutePath} is not a directory" }
override val mainLogger: MiraiLogger by lazy {
MiraiConsole.newLogger("main")
}
override val consoleInput: ConsoleInput get() = ConsoleInputImpl
override fun createLoginSolver(requesterBot: Long, configuration: BotConfiguration): LoginSolver {
return DefaultLoginSolver(input = { requestInput("LOGIN> ") })
}
override fun newLogger(identity: String?): MiraiLogger = LoggerCreator(identity)
init {
with(rootPath.toFile()) {
mkdir()
require(isDirectory) { "rootDir $absolutePath is not a directory" }
}
}
}
private object ConsoleInputImpl : ConsoleInput {
override suspend fun requestInput(hint: String): String = ConsoleUtils.miraiLineReader(hint)
}
private object ConsoleFrontEndDescImpl : MiraiConsoleFrontEndDescription {
override val name: String get() = "Pure"
override val vendor: String get() = "Mamoe Technologies"
override val version: Semver = net.mamoe.mirai.console.internal.MiraiConsoleBuildConstants.version
}

View File

@ -20,18 +20,9 @@
package net.mamoe.mirai.console.pure
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.alsoLogin
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register
import net.mamoe.mirai.console.command.CommandPermission
import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.console.command.ConsoleCommandSender
import net.mamoe.mirai.console.command.SimpleCommand
import net.mamoe.mirai.console.util.ConsoleInternalAPI
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.nameCardOrNick
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.content
import net.mamoe.mirai.utils.DefaultLogger
@ -44,29 +35,12 @@ object MiraiConsolePureLoader {
@JvmStatic
fun main(args: Array<String>?) {
startup()
YellowCommand.register()
runBlocking { MiraiConsole.addBot(1994701021, "Asd123456789asd").alsoLogin() }
}
}
object YellowCommand : SimpleCommand(
net.mamoe.mirai.console.command.ConsoleCommandOwner, "", "sleep",
prefixOptional = true,
description = "睡一个人",
permission = CommandPermission.Any
) {
@Handler
suspend fun CommandSender.handle(target: Member) {
target.mute(1)
sendMessage("${this.name} 睡了 ${target.nameCardOrNick}")
}
}
internal fun startup() {
DefaultLogger = { MiraiConsoleFrontEndPure.loggerFor(it) }
overrideSTD()
MiraiConsoleImplementationPure().start()
overrideSTD()
startupConsoleThread()
}