diff --git a/app/src/main/java/com/m57/hermescontrol/data/remote/PersistentCookieJar.kt b/app/src/main/java/com/m57/hermescontrol/data/remote/PersistentCookieJar.kt index e6cd43d0..e518fd17 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/remote/PersistentCookieJar.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/remote/PersistentCookieJar.kt @@ -11,6 +11,7 @@ import okhttp3.CookieJar import okhttp3.HttpUrl import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReference /** @@ -63,6 +64,9 @@ class PersistentCookieJar( */ fun getCookie(name: String): Cookie? { val serverId = currentServerId.get() + if (!loadedScopes.contains(serverId)) { + loadLatches[serverId]?.await(5, TimeUnit.SECONDS) + } val hosts = cache[serverId] ?: return null for (bucket in hosts.values) { synchronized(bucket) { @@ -126,6 +130,9 @@ class PersistentCookieJar( override fun loadForRequest(url: HttpUrl): List { val serverId = currentServerId.get() + if (!loadedScopes.contains(serverId)) { + loadLatches[serverId]?.await(5, TimeUnit.SECONDS) + } val hostKey = url.host val hosts = cache[serverId] ?: return emptyList() val now = System.currentTimeMillis() @@ -152,16 +159,21 @@ class PersistentCookieJar( private suspend fun ensureLoaded(serverId: String) { if (loadedScopes.contains(serverId)) return - val persisted = store.load(serverId) - val byHost = ConcurrentHashMap>() - for (cookie in persisted) { - // Blank domain => host-only (e.g. legacy migrated session cookie). - // Bucket it under a wildcard "*" so it is returned for every host. - val host = cookie.domain.removePrefix(".").ifBlank { WILDCARD_HOST } - byHost.getOrPut(host) { mutableListOf() }.add(cookie) + val latch = loadLatches.computeIfAbsent(serverId) { CountDownLatch(1) } + try { + val persisted = store.load(serverId) + val byHost = ConcurrentHashMap>() + for (cookie in persisted) { + // Blank domain => host-only (e.g. legacy migrated session cookie). + // Bucket it under a wildcard "*" so it is returned for every host. + val host = cookie.domain.removePrefix(".").ifBlank { WILDCARD_HOST } + byHost.getOrPut(host) { mutableListOf() }.add(cookie) + } + cache[serverId] = byHost + loadedScopes.add(serverId) + } finally { + latch.countDown() } - cache[serverId] = byHost - loadedScopes.add(serverId) } private fun persist(serverId: String) { @@ -195,6 +207,7 @@ class PersistentCookieJar( val scope = currentServerId.get() cache.remove(scope) loadedScopes.remove(scope) + loadLatches.remove(scope) storeScope.launch { store.clear(scope) } } @@ -202,6 +215,7 @@ class PersistentCookieJar( fun clearAll() { cache.clear() loadedScopes.clear() + loadLatches.clear() storeScope.launch { store.clearAll() } } diff --git a/app/src/main/java/com/m57/hermescontrol/data/ws/HermesWsClient.kt b/app/src/main/java/com/m57/hermescontrol/data/ws/HermesWsClient.kt index 2baad4e1..27817804 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/ws/HermesWsClient.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/ws/HermesWsClient.kt @@ -4,6 +4,7 @@ import android.util.Log import androidx.annotation.VisibleForTesting import com.m57.hermescontrol.BuildConfig import com.m57.hermescontrol.data.local.AuthManager +import com.m57.hermescontrol.data.remote.CookieManager import com.m57.hermescontrol.data.remote.DashboardSessionTokenRefresher import com.m57.hermescontrol.data.remote.NetworkMonitor import com.m57.hermescontrol.data.remote.OkHttpProvider @@ -493,6 +494,9 @@ object HermesWsClient { // read throws NetworkOnMainThreadException and the mint fails. val (code, body) = kotlinx.coroutines.runBlocking(Dispatchers.IO) { + if (CookieManager.isInitialized()) { + CookieManager.useStore(CookieManager.cookieJar.currentServer()) + } client.newCall(request).execute().use { resp -> resp.code to resp.body.string() } diff --git a/app/src/test/java/com/m57/hermescontrol/data/remote/PersistentCookieJarTest.kt b/app/src/test/java/com/m57/hermescontrol/data/remote/PersistentCookieJarTest.kt index e729a6f2..bc394556 100644 --- a/app/src/test/java/com/m57/hermescontrol/data/remote/PersistentCookieJarTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/data/remote/PersistentCookieJarTest.kt @@ -1,5 +1,9 @@ package com.m57.hermescontrol.data.remote +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import okhttp3.Cookie import okhttp3.HttpUrl.Companion.toHttpUrl @@ -239,4 +243,39 @@ class PersistentCookieJarTest { assertEquals("secure-value", jar.getSessionCookieValue()) } + + @Test + fun loadForRequest_awaitsAsyncScopeHydration() = + runTest { + val fakeStore = FakeEncryptedCookieStore() + fakeStore.save("cold-server", listOf(sessionCookie("dash.local", "cold-cookie-xyz"))) + + val store = + object : CookieStore by fakeStore { + override suspend fun load(serverId: String): List { + delay(50) + return fakeStore.load(serverId) + } + } + + val testScope = CoroutineScope(Dispatchers.IO) + val jar = + PersistentCookieJar( + store = store, + storeScope = testScope, + initialServerId = "cold-server", + ) + + val job = + testScope.launch { + jar.useStore("cold-server") + } + + delay(10) + + val loaded = jar.loadForRequest("http://dash.local/api/status".toHttpUrl()) + assertEquals(1, loaded.size) + assertEquals("cold-cookie-xyz", loaded[0].value) + job.join() + } }