diff --git a/docs/DebuggingNetwork.md b/docs/DebuggingNetwork.md index eebf6eb6759..3bd44422b20 100644 --- a/docs/DebuggingNetwork.md +++ b/docs/DebuggingNetwork.md @@ -14,7 +14,8 @@ |:----------------------------------------------------|:---------------------------------|:---------------------------------------------------------------------------------------| | `mirai.network.handler.selector.max.attempts` | `[1, 2147483647]` | 最大重连尝试次数 | | `mirai.network.reconnect.delay` | `[0, 9223372036854775807]` | 两次重连尝试的间隔毫秒数 | -| `mirai.network.handle.selector.logging` | `true`/`false` | 启用执行重连时的详细日志 | +| `mirai.network.handler.selector.logging` | `true`/`false` | 启用执行重连时的详细日志 | +| `mirai.network.handler.cancellation.trace` | `true`/`false` | 让网络层的异常时包含详细原因 | | `mirai.network.state.observer.logging` | `true`/`on`/`false`/`off`/`full` | 启用网络层状态变更的日志 | | `mirai.event.launch.undispatched` | `true`/`false` | 详见 [源码内注释][launch-undispatched] | | `mirai.resource.creation.stack.enabled` | `true`/`false` | 启用 `ExternalResource` 创建时的 stacktrace 记录 (影响性能), 在资源泄露时展示 | @@ -31,5 +32,5 @@ 修改示例: -在启动 JVM 时添加参数 `-Dmirai.network.handle.selector.logging=true` +在启动 JVM 时添加参数 `-Dmirai.network.handler.selector.logging=true` 则启用执行重连时的详细日志 diff --git a/mirai-core-api/src/commonMain/kotlin/network/LoginFailedException.kt b/mirai-core-api/src/commonMain/kotlin/network/LoginFailedException.kt index ab08c87e704..aeb2284d712 100644 --- a/mirai-core-api/src/commonMain/kotlin/network/LoginFailedException.kt +++ b/mirai-core-api/src/commonMain/kotlin/network/LoginFailedException.kt @@ -27,6 +27,8 @@ public sealed class LoginFailedException( message: String? = null, cause: Throwable? = null ) : RuntimeException(message, cause) +// 实现提示 (仅供网络层实现者参考): `LoginFailedException` 会被包装为 `NetworkException` (`LoginFailedExceptionAsNetworkException`), +// 并在 `bot.login` 时 unwrap. /** * 密码输入错误 (有时候也会是其他错误, 如 `"当前上网环境异常,请更换网络环境或在常用设备上登录或稍后再试。"`) diff --git a/mirai-core-utils/src/androidMain/kotlin/Actuals.kt b/mirai-core-utils/src/androidMain/kotlin/Actuals.kt index 0d61475ccfe..d90a8533c64 100644 --- a/mirai-core-utils/src/androidMain/kotlin/Actuals.kt +++ b/mirai-core-utils/src/androidMain/kotlin/Actuals.kt @@ -30,13 +30,18 @@ internal class StacktraceException(override val message: String?, private val st override fun getStackTrace(): Array = stacktrace } -public actual inline fun Throwable.unwrap(): Throwable { +public actual inline fun Throwable.unwrap(addSuppressed: Boolean): Throwable { if (this !is E) return this - val e = StacktraceException("Unwrapped exception: $this", this.stackTrace) - for (throwable in this.suppressed) { - e.addSuppressed(throwable) + return if (addSuppressed) { + val e = StacktraceException("Unwrapped exception: $this", this.stackTrace) + for (throwable in this.suppressed) { + e.addSuppressed(throwable) + } + this.findCause { it !is E } + ?.also { it.addSuppressed(e) } + ?: this + } else { + this.findCause { it !is E } + ?: this } - return this.findCause { it !is E } - ?.also { it.addSuppressed(e) } - ?: this } diff --git a/mirai-core-utils/src/commonMain/kotlin/CoroutineUtils.kt b/mirai-core-utils/src/commonMain/kotlin/CoroutineUtils.kt index f6bd1b4a27f..7aa820fd2c3 100644 --- a/mirai-core-utils/src/commonMain/kotlin/CoroutineUtils.kt +++ b/mirai-core-utils/src/commonMain/kotlin/CoroutineUtils.kt @@ -118,20 +118,8 @@ public fun CoroutineContext.childScopeContext( else it } -public inline fun runUnwrapCancellationException(block: () -> R): R { - try { - return block() - } catch (e: CancellationException) { - // e is like `Exception in thread "main" kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=JobImpl{Cancelled}@f252f300` - // and this is useless. - throw e.unwrapCancellationException() - - // if (e.suppressed.isNotEmpty()) throw e // preserve details. - // throw e.findCause { it !is CancellationException } ?: e - } -} - -public fun Throwable.unwrapCancellationException(): Throwable = unwrap() +public fun Throwable.unwrapCancellationException(addSuppressed: Boolean = true): Throwable = + unwrap(addSuppressed) /** * For code @@ -178,6 +166,6 @@ public fun Throwable.unwrapCancellationException(): Throwable = unwrap Throwable.unwrap(): Throwable +public expect inline fun Throwable.unwrap(addSuppressed: Boolean = true): Throwable public val CoroutineContext.coroutineName: String get() = this[CoroutineName]?.name ?: "unnamed" \ No newline at end of file diff --git a/mirai-core-utils/src/commonMain/kotlin/StandardUtils.kt b/mirai-core-utils/src/commonMain/kotlin/StandardUtils.kt index d2e17363e57..ab71bd957fd 100644 --- a/mirai-core-utils/src/commonMain/kotlin/StandardUtils.kt +++ b/mirai-core-utils/src/commonMain/kotlin/StandardUtils.kt @@ -83,12 +83,15 @@ public fun Throwable.causes(maxDepth: Int = 20): Sequence = sequence public inline fun Throwable.findCause(maxDepth: Int = 20, filter: (Throwable) -> Boolean): Throwable? { var depth = 0 - var rootCause: Throwable? = this + var curr: Throwable? = this while (true) { - if (rootCause?.cause === rootCause) return rootCause - val current = rootCause?.cause ?: return null - if (filter(current)) return current - rootCause = rootCause.cause + if (curr == null) return null + val cause = curr.cause ?: return null + if (filter(cause)) return cause + + if (curr.cause === curr) return null // circular reference + curr = curr.cause + if (depth++ >= maxDepth) return null } } diff --git a/mirai-core-utils/src/jvmMain/kotlin/Actuals.kt b/mirai-core-utils/src/jvmMain/kotlin/Actuals.kt index 9b100f3f17d..6af31de854a 100644 --- a/mirai-core-utils/src/jvmMain/kotlin/Actuals.kt +++ b/mirai-core-utils/src/jvmMain/kotlin/Actuals.kt @@ -23,9 +23,9 @@ public actual fun String.decodeBase64(): ByteArray { return Base64.getDecoder().decode(this) } -public actual inline fun Throwable.unwrap(): Throwable { +public actual inline fun Throwable.unwrap(addSuppressed: Boolean): Throwable { if (this !is E) return this return this.findCause { it !is E } - ?.also { it.addSuppressed(this) } + ?.also { if (addSuppressed) it.addSuppressed(this) } ?: this } \ No newline at end of file diff --git a/mirai-core-utils/src/nativeMain/kotlin/CoroutineUtils.kt b/mirai-core-utils/src/nativeMain/kotlin/CoroutineUtils.kt index e3df32ca5d9..6beaf0c77b4 100644 --- a/mirai-core-utils/src/nativeMain/kotlin/CoroutineUtils.kt +++ b/mirai-core-utils/src/nativeMain/kotlin/CoroutineUtils.kt @@ -64,9 +64,9 @@ public actual suspend inline fun T.runBIO(crossinline block: T.() -> R): * ``` */ @Suppress("unused") -public actual inline fun Throwable.unwrap(): Throwable { +public actual inline fun Throwable.unwrap(addSuppressed: Boolean): Throwable { if (this !is E) return this return this.findCause { it !is E } - ?.also { it.addSuppressed(this) } + ?.also { if (addSuppressed) it.addSuppressed(this) } ?: this } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/AbstractBot.kt b/mirai-core/src/commonMain/kotlin/AbstractBot.kt index 56391339674..d6153fb93fd 100644 --- a/mirai-core/src/commonMain/kotlin/AbstractBot.kt +++ b/mirai-core/src/commonMain/kotlin/AbstractBot.kt @@ -135,13 +135,18 @@ internal abstract class AbstractBot constructor( try { network.resumeConnection() } catch (e: Throwable) { // failed to init - val cause = e.unwrap() + // lift cause to the top of the exception chain. e.g. LoginFailedException + val cause = if (e is NetworkException) { + e.unwrapForPublicApi() + } else e + + // close bot if it hadn't been done during `resumeConnection()` if (!components[SsoProcessor].firstLoginSucceed) { - this.close(cause) // failed to do first login. - } - if (cause is LoginFailedException && cause.killBot) { - close(cause) + close(cause) // failed to do first login, close bot + } else if (cause is LoginFailedException && cause.killBot) { + close(cause) // re-login failed and has caused bot being somehow killed by server } + throw cause } logger.info { "Bot login successful." } diff --git a/mirai-core/src/commonMain/kotlin/network/components/BotOfflineEventMonitor.kt b/mirai-core/src/commonMain/kotlin/network/components/BotOfflineEventMonitor.kt index 21fadc0d6e2..23ba319898e 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/BotOfflineEventMonitor.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/BotOfflineEventMonitor.kt @@ -23,7 +23,7 @@ import net.mamoe.mirai.internal.network.handler.selector.NetworkException import net.mamoe.mirai.utils.* /** - * Handles [BotOfflineEvent] + * Handles [BotOfflineEvent]. It launches recovery jobs when receiving offline events from server. */ internal interface BotOfflineEventMonitor { companion object : ComponentKey @@ -70,10 +70,20 @@ internal class BotOfflineEventMonitorImpl : BotOfflineEventMonitor { closeNetwork() } is BotOfflineEvent.Force -> { - bot.logger.warning { "Connection occupied by another android device: ${event.message}" } + bot.logger.warning { "Connection occupied by another android device. Will try to resume connection. (${event.message})" } closeNetwork() } - is BotOfflineEvent.MsfOffline, + is BotOfflineEvent.MsfOffline -> { + // This normally means bot is blocked and requires manual action. + bot.logger.warning { "Server notifies offline. (${event.cause?.message ?: event.toString()})" } + closeNetwork() + // `closeNetwork` will close NetworkHandler, + // after which NetworkHandlerSelector will create a new instance to try to fix the problem. + // A new login attempt will fail because the bot is blocked, with LoginFailedException which then wrapped into NetworkException. (LoginFailedExceptionAsNetworkException) + // Selector will handle this exception, and close the block while logging this down. + + // See SelectorNetworkHandler.instance for more information on how the Selector handles the exception. + } is BotOfflineEvent.Dropped, is BotOfflineEvent.RequireReconnect, -> { diff --git a/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt b/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt index 047184cdd4e..751bab4a05f 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt @@ -17,6 +17,7 @@ import net.mamoe.mirai.internal.network.QQAndroidClient import net.mamoe.mirai.internal.network.WLoginSigInfo import net.mamoe.mirai.internal.network.component.ComponentKey import net.mamoe.mirai.internal.network.handler.NetworkHandler +import net.mamoe.mirai.internal.network.handler.selector.NetworkException import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin.Login.LoginPacketResponse @@ -57,6 +58,17 @@ internal interface SsoProcessor { companion object : ComponentKey } +/** + * Wraps [LoginFailedException] into [NetworkException] + */ +internal class LoginFailedExceptionAsNetworkException( + private val underlying: LoginFailedException +) : NetworkException(underlying.message ?: "Login failed", underlying, !underlying.killBot) { + override fun unwrapForPublicApi(): Throwable { + return underlying + } +} + internal enum class FirstLoginResult( val success: Boolean, val canRecoverOnFirstLogin: Boolean, diff --git a/mirai-core/src/commonMain/kotlin/network/handler/CommonNetworkHandler.kt b/mirai-core/src/commonMain/kotlin/network/handler/CommonNetworkHandler.kt index a3f7ad616a0..a3183a7e3f8 100644 --- a/mirai-core/src/commonMain/kotlin/network/handler/CommonNetworkHandler.kt +++ b/mirai-core/src/commonMain/kotlin/network/handler/CommonNetworkHandler.kt @@ -14,11 +14,13 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.onFailure import net.mamoe.mirai.internal.network.components.* +import net.mamoe.mirai.internal.network.handler.NetworkHandler.Companion.runUnwrapCancellationException import net.mamoe.mirai.internal.network.handler.selector.NetworkException import net.mamoe.mirai.internal.network.handler.selector.NetworkHandlerSelector import net.mamoe.mirai.internal.network.handler.state.StateObserver import net.mamoe.mirai.internal.network.impl.HeartbeatFailedException import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket +import net.mamoe.mirai.network.LoginFailedException import net.mamoe.mirai.utils.* import kotlin.coroutines.CoroutineContext @@ -171,14 +173,14 @@ internal abstract class CommonNetworkHandler( /////////////////////////////////////////////////////////////////////////// override fun close(cause: Throwable?) { - super.close(cause) // cancel coroutine scope if (state == NetworkHandler.State.CLOSED) return // quick check if already closed if (setState { StateClosed(cause) } == null) return // atomic check + super.close(cause) // cancel coroutine scope } init { coroutineContext.job.invokeOnCompletion { e -> - close(e?.unwrapCancellationException()) + close(e) } } @@ -231,7 +233,6 @@ internal abstract class CommonNetworkHandler( ) : CommonState(NetworkHandler.State.CONNECTING) { private lateinit var connection: Deferred - @Suppress("JoinDeclarationAndAssignment") private lateinit var connectResult: Deferred override fun startState() { @@ -241,18 +242,27 @@ internal abstract class CommonNetworkHandler( connectResult = async { connection.join() - context[SsoProcessor].login(this@CommonNetworkHandler) + try { + context[SsoProcessor].login(this@CommonNetworkHandler) + } catch (e: LoginFailedException) { + throw LoginFailedExceptionAsNetworkException(e) + } } connectResult.invokeOnCompletion { error -> if (error == null) { - this@CommonNetworkHandler.launch { resumeConnection() } + this@CommonNetworkHandler.launch { resumeConnection() } // go to next state. } else { // failed in SSO stage context[SsoProcessor].firstLoginResult.compareAndSet(null, FirstLoginResult.OTHER_FAILURE) - if (error is StateSwitchingException && error.new is CommonNetworkHandler<*>.StateConnecting) { - return@invokeOnCompletion // state already switched, so do not do it again. + if (error is CancellationException) { + // CancellationException is either caused by parent cancellation or manual `connectResult.cancel`. + // The later should not happen, so it's definitely due to the parent cancellation. + // It means that the super scope, the NetworkHandler is closed. + // If we don't `return` here, state will be set to StateClosed with CancellationException, which isn't the real cause. + return@invokeOnCompletion } + setState { // logon failure closes the network handler. StateClosed(collectiveExceptions.collectGet(error)) @@ -304,17 +314,14 @@ internal abstract class CommonNetworkHandler( return true } - private val configPush = this@CommonNetworkHandler.launch(CoroutineName("ConfigPush sync")) { - context[ConfigPushProcessor].syncConfigPush(this@CommonNetworkHandler) - } + // Yes, nothing to do in this state. override suspend fun resumeConnection0(): Unit = runUnwrapCancellationException { (coroutineContext.job as CompletableJob).run { complete() join() } - joinCompleted(configPush) // throw exception - setState { StateOK(connection, configPush) } + setState { StateOK(connection) } } // noop override fun toString(): String = "StateLoading" @@ -322,7 +329,6 @@ internal abstract class CommonNetworkHandler( protected inner class StateOK( private val connection: Conn, - private val configPush: Job, ) : CommonState(NetworkHandler.State.OK) { override fun startState() { coroutineContext.job.invokeOnCompletion { err -> @@ -346,6 +352,11 @@ internal abstract class CommonNetworkHandler( context[KeyRefreshProcessor].keyRefreshLoop(this@CommonNetworkHandler) } + private val configPush = this@CommonNetworkHandler.launch(CoroutineName("ConfigPush sync")) { + context[ConfigPushProcessor].syncConfigPush(this@CommonNetworkHandler) + } + + override suspend fun sendPacketImpl(packet: OutgoingPacket): Boolean { connection.writeAndFlushOrCloseAsync(packet) return true diff --git a/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandler.kt b/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandler.kt index 05e69a9c4f4..4a3372b6cba 100644 --- a/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandler.kt +++ b/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandler.kt @@ -9,8 +9,10 @@ package net.mamoe.mirai.internal.network.handler +import kotlinx.atomicfu.atomic import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.collect @@ -18,15 +20,23 @@ import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.takeWhile import net.mamoe.mirai.Bot +import net.mamoe.mirai.internal.AbstractBot import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.components.BotInitProcessor +import net.mamoe.mirai.internal.network.components.HeartbeatScheduler import net.mamoe.mirai.internal.network.components.SsoProcessor +import net.mamoe.mirai.internal.network.handler.NetworkHandler.State +import net.mamoe.mirai.internal.network.handler.NetworkHandler.State.* +import net.mamoe.mirai.internal.network.handler.selector.MaxAttemptsReachedException +import net.mamoe.mirai.internal.network.handler.selector.NetworkException import net.mamoe.mirai.internal.network.handler.selector.SelectorNetworkHandler import net.mamoe.mirai.internal.network.handler.state.StateObserver import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType import net.mamoe.mirai.internal.network.protocol.packet.PacketFactory import net.mamoe.mirai.utils.MiraiLogger +import net.mamoe.mirai.utils.systemProp +import net.mamoe.mirai.utils.unwrapCancellationException /** * Coroutine-based network framework. Usually wrapped with [SelectorNetworkHandler] to enable retrying. @@ -35,6 +45,8 @@ import net.mamoe.mirai.utils.MiraiLogger * * Instances are often created by [NetworkHandlerFactory]. * + * For more information, see [State]. + * * @see NetworkHandlerSupport * @see NetworkHandlerFactory */ @@ -42,7 +54,7 @@ internal interface NetworkHandler : CoroutineScope { val context: NetworkHandlerContext /** - * Current state of this handler. This is volatile. + * Current state of this handler. */ val state: State @@ -60,7 +72,7 @@ internal interface NetworkHandler : CoroutineScope { * * There are 5 [State]s, each of which encapsulates the state of the network connection. * - * Initial state is [State.INITIALIZED], at which no packets can be send before [resumeConnection], which transmits state into [State.CONNECTING]. + * Initial state is [State.INITIALIZED], at which no packets can be sent before [resumeConnection], which transmits state into [State.CONNECTING]. * On [State.CONNECTING], [NetworkHandler] establishes a connection with the server while [SsoProcessor] takes responsibility in the single-sign-on process. * * Successful logon turns state to [State.LOADING], an **open state**, which does nothing by default. Jobs can be *attached* by [StateObserver]s. @@ -72,6 +84,41 @@ internal interface NetworkHandler : CoroutineScope { * When connection is lost (e.g. due to Internet unavailability), it does NOT return to [State.CONNECTING] but to [State.CLOSED]. No reconnection is allowed. * Retrial may only be performed in [SelectorNetworkHandler]. * + * ## 深入了解状态维护机制 + * + * ### 登录时的状态转移 + * + * [NetworkHandler] 实例会在 [登录][Bot.login] 时创建 (定义在 [AbstractBot.network]), 此时初始状态为 [INITIALIZED]. + * + * [Bot.login] 会在创建该实例后就执行 [NetworkHandler.resumeConnection], 意图是将 [NetworkHandler] **调整**为可以收发功能数据包的正常工作状态, 即 [OK]. + * 为了达成这个状态, [NetworkHandler] 将需要通过 [INITIALIZED] -> [CONNECTING] -> [LOADING] -> [OK], 即**初始化** -> **连接服务器** -> **载入** -> **完成**流程. + * + * 在 [CONNECTING] 状态中, [NetworkHandler] 将会与服务器建立网络连接, 并执行登录. + * 若成功, 状态进入 [LOADING], 此时 [NetworkHandler] 将会从服务器拉取好友列表等必要信息. + * 拉取顺利完成后状态进入 [OK], 用于维护登录会话的心跳任务等协程将会由启动 [HeartbeatScheduler] 启动, [NetworkHandler] 现在可以收发功能数据包. [NetworkHandler.resumeConnection] 返回, [Bot.login] 返回. + * + * ### 单向状态转移与"重置" + * + * [NetworkHandler] 的实现除了 [SelectorNetworkHandler] 外, 都只设计实现单向的状态转移, + * 这意味着它们最终的状态都是 [CLOSED], 并且一旦到达 [CLOSED] 就无法回到之前的状态. + * + * [Selector][SelectorNetworkHandler] 是对某一个 [NetworkHandler] 的包装, 它为 [NetworkHandler] 增加异常处理和"可重置"的状态转移能力. + * 若在单向状态转移过程 (比如登录) 中出现异常, 异常会被传递到 [NetworkHandler.resumeConnection] 抛出. + * + * ### 异常的区分 + * + * 网络系统中的异常是有语义的 — 它如果是 [NetworkException], [Selector][SelectorNetworkHandler] 会使用异常的 [NetworkException.recoverable] 信息判断是否应该"重置"状态. + * [NetworkException] 是已知的异常. 而任何其他的异常, 例如 [IllegalStateException], [NoSuchElementException], 都是未知的异常. + * 未知的异常会被认为是严重的内部错误, [Selector][SelectorNetworkHandler] 会[关闭 bot][Bot.close]. (提醒: 关闭 bot 是终止操作, 标志 bot 实例的结束) + * + * 你在 [NetworkHandler.resumeConnection] 和 [Selector][SelectorNetworkHandler] 中需要注意异常类型, 为函数清晰注释其可能抛出的异常类型. + * + * [Selector][SelectorNetworkHandler] 不会立即将异常向上传递, 而是会根据异常类型尝试"重置"状态, 以处理网络状态不佳或服务器要求重连到其他地址等情况. + * + * ### Selector 重置状态 + * + * [Selector][SelectorNetworkHandler] 通过实例化新的 [NetworkHandler] 来获得状态为 [INITIALIZED] 的实例, 对外来说就是"重置"了状态. + * * @see state */ enum class State { @@ -117,14 +164,18 @@ internal interface NetworkHandler : CoroutineScope { } /** - * Suspends the coroutine until [sendAndExpect] can be executed without suspension or state is [State.CLOSED]. + * Suspends the coroutine until [sendAndExpect] can be executed without suspension. + * + * If this functions returns normally, it indicates that [state] is [State.LOADING] or [State.OK] * - * In other words, if this functions returns, it indicates that [state] is [State.LOADING] or [State.OK] + * @throws NetworkException 已知的异常 + * @throws Throwable 其他内部错误 + * @throws MaxAttemptsReachedException 重试次数达到上限 + * @throws CancellationException 协程被取消 * - * May throw exception that had caused current state to fail. - * @see State + * @see SelectorNetworkHandler.instance */ - @Throws(Exception::class) + @Throws(NetworkException::class, Throwable::class) suspend fun resumeConnection() @@ -173,9 +224,28 @@ internal interface NetworkHandler : CoroutineScope { */ fun close(cause: Throwable?) - /////////////////////////////////////////////////////////////////////////// - // compatibility - /////////////////////////////////////////////////////////////////////////// + companion object { + /** + * [unwrapCancellationException] will give a rather long and complicated trace showing all internal traces. + * The traces may help locate where the [Job.cancel] is called, but this is not usually important. + * @since 2.13 + */ + var CANCELLATION_TRACE by atomic(systemProp("mirai.network.handler.cancellation.trace", false)) + + inline fun runUnwrapCancellationException(block: () -> R): R { + try { + return block() + } catch (e: CancellationException) { + // e is like `Exception in thread "main" kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=JobImpl{Cancelled}@f252f300` + // and this is useless. + + throw e.unwrapCancellationException(CANCELLATION_TRACE) + + // if (e.suppressed.isNotEmpty()) throw e // preserve details. + // throw e.findCause { it !is CancellationException } ?: e + } + } + } } internal val NetworkHandler.logger: MiraiLogger get() = context.logger diff --git a/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandlerSupport.kt b/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandlerSupport.kt index 9fdce80bdf6..d297646dd41 100644 --- a/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandlerSupport.kt +++ b/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandlerSupport.kt @@ -70,7 +70,13 @@ internal abstract class NetworkHandlerSupport( override fun close(cause: Throwable?) { if (coroutineContext.job.isActive) { - coroutineContext.job.cancel("NetworkHandler closed", cause) + coroutineContext.job.cancel( + if (cause is CancellationException) { + cause + } else { + CancellationException("NetworkHandler closed", cause) + } + ) } } @@ -228,14 +234,10 @@ internal abstract class NetworkHandlerSupport( @Throws(Exception::class) suspend fun resumeConnection() { val observer = context.getOrNull(StateObserver) - if (observer != null) { - observer.beforeStateResume(this@NetworkHandlerSupport, _state) - val result = kotlin.runCatching { resumeConnection0() } - observer.afterStateResume(this@NetworkHandlerSupport, _state, result) - result.getOrThrow() - } else { - resumeConnection0() - } + observer?.beforeStateResume(this@NetworkHandlerSupport, _state) + val result = kotlin.runCatching { resumeConnection0() } + observer?.afterStateResume(this@NetworkHandlerSupport, _state, result) + result.getOrThrow() } protected abstract suspend fun resumeConnection0() diff --git a/mirai-core/src/commonMain/kotlin/network/handler/selector/AbstractKeepAliveNetworkHandlerSelector.kt b/mirai-core/src/commonMain/kotlin/network/handler/selector/AbstractKeepAliveNetworkHandlerSelector.kt index 9c12fddd124..9be391ea521 100644 --- a/mirai-core/src/commonMain/kotlin/network/handler/selector/AbstractKeepAliveNetworkHandlerSelector.kt +++ b/mirai-core/src/commonMain/kotlin/network/handler/selector/AbstractKeepAliveNetworkHandlerSelector.kt @@ -9,22 +9,17 @@ package net.mamoe.mirai.internal.network.handler.selector +import io.ktor.client.utils.* import kotlinx.atomicfu.atomic import kotlinx.atomicfu.locks.SynchronizedObject import kotlinx.atomicfu.locks.synchronized -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.yield +import kotlinx.coroutines.* import net.mamoe.mirai.internal.network.components.SsoProcessor import net.mamoe.mirai.internal.network.handler.NetworkHandler import net.mamoe.mirai.internal.network.handler.NetworkHandlerFactory import net.mamoe.mirai.internal.network.handler.logger -import net.mamoe.mirai.network.LoginFailedException -import net.mamoe.mirai.network.RetryLaterException import net.mamoe.mirai.utils.* -import kotlin.jvm.JvmField -import kotlin.native.concurrent.ThreadLocal +import kotlin.jvm.Volatile /** * A lazy stateful implementation of [NetworkHandlerSelector]. @@ -65,7 +60,7 @@ internal abstract class AbstractKeepAliveNetworkHandlerSelector String) { if (SELECTOR_LOGGING) { logger.debug { "Attempt #$attempted: ${block.invoke()}" } @@ -74,14 +69,42 @@ internal abstract class AbstractKeepAliveNetworkHandlerSelector= maxAttempts) { logIfEnabled { "Max attempt $maxAttempts reached." } throw MaxAttemptsReachedException(exceptionCollector.getLast()) -// throw exceptionCollector.getLast()?.apply { addSuppressed(MaxAttemptsReachedException(null)) } -// ?: MaxAttemptsReachedException(null) } if (!currentCoroutineContext().isActive) { yield() // check cancellation @@ -106,51 +127,48 @@ internal abstract class AbstractKeepAliveNetworkHandlerSelector { + current.resumeInstanceCatchingException() + // This may return false, meaning the error causing the state to be CLOSED is recoverable. + // Otherwise, it throws, meaning it is unrecoverable. + if (this@AbstractKeepAliveNetworkHandlerSelector.current.compareAndSet(current, null)) { logIfEnabled { "... Set current to null." } // invalidate the instance and try again. - val lastFailure = current.getLastFailure()?.unwrapCancellationException() + // NetworkHandler is CancellationException if closed by `NetworkHandler.close` + // `unwrapCancellationException` will give a rather long and complicated trace showing all internal traces. + // The traces may help locate exactly where the `NetworkHandler.close` is called, + // but this is not usually desired. + val lastFailure = + current.getLastFailure()?.unwrapCancellationException(NetworkHandler.CANCELLATION_TRACE) + logIfEnabled { "... Last failure was $lastFailure." } exceptionCollector.collectException(lastFailure) } @@ -186,9 +214,13 @@ internal abstract class AbstractKeepAliveNetworkHandlerSelector { - current.resumeInstanceCatchingException() - logIfEnabled { "RETURN" } - return current + if (current.resumeInstanceCatchingException()) { + logIfEnabled { "RETURN" } + return current + } else { + attempted += 1 + return runImpl() + } } } } else { @@ -208,22 +240,20 @@ internal abstract class AbstractKeepAliveNetworkHandlerSelector() + } } diff --git a/mirai-core/src/commonMain/kotlin/network/handler/selector/NetworkHandlerSelector.kt b/mirai-core/src/commonMain/kotlin/network/handler/selector/NetworkHandlerSelector.kt index 78349b45a87..e396955119b 100644 --- a/mirai-core/src/commonMain/kotlin/network/handler/selector/NetworkHandlerSelector.kt +++ b/mirai-core/src/commonMain/kotlin/network/handler/selector/NetworkHandlerSelector.kt @@ -1,14 +1,15 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * Copyright 2019-2022 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. + * 此源代码的使用受 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 + * https://github.com/mamoe/mirai/blob/dev/LICENSE */ package net.mamoe.mirai.internal.network.handler.selector +import kotlinx.coroutines.CancellationException import net.mamoe.mirai.internal.network.handler.NetworkHandler import net.mamoe.mirai.internal.network.handler.NetworkHandlerFactory @@ -37,10 +38,13 @@ internal interface NetworkHandlerSelector { /** * Returns an alive [NetworkHandler], or suspends the coroutine until the connection has been made again. - * Returned [H] can be in [NetworkHandler.State.OK] only (but it may happen that the state changed just after returning from this function). + * Returned [H] can be in [NetworkHandler.State.LOADING] and [NetworkHandler.State.OK] only (but it may happen that the state changed just after returning from this function). * - * This function may throw exceptions, which would be propagated to the original caller of [SelectorNetworkHandler.resumeConnection]. - * @throws MaxAttemptsReachedException + * @throws NetworkException [NetworkHandler.resumeConnection] 抛出了 [NetworkException] 并且 [NetworkException.recoverable] 为 `false`. + * @throws Throwable [NetworkHandler.resumeConnection] 抛出了其他异常. 任何其他异常都属于内部错误并会原样抛出. + * @throws MaxAttemptsReachedException 重试次数达到上限 (由 selector 构造) + * @throws CancellationException 协程被取消 */ + @Throws(NetworkException::class, MaxAttemptsReachedException::class, CancellationException::class, Throwable::class) suspend fun awaitResumeInstance(): H } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/handler/selector/SelectorNetworkHandler.kt b/mirai-core/src/commonMain/kotlin/network/handler/selector/SelectorNetworkHandler.kt index b1d710b4246..a1e713e956e 100644 --- a/mirai-core/src/commonMain/kotlin/network/handler/selector/SelectorNetworkHandler.kt +++ b/mirai-core/src/commonMain/kotlin/network/handler/selector/SelectorNetworkHandler.kt @@ -19,6 +19,7 @@ import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.handler.NetworkHandler import net.mamoe.mirai.internal.network.handler.NetworkHandler.State import net.mamoe.mirai.internal.network.handler.NetworkHandlerContext +import net.mamoe.mirai.internal.network.handler.logger import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType import net.mamoe.mirai.utils.addNameHierarchically @@ -56,12 +57,28 @@ internal open class SelectorNetworkHandler( } private val lock = SynchronizedObject() + /** + * 挂起协程直到获取一个可用的 [instance]. 此函数有副作用: 获取 [instance] 应当是必须成功的, 不成功(在本函数重新抛出捕获的异常之前)则会关闭 bot. + * + * "不成功"包括多次重连后仍然不成功, bot 被 ban, 内部错误等已经被适当重试过了的情况. + * + * @see NetworkHandlerSelector.awaitResumeInstance + */ + @Throws(Exception::class) protected suspend inline fun instance(): H { if (!scope.isActive) { throw lastCancellationCause?.let(::CancellationException) ?: CancellationException("SelectorNetworkHandler is already closed") } - return selector.awaitResumeInstance() + + return try { + selector.awaitResumeInstance() + } catch (e: Throwable) { + // selector 抛出了无法处理的, 不可挽救的异常. close bot. + logger.warning("Network selector received exception, closing bot. (${e})") + context.bot.close(e) + throw e + } } override val state: State diff --git a/mirai-core/src/commonMain/kotlin/network/handler/state/LoggingStateObserver.kt b/mirai-core/src/commonMain/kotlin/network/handler/state/LoggingStateObserver.kt index 077ee598df3..e64ebc758b9 100644 --- a/mirai-core/src/commonMain/kotlin/network/handler/state/LoggingStateObserver.kt +++ b/mirai-core/src/commonMain/kotlin/network/handler/state/LoggingStateObserver.kt @@ -11,10 +11,7 @@ package net.mamoe.mirai.internal.network.handler.state import net.mamoe.mirai.internal.network.handler.NetworkHandler import net.mamoe.mirai.internal.network.handler.NetworkHandlerSupport -import net.mamoe.mirai.utils.MiraiLogger -import net.mamoe.mirai.utils.coroutineName -import net.mamoe.mirai.utils.debug -import net.mamoe.mirai.utils.systemProp +import net.mamoe.mirai.utils.* import kotlin.coroutines.coroutineContext import kotlin.native.concurrent.ThreadLocal @@ -68,7 +65,10 @@ internal class LoggingStateObserver( logger.debug { "State resumed: [${coroutineContext.coroutineName}] ${state.correspondingState}." } }, onFailure = { - logger.debug { "State resumed: [${coroutineContext.coroutineName}] ${state.correspondingState} ${result.exceptionOrNull()}" } + logger.debug { + "State resumed: [${coroutineContext.coroutineName}] ${state.correspondingState} " + + "${result.exceptionOrNull()?.unwrapCancellationException(false)}" + } } ) } diff --git a/mirai-core/src/commonTest/kotlin/network/framework/AbstractCommonNHTest.kt b/mirai-core/src/commonTest/kotlin/network/framework/AbstractCommonNHTest.kt index 90f833b9392..3ff229f6df7 100644 --- a/mirai-core/src/commonTest/kotlin/network/framework/AbstractCommonNHTest.kt +++ b/mirai-core/src/commonTest/kotlin/network/framework/AbstractCommonNHTest.kt @@ -9,7 +9,6 @@ package net.mamoe.mirai.internal.network.framework -import kotlinx.coroutines.CompletableDeferred import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.handler.* @@ -63,7 +62,7 @@ internal abstract class TestCommonNetworkHandler( override fun setStateOK(conn: PlatformConn, exception: Throwable?): NetworkHandlerSupport.BaseStateImpl? { exception?.printStackTrace() - return setState { StateOK(conn, CompletableDeferred(Unit)) } + return setState { StateOK(conn) } } override fun setStateLoading(conn: PlatformConn): NetworkHandlerSupport.BaseStateImpl? { diff --git a/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt b/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt index 94cff788b51..dd008b73933 100644 --- a/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt +++ b/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt @@ -206,7 +206,11 @@ internal abstract class AbstractRealNetworkHandlerTest : Abs } val eventDispatcher get() = bot.components[EventDispatcher] - val firstLoginResult: FirstLoginResult? get() = bot.components[SsoProcessor].firstLoginResult.value + var firstLoginResult: FirstLoginResult? + get() = bot.components[SsoProcessor].firstLoginResult.value + set(value) { + bot.components[SsoProcessor].firstLoginResult.value = value + } } internal fun AbstractRealNetworkHandlerTest<*>.setSsoProcessor(action: suspend SsoProcessor.(handler: NetworkHandler) -> Unit) { diff --git a/mirai-core/src/commonTest/kotlin/network/framework/components/TestEventDispatcherImpl.kt b/mirai-core/src/commonTest/kotlin/network/framework/components/TestEventDispatcherImpl.kt index ca430cc3f6d..65a088b0d98 100644 --- a/mirai-core/src/commonTest/kotlin/network/framework/components/TestEventDispatcherImpl.kt +++ b/mirai-core/src/commonTest/kotlin/network/framework/components/TestEventDispatcherImpl.kt @@ -14,9 +14,9 @@ import kotlinx.coroutines.job import kotlinx.coroutines.launch import net.mamoe.mirai.event.Event import net.mamoe.mirai.internal.network.components.EventDispatcherImpl +import net.mamoe.mirai.internal.network.handler.NetworkHandler.Companion.runUnwrapCancellationException import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.TestOnly -import net.mamoe.mirai.utils.runUnwrapCancellationException import kotlin.coroutines.CoroutineContext internal open class TestEventDispatcherImpl( diff --git a/mirai-core/src/commonTest/kotlin/network/handler/KeepAliveNetworkHandlerSelectorRealTest.kt b/mirai-core/src/commonTest/kotlin/network/handler/KeepAliveNetworkHandlerSelectorRealTest.kt deleted file mode 100644 index 119bc2d48e6..00000000000 --- a/mirai-core/src/commonTest/kotlin/network/handler/KeepAliveNetworkHandlerSelectorRealTest.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2019-2022 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/dev/LICENSE - */ - -@file:OptIn(TestOnly::class) - -package net.mamoe.mirai.internal.network.handler - -import net.mamoe.mirai.internal.network.components.FirstLoginResult -import net.mamoe.mirai.internal.network.components.SsoProcessor -import net.mamoe.mirai.internal.network.framework.AbstractCommonNHTest -import net.mamoe.mirai.internal.network.framework.PlatformConn -import net.mamoe.mirai.internal.network.framework.TestCommonNetworkHandler -import net.mamoe.mirai.internal.network.handler.selector.MaxAttemptsReachedException -import net.mamoe.mirai.internal.network.handler.selector.NetworkException -import net.mamoe.mirai.internal.test.runBlockingUnit -import net.mamoe.mirai.utils.TestOnly -import kotlin.test.* - -internal class KeepAliveNetworkHandlerSelectorRealTest : AbstractCommonNHTest() { - - internal class FakeFailOnCreatingConnection : AbstractCommonNHTest() { - private class MyException : Exception() - - private lateinit var throwException: () -> Nothing - - override val factory: NetworkHandlerFactory = - NetworkHandlerFactory { context, address -> - object : TestCommonNetworkHandler(bot, context, address) { - override suspend fun createConnection(): PlatformConn { - throwException() - } - } - } - - @Test - fun `should not tolerant any exception except NetworkException thrown by states`() = runBlockingUnit { - // selector should not tolerant any exception during state initialization, or in the Jobs launched in states. - throwException = { - throw MyException() - } - - val selector = TestSelector(3) { factory.create(createContext(), createAddress()) } - assertFailsWith { selector.awaitResumeInstance() } - } - - // Since #1963, any error during first login will close the bot. So we assume first login succeed to do our test. - @BeforeTest - private fun setFirstLoginPassed() { - assertEquals(null, bot.components[SsoProcessor].firstLoginResult.value) - bot.components[SsoProcessor].firstLoginResult.value = FirstLoginResult.PASSED - } - - @Test - fun `should tolerant NetworkException thrown by states`() = runBlockingUnit { - // selector should not tolerant any exception during state initialization, or in the Jobs launched in states. - throwException = { - throw object : NetworkException(true) {} - } - - val selector = TestSelector(3) { factory.create(createContext(), createAddress()) } - assertFailsWith { selector.awaitResumeInstance() }.let { - assertIs(it.cause) - } - } - - @Test - fun `throws MaxAttemptsReachedException with cause of original`() = runBlockingUnit { - throwException = { - throw MyException() - } - val selector = TestSelector(3) { factory.create(createContext(), createAddress()) } - assertFailsWith { selector.awaitResumeInstance() }.let { - assertIs(it.cause) - } - } - } - -} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/network/handler/KeepAliveNetworkHandlerSelectorTest.kt b/mirai-core/src/commonTest/kotlin/network/handler/KeepAliveNetworkHandlerSelectorTest.kt index 63221b51aa0..bb1222c6142 100644 --- a/mirai-core/src/commonTest/kotlin/network/handler/KeepAliveNetworkHandlerSelectorTest.kt +++ b/mirai-core/src/commonTest/kotlin/network/handler/KeepAliveNetworkHandlerSelectorTest.kt @@ -36,10 +36,11 @@ internal class TestSelector : this.createInstance0 = createInstance0 } - val createInstanceCount = atomic(0) + private val _createInstanceCount = atomic(0) + val createdInstanceCount get() = _createInstanceCount.value override fun createInstance(): H { - createInstanceCount.incrementAndGet() + _createInstanceCount.incrementAndGet() return this.createInstance0() } } @@ -76,7 +77,7 @@ internal class KeepAliveNetworkHandlerSelectorTest : AbstractMockNetworkHandlerT assertSame(handler, selector.getCurrentInstanceOrNull()) handler.setState(State.CLOSED) runBlockingUnit(timeout = 3.seconds) { selector.awaitResumeInstance() } - assertEquals(1, selector.createInstanceCount.value) + assertEquals(1, selector.createdInstanceCount) } @Test @@ -87,6 +88,6 @@ internal class KeepAliveNetworkHandlerSelectorTest : AbstractMockNetworkHandlerT assertFailsWith { selector.awaitResumeInstance() } - assertEquals(3, selector.createInstanceCount.value) + assertEquals(3, selector.createdInstanceCount) } } \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/network/handler/SelectorRecoveryTest.kt b/mirai-core/src/commonTest/kotlin/network/handler/SelectorRecoveryTest.kt index cf77ad3162e..5b091c3d006 100644 --- a/mirai-core/src/commonTest/kotlin/network/handler/SelectorRecoveryTest.kt +++ b/mirai-core/src/commonTest/kotlin/network/handler/SelectorRecoveryTest.kt @@ -13,16 +13,18 @@ package net.mamoe.mirai.internal.network.handler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import net.mamoe.mirai.internal.network.components.EventDispatcher -import net.mamoe.mirai.internal.network.components.HeartbeatFailureHandler -import net.mamoe.mirai.internal.network.components.HeartbeatScheduler +import net.mamoe.mirai.event.events.BotOfflineEvent +import net.mamoe.mirai.internal.contact.uin +import net.mamoe.mirai.internal.network.components.* import net.mamoe.mirai.internal.network.framework.AbstractCommonNHTestWithSelector +import net.mamoe.mirai.internal.network.framework.components.TestSsoProcessor import net.mamoe.mirai.internal.network.handler.selector.NetworkException +import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc import net.mamoe.mirai.internal.test.runBlockingUnit +import net.mamoe.mirai.network.NoServerAvailableException +import net.mamoe.mirai.network.WrongPasswordException import net.mamoe.mirai.utils.TestOnly -import kotlin.test.Test -import kotlin.test.assertFails -import kotlin.test.assertTrue +import kotlin.test.* /** * Test whether the selector can recover the connection after first successful login. @@ -38,6 +40,23 @@ internal class SelectorRecoveryTest : AbstractCommonNHTestWithSelector() { // println("=".repeat(31) + "END: ${info.displayName}" + "=".repeat(31)) // } + /** + * @see NetworkHandler.State.CONNECTING + */ + var throwExceptionOnLogin: (() -> Unit)? = null + + init { + overrideComponents[SsoProcessor] = object : TestSsoProcessor(bot) { + private val delegate = overrideComponents[SsoProcessor] + override suspend fun login(handler: NetworkHandler) { + delegate.login(handler) + throwExceptionOnLogin?.invoke() + } + }.apply { + firstLoginResult.value = FirstLoginResult.PASSED + } + } + @Test fun `stop on manual close`() = runBlockingUnit { network.resumeConnection() @@ -59,6 +78,52 @@ internal class SelectorRecoveryTest : AbstractCommonNHTestWithSelector() { assertTrue { bot.network.state != NetworkHandler.State.CLOSED } } + @Test + fun `can recover on LoginFailedException with killBot=false`() = runBlockingUnit { + throwExceptionOnLogin = { + if (selector.createdInstanceCount != 3) { + throw NoServerAvailableException(null) + } + } + + bot.login() + eventDispatcher.joinBroadcast() + assertState(NetworkHandler.State.OK) + assertEquals(3, selector.createdInstanceCount) + } + + @Test + fun `can recover on LoginFailedException with killBot=true`() = runBlockingUnit { + throwExceptionOnLogin = { + throw WrongPasswordException("Congratulations! Your bot has been blocked!") + } + + assertFailsWith { bot.login() } + eventDispatcher.joinBroadcast() + assertState(NetworkHandler.State.CLOSED) + assertEquals(1, selector.createdInstanceCount) + } + + @Test + fun `can recover on MsfOffline but fail and close bot on next login`() = runBlockingUnit { + bot.login() + firstLoginResult = FirstLoginResult.PASSED + eventDispatcher.joinBroadcast() + // Now first login succeed, and all events have been processed. + assertState(NetworkHandler.State.OK) + + // Assume bot is blocked. + throwExceptionOnLogin = { + throw WrongPasswordException("Congratulations! Your bot has been blocked!") + } + // When blocked, server sends this event. + eventDispatcher.broadcast(BotOfflineEvent.MsfOffline(bot, StatSvc.ReqMSFOffline.MsfOfflineToken(bot.uin, 1, 1))) + eventDispatcher.joinBroadcast() // Sync for processing the async recovery launched by [BotOfflineEventMonitor] + + // Now we should expect + assertState(NetworkHandler.State.CLOSED) + } + @Test fun `cannot recover on other failures`() = runBlockingUnit { // ISE is considered as an internal error (bug). diff --git a/mirai-core/src/commonTest/kotlin/network/handler/StandaloneSelectorTests.kt b/mirai-core/src/commonTest/kotlin/network/handler/StandaloneSelectorTests.kt new file mode 100644 index 00000000000..ed64eca5162 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/network/handler/StandaloneSelectorTests.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.network.handler + +import kotlinx.atomicfu.atomic +import net.mamoe.mirai.internal.network.components.FirstLoginResult +import net.mamoe.mirai.internal.network.components.SsoProcessor +import net.mamoe.mirai.internal.network.framework.AbstractCommonNHTest +import net.mamoe.mirai.internal.network.framework.PlatformConn +import net.mamoe.mirai.internal.network.framework.TestCommonNetworkHandler +import net.mamoe.mirai.internal.network.handler.selector.MaxAttemptsReachedException +import net.mamoe.mirai.internal.network.handler.selector.NetworkException +import net.mamoe.mirai.internal.test.runBlockingUnit +import kotlin.test.* + +/** + * Every function can have its own selector. + */ +internal class StandaloneSelectorTests : AbstractCommonNHTest() { + private class MyException : Exception() { + override fun toString(): String { + return "MyException" + } + } + + /** + * This simulates an error on [NetworkHandler.State.CONNECTING] + */ + private var throwExceptionOnConnecting: (() -> Nothing)? = null + + // does not use selector + override val factory: NetworkHandlerFactory = + NetworkHandlerFactory { context, address -> + object : TestCommonNetworkHandler(bot, context, address) { + override suspend fun createConnection(): PlatformConn { + return throwExceptionOnConnecting?.invoke() ?: PlatformConn() + } + } + } + + @Test + fun `should not tolerant any exception except NetworkException thrown by states`() = runBlockingUnit { + // selector should not tolerant any exception during state initialization, or in the Jobs launched in states. + throwExceptionOnConnecting = { + throw MyException() + } + + val selector = TestSelector(3) { factory.create(createContext(), createAddress()) } + assertFailsWith { selector.awaitResumeInstance() } + } + + // Since #1963, any error during first login will close the bot. So we assume first login succeed to do our test. + @BeforeTest + private fun setFirstLoginPassed() { + assertEquals(null, bot.components[SsoProcessor].firstLoginResult.value) + bot.components[SsoProcessor].firstLoginResult.value = FirstLoginResult.PASSED + } + + @Test + fun `NetworkException can suggest retrying`() = runBlockingUnit { + // selector should not tolerant any exception during state initialization, or in the Jobs launched in states. + throwExceptionOnConnecting = { + throw object : NetworkException(true) {} + } + + val selector = TestSelector(3) { factory.create(createContext(), createAddress()) } + assertFailsWith { selector.awaitResumeInstance() }.let { + assertIs(it.cause) + } + } + + @Test + fun `NetworkException does not cause retrying if recoverable=false`() = runBlockingUnit { + // selector should not tolerant any exception during state initialization, or in the Jobs launched in states. + val times = atomic(0) + val theException = object : NetworkException(false) {} + throwExceptionOnConnecting = { + times.incrementAndGet() + throw theException + } + + val selector = TestSelector(3) { factory.create(createContext(), createAddress()) } + assertFailsWith { selector.awaitResumeInstance() }.let { + assertEquals(1, times.value) + assertSame(theException, it) + } + } + + @Test + fun `other exceptions considered as internal error and does not trigger reconnect`() = runBlockingUnit { + throwExceptionOnConnecting = { + throw MyException() + } + val selector = TestSelector(3) { factory.create(createContext(), createAddress()) } + assertFailsWith { selector.awaitResumeInstance() } + } +} \ No newline at end of file diff --git a/mirai-core/src/jvmBaseTest/kotlin/test/AbstractTest.kt b/mirai-core/src/jvmBaseTest/kotlin/test/AbstractTest.kt index f460cf802cc..a44ec819d9f 100644 --- a/mirai-core/src/jvmBaseTest/kotlin/test/AbstractTest.kt +++ b/mirai-core/src/jvmBaseTest/kotlin/test/AbstractTest.kt @@ -60,7 +60,7 @@ internal actual abstract class AbstractTest actual constructor() : CommonAbstrac setSystemProp("mirai.network.state.observer.logging", "true") setSystemProp("mirai.network.show.all.components", "true") setSystemProp("mirai.network.show.components.creation.stacktrace", "true") - setSystemProp("mirai.network.handle.selector.logging", "true") + setSystemProp("mirai.network.handler.selector.logging", "true") Exception() // create a exception to load relevant classes to estimate invocation time of test cases more accurately. IMirai::class.simpleName // similarly, load classes. diff --git a/mirai-core/src/nativeTest/kotlin/test/PlatformInitializationTest.kt b/mirai-core/src/nativeTest/kotlin/test/PlatformInitializationTest.kt index a726b28fd2c..050f3b39e7f 100644 --- a/mirai-core/src/nativeTest/kotlin/test/PlatformInitializationTest.kt +++ b/mirai-core/src/nativeTest/kotlin/test/PlatformInitializationTest.kt @@ -44,7 +44,7 @@ internal actual abstract class AbstractTest actual constructor() : CommonAbstrac setSystemProp("mirai.network.state.observer.logging", "true") setSystemProp("mirai.network.show.all.components", "true") setSystemProp("mirai.network.show.components.creation.stacktrace", "true") - setSystemProp("mirai.network.handle.selector.logging", "true") + setSystemProp("mirai.network.handler.selector.logging", "true") Exception() // create a exception to load relevant classes to estimate invocation time of test cases more accurately. IMirai::class.simpleName // similarly, load classes.