diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/server/ServerBinder.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/server/ServerBinder.kt index 756cc73e..43d44309 100644 --- a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/server/ServerBinder.kt +++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/server/ServerBinder.kt @@ -21,7 +21,10 @@ package org.spotube.spotube.core.server import io.ktor.server.application.Application import io.ktor.server.cio.CIO import io.ktor.server.engine.EmbeddedServer +import io.ktor.server.engine.connector import io.ktor.server.engine.embeddedServer +import io.ktor.server.application.serverConfig +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.ensureActive import kotlinx.coroutines.withTimeout @@ -37,6 +40,9 @@ internal class BoundServer(val engine: EmbeddedServer<*, *>, val port: Int, val * preferred port can't be bound, retry on an OS-assigned free port (port 0). * * The caller must read [BoundServer.port] instead of assuming [preferredPort]. + * + * `reuseAddress` lets a restarted app rebind its old port while connections from the previous + * run (e.g. the player's) are still in TIME_WAIT. */ internal suspend fun startServerWithPortFallback( host: String, @@ -47,8 +53,25 @@ internal suspend fun startServerWithPortFallback( val candidates = listOf(preferredPort, 0).distinct() var lastFailure: Throwable? = null + // CIO reports a failed bind from its accept coroutine. Unhandled, that exception kills the + // process on Kotlin/Native (iOS) before the fallback below gets to run; the failure still + // reaches us through resolvedConnectors(), so it only needs to be contained here. + val swallowBindFailure = CoroutineExceptionHandler { _, _ -> } + for (candidate in candidates) { - val engine = embeddedServer(factory = CIO, host = host, port = candidate, module = module) + val engine = embeddedServer( + factory = CIO, + rootConfig = serverConfig { + parentCoroutineContext = swallowBindFailure + module { module() } + }, + configure = { + connector { + this.host = host + this.port = candidate + } + reuseAddress = true + }) try { engine.start(wait = false) val boundPort = withTimeout(5.seconds) { engine.engine.resolvedConnectors().first().port }