Add NetworkHandlerFactory and tests for NetworkHandler

This commit is contained in:
Him188 2021-04-24 14:13:08 +08:00
parent 4f77abca87
commit b91bbfd2b8
19 changed files with 472 additions and 117 deletions

View File

@ -88,6 +88,7 @@ public sealed class BotOfflineEvent : BotEvent, AbstractEvent() {
* returnCode = -10008 等原因掉线 * returnCode = -10008 等原因掉线
*/ */
@MiraiInternalApi("This is very experimental and might be changed") @MiraiInternalApi("This is very experimental and might be changed")
@Deprecated("Deprecated with no replacement", level = DeprecationLevel.ERROR)
public data class PacketFactoryErrorCode @MiraiInternalApi public constructor( public data class PacketFactoryErrorCode @MiraiInternalApi public constructor(
val returnCode: Int, val returnCode: Int,
public override val bot: Bot, public override val bot: Bot,

View File

@ -12,7 +12,10 @@ package net.mamoe.mirai.utils
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
public class ExceptionCollector { public class ExceptionCollector : Sequence<Throwable> {
// TODO: 2021/4/20 drop last
public constructor() public constructor()
public constructor(initial: Throwable?) { public constructor(initial: Throwable?) {
collect(initial) collect(initial)
@ -32,6 +35,12 @@ public class ExceptionCollector {
if (e == null) return if (e == null) return
val last = last val last = last
if (last != null) { if (last != null) {
last.itr().forEach { suppressed ->
if (suppressed.stackTrace.contentEquals(e.stackTrace)) {
// filter out useless duplicates.
return
}
}
e.addSuppressed(last) e.addSuppressed(last)
} }
this.last = e this.last = e
@ -57,6 +66,15 @@ public class ExceptionCollector {
@DslMarker @DslMarker
private annotation class TerminalOperation private annotation class TerminalOperation
private fun Throwable.itr(): Iterator<Throwable> {
return (sequenceOf(this) + this.suppressed.asSequence().flatMap { it.itr().asSequence() }).iterator()
}
override fun iterator(): Iterator<Throwable> {
val last = getLast() ?: return emptyList<Throwable>().iterator()
return last.itr()
}
} }
/** /**

View File

@ -10,14 +10,36 @@
package net.mamoe.mirai.internal.test package net.mamoe.mirai.internal.test
import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.MiraiLogger
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.jupiter.api.Test
import java.security.Security
import kotlin.test.assertTrue
internal actual fun initPlatform() { internal actual fun initPlatform() {
init init
} }
private val init by lazy { private val init: Unit by lazy {
MiraiLogger.setDefaultLoggerCreator { MiraiLogger.setDefaultLoggerCreator {
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
net.mamoe.mirai.internal.utils.StdoutLogger(it) net.mamoe.mirai.internal.utils.StdoutLogger(it)
} }
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) != null) {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
}
Security.addProvider(BouncyCastleProvider())
Unit
}
internal actual class PlatformInitializationTest : AbstractTest() {
@Test
actual fun test() {
assertTrue {
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
MiraiLogger.create("1") is net.mamoe.mirai.internal.utils.StdoutLogger
}
}
} }

View File

@ -27,7 +27,9 @@ import net.mamoe.mirai.internal.network.components.*
import net.mamoe.mirai.internal.network.context.SsoProcessorContext import net.mamoe.mirai.internal.network.context.SsoProcessorContext
import net.mamoe.mirai.internal.network.context.SsoProcessorContextImpl import net.mamoe.mirai.internal.network.context.SsoProcessorContextImpl
import net.mamoe.mirai.internal.network.handler.NetworkHandler import net.mamoe.mirai.internal.network.handler.NetworkHandler
import net.mamoe.mirai.internal.network.handler.NetworkHandler.State
import net.mamoe.mirai.internal.network.handler.NetworkHandlerContextImpl import net.mamoe.mirai.internal.network.handler.NetworkHandlerContextImpl
import net.mamoe.mirai.internal.network.handler.NetworkHandlerSupport
import net.mamoe.mirai.internal.network.handler.selector.FactoryKeepAliveNetworkHandlerSelector import net.mamoe.mirai.internal.network.handler.selector.FactoryKeepAliveNetworkHandlerSelector
import net.mamoe.mirai.internal.network.handler.selector.SelectorNetworkHandler import net.mamoe.mirai.internal.network.handler.selector.SelectorNetworkHandler
import net.mamoe.mirai.internal.network.handler.state.* import net.mamoe.mirai.internal.network.handler.state.*
@ -35,7 +37,6 @@ import net.mamoe.mirai.internal.network.impl.netty.NettyNetworkHandlerFactory
import net.mamoe.mirai.internal.network.impl.netty.asCoroutineExceptionHandler import net.mamoe.mirai.internal.network.impl.netty.asCoroutineExceptionHandler
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket 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.OutgoingPacketWithRespType
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
import net.mamoe.mirai.utils.BotConfiguration import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.systemProp import net.mamoe.mirai.utils.systemProp
@ -61,7 +62,7 @@ internal class BotDebugConfiguration(
) )
@Suppress("INVISIBLE_MEMBER", "BooleanLiteralArgument", "OverridingDeprecatedMember") @Suppress("INVISIBLE_MEMBER", "BooleanLiteralArgument", "OverridingDeprecatedMember")
internal class QQAndroidBot constructor( internal open class QQAndroidBot constructor(
internal val account: BotAccount, internal val account: BotAccount,
configuration: BotConfiguration, configuration: BotConfiguration,
private val debugConfiguration: BotDebugConfiguration = BotDebugConfiguration(), private val debugConfiguration: BotDebugConfiguration = BotDebugConfiguration(),
@ -77,25 +78,42 @@ internal class QQAndroidBot constructor(
// TODO: 2021/4/14 bdhSyncer.loadFromCache() when login // TODO: 2021/4/14 bdhSyncer.loadFromCache() when login
// IDE error, don't move into lazy // IDE error, don't move into lazy
private fun ComponentStorage.stateObserverChain(): StateObserver { fun ComponentStorage.stateObserverChain(): StateObserver {
val components = this val components = this
return StateObserver.chainOfNotNull( return StateObserver.chainOfNotNull(
components[BotInitProcessor].asObserver().safe(networkLogger), components[BotInitProcessor].asObserver(),
StateChangedObserver(NetworkHandler.State.OK) { new -> StateChangedObserver(State.OK) { new ->
new.launch(logger.asCoroutineExceptionHandler()) { bot.launch(logger.asCoroutineExceptionHandler()) {
BotOnlineEvent(bot).broadcast() BotOnlineEvent(bot).broadcast()
if (bot.firstLoginSucceed) { // TODO: 2021/4/21 actually no use if (bot.firstLoginSucceed) { // TODO: 2021/4/21 actually no use
BotReloginEvent(bot, new.getCause()).broadcast() BotReloginEvent(bot, new.getCause()).broadcast()
} }
} }
}, },
StateChangedObserver(NetworkHandler.State.CLOSED) { new -> object : StateObserver {
new.launch(logger.asCoroutineExceptionHandler()) { override fun stateChanged(
networkHandler: NetworkHandlerSupport,
previous: NetworkHandlerSupport.BaseStateImpl,
new: NetworkHandlerSupport.BaseStateImpl
) {
val p = previous.correspondingState
val n = new.correspondingState
when {
p == State.OK && n == State.CONNECTING -> {
bot.launch(logger.asCoroutineExceptionHandler()) {
BotOfflineEvent.Dropped(bot, new.getCause()).broadcast() BotOfflineEvent.Dropped(bot, new.getCause()).broadcast()
} }
}
p == State.OK && n == State.CLOSED -> {
bot.launch(logger.asCoroutineExceptionHandler()) {
BotOfflineEvent.Active(bot, new.getCause()).broadcast()
}
}
}
}
}, },
debugConfiguration.stateObserver debugConfiguration.stateObserver
) ).safe(logger)
} }
@ -135,7 +153,7 @@ internal class QQAndroidBot constructor(
val client get() = components[SsoProcessor].client val client get() = components[SsoProcessor].client
override suspend fun sendLogout() { override suspend fun sendLogout() {
network.sendWithoutExpect(StatSvc.Register.offline(client)) components[SsoProcessor].logout(network)
} }
override fun createNetworkHandler(): NetworkHandler { override fun createNetworkHandler(): NetworkHandler {

View File

@ -11,6 +11,7 @@ package net.mamoe.mirai.internal.network.component
import net.mamoe.mirai.utils.systemProp import net.mamoe.mirai.utils.systemProp
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.LazyThreadSafetyMode.NONE
/** /**
* A thread-safe implementation of [MutableComponentStorage] * A thread-safe implementation of [MutableComponentStorage]
@ -55,4 +56,4 @@ internal class ConcurrentComponentStorage(
} }
} }
private val SHOW_ALL_COMPONENTS = systemProp("mirai.debug.network.show.all.components", false) private val SHOW_ALL_COMPONENTS: Boolean by lazy(NONE) { systemProp("mirai.debug.network.show.all.components", false) }

View File

@ -34,7 +34,7 @@ import net.mamoe.mirai.utils.info
* Facade of [ContactUpdater], [OtherClientUpdater], [ConfigPushSyncer]. * Facade of [ContactUpdater], [OtherClientUpdater], [ConfigPushSyncer].
* Handles initialization jobs after successful logon. * Handles initialization jobs after successful logon.
* *
* Attached to handler state [NetworkHandler.State.LOADING] [as state observer][asObserver]. * Attached to handler state [NetworkHandler.State.LOADING] [as state observer][asObserver] in [QQAndroidBot.stateObserverChain].
*/ */
internal interface BotInitProcessor { internal interface BotInitProcessor {
suspend fun init() suspend fun init()

View File

@ -24,6 +24,7 @@ import net.mamoe.mirai.internal.network.handler.state.StateChangedObserver
import net.mamoe.mirai.internal.network.handler.state.StateObserver import net.mamoe.mirai.internal.network.handler.state.StateObserver
import net.mamoe.mirai.internal.network.impl.netty.NettyNetworkHandler import net.mamoe.mirai.internal.network.impl.netty.NettyNetworkHandler
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType 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 import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin.Login.LoginPacketResponse
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin.Login.LoginPacketResponse.Captcha import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin.Login.LoginPacketResponse.Captcha
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin10 import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin10
@ -57,6 +58,8 @@ internal interface SsoProcessor {
@Throws(LoginFailedException::class) @Throws(LoginFailedException::class)
suspend fun login(handler: NetworkHandler) suspend fun login(handler: NetworkHandler)
suspend fun logout(handler: NetworkHandler)
companion object : ComponentKey<SsoProcessor> companion object : ComponentKey<SsoProcessor>
} }
@ -110,6 +113,10 @@ internal class SsoProcessorImpl(
ssoContext.accountSecretsManager.saveSecrets(ssoContext.account, AccountSecretsImpl(client)) ssoContext.accountSecretsManager.saveSecrets(ssoContext.account, AccountSecretsImpl(client))
} }
override suspend fun logout(handler: NetworkHandler) {
handler.sendWithoutExpect(StatSvc.Register.offline(client))
}
private fun createClient(bot: QQAndroidBot): QQAndroidClient { private fun createClient(bot: QQAndroidBot): QQAndroidClient {
val device = ssoContext.device val device = ssoContext.device
return QQAndroidClient( return QQAndroidClient(

View File

@ -10,22 +10,25 @@
package net.mamoe.mirai.internal.network.handler package net.mamoe.mirai.internal.network.handler
import kotlinx.coroutines.selects.SelectClause1 import kotlinx.coroutines.selects.SelectClause1
import net.mamoe.mirai.Bot
import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.handler.NetworkHandler.State import net.mamoe.mirai.internal.network.components.BotInitProcessor
import net.mamoe.mirai.internal.network.components.SsoProcessor
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.OutgoingPacket
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType
import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.MiraiLogger
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.SocketAddress
import java.util.concurrent.CancellationException
/** /**
* Basic interface available to application. Usually wrapped with [SelectorNetworkHandler]. * Basic interface available to application. Usually wrapped with [SelectorNetworkHandler].
* *
* Implementation is usually subclass of [NetworkHandlerSupport]. * Implementation is usually subclass of [NetworkHandlerSupport].
* *
* Instances are often created by [NetworkHandlerFactory].
*
* @see NetworkHandlerSupport * @see NetworkHandlerSupport
* @see NetworkHandlerFactory
*/ */
internal interface NetworkHandler { internal interface NetworkHandler {
val context: NetworkHandlerContext val context: NetworkHandlerContext
@ -33,7 +36,7 @@ internal interface NetworkHandler {
fun isOk() = state == State.OK fun isOk() = state == State.OK
/** /**
* State of this handler. * Current state of this handler. This is volatile.
*/ */
val state: State val state: State
@ -42,6 +45,28 @@ internal interface NetworkHandler {
*/ */
val onStateChanged: SelectClause1<State> val onStateChanged: SelectClause1<State>
/**
* State of this handler.
*
* ## States transition overview
*
* 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].
* 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.
* For example, attaching a [BotInitProcessor] to handle session-relevant jobs to the [Bot].
*
* Failure during [State.CONNECTING] and [State.LOADING] switches state to [State.CLOSED], on which [NetworkHandler] is considered permanently dead.
*
* The state after finish of [State.LOADING] is [State.OK]. This state lasts for the majority of time.
*
* When connection is lost (e.g. due to Internet unavailability), it returns to [State.CONNECTING] and repeatedly attempts to reconnect.
* Immediately after successful recovery, [State.OK] will be set.
*
* @see state
*/
enum class State { enum class State {
/** /**
* Just created and no connection has been made. * Just created and no connection has been made.
@ -73,18 +98,18 @@ internal interface NetworkHandler {
OK, OK,
/** /**
* Cannot resume anymore. Both [resumeConnection] and [sendAndExpect] throw a [CancellationException]. * The terminal state. Cannot resume anymore. Both [resumeConnection] and [sendAndExpect] throw a [IllegalStateException].
* *
* When a handler reached [CLOSED] state, it is finalized and cannot be restored to any other states. * When a handler reached [CLOSED] state, it is finalized and cannot be restored to any other states.
* *
* At this state [resumeConnection] throws the exception caught from underlying socket implementation (i.e netty). * At this state [resumeConnection] throws the exception caught from underlying socket implementation (i.e netty).
* [sendAndExpect] throws [IllegalStateException] * [sendAndExpect] throws [IllegalStateException].
*/ */
CLOSED, CLOSED,
} }
/** /**
* Attempts to resume the connection. * Suspends the coroutine until [sendAndExpect] can be executed without suspension.
* *
* May throw exception that had caused current state to fail. * May throw exception that had caused current state to fail.
* @see State * @see State
@ -95,17 +120,23 @@ internal interface NetworkHandler {
/** /**
* Sends [packet] and expects to receive a response from the server. * Sends [packet] and expects to receive a response from the server.
*
* Coroutine suspension may happen if connection if not yet available however, [IllegalStateException] is thrown if [NetworkHandler] is already in [State.CLOSED]
*
* @param attempts ranges `1..INFINITY` * @param attempts ranges `1..INFINITY`
*/ */
suspend fun sendAndExpect(packet: OutgoingPacket, timeout: Long = 5000, attempts: Int = 2): Packet? suspend fun sendAndExpect(packet: OutgoingPacket, timeout: Long = 5000, attempts: Int = 2): Packet?
/** /**
* Sends [packet] and does not expect any response. (Response is still processed but not passed as a return value of this function.) * Sends [packet] and does not expect any response.
*
* Response is still being processed but not passed as a return value of this function, so it does not suspends this function.
* However, coroutine is still suspended if connection if not yet available, and [IllegalStateException] is thrown if [NetworkHandler] is already in [State.CLOSED]
*/ */
suspend fun sendWithoutExpect(packet: OutgoingPacket) suspend fun sendWithoutExpect(packet: OutgoingPacket)
/** /**
* Closes this handler gracefully. * Closes this handler gracefully (i.e. asynchronously).
*/ */
fun close(cause: Throwable?) fun close(cause: Throwable?)
@ -142,18 +173,3 @@ internal interface NetworkHandler {
internal val NetworkHandler.logger: MiraiLogger get() = context.logger internal val NetworkHandler.logger: MiraiLogger get() = context.logger
/**
* Factory for a specific [NetworkHandler] implementation.
*/
internal interface NetworkHandlerFactory<H : NetworkHandler> {
fun create(context: NetworkHandlerContext, host: String, port: Int): H =
create(context, InetSocketAddress.createUnresolved(host, port))
fun create(context: NetworkHandlerContext, host: InetAddress, port: Int): H =
create(context, InetSocketAddress(host, port))
/**
* Create an instance of [H]. The returning [H] has [NetworkHandler.state] of [State.INITIALIZED]
*/
fun create(context: NetworkHandlerContext, address: SocketAddress): H
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2019-2021 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/master/LICENSE
*/
package net.mamoe.mirai.internal.network.handler
import net.mamoe.mirai.internal.network.handler.NetworkHandler.State
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.SocketAddress
/**
* Factory for a specific [NetworkHandler] implementation.
*/
internal interface NetworkHandlerFactory<H : NetworkHandler> {
fun create(context: NetworkHandlerContext, host: String, port: Int): H =
create(context, InetSocketAddress.createUnresolved(host, port))
fun create(context: NetworkHandlerContext, host: InetAddress, port: Int): H =
create(context, InetSocketAddress(host, port))
/**
* Create an instance of [H]. The returning [H] has [NetworkHandler.state] of [State.INITIALIZED]
*/
fun create(context: NetworkHandlerContext, address: SocketAddress): H
}

View File

@ -36,7 +36,7 @@ import java.net.SocketAddress
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import io.netty.channel.Channel as NettyChannel import io.netty.channel.Channel as NettyChannel
internal class NettyNetworkHandler( internal open class NettyNetworkHandler(
context: NetworkHandlerContext, context: NetworkHandlerContext,
private val address: SocketAddress, private val address: SocketAddress,
) : NetworkHandlerSupport(context) { ) : NetworkHandlerSupport(context) {
@ -86,7 +86,16 @@ internal class NettyNetworkHandler(
} }
} }
private suspend fun createConnection(decodePipeline: PacketDecodePipeline): NettyChannel { protected open fun setupChannelPipeline(pipeline: ChannelPipeline, decodePipeline: PacketDecodePipeline) {
pipeline
.addLast(OutgoingPacketEncoder())
.addLast(LengthFieldBasedFrameDecoder(Int.MAX_VALUE, 0, 4, -4, 4))
.addLast(ByteBufToIncomingPacketDecoder())
.addLast(RawIncomingPacketCollector(decodePipeline))
}
// can be overridden for tests
protected open suspend fun createConnection(decodePipeline: PacketDecodePipeline): NettyChannel {
val contextResult = CompletableDeferred<NettyChannel>() val contextResult = CompletableDeferred<NettyChannel>()
val eventLoopGroup = NioEventLoopGroup() val eventLoopGroup = NioEventLoopGroup()
@ -101,10 +110,8 @@ internal class NettyNetworkHandler(
eventLoopGroup.shutdownGracefully() eventLoopGroup.shutdownGracefully()
} }
}) })
.addLast(OutgoingPacketEncoder())
.addLast(LengthFieldBasedFrameDecoder(Int.MAX_VALUE, 0, 4, -4, 4)) setupChannelPipeline(ch.pipeline(), decodePipeline)
.addLast(ByteBufToIncomingPacketDecoder())
.addLast(RawIncomingPacketCollector(decodePipeline))
} }
}) })
.connect(address) .connect(address)
@ -125,9 +132,9 @@ internal class NettyNetworkHandler(
return contextResult.await() return contextResult.await()
} }
private val decodePipeline = PacketDecodePipeline(this@NettyNetworkHandler.coroutineContext) protected val decodePipeline = PacketDecodePipeline(this@NettyNetworkHandler.coroutineContext)
private inner class PacketDecodePipeline(parentContext: CoroutineContext) : protected inner class PacketDecodePipeline(parentContext: CoroutineContext) :
CoroutineScope by parentContext.childScope() { CoroutineScope by parentContext.childScope() {
private val channel: Channel<RawIncomingPacket> = Channel(Channel.BUFFERED) private val channel: Channel<RawIncomingPacket> = Channel(Channel.BUFFERED)
private val packetCodec: PacketCodec by lazy { context[PacketCodec] } private val packetCodec: PacketCodec by lazy { context[PacketCodec] }
@ -159,13 +166,13 @@ internal class NettyNetworkHandler(
* *
* @see StateObserver * @see StateObserver
*/ */
private abstract inner class NettyState( protected abstract inner class NettyState(
correspondingState: State correspondingState: State
) : BaseStateImpl(correspondingState) { ) : BaseStateImpl(correspondingState) {
abstract suspend fun sendPacketImpl(packet: OutgoingPacket) abstract suspend fun sendPacketImpl(packet: OutgoingPacket)
} }
private inner class StateInitialized : NettyState(State.INITIALIZED) { protected inner class StateInitialized : NettyState(State.INITIALIZED) {
override suspend fun sendPacketImpl(packet: OutgoingPacket) { override suspend fun sendPacketImpl(packet: OutgoingPacket) {
error("Cannot send packet when connection is not set. (resumeConnection not called.)") error("Cannot send packet when connection is not set. (resumeConnection not called.)")
} }
@ -181,16 +188,20 @@ internal class NettyNetworkHandler(
/** /**
* 1. Connect to server. * 1. Connect to server.
* 2. Perform SSO login with [SsoProcessor] * 2. Perform SSO login with [SsoProcessor]
* 3. If failure, set state to [StateClosed] *
* 4. If success, set state to [StateOK] * If failure, set state to [StateClosed]
* If success, set state to [StateOK]
*/ */
private inner class StateConnecting( protected inner class StateConnecting(
/** /**
* Collected (suppressed) exceptions that have led this state. * Collected (suppressed) exceptions that have led this state.
* *
* Dropped when state becomes [StateOK]. * Dropped when state becomes [StateOK].
*/ */
private val collectiveExceptions: ExceptionCollector, private val collectiveExceptions: ExceptionCollector,
/**
* If `true`, [delay] 5 seconds before connecting.
*/
wait: Boolean = false wait: Boolean = false
) : NettyState(State.CONNECTING) { ) : NettyState(State.CONNECTING) {
private val connection = async { private val connection = async {
@ -238,7 +249,7 @@ internal class NettyNetworkHandler(
* @see BotInitProcessor * @see BotInitProcessor
* @see StateObserver * @see StateObserver
*/ */
private inner class StateLoading( protected inner class StateLoading(
private val connection: NettyChannel private val connection: NettyChannel
) : NettyState(State.LOADING) { ) : NettyState(State.LOADING) {
override suspend fun sendPacketImpl(packet: OutgoingPacket) { override suspend fun sendPacketImpl(packet: OutgoingPacket) {
@ -256,7 +267,7 @@ internal class NettyNetworkHandler(
override fun toString(): String = "StateLoading" override fun toString(): String = "StateLoading"
} }
private inner class StateOK( protected inner class StateOK(
private val connection: NettyChannel private val connection: NettyChannel
) : NettyState(State.OK) { ) : NettyState(State.OK) {
init { init {
@ -317,7 +328,7 @@ internal class NettyNetworkHandler(
override fun toString(): String = "StateOK" override fun toString(): String = "StateOK"
} }
private inner class StateClosed( protected inner class StateClosed(
val exception: Throwable? val exception: Throwable?
) : NettyState(State.CLOSED) { ) : NettyState(State.CLOSED) {
init { init {

View File

@ -12,34 +12,46 @@
package net.mamoe.mirai.internal package net.mamoe.mirai.internal
import net.mamoe.mirai.internal.network.handler.NetworkHandler
import net.mamoe.mirai.utils.BotConfiguration import net.mamoe.mirai.utils.BotConfiguration
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
internal val MockAccount = BotAccount(1, "pwd") internal val MockAccount = BotAccount(1, "pwd")
internal val MockConfiguration = BotConfiguration { internal val MockConfiguration = BotConfiguration {
randomDeviceInfo()
} }
internal class MockBotBuilder( internal class MockBotBuilder(
val conf: BotConfiguration = BotConfiguration(), val conf: BotConfiguration = BotConfiguration(),
val debugConf: BotDebugConfiguration = BotDebugConfiguration() val debugConf: BotDebugConfiguration = BotDebugConfiguration(),
) { ) {
var nhProvider: (QQAndroidBot.(bot: QQAndroidBot) -> NetworkHandler)? = null
fun conf(action: BotConfiguration.() -> Unit): MockBotBuilder { fun conf(action: BotConfiguration.() -> Unit): MockBotBuilder {
contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
conf.apply(action) conf.apply(action)
return this return this
} }
fun debugConf(action: BotDebugConfiguration.() -> Unit): MockBotBuilder { fun debugConf(action: BotDebugConfiguration.() -> Unit): MockBotBuilder {
contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
debugConf.apply(action) debugConf.apply(action)
return this return this
} }
fun networkHandlerProvider(provider: QQAndroidBot.(bot: QQAndroidBot) -> NetworkHandler): MockBotBuilder {
this.nhProvider = provider
return this
}
} }
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
internal fun MockBot(conf: MockBotBuilder.() -> Unit) = internal fun MockBot(conf: MockBotBuilder.() -> Unit = {}) =
MockBotBuilder(MockConfiguration.copy()).apply(conf).run { MockBotBuilder(MockConfiguration.copy()).apply(conf).run {
QQAndroidBot(MockAccount, this.conf, debugConf) object : QQAndroidBot(MockAccount, this.conf, debugConf) {
override fun createNetworkHandler(): NetworkHandler =
nhProvider?.invoke(this, this) ?: super.createNetworkHandler()
}
} }
@Suppress("TestFunctionName")
internal fun MockBot() =
QQAndroidBot(MockAccount, MockConfiguration.copy())

View File

@ -31,11 +31,20 @@ import java.net.InetSocketAddress
/** /**
* With real factory and components as in [QQAndroidBot.components]. * With real factory and components as in [QQAndroidBot.components].
*/ */
internal abstract class AbstractRealNetworkHandlerTest<H : NetworkHandler>( internal abstract class AbstractRealNetworkHandlerTest<H : NetworkHandler> : AbstractTest() {
private val factory: NetworkHandlerFactory<H>, init {
) : AbstractTest() { System.setProperty("mirai.debug.network.state.observer.logging", "true")
val bot = MockBot() System.setProperty("mirai.debug.network.show.all.components", "true")
val networkLogger = MiraiLogger.TopLevel }
protected abstract val factory: NetworkHandlerFactory<H>
protected open val bot: QQAndroidBot by lazy {
MockBot {
networkHandlerProvider { createHandler() }
}
}
protected open val networkLogger = MiraiLogger.TopLevel
protected open val defaultComponents = ConcurrentComponentStorage().apply { protected open val defaultComponents = ConcurrentComponentStorage().apply {
val components = this val components = this
@ -44,18 +53,27 @@ internal abstract class AbstractRealNetworkHandlerTest<H : NetworkHandler>(
set(SsoProcessor, object : SsoProcessor { set(SsoProcessor, object : SsoProcessor {
override val client: QQAndroidClient get() = bot.client override val client: QQAndroidClient get() = bot.client
override val ssoSession: SsoSession get() = bot.client override val ssoSession: SsoSession get() = bot.client
override fun createObserverChain(): StateObserver = StateObserver.NOP override fun createObserverChain(): StateObserver = get(StateObserver)
override suspend fun login(handler: NetworkHandler) { override suspend fun login(handler: NetworkHandler) {
networkLogger.debug { "SsoProcessor.login" } networkLogger.debug { "SsoProcessor.login" }
} }
override suspend fun logout(handler: NetworkHandler) {
networkLogger.debug { "SsoProcessor.logout" }
}
}) })
set(HeartbeatProcessor, object : HeartbeatProcessor { set(HeartbeatProcessor, object : HeartbeatProcessor {
override suspend fun doHeartbeatNow(networkHandler: NetworkHandler) { override suspend fun doHeartbeatNow(networkHandler: NetworkHandler) {
networkLogger.debug { "HeartbeatProcessor.doHeartbeatNow" } networkLogger.debug { "HeartbeatProcessor.doHeartbeatNow" }
} }
}) })
set(KeyRefreshProcessor, KeyRefreshProcessorImpl(networkLogger)) set(KeyRefreshProcessor, object : KeyRefreshProcessor {
set(ConfigPushProcessor, ConfigPushProcessorImpl(networkLogger)) override suspend fun keyRefreshLoop(handler: NetworkHandler) {}
override suspend fun refreshKeysNow(handler: NetworkHandler) {}
})
set(ConfigPushProcessor, object : ConfigPushProcessor {
override suspend fun syncConfigPush(network: NetworkHandler) {}
})
set(BotInitProcessor, object : BotInitProcessor { set(BotInitProcessor, object : BotInitProcessor {
override suspend fun init() { override suspend fun init() {
@ -71,11 +89,10 @@ internal abstract class AbstractRealNetworkHandlerTest<H : NetworkHandler>(
set(OtherClientUpdater, OtherClientUpdaterImpl(bot, components, bot.logger)) set(OtherClientUpdater, OtherClientUpdaterImpl(bot, components, bot.logger))
set(ConfigPushSyncer, ConfigPushSyncerImpl()) set(ConfigPushSyncer, ConfigPushSyncerImpl())
set(StateObserver, StateObserver.NOP) set(StateObserver, bot.run { stateObserverChain() })
} }
protected open fun createHandler(additionalComponents: ComponentStorage? = null): NetworkHandler { protected open fun createHandler(additionalComponents: ComponentStorage? = null): H {
return factory.create( return factory.create(
NetworkHandlerContextImpl( NetworkHandlerContextImpl(
bot, bot,

View File

@ -0,0 +1,69 @@
/*
* Copyright 2019-2021 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/master/LICENSE
*/
package net.mamoe.mirai.internal.network.framework.test
import net.mamoe.mirai.event.AbstractEvent
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.internal.test.AbstractTest
import net.mamoe.mirai.internal.test.assertEventBroadcasts
import net.mamoe.mirai.internal.test.runBlockingUnit
import kotlin.test.Test
import kotlin.test.assertFails
internal class FrameworkEventTest : AbstractTest() {
class TestEvent : AbstractEvent()
class TestEvent2 : AbstractEvent()
@Test
fun `can observe event`() = runBlockingUnit {
assertEventBroadcasts<TestEvent> {
TestEvent().broadcast()
}
}
@Test
fun `observes expected event`() = runBlockingUnit {
assertEventBroadcasts<TestEvent>(1) {
TestEvent().broadcast()
TestEvent2().broadcast()
}
}
@Test
fun `can observe event multiple times`() = runBlockingUnit {
assertEventBroadcasts<TestEvent>(2) {
TestEvent().broadcast()
TestEvent().broadcast()
}
}
@Test
fun `can observe event only in block`() = runBlockingUnit {
TestEvent().broadcast()
assertEventBroadcasts<TestEvent>(1) {
TestEvent().broadcast()
}
}
@Test
fun `fails if times not match`() = runBlockingUnit {
assertFails {
assertEventBroadcasts<TestEvent>(2) {
TestEvent().broadcast()
}
}
assertFails {
assertEventBroadcasts<TestEvent>(0) {
TestEvent().broadcast()
}
}
}
}

View File

@ -1,24 +0,0 @@
/*
* Copyright 2019-2021 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/master/LICENSE
*/
package net.mamoe.mirai.internal.network.handler
import net.mamoe.mirai.internal.network.framework.AbstractRealNetworkHandlerTest
import net.mamoe.mirai.internal.network.impl.netty.NettyNetworkHandler
import net.mamoe.mirai.internal.network.impl.netty.NettyNetworkHandlerFactory
import net.mamoe.mirai.internal.test.runBlockingUnit
import kotlin.test.Test
internal class HandlerEventTest : AbstractRealNetworkHandlerTest<NettyNetworkHandler>(NettyNetworkHandlerFactory) {
@Test
fun `BotOnlineEvent after successful logon`() = runBlockingUnit {
bot.login()
}
}

View File

@ -12,6 +12,7 @@ package net.mamoe.mirai.internal.network.handler
import net.mamoe.mirai.internal.network.framework.AbstractMockNetworkHandlerTest import net.mamoe.mirai.internal.network.framework.AbstractMockNetworkHandlerTest
import net.mamoe.mirai.internal.network.handler.NetworkHandler.State.CONNECTING import net.mamoe.mirai.internal.network.handler.NetworkHandler.State.CONNECTING
import net.mamoe.mirai.internal.network.handler.NetworkHandler.State.INITIALIZED import net.mamoe.mirai.internal.network.handler.NetworkHandler.State.INITIALIZED
import net.mamoe.mirai.internal.network.handler.state.CombinedStateObserver.Companion.plus
import net.mamoe.mirai.internal.network.handler.state.StateChangedObserver import net.mamoe.mirai.internal.network.handler.state.StateChangedObserver
import net.mamoe.mirai.internal.network.handler.state.StateObserver import net.mamoe.mirai.internal.network.handler.state.StateObserver
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -65,4 +66,36 @@ internal class StateObserverTest : AbstractMockNetworkHandlerTest() {
assertEquals(INITIALIZED, called[0].first.correspondingState) assertEquals(INITIALIZED, called[0].first.correspondingState)
assertEquals(CONNECTING, called[0].second.correspondingState) assertEquals(CONNECTING, called[0].second.correspondingState)
} }
@Test
fun `can combine`() {
val called = ArrayList<Pair<NetworkHandlerSupport.BaseStateImpl, NetworkHandlerSupport.BaseStateImpl>>()
components[StateObserver] = object : StateChangedObserver(CONNECTING) {
override fun stateChanged0(
networkHandler: NetworkHandlerSupport,
previous: NetworkHandlerSupport.BaseStateImpl,
new: NetworkHandlerSupport.BaseStateImpl
) {
called.add(previous to new)
}
} + object : StateChangedObserver(CONNECTING) {
override fun stateChanged0(
networkHandler: NetworkHandlerSupport,
previous: NetworkHandlerSupport.BaseStateImpl,
new: NetworkHandlerSupport.BaseStateImpl
) {
called.add(previous to new)
}
}
val handler = createNetworkHandler()
assertEquals(0, called.size)
handler.setState(INITIALIZED)
assertEquals(0, called.size)
handler.setState(CONNECTING)
assertEquals(2, called.size)
assertEquals(INITIALIZED, called[0].first.correspondingState)
assertEquals(CONNECTING, called[0].second.correspondingState)
assertEquals(INITIALIZED, called[1].first.correspondingState)
assertEquals(CONNECTING, called[1].second.correspondingState)
}
} }

View File

@ -0,0 +1,110 @@
/*
* Copyright 2019-2021 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/master/LICENSE
*/
package net.mamoe.mirai.internal.network.impl.netty
import io.netty.channel.Channel
import io.netty.channel.embedded.EmbeddedChannel
import kotlinx.coroutines.delay
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.framework.AbstractRealNetworkHandlerTest
import net.mamoe.mirai.internal.network.handler.NetworkHandler.State
import net.mamoe.mirai.internal.network.handler.NetworkHandlerContext
import net.mamoe.mirai.internal.network.handler.NetworkHandlerFactory
import net.mamoe.mirai.internal.test.assertEventBroadcasts
import net.mamoe.mirai.internal.test.runBlockingUnit
import net.mamoe.mirai.utils.ExceptionCollector
import java.net.SocketAddress
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.time.seconds
internal open class TestNettyNH(
context: NetworkHandlerContext,
address: SocketAddress
) : NettyNetworkHandler(context, address) {
fun setStateClosed(exception: Throwable? = null) {
setState { StateClosed(exception) }
}
fun setStateConnecting(exception: Throwable? = null) {
setState { StateConnecting(ExceptionCollector(exception), false) }
}
}
internal class NettyHandlerEventTest : AbstractRealNetworkHandlerTest<TestNettyNH>() {
val channel = EmbeddedChannel()
val network get() = bot.network as TestNettyNH
override val factory: NetworkHandlerFactory<TestNettyNH> =
object : NetworkHandlerFactory<TestNettyNH> {
override fun create(context: NetworkHandlerContext, address: SocketAddress): TestNettyNH {
return object : TestNettyNH(context, address) {
override suspend fun createConnection(decodePipeline: PacketDecodePipeline): Channel =
channel.apply { setupChannelPipeline(pipeline(), decodePipeline) }
}
}
}
@Test
fun `BotOnlineEvent after successful logon`() = runBlockingUnit {
assertEventBroadcasts<BotOnlineEvent> {
assertEquals(State.INITIALIZED, network.state)
bot.login() // launches a job which broadcasts the event
delay(3.seconds)
assertEquals(State.OK, network.state)
}
}
@Test
fun `BotReloginEvent after successful reconnection`() = runBlockingUnit {
assertEventBroadcasts<BotReloginEvent> {
assertEquals(State.INITIALIZED, network.state)
bot.login()
bot.firstLoginSucceed = true
network.setStateConnecting()
network.resumeConnection()
delay(3.seconds) // `login` launches a job which broadcasts the event
assertEquals(State.OK, network.state)
}
}
@Test
fun `BotOnlineEvent after successful reconnection`() = runBlockingUnit {
assertEquals(State.INITIALIZED, network.state)
bot.login()
bot.firstLoginSucceed = true
delay(3.seconds) // `login` launches a job which broadcasts the event
assertEventBroadcasts<BotOnlineEvent>(1) {
network.setStateConnecting()
network.resumeConnection()
delay(3.seconds)
assertEquals(State.OK, network.state)
}
}
@Test
fun `BotOfflineEvent after successful reconnection`() = runBlockingUnit {
assertEquals(State.INITIALIZED, network.state)
bot.login()
bot.firstLoginSucceed = true
assertEquals(State.OK, network.state)
delay(3.seconds) // `login` launches a job which broadcasts the event
assertEventBroadcasts<BotOfflineEvent>(1) {
network.setStateClosed()
delay(3.seconds)
assertEquals(State.CLOSED, network.state)
}
}
}

View File

@ -12,41 +12,37 @@ package net.mamoe.mirai.internal.test
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import net.mamoe.mirai.event.Event import net.mamoe.mirai.event.Event
import net.mamoe.mirai.event.GlobalEventChannel import net.mamoe.mirai.event.GlobalEventChannel
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.ConcurrentLinkedQueue
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.test.assertEquals import kotlin.test.assertEquals
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
internal inline fun <reified T : Event, R> assertEventBroadcasts(times: Int = 1, block: () -> R): R { internal inline fun <reified T : Event, R> assertEventBroadcasts(times: Int = 1, block: () -> R): R {
val receivedEvents = AtomicInteger(0) assertEventBroadcasts<T>(times) {
val listener = GlobalEventChannel.subscribeAlways<T> {
receivedEvents.incrementAndGet()
}
try {
return block() return block()
} finally {
listener.complete()
assertEquals(
times,
receivedEvents.get(),
"Expected event ${T::class.simpleName} broadcast $times time(s). But actual is ${receivedEvents.get()}."
)
} }
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
internal inline fun <reified T : Event> assertEventBroadcasts(times: Int = 1, block: () -> Unit) { internal inline fun <reified T : Event> assertEventBroadcasts(times: Int = 1, block: () -> Unit) {
val receivedEvents = AtomicInteger(0) contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
val listener = GlobalEventChannel.subscribeAlways<T> {
receivedEvents.incrementAndGet() val receivedEvents = ConcurrentLinkedQueue<Event>()
val listener = GlobalEventChannel.subscribeAlways<Event> { event ->
receivedEvents.add(event)
} }
try { try {
return block() return block()
} finally { } finally {
val actual = receivedEvents.filterIsInstance<T>().count()
listener.complete() listener.complete()
assertEquals( assertEquals(
times, times,
receivedEvents.get(), actual,
"Expected event ${T::class.simpleName} broadcast $times time(s). But actual is ${receivedEvents.get()}." "Expected event ${T::class.simpleName} broadcast $times time(s). " +
"But actual count is ${actual}. " +
"\nAll received events: ${receivedEvents.joinToString(", ", "[", "]")}"
) )
} }
} }

View File

@ -9,6 +9,8 @@
package net.mamoe.mirai.internal.test package net.mamoe.mirai.internal.test
import org.junit.jupiter.api.Test
internal expect fun initPlatform() internal expect fun initPlatform()
/** /**
@ -19,3 +21,8 @@ abstract class AbstractTest {
initPlatform() initPlatform()
} }
} }
internal expect class PlatformInitializationTest() : AbstractTest {
@Test
fun test()
}

View File

@ -9,6 +9,16 @@
package net.mamoe.mirai.internal.test package net.mamoe.mirai.internal.test
import org.junit.jupiter.api.Test
internal actual fun initPlatform() { internal actual fun initPlatform() {
// nothing to do // nothing to do
} }
internal actual class PlatformInitializationTest : AbstractTest() {
@Test
actual fun test() {
// nop
}
}