Close bot if first login failed

This commit is contained in:
Him188 2021-04-25 15:11:19 +08:00
parent 35a6d12dde
commit 95d634233c
13 changed files with 124 additions and 45 deletions

View File

@ -165,6 +165,9 @@ public inline fun <E> MutableList<E>.replaceAllKotlin(operator: (E) -> E) {
}
}
public fun systemProp(name: String, default: String): String =
System.getProperty(name, default) ?: default
public fun systemProp(name: String, default: Boolean): Boolean =
System.getProperty(name, default.toString())?.toBoolean() ?: default

View File

@ -21,6 +21,7 @@ import net.mamoe.mirai.internal.contact.info.FriendInfoImpl
import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl
import net.mamoe.mirai.internal.contact.uin
import net.mamoe.mirai.internal.network.component.ConcurrentComponentStorage
import net.mamoe.mirai.internal.network.components.SsoProcessor
import net.mamoe.mirai.internal.network.handler.NetworkHandler
import net.mamoe.mirai.internal.network.impl.netty.asCoroutineExceptionHandler
import net.mamoe.mirai.supervisorJob
@ -110,11 +111,18 @@ internal abstract class AbstractBot constructor(
// network
///////////////////////////////////////////////////////////////////////////
val network: NetworkHandler by lazy { createNetworkHandler() }
val network: NetworkHandler by lazy { createNetworkHandler() } // the selector handles renewal of [NetworkHandler]
final override suspend fun login() {
if (!isActive) error("Bot is already closed and cannot relogin. Please create a new Bot instance then do login.")
network.resumeConnection()
try {
network.resumeConnection()
} catch (e: Throwable) { // failed to init
if (!components[SsoProcessor].firstLoginSucceed) {
this.close() // failed to do first login.
}
throw e
}
}
protected abstract fun createNetworkHandler(): NetworkHandler

View File

@ -51,12 +51,25 @@ internal fun Bot.asQQAndroidBot(): QQAndroidBot {
}
internal class BotDebugConfiguration(
var stateObserver: StateObserver? = when {
systemProp("mirai.debug.network.state.observer.logging", false) ->
var stateObserver: StateObserver? = when (systemProp(
"mirai.debug.network.state.observer.logging",
"off"
).toLowerCase()) {
"full" -> {
SafeStateObserver(
LoggingStateObserver(MiraiLogger.create("States")),
LoggingStateObserver(MiraiLogger.create("States"), true),
MiraiLogger.create("LoggingStateObserver errors")
)
}
"off", "false" -> {
null
}
"on", "true" -> {
SafeStateObserver(
LoggingStateObserver(MiraiLogger.create("States"), false),
MiraiLogger.create("LoggingStateObserver errors")
)
}
else -> null
}
)
@ -69,6 +82,14 @@ internal open class QQAndroidBot constructor(
) : AbstractBot(configuration, account.id) {
override val bot: QQAndroidBot get() = this
@Deprecated(
"",
replaceWith = ReplaceWith(
"this.components[SsoProcessor].firstLoginSucceed",
"net.mamoe.mirai.internal.network.components.SsoProcessor"
)
)
internal var firstLoginSucceed: Boolean = false
///////////////////////////////////////////////////////////////////////////
@ -85,7 +106,7 @@ internal open class QQAndroidBot constructor(
StateChangedObserver(to = State.OK) { new ->
bot.launch(logger.asCoroutineExceptionHandler()) {
BotOnlineEvent(bot).broadcast()
if (bot.firstLoginSucceed) { // TODO: 2021/4/21 actually no use
if (bot.components[SsoProcessor].firstLoginSucceed) { // TODO: 2021/4/21 actually no use
BotReloginEvent(bot, new.getCause()).broadcast()
}
}

View File

@ -75,7 +75,7 @@ internal class BotInitProcessorImpl(
launch { context[ContactUpdater].loadAll(registerResp.origin) }
}
bot.firstLoginSucceed = true
bot.components[SsoProcessor].firstLoginSucceed = true
}
private suspend fun registerClientOnline(): StatSvc.Register.Response {

View File

@ -10,8 +10,10 @@
package net.mamoe.mirai.internal.network.components
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.yield
import net.mamoe.mirai.event.ConcurrencyKind
import net.mamoe.mirai.event.EventPriority
import net.mamoe.mirai.event.events.BotOfflineEvent
@ -19,17 +21,27 @@ import net.mamoe.mirai.event.subscribeAlways
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.asQQAndroidBot
import net.mamoe.mirai.internal.network.component.ComponentKey
import net.mamoe.mirai.internal.network.handler.NetworkHandler
import net.mamoe.mirai.internal.network.handler.NetworkHandler.State
import net.mamoe.mirai.utils.castOrNull
import net.mamoe.mirai.utils.info
import net.mamoe.mirai.utils.toHumanReadableString
import kotlin.time.measureTime
/**
* Handles [BotOfflineEvent]
*/
internal interface BotOfflineEventMonitor {
companion object : ComponentKey<BotOfflineEventMonitor>
/**
* Attach a listener to the [scope]. [scope] is usually the scope of [NetworkHandler.State.OK].
*/
fun attachJob(bot: QQAndroidBot, scope: CoroutineScope)
}
private data class BotClosedByEvent(val event: BotOfflineEvent) : RuntimeException("Bot is closed by event '$event'.")
internal class BotOfflineEventMonitorImpl : BotOfflineEventMonitor {
override fun attachJob(bot: QQAndroidBot, scope: CoroutineScope) {
bot.eventChannel.parentScope(scope).subscribeAlways(
@ -39,38 +51,37 @@ internal class BotOfflineEventMonitorImpl : BotOfflineEventMonitor {
)
}
// TODO: 2021/4/25 Review BotOfflineEventMonitor
private suspend fun onEvent(event: BotOfflineEvent) {
private suspend fun onEvent(event: BotOfflineEvent) = coroutineScope {
val bot = event.bot.asQQAndroidBot()
val network = bot.network
if (
!event.bot.isActive // bot closed
// || _isConnecting // bot 还在登入 // TODO: 2021/4/14 处理还在登入?
) {
// Close network to avoid endless reconnection while network is ok
// https://github.com/mamoe/mirai/issues/894
kotlin.runCatching { network.close(null) }
return
fun closeNetwork() {
if (network.state == State.CLOSED) return // avoid recursive calls.
launch {
// suspend until state becomes CLOSED to hang [onEvent] for synchronization.
while (select { network.onStateChanged { it != State.CLOSED } }) yield()
}
bot.launch { network.close(BotClosedByEvent(event)) }
}
/*
if (network.areYouOk() && event !is BotOfflineEvent.Force && event !is BotOfflineEvent.MsfOffline) {
// network 运行正常
return@subscribeAlways
}*/
when (event) {
is BotOfflineEvent.Active -> {
// This event might also be broadcast by the network handler by a state observer.
// In that case, `network.state` will be `CLOSED` then `closeNetwork` returns immediately.
// So there won't be recursive calls.
val cause = event.cause
val msg = if (cause == null) "" else " with exception: $cause"
bot.logger.info("Bot is closed manually $msg", cause)
network.close(null)
closeNetwork()
}
is BotOfflineEvent.Force -> {
bot.logger.info { "Connection occupied by another android device: ${event.message}" }
closeNetwork()
if (event.reconnect) {
bot.logger.info { "Reconnecting..." }
// delay(3000)
} else {
network.close(null)
}
}
is BotOfflineEvent.MsfOffline,
@ -82,11 +93,6 @@ internal class BotOfflineEventMonitorImpl : BotOfflineEventMonitor {
}
if (event.reconnect) {
if (!network.isOk()) {
// normally closed
return
}
val causeMessage = event.castOrNull<BotOfflineEvent.CauseAware>()?.cause?.toString() ?: event.toString()
bot.logger.info { "Connection lost, retrying login ($causeMessage)." }
@ -94,8 +100,8 @@ internal class BotOfflineEventMonitorImpl : BotOfflineEventMonitor {
val success: Boolean
val time = measureTime {
success = kotlin.runCatching {
bot.login()
}.isSuccess // resume connection
bot.login() // selector will create new NH to replace the old, closed one, with some further comprehensive considerations. For example, limitation for attempts.
}.isSuccess
}
if (success) {

View File

@ -45,6 +45,8 @@ internal interface SsoProcessor {
val client: QQAndroidClient
val ssoSession: SsoSession
var firstLoginSucceed: Boolean
/**
* The observers to launch jobs for states.
*
@ -78,6 +80,8 @@ internal class SsoProcessorImpl(
// public
///////////////////////////////////////////////////////////////////////////
override var firstLoginSucceed: Boolean = false
@Volatile
override var client = createClient(ssoContext.bot)
@ -111,6 +115,7 @@ internal class SsoProcessorImpl(
SlowLoginImpl(handler).doLogin()
}
ssoContext.accountSecretsManager.saveSecrets(ssoContext.account, AccountSecretsImpl(client))
ssoContext.bot.logger.info { "Login successful." }
}
override suspend fun logout(handler: NetworkHandler) {

View File

@ -98,9 +98,10 @@ internal interface NetworkHandler {
OK,
/**
* The terminal state. Cannot resume anymore. Both [resumeConnection] and [sendAndExpect] throw a [IllegalStateException].
* The terminal state. Both [resumeConnection] and [sendAndExpect] throw a [IllegalStateException].
*
* When a handler reached [CLOSED] state, it is finalized and cannot be restored to any other states.
* **Important nodes**: if [NetworkHandler] is [SelectorNetworkHandler], it might return to a normal state e.g. [INITIALIZED] if new instance of [NetworkHandler] is created.
* However callers usually do not need to pay extra attention on this behavior. Everything will just work fine if you consider [CLOSED] as a final, non-recoverable state.
*
* At this state [resumeConnection] throws the exception caught from underlying socket implementation (i.e netty).
* [sendAndExpect] throws [IllegalStateException].

View File

@ -15,7 +15,8 @@ import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.debug
internal class LoggingStateObserver(
val logger: MiraiLogger
val logger: MiraiLogger,
private val showStacktrace: Boolean = false
) : StateObserver {
override fun toString(): String {
return "LoggingStateObserver"
@ -26,7 +27,10 @@ internal class LoggingStateObserver(
previous: NetworkHandlerSupport.BaseStateImpl,
new: NetworkHandlerSupport.BaseStateImpl
) {
logger.debug { "State changed: ${previous.correspondingState} -> ${new.correspondingState}" }
logger.debug(
{ "State changed: ${previous.correspondingState} -> ${new.correspondingState}" },
if (showStacktrace) Exception("Show stacktrace") else null
)
}
override fun exceptionOnCreatingNewState(
@ -34,7 +38,7 @@ internal class LoggingStateObserver(
previousState: NetworkHandlerSupport.BaseStateImpl,
exception: Throwable
) {
logger.debug({ "State changed: ${previousState.correspondingState} -> $exception" }, exception)
logger.debug { "State changed: ${previousState.correspondingState} -> $exception" }
}
override fun beforeStateResume(networkHandler: NetworkHandler, state: NetworkHandlerSupport.BaseStateImpl) {

View File

@ -36,6 +36,7 @@ import net.mamoe.mirai.internal.message.toMessageChainOnline
import net.mamoe.mirai.internal.network.MultiPacket
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.components.SsoProcessor
import net.mamoe.mirai.internal.network.handler.logger
import net.mamoe.mirai.internal.network.protocol.data.proto.FrdSysMsg
import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
@ -378,7 +379,7 @@ internal suspend fun MsgComm.Msg.transform(bot: QQAndroidBot, fromSync: Boolean
}
return null
}
if (!bot.firstLoginSucceed) {
if (!bot.components[SsoProcessor].firstLoginSucceed) {
return null
}
val fromUin = if (fromSync) {
@ -483,7 +484,7 @@ internal suspend fun MsgComm.Msg.transform(bot: QQAndroidBot, fromSync: Boolean
}
141 -> {
if (!bot.firstLoginSucceed || msgHead.fromUin == bot.id && !fromSync) {
if (!bot.components[SsoProcessor].firstLoginSucceed || msgHead.fromUin == bot.id && !fromSync) {
return null
}
val tmpHead = msgHead.c2cTmpMsgHead ?: return null

View File

@ -26,6 +26,7 @@ import net.mamoe.mirai.internal.contact.info.MemberInfoImpl
import net.mamoe.mirai.internal.contact.newAnonymous
import net.mamoe.mirai.internal.message.toMessageChainOnline
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.components.SsoProcessor
import net.mamoe.mirai.internal.network.handler.logger
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
@ -70,7 +71,7 @@ internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory<Packet?>("Onlin
@OptIn(ExperimentalStdlibApi::class)
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): Packet? {
// 00 00 02 E4 0A D5 05 0A 4F 08 A2 FF 8C F0 03 10 DD F1 92 B7 07 18 52 20 00 28 BC 3D 30 8C 82 AB F1 05 38 D2 80 E0 8C 80 80 80 80 02 4A 21 08 E7 C1 AD B8 02 10 01 18 BA 05 22 09 48 69 6D 31 38 38 6D 6F 65 30 06 38 02 42 05 4D 69 72 61 69 50 01 58 01 60 00 88 01 08 12 06 08 01 10 00 18 00 1A F9 04 0A F6 04 0A 26 08 00 10 87 82 AB F1 05 18 B7 B4 BF 30 20 00 28 0C 30 00 38 86 01 40 22 4A 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91 12 E6 03 42 E3 03 12 2A 7B 34 45 31 38 35 38 32 32 2D 30 45 37 42 2D 46 38 30 46 2D 43 35 42 31 2D 33 34 34 38 38 33 37 34 44 33 39 43 7D 2E 6A 70 67 22 00 2A 04 03 00 00 00 32 60 15 36 20 39 36 6B 45 31 41 38 35 32 32 39 64 63 36 39 38 34 37 39 37 37 62 20 20 20 20 20 20 35 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 7B 34 45 31 38 35 38 32 32 2D 30 45 37 42 2D 46 38 30 46 2D 43 35 42 31 2D 33 34 34 38 38 33 37 34 44 33 39 43 7D 2E 6A 70 67 31 32 31 32 41 38 C6 BB 8A A9 08 40 FB AE 9E C2 09 48 50 50 41 5A 00 60 01 6A 10 4E 18 58 22 0E 7B F8 0F C5 B1 34 48 83 74 D3 9C 72 59 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 31 39 38 3F 74 65 72 6D 3D 32 82 01 57 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 30 3F 74 65 72 6D 3D 32 B0 01 4D B8 01 2E C8 01 FF 05 D8 01 4D E0 01 2E FA 01 59 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 34 30 30 3F 74 65 72 6D 3D 32 80 02 4D 88 02 2E 12 45 AA 02 42 50 03 60 00 68 00 9A 01 39 08 09 20 BF 50 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 04 08 02 08 01 90 04 80 80 80 10 B8 04 00 C0 04 00 12 06 4A 04 08 00 40 01 12 14 82 01 11 0A 09 48 69 6D 31 38 38 6D 6F 65 18 06 20 08 28 03 10 8A CA 9D A1 07 1A 00
if (!bot.firstLoginSucceed) return null
if (!bot.components[SsoProcessor].firstLoginSucceed) return null
val pbPushMsg = readProtoBuf(MsgOnlinePush.PbPushMsg.serializer())
val msgHead = pbPushMsg.msg.msgHead

View File

@ -71,6 +71,7 @@ internal abstract class AbstractRealNetworkHandlerTest<H : NetworkHandler> : Abs
set(SsoProcessor, object : SsoProcessor {
override val client: QQAndroidClient get() = bot.client
override val ssoSession: SsoSession get() = bot.client
override var firstLoginSucceed: Boolean = false
override fun createObserverChain(): StateObserver = get(StateObserver)
override suspend fun login(handler: NetworkHandler) {
nhEvents.add(NHEvent.Login)

View File

@ -13,7 +13,11 @@ import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.internal.MockBot
import net.mamoe.mirai.internal.network.components.BotOfflineEventMonitor
import net.mamoe.mirai.internal.network.components.BotOfflineEventMonitorImpl
import net.mamoe.mirai.internal.network.handler.NetworkHandler.State.*
import net.mamoe.mirai.internal.test.runBlockingUnit
import net.mamoe.mirai.supervisorJob
@ -24,6 +28,29 @@ import kotlin.test.assertTrue
internal class NettyBotLifecycleTest : AbstractNettyNHTest() {
@Test
fun `closed on Force offline with BotOfflineEventMonitor`() = runBlockingUnit {
defaultComponents[BotOfflineEventMonitor] = BotOfflineEventMonitorImpl()
bot.login()
assertState(OK)
BotOfflineEvent.Force(bot, "test", "test").broadcast()
assertState(CLOSED)
assertFalse { network.isActive }
assertTrue { bot.isActive }
}
@Test
fun `closed on Active offline with BotOfflineEventMonitor`() = runBlockingUnit {
defaultComponents[BotOfflineEventMonitor] = BotOfflineEventMonitorImpl()
bot.login()
assertState(OK)
BotOfflineEvent.Active(bot, null).broadcast()
assertState(CLOSED)
assertFalse { network.isActive }
assertTrue { bot.isActive }
}
@Test
fun `send logout on exit`() = runBlockingUnit {
assertState(INITIALIZED)

View File

@ -14,6 +14,7 @@ import net.mamoe.mirai.event.Event
import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.event.events.BotOnlineEvent
import net.mamoe.mirai.event.events.BotReloginEvent
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.INITIALIZED
import net.mamoe.mirai.internal.network.handler.NetworkHandler.State.OK
@ -40,7 +41,7 @@ internal class NettyHandlerEventTest : AbstractNettyNHTest() {
assertEventBroadcasts<BotReloginEvent> {
assertEquals(INITIALIZED, network.state)
bot.login()
bot.firstLoginSucceed = true
bot.components[SsoProcessor].firstLoginSucceed = true
network.setStateConnecting()
network.resumeConnection()
delay(3.seconds) // `login` launches a job which broadcasts the event
@ -52,7 +53,7 @@ internal class NettyHandlerEventTest : AbstractNettyNHTest() {
fun `BotOnlineEvent after successful reconnection`() = runBlockingUnit {
assertEquals(INITIALIZED, network.state)
bot.login()
bot.firstLoginSucceed = true
bot.components[SsoProcessor].firstLoginSucceed = true
assertEquals(OK, network.state)
delay(3.seconds) // `login` launches a job which broadcasts the event
assertEventBroadcasts<BotOnlineEvent>(1) {
@ -67,7 +68,7 @@ internal class NettyHandlerEventTest : AbstractNettyNHTest() {
fun `BotOfflineEvent after successful reconnection`() = runBlockingUnit {
assertEquals(INITIALIZED, network.state)
bot.login()
bot.firstLoginSucceed = true
bot.components[SsoProcessor].firstLoginSucceed = true
assertEquals(OK, network.state)
delay(3.seconds) // `login` launches a job which broadcasts the event
assertEventBroadcasts<BotOfflineEvent>(1) {
@ -81,7 +82,7 @@ internal class NettyHandlerEventTest : AbstractNettyNHTest() {
private fun noEventOn(setState: () -> Unit) = runBlockingUnit {
assertState(INITIALIZED)
bot.login()
bot.firstLoginSucceed = true
bot.components[SsoProcessor].firstLoginSucceed = true
assertState(OK)
network.setStateConnecting()
delay(3.seconds) // `login` launches a job which broadcasts the event