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 app.hermes.mobile.core.security.TokenVault
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.job import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.BufferedReader import java.io.BufferedReader
import java.io.InputStreamReader import java.io.InputStreamReader
@ -62,11 +62,6 @@ class PkceLoopbackAuthManager(
val port = serverSocket.localPort val port = serverSocket.localPort
serverSocket.soTimeout = 180_000 // 3 minutes timeout serverSocket.soTimeout = 180_000 // 3 minutes timeout
currentCoroutineContext().job.invokeOnCompletion {
try {
serverSocket?.close()
} catch (_: Throwable) {}
}
val redirectUri = "http://127.0.0.1:$port/callback" val redirectUri = "http://127.0.0.1:$port/callback"
@ -92,7 +87,22 @@ class PkceLoopbackAuthManager(
var retries = 0 var retries = 0
val MAX_RETRIES = 5 val MAX_RETRIES = 5
while (authCode == null && retries < MAX_RETRIES) { 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 { try {
authCode = handleCallbackSocket(socket, state) authCode = handleCallbackSocket(socket, state)
} catch (e: Exception) { } catch (e: Exception) {

View file

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

View file

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