Improve Bot life cycle management, close #317

This commit is contained in:
Him188 2020-05-10 15:41:08 +08:00
parent aa2805b81f
commit 06205cb69b
11 changed files with 101 additions and 53 deletions

View File

@ -404,27 +404,15 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
logger.info { "Syncing friend message history: Success" } logger.info { "Syncing friend message history: Success" }
} }
private suspend fun doHeartBeat(): Exception? { private suspend fun doHeartBeat(): Throwable? {
val lastException: Exception? return retryCatching(2) {
try {
kotlin.runCatching {
Heartbeat.Alive(bot.client)
.sendAndExpect<Heartbeat.Alive.Response>(
timeoutMillis = bot.configuration.heartbeatTimeoutMillis,
retry = 2
)
return null
}
Heartbeat.Alive(bot.client) Heartbeat.Alive(bot.client)
.sendAndExpect<Heartbeat.Alive.Response>( .sendAndExpect<Heartbeat.Alive.Response>(
timeoutMillis = bot.configuration.heartbeatTimeoutMillis, timeoutMillis = bot.configuration.heartbeatTimeoutMillis,
retry = 2 retry = 2
) )
return null return null
} catch (e: Exception) { }.exceptionOrNull()
lastException = e
}
return lastException
} }
/** /**

View File

@ -24,6 +24,7 @@ internal object MessageSvcPushForceOffline :
OutgoingPacketFactory<BotOfflineEvent.Force>("MessageSvc.PushForceOffline") { OutgoingPacketFactory<BotOfflineEvent.Force>("MessageSvc.PushForceOffline") {
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): BotOfflineEvent.Force { override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): BotOfflineEvent.Force {
val struct = this.readUniPacket(RequestPushForceOffline.serializer()) val struct = this.readUniPacket(RequestPushForceOffline.serializer())
@Suppress("INVISIBLE_MEMBER")
return BotOfflineEvent.Force(bot, title = struct.title ?: "", message = struct.tips ?: "") return BotOfflineEvent.Force(bot, title = struct.title ?: "", message = struct.tips ?: "")
} }
} }

View File

@ -189,6 +189,7 @@ internal class StatSvc {
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): BotOfflineEvent.Dropped { override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): BotOfflineEvent.Dropped {
val decodeUniPacket = readUniPacket(RequestMSFForceOffline.serializer()) val decodeUniPacket = readUniPacket(RequestMSFForceOffline.serializer())
@Suppress("INVISIBLE_MEMBER")
return BotOfflineEvent.Dropped(bot, MsfOfflineToken(decodeUniPacket.uin, decodeUniPacket.iSeqno, 0)) return BotOfflineEvent.Dropped(bot, MsfOfflineToken(decodeUniPacket.uin, decodeUniPacket.iSeqno, 0))
} }

View File

@ -7,15 +7,14 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file:Suppress("EXPERIMENTAL_API_USAGE", "unused", "FunctionName", "NOTHING_TO_INLINE", "UnusedImport", @file:Suppress(
"EXPERIMENTAL_OVERRIDE") "EXPERIMENTAL_API_USAGE", "unused", "FunctionName", "NOTHING_TO_INLINE", "UnusedImport",
"EXPERIMENTAL_OVERRIDE"
)
package net.mamoe.mirai package net.mamoe.mirai
import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import net.mamoe.mirai.contact.* import net.mamoe.mirai.contact.*
import net.mamoe.mirai.event.events.BotInvitedJoinGroupRequestEvent import net.mamoe.mirai.event.events.BotInvitedJoinGroupRequestEvent
import net.mamoe.mirai.event.events.MemberJoinRequestEvent import net.mamoe.mirai.event.events.MemberJoinRequestEvent
@ -35,13 +34,12 @@ import kotlin.jvm.JvmSynthetic
*/ */
@JvmSynthetic @JvmSynthetic
suspend inline fun <B : Bot> B.alsoLogin(): B = also { login() } suspend inline fun <B : Bot> B.alsoLogin(): B = also { login() }
// 任何人都能看到这个方法
/** /**
* 机器人对象. 一个机器人实例登录一个 QQ 账号. * 机器人对象. 一个机器人实例登录一个 QQ 账号.
* Mirai 为多账号设计, 可同时维护多个机器人. * Mirai 为多账号设计, 可同时维护多个机器人.
* *
* : Bot 为全协程实现, 没有其他任务时若不使用 [join], 主线程将会退出. * 有关 [Bot] 生命管理, 请查看 [BotConfiguration.inheritCoroutineContext]
* *
* @see Contact 联系人 * @see Contact 联系人
* @see kotlinx.coroutines.isActive 判断 [Bot] 是否正常运行中. (在线, 且没有被 [close]) * @see kotlinx.coroutines.isActive 判断 [Bot] 是否正常运行中. (在线, 且没有被 [close])
@ -278,6 +276,13 @@ abstract class Bot : CoroutineScope, LowLevelBotAPIAccessor, BotJavaFriendlyAPI(
abstract val network: BotNetworkHandler abstract val network: BotNetworkHandler
} }
/**
* 获取 [Job] 的协程 [Job]. [Job] 为一个 [SupervisorJob]
*/
@get:JvmSynthetic
inline val Bot.supervisorJob: CompletableJob
get() = this.coroutineContext[Job] as CompletableJob
/** /**
* 挂起协程直到 [Bot] 下线. * 挂起协程直到 [Bot] 下线.
*/ */
@ -305,13 +310,13 @@ suspend inline fun Bot.recall(message: MessageChain) =
* @see recall * @see recall
*/ */
@JvmSynthetic @JvmSynthetic
inline fun Bot.recallIn( inline fun CoroutineScope.recallIn(
source: MessageSource, source: MessageSource,
millis: Long, millis: Long,
coroutineContext: CoroutineContext = EmptyCoroutineContext coroutineContext: CoroutineContext = EmptyCoroutineContext
): Job = this.launch(coroutineContext + CoroutineName("MessageRecall")) { ): Job = this.launch(coroutineContext + CoroutineName("MessageRecall")) {
kotlinx.coroutines.delay(millis) delay(millis)
recall(source) source.recall()
} }
/** /**
@ -322,13 +327,13 @@ inline fun Bot.recallIn(
* @see recall * @see recall
*/ */
@JvmSynthetic @JvmSynthetic
inline fun Bot.recallIn( inline fun CoroutineScope.recallIn(
message: MessageChain, message: MessageChain,
millis: Long, millis: Long,
coroutineContext: CoroutineContext = EmptyCoroutineContext coroutineContext: CoroutineContext = EmptyCoroutineContext
): Job = this.launch(coroutineContext + CoroutineName("MessageRecall")) { ): Job = this.launch(coroutineContext + CoroutineName("MessageRecall")) {
kotlinx.coroutines.delay(millis) delay(millis)
recall(message) message.recall()
} }
/** /**

View File

@ -91,11 +91,15 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
@Suppress("unused") @Suppress("unused")
private val offlineListener: Listener<BotOfflineEvent> = private val offlineListener: Listener<BotOfflineEvent> =
this@BotImpl.subscribeAlways(concurrency = Listener.ConcurrencyKind.LOCKED) { event -> 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 return@subscribeAlways
} }
if (network.areYouOk() && event !is BotOfflineEvent.Force) { if (network.areYouOk() && event !is BotOfflineEvent.Force) {
// avoid concurrent re-login tasks // network 运行正常
return@subscribeAlways return@subscribeAlways
} }
when (event) { when (event) {
@ -262,14 +266,14 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
this.launch { this.launch {
BotOfflineEvent.Active(this@BotImpl, cause).broadcast() BotOfflineEvent.Active(this@BotImpl, cause).broadcast()
} }
logger.info { "Bot cancelled" + cause?.message?.let { ": $it" }.orEmpty() }
if (cause == null) { if (cause == null) {
this.cancel() supervisorJob.cancel()
} else { } else {
this.cancel(CancellationException("bot cancelled", cause)) supervisorJob.cancel(CancellationException("Bot closed", cause))
} }
} }
} }
@RequiresOptIn(level = RequiresOptIn.Level.ERROR) @RequiresOptIn(level = RequiresOptIn.Level.ERROR)
internal annotation class ThisApiMustBeUsedInWithConnectionLockBlock internal annotation class ThisApiMustBeUsedInWithConnectionLockBlock

View File

@ -51,7 +51,7 @@ class EventCancelledException : RuntimeException {
/** /**
* [Bot] 登录完成, 好友列表, 群组列表初始化完成 * [Bot] 登录完成, 好友列表, 群组列表初始化完成
*/ */
data class BotOnlineEvent(override val bot: Bot) : BotActiveEvent, AbstractEvent() data class BotOnlineEvent internal constructor(override val bot: Bot) : BotActiveEvent, AbstractEvent()
/** /**
* [Bot] 离线. * [Bot] 离线.
@ -59,31 +59,33 @@ data class BotOnlineEvent(override val bot: Bot) : BotActiveEvent, AbstractEvent
sealed class BotOfflineEvent : BotEvent, AbstractEvent() { sealed class BotOfflineEvent : BotEvent, AbstractEvent() {
/** /**
* 主动离线 * 主动离线. 主动广播这个事件也可以让 [Bot] 关闭.
*/ */
data class Active(override val bot: Bot, val cause: Throwable?) : BotOfflineEvent(), BotActiveEvent 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 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] 主动或被动重新登录. * [Bot] 主动或被动重新登录.
*/ */
data class BotReloginEvent( data class BotReloginEvent internal constructor(
override val bot: Bot, override val bot: Bot,
val cause: Throwable? val cause: Throwable?
) : BotEvent, BotActiveEvent, AbstractEvent() ) : BotEvent, BotActiveEvent, AbstractEvent()

View File

@ -13,15 +13,18 @@ import net.mamoe.mirai.utils.MiraiInternalAPI
/** /**
* 表明这个 API 是为了让 Java 使用者调用更方便. * 表明这个 API 是为了让 Java 使用者调用更方便.
*
* 一般有一定的性能损失, 且不能在 JVM/Android 以外平台使用. 不要在 Kotlin 调用它. * 一般有一定的性能损失, 且不能在 JVM/Android 以外平台使用. 不要在 Kotlin 调用它.
*/ */
@MiraiInternalAPI @MiraiInternalAPI
@RequiresOptIn(level = RequiresOptIn.Level.ERROR) @RequiresOptIn(level = RequiresOptIn.Level.ERROR)
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.TYPE, AnnotationTarget.CLASS) @Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.TYPE, AnnotationTarget.CLASS)
annotation class JavaFriendlyAPI internal annotation class JavaFriendlyAPI
/** /**
* [Bot] 中为了让 Java 使用者调用更方便的 API 列表. * [Bot] 中为了让 Java 使用者调用更方便的 API 列表.
*
* **注意**: 不应该把这个类作为一个类型, 只应使用其中的方法
*/ */
@MiraiInternalAPI @MiraiInternalAPI
@Suppress("FunctionName", "INAPPLICABLE_JVM_NAME", "unused") @Suppress("FunctionName", "INAPPLICABLE_JVM_NAME", "unused")

View File

@ -30,6 +30,10 @@ annotation class LowLevelAPI
/** /**
* [Bot] 相关协议层低级 API. * [Bot] 相关协议层低级 API.
*
* **注意**: 不应该把这个类作为一个类型, 只应使用其中的方法
*
* **警告**: 所有的低级 API 都可能在任意时刻不经过任何警告和迭代就被修改. 因此非常不建议在任何情况下使用这些 API.
*/ */
@MiraiExperimentalAPI @MiraiExperimentalAPI
@Suppress("FunctionName", "unused") @Suppress("FunctionName", "unused")

View File

@ -331,19 +331,15 @@ inline fun MessageSource.isAboutFriend(): Boolean {
* 引用这条消息 * 引用这条消息
* @see QuoteReply * @see QuoteReply
*/ */
fun MessageSource.quote(): QuoteReply { @JvmSynthetic
inline fun MessageSource.quote(): QuoteReply = QuoteReply(this)
return QuoteReply(this)
}
/** /**
* 引用这条消息. 仅从服务器接收的消息 (即来自 [MessageEvent]) 才可以通过这个方式被引用. * 引用这条消息. 仅从服务器接收的消息 (即来自 [MessageEvent]) 才可以通过这个方式被引用.
* @see QuoteReply * @see QuoteReply
*/ */
fun MessageChain.quote(): QuoteReply { @JvmSynthetic
inline fun MessageChain.quote(): QuoteReply = QuoteReply(this.source)
return QuoteReply(this.source)
}
/** /**
* 撤回这条消息. 可撤回自己 2 分钟内发出的消息, 和任意时间的群成员的消息. * 撤回这条消息. 可撤回自己 2 分钟内发出的消息, 和任意时间的群成员的消息.

View File

@ -27,7 +27,7 @@ sealed class LoginFailedException constructor(
) : RuntimeException(message, cause) ) : RuntimeException(message, cause)
/** /**
* 密码输入错误 * 密码输入错误 (有时候也会是其他错误, `"当前上网环境异常,请更换网络环境或在常用设备上登录或稍后再试。"`)
*/ */
class WrongPasswordException(message: String?) : LoginFailedException(true, message) class WrongPasswordException(message: String?) : LoginFailedException(true, message)

View File

@ -11,6 +11,7 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.network.BotNetworkHandler import net.mamoe.mirai.network.BotNetworkHandler
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -34,7 +35,7 @@ open class BotConfiguration {
/** 设备信息覆盖. 默认使用随机的设备信息. */ /** 设备信息覆盖. 默认使用随机的设备信息. */
var deviceInfo: ((Context) -> DeviceInfo)? = null var deviceInfo: ((Context) -> DeviceInfo)? = null
/** 父 [CoroutineContext]. [Bot] 创建后会覆盖其 [Job], 但会将这个 [Job] 作为父 [Job] */ /** 父 [CoroutineContext]. [Bot] 创建后会使用 [SupervisorJob] 覆盖其 [Job], 但会将这个 [Job] 作为父 [Job] */
var parentCoroutineContext: CoroutineContext = EmptyCoroutineContext var parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
/** 心跳周期. 过长会导致被服务器断开连接. */ /** 心跳周期. 过长会导致被服务器断开连接. */
@ -115,21 +116,64 @@ open class BotConfiguration {
} }
/** /**
* 使用当前协程的 [coroutineContext] 作为 [parentCoroutineContext] * 使用当前协程的 [coroutineContext] 作为 [parentCoroutineContext].
*
* Bot 将会使用一个 [SupervisorJob] 覆盖 [coroutineContext] 当前协程的 [Job], 并使用当前协程的 [Job] 作为父 [Job]
* *
* 用例: * 用例:
* ``` * ```
* coroutineScope { * coroutineScope {
* val bot = Bot(...) * val bot = Bot(...) {
* inheritCoroutineContext()
* }
* bot.login() * bot.login()
* } // coroutineScope 会等待 Bot 退出 * } // 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 @ConfigurationDsl
suspend inline fun inheritCoroutineContext() { suspend inline fun inheritCoroutineContext() {
parentCoroutineContext = coroutineContext parentCoroutineContext = coroutineContext
} }
/** 标注一个配置 DSL 函数 */
@Target(AnnotationTarget.FUNCTION)
@DslMarker @DslMarker
annotation class ConfigurationDsl annotation class ConfigurationDsl