From 06205cb69bde6d6ca4b4269a9038c8fa31759ddf Mon Sep 17 00:00:00 2001 From: Him188 Date: Sun, 10 May 2020 15:41:08 +0800 Subject: [PATCH] Improve Bot life cycle management, close #317 --- .../network/QQAndroidBotNetworkHandler.kt | 18 ++----- .../receive/MessageSvc.PushForceOffline.kt | 1 + .../network/protocol/packet/login/StatSvc.kt | 1 + .../commonMain/kotlin/net.mamoe.mirai/Bot.kt | 33 ++++++------ .../kotlin/net.mamoe.mirai/BotImpl.kt | 14 ++++-- .../net.mamoe.mirai/event/events/BotEvents.kt | 14 +++--- .../kotlin/net.mamoe.mirai/javaFriendly.kt | 5 +- .../kotlin/net.mamoe.mirai/lowLevelApi.kt | 4 ++ .../message/data/MessageSource.kt | 12 ++--- .../network/LoginFailedException.kt | 2 +- .../net.mamoe.mirai/utils/BotConfiguration.kt | 50 +++++++++++++++++-- 11 files changed, 101 insertions(+), 53 deletions(-) diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt index e3a74a567..121831686 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt @@ -404,27 +404,15 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo logger.info { "Syncing friend message history: Success" } } - private suspend fun doHeartBeat(): Exception? { - val lastException: Exception? - try { - kotlin.runCatching { - Heartbeat.Alive(bot.client) - .sendAndExpect( - timeoutMillis = bot.configuration.heartbeatTimeoutMillis, - retry = 2 - ) - return null - } + private suspend fun doHeartBeat(): Throwable? { + return retryCatching(2) { Heartbeat.Alive(bot.client) .sendAndExpect( timeoutMillis = bot.configuration.heartbeatTimeoutMillis, retry = 2 ) return null - } catch (e: Exception) { - lastException = e - } - return lastException + }.exceptionOrNull() } /** diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.PushForceOffline.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.PushForceOffline.kt index cc194bfd2..688db6da5 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.PushForceOffline.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.PushForceOffline.kt @@ -24,6 +24,7 @@ internal object MessageSvcPushForceOffline : OutgoingPacketFactory("MessageSvc.PushForceOffline") { override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): BotOfflineEvent.Force { val struct = this.readUniPacket(RequestPushForceOffline.serializer()) + @Suppress("INVISIBLE_MEMBER") return BotOfflineEvent.Force(bot, title = struct.title ?: "", message = struct.tips ?: "") } } diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/StatSvc.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/StatSvc.kt index 4ec673d9e..11e0a9912 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/StatSvc.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/StatSvc.kt @@ -189,6 +189,7 @@ internal class StatSvc { override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): BotOfflineEvent.Dropped { val decodeUniPacket = readUniPacket(RequestMSFForceOffline.serializer()) + @Suppress("INVISIBLE_MEMBER") return BotOfflineEvent.Dropped(bot, MsfOfflineToken(decodeUniPacket.uin, decodeUniPacket.iSeqno, 0)) } diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt index 791a1b8b8..2c9e8167f 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt @@ -7,15 +7,14 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -@file:Suppress("EXPERIMENTAL_API_USAGE", "unused", "FunctionName", "NOTHING_TO_INLINE", "UnusedImport", - "EXPERIMENTAL_OVERRIDE") +@file:Suppress( + "EXPERIMENTAL_API_USAGE", "unused", "FunctionName", "NOTHING_TO_INLINE", "UnusedImport", + "EXPERIMENTAL_OVERRIDE" +) package net.mamoe.mirai -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import net.mamoe.mirai.contact.* import net.mamoe.mirai.event.events.BotInvitedJoinGroupRequestEvent import net.mamoe.mirai.event.events.MemberJoinRequestEvent @@ -35,13 +34,12 @@ import kotlin.jvm.JvmSynthetic */ @JvmSynthetic suspend inline fun B.alsoLogin(): B = also { login() } -// 任何人都能看到这个方法 /** * 机器人对象. 一个机器人实例登录一个 QQ 账号. * Mirai 为多账号设计, 可同时维护多个机器人. * - * 注: Bot 为全协程实现, 没有其他任务时若不使用 [join], 主线程将会退出. + * 有关 [Bot] 生命管理, 请查看 [BotConfiguration.inheritCoroutineContext] * * @see Contact 联系人 * @see kotlinx.coroutines.isActive 判断 [Bot] 是否正常运行中. (在线, 且没有被 [close]) @@ -278,6 +276,13 @@ abstract class Bot : CoroutineScope, LowLevelBotAPIAccessor, BotJavaFriendlyAPI( abstract val network: BotNetworkHandler } +/** + * 获取 [Job] 的协程 [Job]. 此 [Job] 为一个 [SupervisorJob] + */ +@get:JvmSynthetic +inline val Bot.supervisorJob: CompletableJob + get() = this.coroutineContext[Job] as CompletableJob + /** * 挂起协程直到 [Bot] 下线. */ @@ -305,13 +310,13 @@ suspend inline fun Bot.recall(message: MessageChain) = * @see recall */ @JvmSynthetic -inline fun Bot.recallIn( +inline fun CoroutineScope.recallIn( source: MessageSource, millis: Long, coroutineContext: CoroutineContext = EmptyCoroutineContext ): Job = this.launch(coroutineContext + CoroutineName("MessageRecall")) { - kotlinx.coroutines.delay(millis) - recall(source) + delay(millis) + source.recall() } /** @@ -322,13 +327,13 @@ inline fun Bot.recallIn( * @see recall */ @JvmSynthetic -inline fun Bot.recallIn( +inline fun CoroutineScope.recallIn( message: MessageChain, millis: Long, coroutineContext: CoroutineContext = EmptyCoroutineContext ): Job = this.launch(coroutineContext + CoroutineName("MessageRecall")) { - kotlinx.coroutines.delay(millis) - recall(message) + delay(millis) + message.recall() } /** diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/BotImpl.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/BotImpl.kt index e470147d3..6747ea7b7 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/BotImpl.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/BotImpl.kt @@ -91,11 +91,15 @@ abstract class BotImpl constructor( @Suppress("unused") private val offlineListener: Listener = this@BotImpl.subscribeAlways(concurrency = Listener.ConcurrencyKind.LOCKED) { event -> - if (event.bot != this.bot) { + if (event.bot != this@BotImpl) { + return@subscribeAlways + } + if (!::_network.isInitialized) { + // bot 还未登录就被 close return@subscribeAlways } if (network.areYouOk() && event !is BotOfflineEvent.Force) { - // avoid concurrent re-login tasks + // network 运行正常 return@subscribeAlways } when (event) { @@ -262,14 +266,14 @@ abstract class BotImpl constructor( this.launch { BotOfflineEvent.Active(this@BotImpl, cause).broadcast() } + logger.info { "Bot cancelled" + cause?.message?.let { ": $it" }.orEmpty() } if (cause == null) { - this.cancel() + supervisorJob.cancel() } else { - this.cancel(CancellationException("bot cancelled", cause)) + supervisorJob.cancel(CancellationException("Bot closed", cause)) } } } - @RequiresOptIn(level = RequiresOptIn.Level.ERROR) internal annotation class ThisApiMustBeUsedInWithConnectionLockBlock \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/events/BotEvents.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/events/BotEvents.kt index a34bb4736..57e84569a 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/events/BotEvents.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/events/BotEvents.kt @@ -51,7 +51,7 @@ class EventCancelledException : RuntimeException { /** * [Bot] 登录完成, 好友列表, 群组列表初始化完成 */ -data class BotOnlineEvent(override val bot: Bot) : BotActiveEvent, AbstractEvent() +data class BotOnlineEvent internal constructor(override val bot: Bot) : BotActiveEvent, AbstractEvent() /** * [Bot] 离线. @@ -59,31 +59,33 @@ data class BotOnlineEvent(override val bot: Bot) : BotActiveEvent, AbstractEvent sealed class BotOfflineEvent : BotEvent, AbstractEvent() { /** - * 主动离线 + * 主动离线. 主动广播这个事件也可以让 [Bot] 关闭. */ data class Active(override val bot: Bot, val cause: Throwable?) : BotOfflineEvent(), BotActiveEvent /** * 被挤下线 */ - data class Force(override val bot: Bot, val title: String, val message: String) : BotOfflineEvent(), Packet, + data class Force internal constructor(override val bot: Bot, val title: String, val message: String) : + BotOfflineEvent(), Packet, BotPassiveEvent /** * 被服务器断开或因网络问题而掉线 */ - data class Dropped(override val bot: Bot, val cause: Throwable?) : BotOfflineEvent(), Packet, BotPassiveEvent + data class Dropped internal constructor(override val bot: Bot, val cause: Throwable?) : BotOfflineEvent(), Packet, + BotPassiveEvent /** * 服务器主动要求更换另一个服务器 */ - data class RequireReconnect(override val bot: Bot) : BotOfflineEvent(), Packet, BotPassiveEvent + data class RequireReconnect internal constructor(override val bot: Bot) : BotOfflineEvent(), Packet, BotPassiveEvent } /** * [Bot] 主动或被动重新登录. */ -data class BotReloginEvent( +data class BotReloginEvent internal constructor( override val bot: Bot, val cause: Throwable? ) : BotEvent, BotActiveEvent, AbstractEvent() diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/javaFriendly.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/javaFriendly.kt index 3d5a3ce4f..3488d7d18 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/javaFriendly.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/javaFriendly.kt @@ -13,15 +13,18 @@ import net.mamoe.mirai.utils.MiraiInternalAPI /** * 表明这个 API 是为了让 Java 使用者调用更方便. + * * 一般有一定的性能损失, 且不能在 JVM/Android 以外平台使用. 不要在 Kotlin 调用它. */ @MiraiInternalAPI @RequiresOptIn(level = RequiresOptIn.Level.ERROR) @Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.TYPE, AnnotationTarget.CLASS) -annotation class JavaFriendlyAPI +internal annotation class JavaFriendlyAPI /** * [Bot] 中为了让 Java 使用者调用更方便的 API 列表. + * + * **注意**: 不应该把这个类作为一个类型, 只应使用其中的方法 */ @MiraiInternalAPI @Suppress("FunctionName", "INAPPLICABLE_JVM_NAME", "unused") diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/lowLevelApi.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/lowLevelApi.kt index d21d779f2..89597d4ca 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/lowLevelApi.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/lowLevelApi.kt @@ -30,6 +30,10 @@ annotation class LowLevelAPI /** * [Bot] 相关协议层低级 API. + * + * **注意**: 不应该把这个类作为一个类型, 只应使用其中的方法 + * + * **警告**: 所有的低级 API 都可能在任意时刻不经过任何警告和迭代就被修改. 因此非常不建议在任何情况下使用这些 API. */ @MiraiExperimentalAPI @Suppress("FunctionName", "unused") diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageSource.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageSource.kt index a6a310bea..02e43c8eb 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageSource.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageSource.kt @@ -331,19 +331,15 @@ inline fun MessageSource.isAboutFriend(): Boolean { * 引用这条消息 * @see QuoteReply */ -fun MessageSource.quote(): QuoteReply { - - return QuoteReply(this) -} +@JvmSynthetic +inline fun MessageSource.quote(): QuoteReply = QuoteReply(this) /** * 引用这条消息. 仅从服务器接收的消息 (即来自 [MessageEvent]) 才可以通过这个方式被引用. * @see QuoteReply */ -fun MessageChain.quote(): QuoteReply { - - return QuoteReply(this.source) -} +@JvmSynthetic +inline fun MessageChain.quote(): QuoteReply = QuoteReply(this.source) /** * 撤回这条消息. 可撤回自己 2 分钟内发出的消息, 和任意时间的群成员的消息. diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/LoginFailedException.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/LoginFailedException.kt index 1499edf8f..bb7356549 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/LoginFailedException.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/LoginFailedException.kt @@ -27,7 +27,7 @@ sealed class LoginFailedException constructor( ) : RuntimeException(message, cause) /** - * 密码输入错误 + * 密码输入错误 (有时候也会是其他错误, 如 `"当前上网环境异常,请更换网络环境或在常用设备上登录或稍后再试。"`) */ class WrongPasswordException(message: String?) : LoginFailedException(true, message) diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt index bc964c2d3..16104b1e8 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt @@ -11,6 +11,7 @@ package net.mamoe.mirai.utils import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import net.mamoe.mirai.Bot import net.mamoe.mirai.network.BotNetworkHandler import kotlin.coroutines.CoroutineContext @@ -34,7 +35,7 @@ open class BotConfiguration { /** 设备信息覆盖. 默认使用随机的设备信息. */ var deviceInfo: ((Context) -> DeviceInfo)? = null - /** 父 [CoroutineContext]. [Bot] 创建后会覆盖其 [Job], 但会将这个 [Job] 作为父 [Job] */ + /** 父 [CoroutineContext]. [Bot] 创建后会使用 [SupervisorJob] 覆盖其 [Job], 但会将这个 [Job] 作为父 [Job] */ var parentCoroutineContext: CoroutineContext = EmptyCoroutineContext /** 心跳周期. 过长会导致被服务器断开连接. */ @@ -115,21 +116,64 @@ open class BotConfiguration { } /** - * 使用当前协程的 [coroutineContext] 作为 [parentCoroutineContext] + * 使用当前协程的 [coroutineContext] 作为 [parentCoroutineContext]. + * + * Bot 将会使用一个 [SupervisorJob] 覆盖 [coroutineContext] 当前协程的 [Job], 并使用当前协程的 [Job] 作为父 [Job] * * 用例: * ``` * coroutineScope { - * val bot = Bot(...) + * val bot = Bot(...) { + * inheritCoroutineContext() + * } * bot.login() * } // coroutineScope 会等待 Bot 退出 * ``` + * + * + * **注意**: `bot.cancel` 时将会让父 [Job] 也被 cancel. + * ``` + * coroutineScope { // this: CoroutineScope + * launch { + * while(isActive) { + * delay(500) + * println("I'm alive") + * } + * } + * + * val bot = Bot(...) { + * inheritCoroutineContext() // 使用 `coroutineScope` 的 Job 作为父 Job + * } + * bot.login() + * bot.cancel() // 取消了整个 `coroutineScope`, 因此上文不断打印 `"I'm alive"` 的协程也会被取消. + * } + * ``` + * + * 因此, 此函数尤为适合在 `suspend fun main()` 中使用, 它能阻止主线程退出: + * ``` + * suspend fun main() { + * val bot = Bot() { + * inheritCoroutineContext() + * } + * bot.subscribe { ... } + * + * // 主线程不会退出, 直到 Bot 离线. + * } + * ``` + * + * 简言之, + * - 若想让 [Bot] 作为 '守护进程' 运行, 则无需调用 [inheritCoroutineContext]. + * - 若想让 [Bot] 依赖于当前协程, 让当前协程等待 [Bot] 运行, 则使用 [inheritCoroutineContext] + * + * @see parentCoroutineContext */ @ConfigurationDsl suspend inline fun inheritCoroutineContext() { parentCoroutineContext = coroutineContext } + /** 标注一个配置 DSL 函数 */ + @Target(AnnotationTarget.FUNCTION) @DslMarker annotation class ConfigurationDsl