mirror of
https://github.com/mamoe/mirai.git
synced 2025-02-09 05:49:17 +08:00
Add NetworkHandlerFactory and tests for NetworkHandler
This commit is contained in:
parent
4f77abca87
commit
b91bbfd2b8
@ -88,6 +88,7 @@ public sealed class BotOfflineEvent : BotEvent, AbstractEvent() {
|
||||
* 因 returnCode = -10008 等原因掉线
|
||||
*/
|
||||
@MiraiInternalApi("This is very experimental and might be changed")
|
||||
@Deprecated("Deprecated with no replacement", level = DeprecationLevel.ERROR)
|
||||
public data class PacketFactoryErrorCode @MiraiInternalApi public constructor(
|
||||
val returnCode: Int,
|
||||
public override val bot: Bot,
|
||||
|
@ -12,7 +12,10 @@ package net.mamoe.mirai.utils
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
public class ExceptionCollector {
|
||||
public class ExceptionCollector : Sequence<Throwable> {
|
||||
|
||||
// TODO: 2021/4/20 drop last
|
||||
|
||||
public constructor()
|
||||
public constructor(initial: Throwable?) {
|
||||
collect(initial)
|
||||
@ -32,6 +35,12 @@ public class ExceptionCollector {
|
||||
if (e == null) return
|
||||
val last = last
|
||||
if (last != null) {
|
||||
last.itr().forEach { suppressed ->
|
||||
if (suppressed.stackTrace.contentEquals(e.stackTrace)) {
|
||||
// filter out useless duplicates.
|
||||
return
|
||||
}
|
||||
}
|
||||
e.addSuppressed(last)
|
||||
}
|
||||
this.last = e
|
||||
@ -57,6 +66,15 @@ public class ExceptionCollector {
|
||||
|
||||
@DslMarker
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -10,14 +10,36 @@
|
||||
package net.mamoe.mirai.internal.test
|
||||
|
||||
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() {
|
||||
init
|
||||
}
|
||||
|
||||
private val init by lazy {
|
||||
private val init: Unit by lazy {
|
||||
MiraiLogger.setDefaultLoggerCreator {
|
||||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -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.SsoProcessorContextImpl
|
||||
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.NetworkHandlerSupport
|
||||
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.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.protocol.packet.OutgoingPacket
|
||||
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.MiraiLogger
|
||||
import net.mamoe.mirai.utils.systemProp
|
||||
@ -61,7 +62,7 @@ internal class BotDebugConfiguration(
|
||||
)
|
||||
|
||||
@Suppress("INVISIBLE_MEMBER", "BooleanLiteralArgument", "OverridingDeprecatedMember")
|
||||
internal class QQAndroidBot constructor(
|
||||
internal open class QQAndroidBot constructor(
|
||||
internal val account: BotAccount,
|
||||
configuration: BotConfiguration,
|
||||
private val debugConfiguration: BotDebugConfiguration = BotDebugConfiguration(),
|
||||
@ -77,25 +78,42 @@ internal class QQAndroidBot constructor(
|
||||
// TODO: 2021/4/14 bdhSyncer.loadFromCache() when login
|
||||
|
||||
// IDE error, don't move into lazy
|
||||
private fun ComponentStorage.stateObserverChain(): StateObserver {
|
||||
fun ComponentStorage.stateObserverChain(): StateObserver {
|
||||
val components = this
|
||||
return StateObserver.chainOfNotNull(
|
||||
components[BotInitProcessor].asObserver().safe(networkLogger),
|
||||
StateChangedObserver(NetworkHandler.State.OK) { new ->
|
||||
new.launch(logger.asCoroutineExceptionHandler()) {
|
||||
components[BotInitProcessor].asObserver(),
|
||||
StateChangedObserver(State.OK) { new ->
|
||||
bot.launch(logger.asCoroutineExceptionHandler()) {
|
||||
BotOnlineEvent(bot).broadcast()
|
||||
if (bot.firstLoginSucceed) { // TODO: 2021/4/21 actually no use
|
||||
BotReloginEvent(bot, new.getCause()).broadcast()
|
||||
}
|
||||
}
|
||||
},
|
||||
StateChangedObserver(NetworkHandler.State.CLOSED) { new ->
|
||||
new.launch(logger.asCoroutineExceptionHandler()) {
|
||||
object : StateObserver {
|
||||
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()
|
||||
}
|
||||
}
|
||||
p == State.OK && n == State.CLOSED -> {
|
||||
bot.launch(logger.asCoroutineExceptionHandler()) {
|
||||
BotOfflineEvent.Active(bot, new.getCause()).broadcast()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
debugConfiguration.stateObserver
|
||||
)
|
||||
).safe(logger)
|
||||
}
|
||||
|
||||
|
||||
@ -135,7 +153,7 @@ internal class QQAndroidBot constructor(
|
||||
val client get() = components[SsoProcessor].client
|
||||
|
||||
override suspend fun sendLogout() {
|
||||
network.sendWithoutExpect(StatSvc.Register.offline(client))
|
||||
components[SsoProcessor].logout(network)
|
||||
}
|
||||
|
||||
override fun createNetworkHandler(): NetworkHandler {
|
||||
|
@ -11,6 +11,7 @@ package net.mamoe.mirai.internal.network.component
|
||||
|
||||
import net.mamoe.mirai.utils.systemProp
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
|
||||
/**
|
||||
* 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) }
|
@ -34,7 +34,7 @@ import net.mamoe.mirai.utils.info
|
||||
* Facade of [ContactUpdater], [OtherClientUpdater], [ConfigPushSyncer].
|
||||
* 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 {
|
||||
suspend fun init()
|
||||
|
@ -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.impl.netty.NettyNetworkHandler
|
||||
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.Captcha
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin10
|
||||
@ -57,6 +58,8 @@ internal interface SsoProcessor {
|
||||
@Throws(LoginFailedException::class)
|
||||
suspend fun login(handler: NetworkHandler)
|
||||
|
||||
suspend fun logout(handler: NetworkHandler)
|
||||
|
||||
companion object : ComponentKey<SsoProcessor>
|
||||
}
|
||||
|
||||
@ -110,6 +113,10 @@ internal class SsoProcessorImpl(
|
||||
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 {
|
||||
val device = ssoContext.device
|
||||
return QQAndroidClient(
|
||||
|
@ -10,22 +10,25 @@
|
||||
package net.mamoe.mirai.internal.network.handler
|
||||
|
||||
import kotlinx.coroutines.selects.SelectClause1
|
||||
import net.mamoe.mirai.Bot
|
||||
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.OutgoingPacketWithRespType
|
||||
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].
|
||||
*
|
||||
* Implementation is usually subclass of [NetworkHandlerSupport].
|
||||
*
|
||||
* Instances are often created by [NetworkHandlerFactory].
|
||||
*
|
||||
* @see NetworkHandlerSupport
|
||||
* @see NetworkHandlerFactory
|
||||
*/
|
||||
internal interface NetworkHandler {
|
||||
val context: NetworkHandlerContext
|
||||
@ -33,7 +36,7 @@ internal interface NetworkHandler {
|
||||
fun isOk() = state == State.OK
|
||||
|
||||
/**
|
||||
* State of this handler.
|
||||
* Current state of this handler. This is volatile.
|
||||
*/
|
||||
val state: State
|
||||
|
||||
@ -42,6 +45,28 @@ internal interface NetworkHandler {
|
||||
*/
|
||||
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 {
|
||||
/**
|
||||
* Just created and no connection has been made.
|
||||
@ -73,18 +98,18 @@ internal interface NetworkHandler {
|
||||
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.
|
||||
*
|
||||
* At this state [resumeConnection] throws the exception caught from underlying socket implementation (i.e netty).
|
||||
* [sendAndExpect] throws [IllegalStateException]
|
||||
* [sendAndExpect] throws [IllegalStateException].
|
||||
*/
|
||||
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.
|
||||
* @see State
|
||||
@ -95,17 +120,23 @@ internal interface NetworkHandler {
|
||||
|
||||
/**
|
||||
* 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`
|
||||
*/
|
||||
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)
|
||||
|
||||
/**
|
||||
* Closes this handler gracefully.
|
||||
* Closes this handler gracefully (i.e. asynchronously).
|
||||
*/
|
||||
fun close(cause: Throwable?)
|
||||
|
||||
@ -142,18 +173,3 @@ internal interface NetworkHandler {
|
||||
|
||||
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
|
||||
}
|
@ -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
|
||||
}
|
@ -36,7 +36,7 @@ import java.net.SocketAddress
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import io.netty.channel.Channel as NettyChannel
|
||||
|
||||
internal class NettyNetworkHandler(
|
||||
internal open class NettyNetworkHandler(
|
||||
context: NetworkHandlerContext,
|
||||
private val address: SocketAddress,
|
||||
) : 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 eventLoopGroup = NioEventLoopGroup()
|
||||
|
||||
@ -101,10 +110,8 @@ internal class NettyNetworkHandler(
|
||||
eventLoopGroup.shutdownGracefully()
|
||||
}
|
||||
})
|
||||
.addLast(OutgoingPacketEncoder())
|
||||
.addLast(LengthFieldBasedFrameDecoder(Int.MAX_VALUE, 0, 4, -4, 4))
|
||||
.addLast(ByteBufToIncomingPacketDecoder())
|
||||
.addLast(RawIncomingPacketCollector(decodePipeline))
|
||||
|
||||
setupChannelPipeline(ch.pipeline(), decodePipeline)
|
||||
}
|
||||
})
|
||||
.connect(address)
|
||||
@ -125,9 +132,9 @@ internal class NettyNetworkHandler(
|
||||
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() {
|
||||
private val channel: Channel<RawIncomingPacket> = Channel(Channel.BUFFERED)
|
||||
private val packetCodec: PacketCodec by lazy { context[PacketCodec] }
|
||||
@ -159,13 +166,13 @@ internal class NettyNetworkHandler(
|
||||
*
|
||||
* @see StateObserver
|
||||
*/
|
||||
private abstract inner class NettyState(
|
||||
protected abstract inner class NettyState(
|
||||
correspondingState: State
|
||||
) : BaseStateImpl(correspondingState) {
|
||||
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) {
|
||||
error("Cannot send packet when connection is not set. (resumeConnection not called.)")
|
||||
}
|
||||
@ -181,16 +188,20 @@ internal class NettyNetworkHandler(
|
||||
/**
|
||||
* 1. Connect to server.
|
||||
* 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.
|
||||
*
|
||||
* Dropped when state becomes [StateOK].
|
||||
*/
|
||||
private val collectiveExceptions: ExceptionCollector,
|
||||
/**
|
||||
* If `true`, [delay] 5 seconds before connecting.
|
||||
*/
|
||||
wait: Boolean = false
|
||||
) : NettyState(State.CONNECTING) {
|
||||
private val connection = async {
|
||||
@ -238,7 +249,7 @@ internal class NettyNetworkHandler(
|
||||
* @see BotInitProcessor
|
||||
* @see StateObserver
|
||||
*/
|
||||
private inner class StateLoading(
|
||||
protected inner class StateLoading(
|
||||
private val connection: NettyChannel
|
||||
) : NettyState(State.LOADING) {
|
||||
override suspend fun sendPacketImpl(packet: OutgoingPacket) {
|
||||
@ -256,7 +267,7 @@ internal class NettyNetworkHandler(
|
||||
override fun toString(): String = "StateLoading"
|
||||
}
|
||||
|
||||
private inner class StateOK(
|
||||
protected inner class StateOK(
|
||||
private val connection: NettyChannel
|
||||
) : NettyState(State.OK) {
|
||||
init {
|
||||
@ -317,7 +328,7 @@ internal class NettyNetworkHandler(
|
||||
override fun toString(): String = "StateOK"
|
||||
}
|
||||
|
||||
private inner class StateClosed(
|
||||
protected inner class StateClosed(
|
||||
val exception: Throwable?
|
||||
) : NettyState(State.CLOSED) {
|
||||
init {
|
||||
|
@ -12,34 +12,46 @@
|
||||
|
||||
package net.mamoe.mirai.internal
|
||||
|
||||
import net.mamoe.mirai.internal.network.handler.NetworkHandler
|
||||
import net.mamoe.mirai.utils.BotConfiguration
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
internal val MockAccount = BotAccount(1, "pwd")
|
||||
|
||||
internal val MockConfiguration = BotConfiguration {
|
||||
randomDeviceInfo()
|
||||
}
|
||||
|
||||
internal class MockBotBuilder(
|
||||
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 {
|
||||
contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
|
||||
conf.apply(action)
|
||||
return this
|
||||
}
|
||||
|
||||
fun debugConf(action: BotDebugConfiguration.() -> Unit): MockBotBuilder {
|
||||
contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
|
||||
debugConf.apply(action)
|
||||
return this
|
||||
}
|
||||
|
||||
fun networkHandlerProvider(provider: QQAndroidBot.(bot: QQAndroidBot) -> NetworkHandler): MockBotBuilder {
|
||||
this.nhProvider = provider
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TestFunctionName")
|
||||
internal fun MockBot(conf: MockBotBuilder.() -> Unit) =
|
||||
internal fun MockBot(conf: MockBotBuilder.() -> Unit = {}) =
|
||||
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())
|
@ -31,11 +31,20 @@ import java.net.InetSocketAddress
|
||||
/**
|
||||
* With real factory and components as in [QQAndroidBot.components].
|
||||
*/
|
||||
internal abstract class AbstractRealNetworkHandlerTest<H : NetworkHandler>(
|
||||
private val factory: NetworkHandlerFactory<H>,
|
||||
) : AbstractTest() {
|
||||
val bot = MockBot()
|
||||
val networkLogger = MiraiLogger.TopLevel
|
||||
internal abstract class AbstractRealNetworkHandlerTest<H : NetworkHandler> : AbstractTest() {
|
||||
init {
|
||||
System.setProperty("mirai.debug.network.state.observer.logging", "true")
|
||||
System.setProperty("mirai.debug.network.show.all.components", "true")
|
||||
}
|
||||
|
||||
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 {
|
||||
val components = this
|
||||
@ -44,18 +53,27 @@ internal abstract class AbstractRealNetworkHandlerTest<H : NetworkHandler>(
|
||||
set(SsoProcessor, object : SsoProcessor {
|
||||
override val client: QQAndroidClient 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) {
|
||||
networkLogger.debug { "SsoProcessor.login" }
|
||||
}
|
||||
|
||||
override suspend fun logout(handler: NetworkHandler) {
|
||||
networkLogger.debug { "SsoProcessor.logout" }
|
||||
}
|
||||
})
|
||||
set(HeartbeatProcessor, object : HeartbeatProcessor {
|
||||
override suspend fun doHeartbeatNow(networkHandler: NetworkHandler) {
|
||||
networkLogger.debug { "HeartbeatProcessor.doHeartbeatNow" }
|
||||
}
|
||||
})
|
||||
set(KeyRefreshProcessor, KeyRefreshProcessorImpl(networkLogger))
|
||||
set(ConfigPushProcessor, ConfigPushProcessorImpl(networkLogger))
|
||||
set(KeyRefreshProcessor, object : KeyRefreshProcessor {
|
||||
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 {
|
||||
override suspend fun init() {
|
||||
@ -71,11 +89,10 @@ internal abstract class AbstractRealNetworkHandlerTest<H : NetworkHandler>(
|
||||
set(OtherClientUpdater, OtherClientUpdaterImpl(bot, components, bot.logger))
|
||||
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(
|
||||
NetworkHandlerContextImpl(
|
||||
bot,
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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.handler.NetworkHandler.State.CONNECTING
|
||||
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.StateObserver
|
||||
import org.junit.jupiter.api.Test
|
||||
@ -65,4 +66,36 @@ internal class StateObserverTest : AbstractMockNetworkHandlerTest() {
|
||||
assertEquals(INITIALIZED, called[0].first.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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -12,41 +12,37 @@ package net.mamoe.mirai.internal.test
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import net.mamoe.mirai.event.Event
|
||||
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
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal inline fun <reified T : Event, R> assertEventBroadcasts(times: Int = 1, block: () -> R): R {
|
||||
val receivedEvents = AtomicInteger(0)
|
||||
val listener = GlobalEventChannel.subscribeAlways<T> {
|
||||
receivedEvents.incrementAndGet()
|
||||
}
|
||||
try {
|
||||
assertEventBroadcasts<T>(times) {
|
||||
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)
|
||||
internal inline fun <reified T : Event> assertEventBroadcasts(times: Int = 1, block: () -> Unit) {
|
||||
val receivedEvents = AtomicInteger(0)
|
||||
val listener = GlobalEventChannel.subscribeAlways<T> {
|
||||
receivedEvents.incrementAndGet()
|
||||
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
|
||||
|
||||
val receivedEvents = ConcurrentLinkedQueue<Event>()
|
||||
val listener = GlobalEventChannel.subscribeAlways<Event> { event ->
|
||||
receivedEvents.add(event)
|
||||
}
|
||||
try {
|
||||
return block()
|
||||
} finally {
|
||||
val actual = receivedEvents.filterIsInstance<T>().count()
|
||||
listener.complete()
|
||||
assertEquals(
|
||||
times,
|
||||
receivedEvents.get(),
|
||||
"Expected event ${T::class.simpleName} broadcast $times time(s). But actual is ${receivedEvents.get()}."
|
||||
actual,
|
||||
"Expected event ${T::class.simpleName} broadcast $times time(s). " +
|
||||
"But actual count is ${actual}. " +
|
||||
"\nAll received events: ${receivedEvents.joinToString(", ", "[", "]")}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,8 @@
|
||||
|
||||
package net.mamoe.mirai.internal.test
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal expect fun initPlatform()
|
||||
|
||||
/**
|
||||
@ -19,3 +21,8 @@ abstract class AbstractTest {
|
||||
initPlatform()
|
||||
}
|
||||
}
|
||||
|
||||
internal expect class PlatformInitializationTest() : AbstractTest {
|
||||
@Test
|
||||
fun test()
|
||||
}
|
@ -9,6 +9,16 @@
|
||||
|
||||
package net.mamoe.mirai.internal.test
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal actual fun initPlatform() {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
|
||||
internal actual class PlatformInitializationTest : AbstractTest() {
|
||||
@Test
|
||||
actual fun test() {
|
||||
// nop
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user