Wait for Android core readiness before applying settings
This commit is contained in:
1 parent
8e0090b54b
commit
11b3ceaa9b
3 files changed
+31
-62
No files matched your search
@@ -3,7 +3,6 @@ package com.utun.chat.headless
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -26,15 +25,15 @@ internal class CoreStartup(
|
||||
private var requested = false
|
||||
private var closed = false
|
||||
|
||||
/** Принимает уже поставленную в очередь подготовку; submit=true подтверждает только создание native-потока. */
|
||||
fun start(config: Deferred<String>, submit: (String) -> Boolean) {
|
||||
/** prepare инициализирует frontend/сеть и возвращает INI; submit=true подтверждает только создание native-потока. */
|
||||
fun start(prepare: suspend () -> String, submit: (String) -> Boolean) {
|
||||
if (job != null || closed) {
|
||||
log("WARN", "startup ignored: requested=$requested closed=$closed")
|
||||
return
|
||||
}
|
||||
job = scope.launch {
|
||||
try {
|
||||
val text = config.await()
|
||||
val text = prepare()
|
||||
val alreadyRunning = isReady()
|
||||
requested = true
|
||||
check(submit(text)) { "native start rejected" }
|
||||
@@ -70,10 +69,6 @@ internal class CoreStartup(
|
||||
} catch (e: Exception) { fail(e) }
|
||||
}
|
||||
|
||||
fun coreStopped() {
|
||||
if (!closed && requested && !ready.isCompleted) fail(IllegalStateException("core stopped before initial readiness"))
|
||||
}
|
||||
|
||||
/** Также используется для ошибок init/регистрации сети до start; stopSelf вызывается один раз. */
|
||||
fun fail(error: Exception) {
|
||||
if (closed) return
|
||||
|
||||
@@ -17,30 +17,33 @@ import com.utun.chat.data.NativeLib
|
||||
import com.utun.chat.data.NetworkActivityMonitor
|
||||
import com.utun.chat.data.NetworkChangeMonitor
|
||||
import com.utun.chat.data.fileStatusResource
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/** Владеет запуском ядра/сетевых наблюдателей; готовность подтверждается событием native SERVICE_STARTED. */
|
||||
class HeadlessService : Service() {
|
||||
private val startupScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
private var startupJob: Job? = null
|
||||
private val startup = CoreStartup(startupScope, NativeLib::isReady, onReady = {
|
||||
LogManager.addLog("INFO", "SRV", "uTun core started")
|
||||
NetworkActivityMonitor.start()
|
||||
NetworkChangeMonitor.onCoreStarted()
|
||||
}, onFailure = ::stopSelf, log = { level, message -> LogManager.addLog(level, "SRV", message) })
|
||||
companion object {
|
||||
private var currentService: HeadlessService? = null
|
||||
|
||||
/** Main thread: изменения из Settings применяются после принятия первоначального запуска Service. */
|
||||
suspend fun awaitStartup() { currentService?.startupJob?.join() }
|
||||
|
||||
fun onCoreStarted() {
|
||||
if (currentService == null || !NativeLib.isReady()) return
|
||||
LogManager.addLog("INFO", "SRV", "uTun core started")
|
||||
NetworkActivityMonitor.start()
|
||||
NetworkChangeMonitor.onCoreStarted()
|
||||
/** Main thread: дождаться готового ядра текущего Service; ошибка/остановка прерывает вызывающую операцию.
|
||||
* Без Service ожидания нет: настройки сохраняются для следующего запуска. */
|
||||
suspend fun awaitStartup() {
|
||||
val service = currentService ?: return
|
||||
service.startup.awaitReady()
|
||||
check(currentService === service && NativeLib.isReady()) { "service core stopped while awaiting startup" }
|
||||
}
|
||||
|
||||
fun onCoreStarted() { currentService?.startup?.coreStarted() }
|
||||
|
||||
fun refreshLanguage() {
|
||||
currentService?.let { service ->
|
||||
service.createNotificationChannel()
|
||||
@@ -60,41 +63,20 @@ class HeadlessService : Service() {
|
||||
createNotificationChannel()
|
||||
startForeground(1, notification())
|
||||
|
||||
val provider = ChatApplication.instance.configProvider
|
||||
val dbDir = provider.getString("db.path") ?: filesDir.absolutePath
|
||||
val listenPort = provider.getInt("node.listen_port")
|
||||
|
||||
LogManager.addLog("INFO", "SRV", "uTun service starting, port=$listenPort db=$dbDir")
|
||||
|
||||
if (!NativeLib.init(dbDir, 9999)) {
|
||||
LogManager.addLog("ERROR", "SRV", "uTun startup rejected: native initialization failed")
|
||||
stopSelf()
|
||||
return
|
||||
}
|
||||
|
||||
if (!NetworkChangeMonitor.start()) {
|
||||
LogManager.addLog("ERROR", "SRV", "uTun startup rejected: network monitoring unavailable")
|
||||
stopSelf()
|
||||
return
|
||||
}
|
||||
val config = provider.buildConfigText()
|
||||
startupJob = startupScope.launch {
|
||||
try {
|
||||
// await отменяется при onDestroy: готовый конфиг не запустит ядро для уже остановленного Service.
|
||||
if (NativeLib.start(config.await())) LogManager.addLog("INFO", "SRV", "uTun startup requested")
|
||||
else {
|
||||
LogManager.addLog("ERROR", "SRV", "uTun start failed")
|
||||
stopSelf()
|
||||
}
|
||||
} catch (e: CancellationException) { throw e
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "SRV", "uTun startup failed: ${e.javaClass.simpleName}: ${e.message}")
|
||||
stopSelf()
|
||||
startup.start(prepare = {
|
||||
val provider = ChatApplication.instance.configProvider
|
||||
val (dbDir, listenPort) = withContext(Dispatchers.IO) {
|
||||
(provider.getString("db.path") ?: filesDir.absolutePath) to provider.getInt("node.listen_port")
|
||||
}
|
||||
}
|
||||
LogManager.addLog("INFO", "SRV", "uTun service starting, port=$listenPort db=$dbDir")
|
||||
check(NativeLib.init(dbDir, 9999)) { "native initialization failed" }
|
||||
check(NetworkChangeMonitor.start()) { "network monitoring unavailable" }
|
||||
provider.buildConfigText().await()
|
||||
}, submit = NativeLib::start)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
startup.stop()
|
||||
startupScope.cancel()
|
||||
if (currentService === this) currentService = null
|
||||
LogManager.addLog("INFO", "SRV", "uTun service stopping")
|
||||
|
||||
@@ -32,7 +32,7 @@ class CoreStartupTest {
|
||||
|
||||
@After fun release() { startup.stop(); scope.cancel() }
|
||||
|
||||
private fun start() = startup.start(config) { submitted.add(it); accepted }
|
||||
private fun start() = startup.start({ config.await() }) { submitted.add(it); accepted }
|
||||
private fun waiter() = scope.async(start = CoroutineStart.UNDISPATCHED) { startup.awaitReady(); readyCalls }
|
||||
|
||||
@Test fun acceptedNativeStartDoesNotReleaseSettingsBeforeReady() = runBlocking {
|
||||
@@ -129,7 +129,7 @@ class CoreStartupTest {
|
||||
|
||||
@Test fun missingNativeReadySignalFailsWithBoundedTimeout() = runBlocking {
|
||||
val timed = coordinator(timeout = 20)
|
||||
timed.start(CompletableDeferred("config")) { true }
|
||||
timed.start({ "config" }) { true }
|
||||
val error = withTimeout(2_000) { runCatching { timed.awaitReady() }.exceptionOrNull() }
|
||||
assertTrue(error is IllegalStateException)
|
||||
assertTrue(error!!.message!!.contains("readiness timeout"))
|
||||
@@ -137,18 +137,10 @@ class CoreStartupTest {
|
||||
assertEquals(1, failures)
|
||||
}
|
||||
|
||||
@Test fun nativeStopBeforeReadyFailsInitialStartup() = runBlocking {
|
||||
start(); config.complete("config")
|
||||
startup.coreStopped()
|
||||
assertTrue(runCatching { startup.awaitReady() }.exceptionOrNull() is IllegalStateException)
|
||||
assertEquals(1, failures)
|
||||
}
|
||||
|
||||
@Test fun readyServiceContinuesResynchronizingLaterCoreInstances() = runBlocking {
|
||||
nativeReady = true
|
||||
start(); config.complete("config")
|
||||
startup.awaitReady()
|
||||
startup.coreStopped()
|
||||
startup.coreStarted()
|
||||
assertEquals(2, readyCalls)
|
||||
assertEquals(0, failures)
|
||||
@@ -189,7 +181,7 @@ class CoreStartupTest {
|
||||
}
|
||||
|
||||
@Test fun synchronousReadySignalDuringNativeSubmissionIsRetained() = runBlocking {
|
||||
startup.start(config) {
|
||||
startup.start({ config.await() }) {
|
||||
nativeReady = true
|
||||
startup.coreStarted()
|
||||
true
|
||||
|
||||
Reference in new issue
Block a user