Commit 06205cb6 authored by Him188's avatar Him188

Improve Bot life cycle management, close #317

parent aa2805b8
......@@ -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<Heartbeat.Alive.Response>(
timeoutMillis = bot.configuration.heartbeatTimeoutMillis,
retry = 2
)
return null
}
private suspend fun doHeartBeat(): Throwable? {
return retryCatching(2) {
Heartbeat.Alive(bot.client)
.sendAndExpect<Heartbeat.Alive.Response>(
timeoutMillis = bot.configuration.heartbeatTimeoutMillis,
retry = 2
)
return null
} catch (e: Exception) {
lastException = e
}
return lastException
}.exceptionOrNull()
}
/**
......
......@@ -24,6 +24,7 @@ internal object MessageSvcPushForceOffline :
OutgoingPacketFactory<BotOfflineEvent.Force>("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 ?: "")
}
}
......@@ -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))
}
......
......@@ -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 : Bot> 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()
}
/**
......
......@@ -91,11 +91,15 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
@Suppress("unused")
private val offlineListener: Listener<BotOfflineEvent> =
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<N : BotNetworkHandler> 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
......@@ -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()
......
......@@ -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")
......
......@@ -30,6 +30,10 @@ annotation class LowLevelAPI
/**
* [Bot] 相关协议层低级 API.
*
* **注意**: 不应该把这个类作为一个类型, 只应使用其中的方法
*
* **警告**: 所有的低级 API 都可能在任意时刻不经过任何警告和迭代就被修改. 因此非常不建议在任何情况下使用这些 API.
*/
@MiraiExperimentalAPI
@Suppress("FunctionName", "unused")
......
......@@ -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 分钟内发出的消息, 和任意时间的群成员的消息.
......
......@@ -27,7 +27,7 @@ sealed class LoginFailedException constructor(
) : RuntimeException(message, cause)
/**
* 密码输入错误
* 密码输入错误 (有时候也会是其他错误, 如 `"当前上网环境异常,请更换网络环境或在常用设备上登录或稍后再试。"`)
*/
class WrongPasswordException(message: String?) : LoginFailedException(true, message)
......
......@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment