fix: Use Job.invokeOnCompletion(onCancelling=true) for responsive socket cancellation

This commit is contained in:
Ochenstarik 2026-08-25 11:02:17 +07:00
parent b54089dacc
commit 163da57ea5
3 changed files with 24 additions and 9 deletions

View file

@ -9,8 +9,8 @@ import app.hermes.mobile.core.network.HermesRestClient
import app.hermes.mobile.core.security.TokenVault
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.job
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.InputStreamReader
@ -62,11 +62,6 @@ class PkceLoopbackAuthManager(
val port = serverSocket.localPort
serverSocket.soTimeout = 180_000 // 3 minutes timeout
currentCoroutineContext().job.invokeOnCompletion {
try {
serverSocket?.close()
} catch (_: Throwable) {}
}
val redirectUri = "http://127.0.0.1:$port/callback"
@ -92,7 +87,22 @@ class PkceLoopbackAuthManager(
var retries = 0
val MAX_RETRIES = 5
while (authCode == null && retries < MAX_RETRIES) {
val socket: Socket = runInterruptible(Dispatchers.IO) { serverSocket!!.accept() }
currentCoroutineContext().ensureActive()
val socket: Socket = suspendCancellableCoroutine { cont ->
cont.invokeOnCancellation {
try {
serverSocket?.close()
} catch (_: Throwable) {}
}
try {
val s = serverSocket!!.accept()
cont.resumeWith(Result.success(s))
} catch (e: Throwable) {
if (!cont.isCancelled) {
cont.resumeWith(Result.failure(e))
}
}
}
try {
authCode = handleCallbackSocket(socket, state)
} catch (e: Exception) {

View file

@ -154,11 +154,13 @@ class HermesConnectionManager(
suspend fun addHost(host: HermesHost) {
hostDao.insertOrUpdateHost(host.toEntity())
_hosts.value = _hosts.value.filter { it.id != host.id } + host
getOrCreateRuntime(host)
}
suspend fun updateHost(host: HermesHost) {
hostDao.insertOrUpdateHost(host.toEntity())
_hosts.value = _hosts.value.map { if (it.id == host.id) host else it }
val rt = runtimes[host.id]
rt?.updateHost(host)
}
@ -168,6 +170,7 @@ class HermesConnectionManager(
rt?.close()
tokenVault.clearTokens(hostId.value)
hostDao.deleteHost(hostId.value)
_hosts.value = _hosts.value.filter { it.id != hostId }
if (_activeHostId.value == hostId) {
_activeHostId.value = _hosts.value.firstOrNull { it.id != hostId }?.id
}
@ -175,6 +178,7 @@ class HermesConnectionManager(
suspend fun connectHost(hostId: HermesHostId): Result<Unit> {
val host = _hosts.value.find { it.id == hostId }
?: hostDao.getHost(hostId.value)?.toDomain()
?: return Result.failure(IllegalArgumentException("Host not found: ${hostId.value}"))
val rt = getOrCreateRuntime(host)
return rt.connect()

View file

@ -27,7 +27,8 @@ class LoopbackCancellationTest {
)
val authUrlDeferred = CompletableDeferred<String>()
val job = launch(Dispatchers.IO) {
val testScope = kotlinx.coroutines.CoroutineScope(Dispatchers.IO)
val job = testScope.launch {
authManager.startAuthFlow(
context = null,
connectionId = "test-host",