From 19930472218459fbc7d8996c52a1b4a7976db0b7 Mon Sep 17 00:00:00 2001
From: Him188 <Him188@mamoe.net>
Date: Fri, 14 Feb 2020 18:41:24 +0800
Subject: [PATCH] Redesign reconnecting logic

---
 .../network/QQAndroidBotNetworkHandler.kt     |  91 ++++++++-------
 .../kotlin/net.mamoe.mirai/BotImpl.kt         | 107 +++++++++++-------
 .../network/BotNetworkHandler.kt              |  14 ++-
 3 files changed, 128 insertions(+), 84 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 9126bc908..46b499888 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
@@ -20,9 +20,13 @@ import kotlinx.io.core.buildPacket
 import kotlinx.io.core.use
 import net.mamoe.mirai.data.MultiPacket
 import net.mamoe.mirai.data.Packet
-import net.mamoe.mirai.event.*
+import net.mamoe.mirai.event.BroadcastControllable
+import net.mamoe.mirai.event.CancellableEvent
+import net.mamoe.mirai.event.Event
+import net.mamoe.mirai.event.broadcast
 import net.mamoe.mirai.event.events.BotOfflineEvent
 import net.mamoe.mirai.network.BotNetworkHandler
+import net.mamoe.mirai.network.WrongPasswordException
 import net.mamoe.mirai.qqandroid.FriendInfoImpl
 import net.mamoe.mirai.qqandroid.GroupImpl
 import net.mamoe.mirai.qqandroid.QQAndroidBot
@@ -37,7 +41,10 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.login.Heartbeat
 import net.mamoe.mirai.qqandroid.network.protocol.packet.login.StatSvc
 import net.mamoe.mirai.qqandroid.network.protocol.packet.login.WtLogin
 import net.mamoe.mirai.utils.*
-import net.mamoe.mirai.utils.io.*
+import net.mamoe.mirai.utils.io.ByteArrayPool
+import net.mamoe.mirai.utils.io.PlatformSocket
+import net.mamoe.mirai.utils.io.readPacket
+import net.mamoe.mirai.utils.io.useBytes
 import kotlin.coroutines.CoroutineContext
 import kotlin.jvm.Volatile
 import kotlin.time.ExperimentalTime
@@ -55,13 +62,42 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
 
     private lateinit var channel: PlatformSocket
 
-    override suspend fun login() {
+    private var _packetReceiverJob: Job? = null
+
+    private val packetReceiveLock: Mutex = Mutex()
+
+    private fun startPacketReceiverJobOrGet(): Job {
+        val job = _packetReceiverJob
+        if (job != null && job.isActive && channel.isOpen) {
+            return job
+        }
+
+        return this.launch(CoroutineName("Incoming Packet Receiver")) {
+            while (channel.isOpen) {
+                val rawInput = try {
+                    channel.read()
+                } catch (e: CancellationException) {
+                    return@launch
+                } catch (e: Throwable) {
+                    BotOfflineEvent.Dropped(bot).broadcast()
+                    return@launch
+                }
+                packetReceiveLock.withLock {
+                    processPacket(rawInput)
+                }
+            }
+        }.also { _packetReceiverJob = it }
+    }
+
+
+    override suspend fun relogin() {
         if (::channel.isInitialized) {
             channel.close()
         }
         channel = PlatformSocket()
+        // TODO: 2020/2/14 连接多个服务器
         channel.connect("113.96.13.208", 8080)
-        this.launch(CoroutineName("Incoming Packet Receiver")) { processReceive() }
+        startPacketReceiverJobOrGet()
 
         // logger.info("Trying login")
         var response: WtLogin.Login.LoginPacketResponse = WtLogin.Login.SubCommand9(bot.client).sendAndExpect()
@@ -94,7 +130,8 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
                     }
                 }
 
-                is WtLogin.Login.LoginPacketResponse.Error -> error(response.toString())
+                is WtLogin.Login.LoginPacketResponse.Error ->
+                    throw WrongPasswordException(response.toString())
 
                 is WtLogin.Login.LoginPacketResponse.DeviceLockLogin -> {
                     response = WtLogin.Login.SubCommand20(
@@ -112,18 +149,14 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
         }
 
         // println("d2key=${bot.client.wLoginSigInfo.d2Key.toUHexString()}")
-        StatSvc.Register(bot.client).sendAndExpect<StatSvc.Register.Response>(6000) // it's slow
+
+        repeat(2) {
+            StatSvc.Register(bot.client).sendAndExpect<StatSvc.Register.Response>(6000) // it's slow
+        }
     }
 
     @UseExperimental(MiraiExperimentalAPI::class, ExperimentalTime::class)
     override suspend fun init(): Unit = coroutineScope {
-        this@QQAndroidBotNetworkHandler.subscribeAlways<BotOfflineEvent> {
-            if (this@QQAndroidBotNetworkHandler.bot == this.bot) {
-                logger.error("被挤下线")
-                close()
-            }
-        }
-
         MessageSvc.PbGetMsg(bot.client, MsgSvc.SyncFlag.START, currentTimeSeconds).sendWithoutExpect()
 
         bot.qqs.delegate.clear()
@@ -172,6 +205,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
                     launch {
                         try {
                             bot.groups.delegate.addLast(
+                                @Suppress("DuplicatedCode")
                                 GroupImpl(
                                     bot = bot,
                                     coroutineContext = bot.coroutineContext,
@@ -218,7 +252,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
                 if (failException != null) {
                     delay(bot.configuration.firstReconnectDelayMillis)
                     close()
-                    bot.tryReinitializeNetworkHandler(failException)
+                    BotOfflineEvent.Dropped(bot).broadcast()
                 }
             }
         }
@@ -408,33 +442,6 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
     }
 
 
-    @UseExperimental(ExperimentalCoroutinesApi::class)
-    private suspend fun processReceive() {
-        while (channel.isOpen) {
-            val rawInput = try {
-                channel.read()
-            } catch (e: ClosedChannelException) {
-                bot.tryReinitializeNetworkHandler(e)
-                return
-            } catch (e: ReadPacketInternalException) {
-                logger.error("Socket channel read failed: ${e.message}")
-                bot.tryReinitializeNetworkHandler(e)
-                return
-            } catch (e: CancellationException) {
-                return
-            } catch (e: Throwable) {
-                logger.error("Caught unexpected exceptions", e)
-                bot.tryReinitializeNetworkHandler(e)
-                return
-            }
-            packetReceiveLock.withLock {
-                processPacket(rawInput)
-            }
-        }
-    }
-
-    private val packetReceiveLock: Mutex = Mutex()
-
     /**
      * 发送一个包, 但不期待任何返回.
      */
@@ -517,5 +524,5 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
         super.close(cause)
     }
 
-    override suspend fun awaitDisconnection() = supervisor.join()
+    override suspend fun awaitDisconnection() = _packetReceiverJob?.join() ?: Unit
 }
\ No newline at end of file
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 3793b2ceb..32a77cdd0 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/BotImpl.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/BotImpl.kt
@@ -12,10 +12,15 @@
 package net.mamoe.mirai
 
 import kotlinx.coroutines.*
+import net.mamoe.mirai.event.Listener
 import net.mamoe.mirai.event.broadcast
 import net.mamoe.mirai.event.events.BotEvent
 import net.mamoe.mirai.event.events.BotOfflineEvent
+import net.mamoe.mirai.event.events.BotReloginEvent
+import net.mamoe.mirai.event.subscribeAlways
 import net.mamoe.mirai.network.BotNetworkHandler
+import net.mamoe.mirai.network.ForceOfflineException
+import net.mamoe.mirai.network.LoginFailedException
 import net.mamoe.mirai.network.closeAndJoin
 import net.mamoe.mirai.utils.*
 import net.mamoe.mirai.utils.io.logStacktrace
@@ -78,60 +83,70 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
     @Suppress("PropertyName")
     internal lateinit var _network: N
 
-    final override suspend fun login() = reinitializeNetworkHandler(null)
+    @Suppress("unused")
+    private val offlineListener: Listener<BotOfflineEvent> = this.subscribeAlways { event ->
+        when (event) {
+            is BotOfflineEvent.Dropped -> {
+                bot.logger.info("Connection dropped or lost by server, retrying login")
 
-    // shouldn't be suspend!! This function MUST NOT inherit the context from the caller because the caller(NetworkHandler) is going to close
-    fun tryReinitializeNetworkHandler(
-        cause: Throwable?
-    ): Job = launch {
-        var lastFailedException: Throwable? = null
-        repeat(configuration.reconnectionRetryTimes) {
-            try {
-                reinitializeNetworkHandler(cause)
-                logger.info("Reconnected successfully")
-                return@launch
-            } catch (e: Throwable) {
-                lastFailedException = e
-                delay(configuration.reconnectPeriodMillis)
+                var lastFailedException: Throwable? = null
+                repeat(configuration.reconnectionRetryTimes) {
+                    try {
+                        network.relogin()
+                        logger.info("Reconnected successfully")
+                        return@subscribeAlways
+                    } catch (e: Throwable) {
+                        lastFailedException = e
+                        delay(configuration.reconnectPeriodMillis)
+                    }
+                }
+                if (lastFailedException != null) {
+                    throw lastFailedException!!
+                }
+            }
+            is BotOfflineEvent.Active -> {
+                val msg = if (event.cause == null) {
+                    ""
+                } else {
+                    " with exception: " + event.cause.message
+                }
+                bot.logger.info("Bot is closed manually$msg")
+                close(CancellationException(event.toString()))
+            }
+            is BotOfflineEvent.Force -> {
+                bot.logger.info("Connection occupied by another android device: ${event.message}")
+                close(ForceOfflineException(event.toString()))
             }
         }
-        if (lastFailedException != null) {
-            throw lastFailedException!!
-        }
     }
 
+    final override suspend fun login() = reinitializeNetworkHandler(null)
+
     private suspend fun reinitializeNetworkHandler(
         cause: Throwable?
     ) {
-        logger.info("BotAccount: $uin")
-        logger.info("Initializing BotNetworkHandler")
-        try {
-            if (::_network.isInitialized) {
-                BotOfflineEvent.Active(this, cause).broadcast()
-                _network.closeAndJoin(cause)
+        suspend fun doRelogin() {
+            while (true) {
+                _network = createNetworkHandler(this.coroutineContext)
+                try {
+                    _network.relogin()
+                    return
+                } catch (e: LoginFailedException) {
+                    throw e
+                } catch (e: Exception) {
+                    network.logger.error(e)
+                    _network.closeAndJoin(e)
+                }
+                logger.warning("Login failed. Retrying in 3s...")
+                delay(3000)
             }
-        } catch (e: Exception) {
-            logger.error("Cannot close network handler", e)
         }
 
-        loginLoop@ while (true) {
-            _network = createNetworkHandler(this.coroutineContext)
-            try {
-                _network.login()
-                break@loginLoop
-            } catch (e: Exception) {
-                e.logStacktrace()
-                _network.closeAndJoin(e)
-            }
-            logger.warning("Login failed. Retrying in 3s...")
-            delay(3000)
-        }
-
-        repeat(1) block@{
+        suspend fun doInit() {
             repeat(2) {
                 try {
                     _network.init()
-                    return@block
+                    return
                 } catch (e: Exception) {
                     e.logStacktrace()
                 }
@@ -141,6 +156,16 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
             logger.error("cannot init. some features may be affected")
         }
 
+        logger.info("Initializing BotNetworkHandler")
+
+        if (::_network.isInitialized) {
+            BotReloginEvent(this, cause).broadcast()
+            doRelogin()
+            return
+        }
+
+        doRelogin()
+        doInit()
     }
 
     protected abstract fun createNetworkHandler(coroutineContext: CoroutineContext): N
@@ -153,9 +178,11 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
             if (cause == null) {
                 network.close()
                 this.botJob.complete()
+                offlineListener.complete()
             } else {
                 network.close(cause)
                 this.botJob.completeExceptionally(cause)
+                offlineListener.completeExceptionally(cause)
             }
         }
         groups.delegate.clear()
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotNetworkHandler.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotNetworkHandler.kt
index c7d286b97..c281223bb 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotNetworkHandler.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotNetworkHandler.kt
@@ -36,6 +36,7 @@ import net.mamoe.mirai.utils.io.PlatformDatagramChannel
  *
  * [BotNetworkHandler.close] 时将会 [取消][Job.cancel] 所有此作用域下的协程
  */
+@MiraiInternalAPI
 @Suppress("PropertyName")
 abstract class BotNetworkHandler : CoroutineScope {
     /**
@@ -55,12 +56,20 @@ abstract class BotNetworkHandler : CoroutineScope {
 
     /**
      * 依次尝试登录到可用的服务器. 在任一服务器登录完成后返回.
-     * 本函数将挂起直到登录成功.
+     *
+     * - 会断开连接并重新登录.
+     * - 不会停止网络层的 [Job].
+     * - 重新登录时不会再次拉取联系人列表.
+     * - 挂起直到登录成功.
      *
      * 不要使用这个 API. 请使用 [Bot.login]
+     *
+     * @throws LoginFailedException 登录失败时
+     * @throws WrongPasswordException 密码错误时
      */
+    @Suppress("SpellCheckingInspection")
     @MiraiInternalAPI
-    abstract suspend fun login()
+    abstract suspend fun relogin()
 
     /**
      * 初始化获取好友列表等值.
@@ -92,6 +101,7 @@ abstract class BotNetworkHandler : CoroutineScope {
     }
 }
 
+@UseExperimental(MiraiInternalAPI::class)
 suspend fun BotNetworkHandler.closeAndJoin(cause: Throwable? = null) {
     this.close(cause)
     this.supervisor.join()