Contain headless foreground, notification and teardown failures

This commit is contained in:
evgeny committed 2026-10-29 13:34:26 +01:00
1 parent d7fb8568b2
commit 6916575380
3 files changed
+41 -13

No files matched your search

@@ -75,7 +75,8 @@ internal class CoreStartup(
closed = true
ready.completeExceptionally(error)
log("ERROR", "core startup failed: ${error.javaClass.simpleName}: ${error.message}")
onFailure()
try { onFailure() }
catch (e: Exception) { log("ERROR", "startup failure cleanup callback failed: ${e.javaClass.simpleName}: ${e.message}") }
}
/** Сначала закрывает готовность; поздний callback и готовый конфиг уже не могут завершить запуск. */
@@ -25,6 +25,7 @@ import kotlinx.coroutines.withContext
/** Владеет запуском ядра/сетевых наблюдателей; готовность подтверждается событием native SERVICE_STARTED. */
class HeadlessService : Service() {
private var foregroundReady = false
private val startupScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
private val startup = CoreStartup(startupScope, NativeLib::isReady, onReady = {
LogManager.addLog("INFO", "SRV", "uTun core started")
@@ -51,13 +52,18 @@ class HeadlessService : Service() {
fun refreshLanguage() {
currentService?.let { service ->
service.createNotificationChannel()
service.getSystemService(NotificationManager::class.java).notify(1, service.notification())
try {
if (!service.foregroundReady) return
service.createNotificationChannel()
service.getSystemService(NotificationManager::class.java).notify(1, service.notification())
} catch (e: Exception) { LogManager.addLog("ERROR", "SRV", "Service notification language update failed: $e") }
}
}
fun refreshFiles() {
currentService?.let { service ->
service.getSystemService(NotificationManager::class.java).notify(1, service.notification())
try {
if (service.foregroundReady) service.getSystemService(NotificationManager::class.java).notify(1, service.notification())
} catch (e: Exception) { LogManager.addLog("ERROR", "SRV", "Service file notification update failed: $e") }
}
}
}
@@ -65,8 +71,16 @@ class HeadlessService : Service() {
override fun onCreate() {
super.onCreate()
currentService = this
createNotificationChannel()
startForeground(1, notification())
try {
createNotificationChannel()
startForeground(1, notification())
foregroundReady = true
LogManager.addLog("INFO", "SRV", "Core service foreground confirmed")
} catch (e: Exception) {
LogManager.addLog("ERROR", "SRV", "Core service foreground failed: $e")
startup.fail(e)
return
}
startup.start(prepare = {
shutdown.await()
@@ -82,19 +96,23 @@ class HeadlessService : Service() {
}
override fun onDestroy() {
startup.stop()
startupScope.cancel()
foregroundReady = false
if (currentService === this) currentService = null
LogManager.addLog("INFO", "SRV", "uTun service stopping")
NetworkChangeMonitor.stop()
NetworkActivityMonitor.stop()
shutdown.request()
super.onDestroy()
try { startup.stop() } catch (e: Exception) { LogManager.addLog("ERROR", "SRV", "Startup cancellation failed: $e") }
try { startupScope.cancel() } catch (e: Exception) { LogManager.addLog("ERROR", "SRV", "Startup scope cancellation failed: $e") }
try { NetworkChangeMonitor.stop() } catch (e: Exception) { LogManager.addLog("ERROR", "SRV", "Network observer stop failed: $e") }
try { NetworkActivityMonitor.stop() } catch (e: Exception) { LogManager.addLog("ERROR", "SRV", "Network activity stop failed: $e") }
try { shutdown.request() } finally { super.onDestroy() }
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (!foregroundReady) {
LogManager.addLog("ERROR", "SRV", "Service command rejected before foreground confirmation startId=$startId")
return START_NOT_STICKY
}
if (intent?.action == "com.utun.chat.CANCEL_FILES") {
ChatApplication.instance.memberFiles.cancel()
return START_STICKY
@@ -124,7 +142,7 @@ class HeadlessService : Service() {
val detail = operation.status?.let { AppLanguage.text(fileStatusResource(it)) } ?: AppLanguage.text(operation.stage)
builder.setContentText("${operation.name}: $detail")
if (operation.active) {
val percent = if (operation.total == 0L) 0 else (operation.done.toDouble() * 100 / operation.total).toInt()
val percent = if (operation.total <= 0L) 0 else (operation.done.toDouble() * 100 / operation.total).toInt().coerceIn(0, 100)
builder.setProgress(100, percent, operation.total == 0L)
val cancel = PendingIntent.getService(this, 21, Intent(this, HeadlessService::class.java)
.setAction("com.utun.chat.CANCEL_FILES"), PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
@@ -189,4 +189,13 @@ class CoreStartupTest {
config.complete("config")
assertEquals(1, waiter().await())
}
@Test fun failingStopCallbackCannotMaskTheStartupFailureOrEscapeToMain() = runBlocking {
val logs = mutableListOf<String>()
val owner = CoreStartup(scope, { false }, {}, { error("stopSelf rejected") }, { _, message -> logs.add(message) })
val failure = IllegalStateException("foreground denied")
owner.fail(failure)
val result = runCatching { owner.awaitReady() }.exceptionOrNull()
assertEquals("foreground denied", result?.message)
assertTrue(logs.any { "stopSelf rejected" in it })
}
}