Commit a0098966 authored by Him188's avatar Him188

Reconnection supported

parent 05ca6424
# UpdateLog
## Main version 0
### 0.3.0
- 更新
\ No newline at end of file
# style guide
kotlin.code.style=official
# config
mirai_version=0.3.0
mirai_version=0.5.0
kotlin.incremental.multiplatform=true
kotlin.parallel.tasks.in.project=true
# kotlin
......
......@@ -12,11 +12,13 @@ import net.mamoe.mirai.contact.internal.QQImpl
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.TIMBotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.packet.login.LoginResult
import net.mamoe.mirai.network.protocol.tim.packet.login.isSuccess
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.DefaultLogger
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.internal.coerceAtLeastOrFail
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.coroutineContext
import kotlin.jvm.JvmOverloads
data class BotAccount(
......@@ -24,6 +26,15 @@ data class BotAccount(
val password: String//todo 不保存 password?
)
@Suppress("FunctionName")
suspend inline fun Bot(account: BotAccount, logger: MiraiLogger): Bot = Bot(account, logger, coroutineContext)
@Suppress("FunctionName")
suspend inline fun Bot(account: BotAccount): Bot = Bot(account, coroutineContext)
@Suppress("FunctionName")
suspend inline fun Bot(qq: UInt, password: String): Bot = Bot(qq, password, coroutineContext)
/**
* Mirai 的机器人. 一个机器人实例登录一个 QQ 账号.
* Mirai 为多账号设计, 可同时维护多个机器人.
......@@ -54,15 +65,16 @@ data class BotAccount(
* @author NaturalHG
* @see Contact
*/
class Bot(val account: BotAccount, val logger: MiraiLogger) : CoroutineScope {
override val coroutineContext: CoroutineContext = SupervisorJob()
class Bot(val account: BotAccount, val logger: MiraiLogger, context: CoroutineContext) : CoroutineScope {
private val supervisorJob = SupervisorJob(context[Job])
override val coroutineContext: CoroutineContext = context + supervisorJob
constructor(qq: UInt, password: String) : this(BotAccount(qq, password))
constructor(account: BotAccount) : this(account, DefaultLogger("Bot(" + account.id + ")"))
constructor(qq: UInt, password: String, context: CoroutineContext) : this(BotAccount(qq, password), context)
constructor(account: BotAccount, context: CoroutineContext) : this(account, DefaultLogger("Bot(" + account.id + ")"), context)
val contacts = ContactSystem()
var network: BotNetworkHandler<*> = TIMBotNetworkHandler(this.coroutineContext, this)
lateinit var network: BotNetworkHandler<*>
init {
launch {
......@@ -76,19 +88,40 @@ class Bot(val account: BotAccount, val logger: MiraiLogger) : CoroutineScope {
* [关闭][BotNetworkHandler.close]网络处理器, 取消所有运行在 [BotNetworkHandler] 下的协程.
* 然后重新启动并尝试登录
*/
@JvmOverloads
suspend fun reinitializeNetworkHandler(
@JvmOverloads // shouldn't be suspend!! This function MUST NOT inherit the context from the caller because the caller(NetworkHandler) is going to close
fun tryReinitializeNetworkHandler(
configuration: BotConfiguration,
cause: Throwable? = null
): Job = launch {
repeat(configuration.reconnectionRetryTimes) {
if (reinitializeNetworkHandlerAsync(configuration, cause).await().isSuccess()) {
logger.info("Reconnected successfully")
return@launch
} else {
delay(configuration.reconnectPeriod.millisecondsLong)
}
}
}
/**
* [关闭][BotNetworkHandler.close]网络处理器, 取消所有运行在 [BotNetworkHandler] 下的协程.
* 然后重新启动并尝试登录
*/
@JvmOverloads // shouldn't be suspend!! This function MUST NOT inherit the context from the caller because the caller(NetworkHandler) is going to close
fun reinitializeNetworkHandlerAsync(
configuration: BotConfiguration,
cause: Throwable? = null
): LoginResult {
): Deferred<LoginResult> = async {
logger.info("Initializing BotNetworkHandler")
try {
network.close(cause)
if (::network.isInitialized) {
network.close(cause)
}
} catch (e: Exception) {
logger.error(e)
logger.error("Cannot close network handler", e)
}
network = TIMBotNetworkHandler(this.coroutineContext, this)
return network.login(configuration)
network = TIMBotNetworkHandler(coroutineContext + configuration, this@Bot)
network.login()
}
/**
......
......@@ -70,13 +70,13 @@ suspend inline fun Bot.login(noinline configuration: BotConfiguration.() -> Unit
contract {
callsInPlace(configuration, InvocationKind.EXACTLY_ONCE)
}
return this.network.login(BotConfiguration().apply(configuration))
return this.reinitializeNetworkHandlerAsync(BotConfiguration().apply(configuration)).await()
}
/**
* 使用默认的配置 ([BotConfiguration.Default]) 登录, 返回登录结果
*/
suspend inline fun Bot.login(): LoginResult = this.network.login(BotConfiguration.Default)
suspend inline fun Bot.login(): LoginResult = this.reinitializeNetworkHandlerAsync(BotConfiguration.Default).await()
/**
* 使用默认的配置 ([BotConfiguration.Default]) 登录, 返回 [this]
......@@ -91,7 +91,7 @@ suspend inline fun Bot.alsoLogin(noinline configuration: BotConfiguration.() ->
contract {
callsInPlace(configuration, InvocationKind.EXACTLY_ONCE)
}
this.network.login(BotConfiguration().apply(configuration)).requireSuccess()
this.reinitializeNetworkHandlerAsync(BotConfiguration().apply(configuration)).await().requireSuccess()
return this
}
......
package net.mamoe.mirai.network
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableJob
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import net.mamoe.mirai.Bot
import net.mamoe.mirai.network.protocol.tim.handler.DataPacketSocketAdapter
import net.mamoe.mirai.network.protocol.tim.handler.TemporaryPacketHandler
......@@ -12,7 +11,6 @@ import net.mamoe.mirai.network.protocol.tim.packet.Packet
import net.mamoe.mirai.network.protocol.tim.packet.login.HeartbeatPacket
import net.mamoe.mirai.network.protocol.tim.packet.login.LoginResult
import net.mamoe.mirai.network.protocol.tim.packet.login.RequestSKeyPacket
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.io.PlatformDatagramChannel
/**
......@@ -38,7 +36,7 @@ interface BotNetworkHandler<Socket : DataPacketSocketAdapter> : CoroutineScope {
val socket: Socket
val bot: Bot
val supervisor get() = SupervisorJob()
val supervisor: CompletableJob
val session: BotSession
......@@ -46,7 +44,7 @@ interface BotNetworkHandler<Socket : DataPacketSocketAdapter> : CoroutineScope {
* 依次尝试登录到可用的服务器. 在任一服务器登录完成后返回登录结果
* 本函数将挂起直到登录成功.
*/
suspend fun login(configuration: BotConfiguration): LoginResult
suspend fun login(): LoginResult
/**
* 添加一个临时包处理器, 并发送相应的包
......@@ -70,6 +68,6 @@ interface BotNetworkHandler<Socket : DataPacketSocketAdapter> : CoroutineScope {
* 关闭网络接口, 停止所有有关协程和任务
*/
suspend fun close(cause: Throwable? = null) {
supervisor.cancelChildren(CancellationException("handler closed", cause))
supervisor.cancel(CancellationException("handler closed", cause))
}
}
\ No newline at end of file
......@@ -20,8 +20,8 @@ import net.mamoe.mirai.network.protocol.tim.handler.TemporaryPacketHandler
import net.mamoe.mirai.network.protocol.tim.packet.*
import net.mamoe.mirai.network.protocol.tim.packet.login.*
import net.mamoe.mirai.qqAccount
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.OnlineStatus
import net.mamoe.mirai.utils.currentBotConfiguration
import net.mamoe.mirai.utils.io.*
import kotlin.coroutines.CoroutineContext
import kotlin.properties.Delegates
......@@ -42,6 +42,7 @@ internal expect val NetworkDispatcher: CoroutineDispatcher
internal class TIMBotNetworkHandler internal constructor(coroutineContext: CoroutineContext, override inline val bot: Bot) :
BotNetworkHandler<TIMBotNetworkHandler.BotSocketAdapter>, CoroutineScope {
override val supervisor: CompletableJob = SupervisorJob(coroutineContext[Job])
override val coroutineContext: CoroutineContext =
coroutineContext + NetworkDispatcher + CoroutineExceptionHandler { context, e ->
......@@ -49,7 +50,6 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
?: "an unnamed coroutine"} under TIMBotNetworkHandler", e)
} + supervisor
override lateinit var socket: BotSocketAdapter
private set
......@@ -60,7 +60,6 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
private lateinit var userContext: CoroutineContext
override suspend fun addHandler(temporaryPacketHandler: TemporaryPacketHandler<*, *>) {
handlersLock.withLock {
temporaryPacketHandlers.add(temporaryPacketHandler)
......@@ -68,14 +67,14 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
temporaryPacketHandler.send(this.session)
}
override suspend fun login(configuration: BotConfiguration): LoginResult {
override suspend fun login(): LoginResult {
userContext = coroutineContext
return withContext(this.coroutineContext) {
TIMProtocol.SERVER_IP.sortedBy { Random.nextInt() }.forEach { ip ->
bot.logger.info("Connecting server $ip")
try {
withTimeout(3000) {
socket = BotSocketAdapter(ip, configuration)
socket = BotSocketAdapter(ip)
}
} catch (e: Exception) {
return@withContext LoginResult.NETWORK_UNAVAILABLE
......@@ -126,7 +125,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
override suspend fun sendPacket(packet: OutgoingPacket) = socket.sendPacket(packet)
internal inner class BotSocketAdapter(override val serverIp: String, val configuration: BotConfiguration) :
internal inner class BotSocketAdapter(override val serverIp: String) :
DataPacketSocketAdapter {
override val channel: PlatformDatagramChannel = PlatformDatagramChannel(serverIp, 8000)
......@@ -202,13 +201,13 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
internal suspend fun resendTouch(): LoginResult /* = coroutineScope */ {
loginHandler?.close()
loginHandler = LoginHandler(configuration)
loginHandler = LoginHandler()
val expect = expectPacket<TouchPacket.TouchResponse>()
launch { processReceive() }
launch {
if (withTimeoutOrNull(configuration.touchTimeout.millisecondsLong) { expect.join() } == null) {
if (withTimeoutOrNull(currentBotConfiguration().touchTimeout.millisecondsLong) { expect.join() } == null) {
loginResult.complete(LoginResult.TIMEOUT)
}
}
......@@ -284,10 +283,9 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
if (e.cause !is CancellationException) {
bot.logger.error("Caught SendPacketInternalException: ${e.cause?.message}")
}
GlobalScope.launch(userContext) {
bot.reinitializeNetworkHandler(configuration, e)
}
val configuration = currentBotConfiguration()
delay(configuration.firstReconnectDelay.millisecondsLong)
bot.tryReinitializeNetworkHandler(configuration, e)
return@withContext
} finally {
buffer.release(IoBuffer.Pool)
......@@ -319,7 +317,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
/**
* 处理登录过程
*/
inner class LoginHandler(private val configuration: BotConfiguration) {
inner class LoginHandler {
private lateinit var token00BA: ByteArray
private lateinit var token0825: ByteArray//56
private var loginTime: Int = 0
......@@ -375,7 +373,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
privateKey = privateKey,
token0825 = token0825,
token00BA = null,
randomDeviceName = socket.configuration.randomDeviceName
randomDeviceName = currentBotConfiguration().randomDeviceName
)
)
}
......@@ -383,7 +381,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
is TouchPacket.TouchResponse.Redirection -> {
withContext(userContext) {
socket.close()
socket = BotSocketAdapter(packet.serverIP!!, socket.configuration)
socket = BotSocketAdapter(packet.serverIP!!)
bot.logger.info("Redirecting to ${packet.serverIP}")
loginResult.complete(socket.resendTouch())
}
......@@ -407,7 +405,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
privateKey = privateKey,
token0825 = token0825,
token00BA = packet.token00BA,
randomDeviceName = socket.configuration.randomDeviceName
randomDeviceName = currentBotConfiguration().randomDeviceName
)
)
}
......@@ -441,6 +439,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
this.captchaCache!!.writeFully(packet.captchaSectionN)
this.token00BA = packet.token00BA
val configuration = currentBotConfiguration()
if (packet.transmissionCompleted) {
if (configuration.failOnCaptcha) {
loginResult.complete(LoginResult.CAPTCHA)
......@@ -455,23 +454,11 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
socket.sendPacket(CaptchaPacket.Refresh(bot.qqAccount, token0825))
} else {
this.captchaSectionId = 0//意味着已经提交验证码
socket.sendPacket(
CaptchaPacket.Submit(
bot.qqAccount,
token0825,
code,
packet.captchaToken
)
)
socket.sendPacket(CaptchaPacket.Submit(bot.qqAccount, token0825, code, packet.captchaToken))
}
} else {
socket.sendPacket(
CaptchaPacket.RequestTransmission(
bot.qqAccount,
token0825,
captchaSectionId++,
packet.token00BA
)
CaptchaPacket.RequestTransmission(bot.qqAccount, token0825, captchaSectionId++, packet.token00BA)
)
}
}
......@@ -479,13 +466,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
is SubmitPasswordPacket.LoginResponse.Success -> {
this.sessionResponseDecryptionKey = packet.sessionResponseDecryptionKey
socket.sendPacket(
RequestSessionPacket(
bot.qqAccount,
socket.serverIp,
packet.token38,
packet.token88,
packet.encryptionKey
)
RequestSessionPacket(bot.qqAccount, socket.serverIp, packet.token38, packet.token88, packet.encryptionKey)
)
}
......@@ -502,7 +483,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
privateKey = privateKey,
token0825 = token0825,
token00BA = packet.tokenUnknown ?: token00BA,
randomDeviceName = socket.configuration.randomDeviceName,
randomDeviceName = currentBotConfiguration().randomDeviceName,
tlv0006 = packet.tlv0006
)
)
......@@ -512,6 +493,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
sessionKey = packet.sessionKey
bot.logger.info("sessionKey = ${sessionKey.value.toUHexString()}")
val configuration = currentBotConfiguration()
heartbeatJob = this@TIMBotNetworkHandler.launch {
while (socket.isOpen) {
delay(configuration.heartbeatPeriod.millisecondsLong)
......@@ -519,15 +501,13 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
class HeartbeatTimeoutException : CancellationException("heartbeat timeout")
if (withTimeoutOrNull(configuration.heartbeatTimeout.millisecondsLong) {
// TODO: 2019/11/26 启动被挤掉线检测
// FIXME: 2019/11/26 启动被挤掉线检测
HeartbeatPacket(
bot.qqAccount,
sessionKey
).sendAndExpect<HeartbeatPacketResponse>()
HeartbeatPacket(bot.qqAccount, sessionKey).sendAndExpect<HeartbeatPacketResponse>()
} == null) {
bot.logger.warning("Heartbeat timed out")
bot.reinitializeNetworkHandler(configuration, HeartbeatTimeoutException())
delay(configuration.firstReconnectDelay.millisecondsLong)
bot.tryReinitializeNetworkHandler(configuration, HeartbeatTimeoutException())
return@launch
}
}
......
......@@ -123,6 +123,13 @@ fun LoginResult.requireSuccess() = requireSuccess { "Login failed: $this" }
*/
fun LoginResult.requireSuccessOrNull(): Unit? = if (this == SUCCESS) Unit else null
/**
* 返回 [this] 是否为 [LoginResult.SUCCESS].
*/
@Suppress("NOTHING_TO_INLINE")
@UseExperimental(ExperimentalContracts::class)
inline fun LoginResult.isSuccess(): Boolean = this == SUCCESS
/**
* 检查 [this] 为 [LoginResult.SUCCESS].
* 失败则返回 `null`
......
......@@ -5,6 +5,8 @@ import com.soywiz.klock.seconds
import kotlinx.io.core.IoBuffer
import net.mamoe.mirai.Bot
import net.mamoe.mirai.network.protocol.tim.packet.login.TouchPacket.TouchResponse
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.coroutineContext
import kotlin.jvm.JvmField
/**
......@@ -22,7 +24,7 @@ expect var DefaultCaptchaSolver: CaptchaSolver
/**
* 网络和连接配置
*/
class BotConfiguration {
class BotConfiguration : CoroutineContext.Element {
/**
* 等待 [TouchResponse] 的时间
*/
......@@ -42,6 +44,18 @@ class BotConfiguration {
* 一旦心跳超时, 整个网络服务将会重启 (将消耗约 1s). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响.
*/
var heartbeatTimeout: TimeSpan = 2.seconds
/**
* 心跳失败后的第一次重连前的等待时间.
*/
var firstReconnectDelay: TimeSpan = 5.seconds
/**
* 重连失败后, 继续尝试的每次等待时间
*/
var reconnectPeriod: TimeSpan = 60.seconds
/**
* 最多尝试多少次重连
*/
var reconnectionRetryTimes: Int = 3
/**
* 有验证码要求就失败
*/
......@@ -51,11 +65,15 @@ class BotConfiguration {
*/
var captchaSolver: CaptchaSolver = DefaultCaptchaSolver
companion object {
companion object Key : CoroutineContext.Key<BotConfiguration> {
/**
* 默认的配置实例
*/
@JvmField
val Default = BotConfiguration()
}
}
\ No newline at end of file
override val key: CoroutineContext.Key<*> get() = Key
}
suspend inline fun currentBotConfiguration(): BotConfiguration = coroutineContext[BotConfiguration] ?: error("No BotConfiguration found")
\ No newline at end of file
......@@ -17,7 +17,6 @@ import net.mamoe.mirai.network.protocol.tim.packet.login.CaptchaKey
import net.mamoe.mirai.network.protocol.tim.packet.login.LoginResult
import net.mamoe.mirai.network.protocol.tim.packet.login.ShareKey
import net.mamoe.mirai.network.protocol.tim.packet.login.TouchKey
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.DecryptionFailedException
import net.mamoe.mirai.utils.decryptBy
import net.mamoe.mirai.utils.io.*
......@@ -301,6 +300,7 @@ when (idHex.substring(0, 5)) {
internal object DebugNetworkHandler : BotNetworkHandler<DataPacketSocketAdapter>, CoroutineScope {
override val supervisor: CompletableJob = SupervisorJob()
override val socket: DataPacketSocketAdapter = object : DataPacketSocketAdapter {
override val serverIp: String
get() = ""
......@@ -320,10 +320,10 @@ internal object DebugNetworkHandler : BotNetworkHandler<DataPacketSocketAdapter>
get() = bot
}
override val bot: Bot = Bot(qq, "")
override val bot: Bot = Bot(qq, "", coroutineContext)
override val session = BotSession(bot, sessionKey, socket, this)
override suspend fun login(configuration: BotConfiguration): LoginResult = LoginResult.SUCCESS
override suspend fun login(): LoginResult = LoginResult.SUCCESS
override suspend fun addHandler(temporaryPacketHandler: TemporaryPacketHandler<*, *>) {
}
......@@ -336,4 +336,5 @@ internal object DebugNetworkHandler : BotNetworkHandler<DataPacketSocketAdapter>
override val coroutineContext: CoroutineContext
get() = GlobalScope.coroutineContext
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment