mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-24 03:53:12 -04:00
Update to Ktor 3 and more (#13782)
* upgrade ktor version to `3.2.3` * Update EndpointImplementations.kt * update `androidx.core:core-ktx` to `1.16.0` * fix `Principal` deprecated message
This commit is contained in:
parent
e059d0dc4c
commit
99fa2cd964
@ -147,11 +147,11 @@ tasks.register<Exec>("run") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("androidx.core:core-ktx:1.15.0")
|
implementation("androidx.core:core-ktx:1.16.0")
|
||||||
implementation("androidx.work:work-runtime-ktx:2.10.0")
|
implementation("androidx.work:work-runtime-ktx:2.10.3")
|
||||||
// Needed to convert e.g. Android 26 API calls to Android 21
|
// Needed to convert e.g. Android 26 API calls to Android 21
|
||||||
// If you remove this run `./gradlew :android:lintDebug` to ensure everything's okay.
|
// If you remove this run `./gradlew :android:lintDebug` to ensure everything's okay.
|
||||||
// If you want to upgrade this, check it's working by building an apk,
|
// If you want to upgrade this, check it's working by building an apk,
|
||||||
// or by running `./gradlew :android:assembleRelease` which does that
|
// or by running `./gradlew :android:assembleRelease` which does that
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ object BuildConfig {
|
|||||||
const val appVersion = "4.17.12"
|
const val appVersion = "4.17.12"
|
||||||
|
|
||||||
const val gdxVersion = "1.13.1"
|
const val gdxVersion = "1.13.1"
|
||||||
const val ktorVersion = "2.3.13"
|
const val ktorVersion = "3.2.3"
|
||||||
const val coroutinesVersion = "1.8.1"
|
const val coroutinesVersion = "1.8.1"
|
||||||
const val jnaVersion = "5.17.0"
|
const val jnaVersion = "5.17.0"
|
||||||
|
|
||||||
|
@ -11,14 +11,11 @@ import com.unciv.logic.multiplayer.storage.ApiV2FileStorageWrapper
|
|||||||
import com.unciv.logic.multiplayer.storage.MultiplayerFileNotFoundException
|
import com.unciv.logic.multiplayer.storage.MultiplayerFileNotFoundException
|
||||||
import com.unciv.utils.Concurrency
|
import com.unciv.utils.Concurrency
|
||||||
import com.unciv.utils.Log
|
import com.unciv.utils.Log
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.plugins.websocket.ClientWebSocketSession
|
import io.ktor.client.plugins.websocket.*
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.*
|
||||||
import io.ktor.http.isSuccess
|
import io.ktor.http.*
|
||||||
import io.ktor.websocket.Frame
|
import io.ktor.websocket.*
|
||||||
import io.ktor.websocket.FrameType
|
|
||||||
import io.ktor.websocket.readText
|
|
||||||
import io.ktor.websocket.close
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
@ -35,7 +32,7 @@ import java.time.Instant
|
|||||||
import java.util.Random
|
import java.util.Random
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import kotlin.collections.set
|
import kotlin.time.Duration
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main class to interact with multiplayer servers implementing [ApiVersion.ApiV2]
|
* Main class to interact with multiplayer servers implementing [ApiVersion.ApiV2]
|
||||||
@ -339,7 +336,7 @@ class ApiV2(private val baseUrl: String) : ApiV2Wrapper(baseUrl), Disposable {
|
|||||||
* [delay] internally to quit waiting for the result of the operation.
|
* [delay] internally to quit waiting for the result of the operation.
|
||||||
* This function may also throw arbitrary exceptions for network failures.
|
* This function may also throw arbitrary exceptions for network failures.
|
||||||
*/
|
*/
|
||||||
suspend fun awaitPing(size: Int = 2, timeout: Long? = null): Double? {
|
suspend fun awaitPing(size: Int = 2, timeout: Duration? = null): Double? {
|
||||||
require(size < 2) { "Size too small to identify ping responses uniquely" }
|
require(size < 2) { "Size too small to identify ping responses uniquely" }
|
||||||
val body = ByteArray(size)
|
val body = ByteArray(size)
|
||||||
Random().nextBytes(body)
|
Random().nextBytes(body)
|
||||||
@ -501,7 +498,10 @@ class ApiV2(private val baseUrl: String) : ApiV2Wrapper(baseUrl), Disposable {
|
|||||||
* Note that this callback might not get called if no new WS connection was created.
|
* Note that this callback might not get called if no new WS connection was created.
|
||||||
* It returns the measured round trip time in milliseconds if everything was fine.
|
* It returns the measured round trip time in milliseconds if everything was fine.
|
||||||
*/
|
*/
|
||||||
suspend fun ensureConnectedWebSocket(timeout: Long = DEFAULT_WEBSOCKET_PING_TIMEOUT, jobCallback: ((Job) -> Unit)? = null): Double? {
|
suspend fun ensureConnectedWebSocket(
|
||||||
|
timeout: Duration = DEFAULT_WEBSOCKET_PING_TIMEOUT,
|
||||||
|
jobCallback: ((Job) -> Unit)? = null
|
||||||
|
): Double? {
|
||||||
val pingMeasurement = try {
|
val pingMeasurement = try {
|
||||||
awaitPing(timeout = timeout)
|
awaitPing(timeout = timeout)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package com.unciv.logic.multiplayer.apiv2
|
package com.unciv.logic.multiplayer.apiv2
|
||||||
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
/** Name of the session cookie returned and expected by the server */
|
/** Name of the session cookie returned and expected by the server */
|
||||||
internal const val SESSION_COOKIE_NAME = "id"
|
internal const val SESSION_COOKIE_NAME = "id"
|
||||||
@ -9,13 +10,13 @@ internal const val SESSION_COOKIE_NAME = "id"
|
|||||||
internal const val DEFAULT_LOBBY_MAX_PLAYERS = 32
|
internal const val DEFAULT_LOBBY_MAX_PLAYERS = 32
|
||||||
|
|
||||||
/** Default ping frequency for outgoing WebSocket connection in seconds */
|
/** Default ping frequency for outgoing WebSocket connection in seconds */
|
||||||
internal const val DEFAULT_WEBSOCKET_PING_FREQUENCY = 15_000L
|
internal val DEFAULT_WEBSOCKET_PING_FREQUENCY = 15.seconds
|
||||||
|
|
||||||
/** Default session timeout expected from multiplayer servers (unreliable) */
|
/** Default session timeout expected from multiplayer servers (unreliable) */
|
||||||
internal val DEFAULT_SESSION_TIMEOUT = Duration.ofSeconds(15 * 60)
|
internal val DEFAULT_SESSION_TIMEOUT = Duration.ofMinutes(15)
|
||||||
|
|
||||||
/** Default cache expiry timeout to indicate that certain data needs to be re-fetched */
|
/** Default cache expiry timeout to indicate that certain data needs to be re-fetched */
|
||||||
internal val DEFAULT_CACHE_EXPIRY = Duration.ofSeconds(30 * 60)
|
internal val DEFAULT_CACHE_EXPIRY = Duration.ofMinutes(30)
|
||||||
|
|
||||||
/** Default timeout for a single request (miliseconds) */
|
/** Default timeout for a single request (miliseconds) */
|
||||||
internal const val DEFAULT_REQUEST_TIMEOUT = 10_000L
|
internal const val DEFAULT_REQUEST_TIMEOUT = 10_000L
|
||||||
@ -24,4 +25,4 @@ internal const val DEFAULT_REQUEST_TIMEOUT = 10_000L
|
|||||||
internal const val DEFAULT_CONNECT_TIMEOUT = 5_000L
|
internal const val DEFAULT_CONNECT_TIMEOUT = 5_000L
|
||||||
|
|
||||||
/** Default timeout for a single WebSocket PING-PONG roundtrip */
|
/** Default timeout for a single WebSocket PING-PONG roundtrip */
|
||||||
internal const val DEFAULT_WEBSOCKET_PING_TIMEOUT = 10_000L
|
internal val DEFAULT_WEBSOCKET_PING_TIMEOUT = 10.seconds
|
||||||
|
@ -7,24 +7,17 @@
|
|||||||
package com.unciv.logic.multiplayer.apiv2
|
package com.unciv.logic.multiplayer.apiv2
|
||||||
|
|
||||||
import com.unciv.utils.Log
|
import com.unciv.utils.Log
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.plugins.cookies.get
|
import io.ktor.client.plugins.cookies.*
|
||||||
import io.ktor.client.request.HttpRequestBuilder
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.request.request
|
import io.ktor.client.request.request
|
||||||
import io.ktor.client.request.setBody
|
import io.ktor.client.statement.*
|
||||||
import io.ktor.client.statement.HttpResponse
|
|
||||||
import io.ktor.client.statement.bodyAsText
|
|
||||||
import io.ktor.client.statement.request
|
import io.ktor.client.statement.request
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.*
|
||||||
import io.ktor.http.HttpMethod
|
import io.ktor.util.network.*
|
||||||
import io.ktor.http.HttpStatusCode
|
|
||||||
import io.ktor.http.contentType
|
|
||||||
import io.ktor.http.isSuccess
|
|
||||||
import io.ktor.http.path
|
|
||||||
import io.ktor.http.setCookie
|
|
||||||
import io.ktor.util.network.UnresolvedAddressException
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@ -46,7 +39,7 @@ private const val DEFAULT_RANDOM_PASSWORD_LENGTH = 32
|
|||||||
/**
|
/**
|
||||||
* Max age of a cached entry before it will be re-queried
|
* Max age of a cached entry before it will be re-queried
|
||||||
*/
|
*/
|
||||||
private const val MAX_CACHE_AGE_SECONDS = 60L
|
private val MAX_CACHE_AGE = Duration.ofSeconds(60)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform a HTTP request via [method] to [endpoint]
|
* Perform a HTTP request via [method] to [endpoint]
|
||||||
@ -218,7 +211,7 @@ private object Cache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper around [request] to cache responses to GET queries up to [MAX_CACHE_AGE_SECONDS]
|
* Wrapper around [request] to cache responses to GET queries up to [MAX_CACHE_AGE]
|
||||||
*/
|
*/
|
||||||
suspend fun get(
|
suspend fun get(
|
||||||
endpoint: String,
|
endpoint: String,
|
||||||
@ -230,7 +223,7 @@ private object Cache {
|
|||||||
retry: (suspend () -> Boolean)? = null
|
retry: (suspend () -> Boolean)? = null
|
||||||
): HttpResponse? {
|
): HttpResponse? {
|
||||||
val result = responseCache[endpoint]
|
val result = responseCache[endpoint]
|
||||||
if (cache && result != null && result.first.plusSeconds(MAX_CACHE_AGE_SECONDS).isAfter(Instant.now())) {
|
if (cache && result != null && (result.first + MAX_CACHE_AGE).isAfter(Instant.now())) {
|
||||||
return result.second
|
return result.second
|
||||||
}
|
}
|
||||||
val response = request(HttpMethod.Get, endpoint, client, authHelper, refine, suppress, retry)
|
val response = request(HttpMethod.Get, endpoint, client, authHelper, refine, suppress, retry)
|
||||||
|
@ -26,6 +26,8 @@ import kotlinx.serialization.json.ClassDiscriminatorMode
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
|
|
||||||
@ -77,17 +79,17 @@ class ChatRestartException : CancellationException("Chat restart requested")
|
|||||||
class ChatStopException : CancellationException("Chat stop requested")
|
class ChatStopException : CancellationException("Chat stop requested")
|
||||||
|
|
||||||
object ChatWebSocket {
|
object ChatWebSocket {
|
||||||
|
private const val MAX_RECONNECTION_ATTEMPTS = 100
|
||||||
|
private val INITIAL_RECONNECT_TIME = 1.seconds
|
||||||
|
private val MAX_RECONNECT_TIME = 64.seconds
|
||||||
|
private val INITIAL_SESSION_WAIT_FOR_TIME = 5.seconds
|
||||||
|
|
||||||
private var isStarted = false
|
private var isStarted = false
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
@OptIn(ExperimentalTime::class)
|
||||||
private var lastRetry = Clock.System.now()
|
private var lastRetry = Clock.System.now()
|
||||||
private var reconnectionAttempts = 0
|
private var reconnectionAttempts = 0
|
||||||
private var reconnectTimeSeconds = INITIAL_RECONNECT_TIME_SECONDS
|
private var reconnectTime = INITIAL_RECONNECT_TIME
|
||||||
|
|
||||||
private const val INITIAL_RECONNECT_TIME_SECONDS = 1
|
|
||||||
private const val MAX_RECONNECTION_ATTEMPTS = 100
|
|
||||||
private const val MAX_RECONNECT_TIME_SECONDS = 64
|
|
||||||
private const val INITIAL_SESSION_WAIT_FOR_MS = 5_000L
|
|
||||||
|
|
||||||
private var job: Job? = null
|
private var job: Job? = null
|
||||||
private var session: DefaultClientWebSocketSession? = null
|
private var session: DefaultClientWebSocketSession? = null
|
||||||
@ -95,7 +97,7 @@ object ChatWebSocket {
|
|||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
private val client = HttpClient(CIO) {
|
private val client = HttpClient(CIO) {
|
||||||
install(WebSockets) {
|
install(WebSockets) {
|
||||||
pingInterval = 30_000
|
pingInterval = 30.seconds
|
||||||
contentConverter = KotlinxWebsocketSerializationConverter(Json {
|
contentConverter = KotlinxWebsocketSerializationConverter(Json {
|
||||||
classDiscriminator = "type"
|
classDiscriminator = "type"
|
||||||
// DO NOT OMIT
|
// DO NOT OMIT
|
||||||
@ -110,7 +112,7 @@ object ChatWebSocket {
|
|||||||
lastRetry = Clock.System.now()
|
lastRetry = Clock.System.now()
|
||||||
|
|
||||||
reconnectionAttempts = 0
|
reconnectionAttempts = 0
|
||||||
reconnectionAttempts = INITIAL_RECONNECT_TIME_SECONDS
|
reconnectTime = INITIAL_RECONNECT_TIME
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getChatUrl(): Url = URLBuilder(
|
private fun getChatUrl(): Url = URLBuilder(
|
||||||
@ -132,9 +134,9 @@ object ChatWebSocket {
|
|||||||
fun requestMessageSend(message: Message) {
|
fun requestMessageSend(message: Message) {
|
||||||
start()
|
start()
|
||||||
Concurrency.run("MultiplayerChatSendMessage") {
|
Concurrency.run("MultiplayerChatSendMessage") {
|
||||||
withTimeoutOrNull(INITIAL_SESSION_WAIT_FOR_MS) {
|
withTimeoutOrNull(INITIAL_SESSION_WAIT_FOR_TIME) {
|
||||||
while (session == null) {
|
while (session == null) {
|
||||||
delay(100)
|
delay(100.milliseconds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
session?.runCatching {
|
session?.runCatching {
|
||||||
@ -241,9 +243,8 @@ object ChatWebSocket {
|
|||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
if (!force) {
|
if (!force) {
|
||||||
// exponential backoff same as described here: https://cloud.google.com/memorystore/docs/redis/exponential-backoff
|
// exponential backoff same as described here: https://cloud.google.com/memorystore/docs/redis/exponential-backoff
|
||||||
delay(Random.nextLong(1000) + 1000L * reconnectTimeSeconds)
|
delay(Random.nextLong(1000).milliseconds + reconnectTime)
|
||||||
reconnectTimeSeconds =
|
reconnectTime = (reconnectTime * 2).coerceAtMost(MAX_RECONNECT_TIME)
|
||||||
(reconnectTimeSeconds * 2).coerceAtMost(MAX_RECONNECT_TIME_SECONDS)
|
|
||||||
if (job?.isActive == true) return@launch
|
if (job?.isActive == true) return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ import java.io.File
|
|||||||
import java.util.Collections.synchronizedMap
|
import java.util.Collections.synchronizedMap
|
||||||
import java.util.Collections.synchronizedSet
|
import java.util.Collections.synchronizedSet
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
@ -140,7 +141,7 @@ private class WebSocketSessionManager {
|
|||||||
data class BasicAuthInfo(
|
data class BasicAuthInfo(
|
||||||
val userId: Uuid,
|
val userId: Uuid,
|
||||||
val password: String,
|
val password: String,
|
||||||
) : Principal
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a [String] is a valid UUID
|
* Checks if a [String] is a valid UUID
|
||||||
@ -264,8 +265,8 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (chatV1Enabled) install(WebSockets) {
|
if (chatV1Enabled) install(WebSockets) {
|
||||||
pingPeriodMillis = 30_000
|
pingPeriod = 30.seconds
|
||||||
timeoutMillis = 60_000
|
timeout = 60.seconds
|
||||||
maxFrameSize = Long.MAX_VALUE
|
maxFrameSize = Long.MAX_VALUE
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
contentConverter = KotlinxWebsocketSerializationConverter(Json {
|
contentConverter = KotlinxWebsocketSerializationConverter(Json {
|
||||||
@ -386,8 +387,7 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
val message = receiveDeserialized<Message>()
|
when (val message = receiveDeserialized<Message>()) {
|
||||||
when (message) {
|
|
||||||
is Message.Chat -> {
|
is Message.Chat -> {
|
||||||
val gameId = message.gameId.toUuidOrNull()
|
val gameId = message.gameId.toUuidOrNull()
|
||||||
if (gameId == null) {
|
if (gameId == null) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user