mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 11:16:12 -04:00
fix: Stop suppressing foreground service timing exceptions
Suppressing ForegroundServiceDidNotStartInTimeException and re-entering Looper.loop() left zombie processes behind that kept collecting ANRs. Always delegate to the previous handler instead.
This commit is contained in:
@@ -1,73 +1,19 @@
|
|||||||
package eu.darken.capod
|
package eu.darken.capod
|
||||||
|
|
||||||
import android.os.Looper
|
|
||||||
import eu.darken.capod.common.debug.Bugs
|
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
|
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
|
||||||
import eu.darken.capod.common.debug.logging.asLog
|
import eu.darken.capod.common.debug.logging.asLog
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
internal class CapodUncaughtExceptionHandler(
|
internal class CapodUncaughtExceptionHandler(
|
||||||
private val previousHandler: Thread.UncaughtExceptionHandler?,
|
private val previousHandler: Thread.UncaughtExceptionHandler?,
|
||||||
private val mainThreadProvider: () -> Thread = { Looper.getMainLooper().thread },
|
|
||||||
private val loopMainThread: () -> Unit = { Looper.loop() },
|
|
||||||
private val reportForegroundServiceTimingException: (Throwable) -> Unit = { throwable ->
|
|
||||||
Bugs.report(
|
|
||||||
tag = App.TAG,
|
|
||||||
message = "Foreground service timing exception suppressed",
|
|
||||||
exception = throwable,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
private val cancelBeforeDelegate: (Throwable) -> Unit = {},
|
private val cancelBeforeDelegate: (Throwable) -> Unit = {},
|
||||||
private val exit: (Int) -> Unit = { exitProcess(it) },
|
private val exit: (Int) -> Unit = { exitProcess(it) },
|
||||||
) : Thread.UncaughtExceptionHandler {
|
) : Thread.UncaughtExceptionHandler {
|
||||||
|
|
||||||
private val foregroundExceptionHandled = AtomicBoolean(false)
|
|
||||||
|
|
||||||
override fun uncaughtException(thread: Thread, throwable: Throwable) {
|
override fun uncaughtException(thread: Thread, throwable: Throwable) {
|
||||||
if (shouldSuppress(thread, throwable)) {
|
|
||||||
runCatching {
|
|
||||||
log(App.TAG, WARN) { "Suppressed foreground service timing exception: ${throwable.asLog()}" }
|
|
||||||
reportForegroundServiceTimingException(throwable)
|
|
||||||
}
|
|
||||||
|
|
||||||
val loopResult = runCatching { loopMainThread() }
|
|
||||||
if (loopResult.isSuccess) return
|
|
||||||
|
|
||||||
val loopFailure = loopResult.exceptionOrNull()!!
|
|
||||||
runCatching {
|
|
||||||
log(App.TAG, ERROR) {
|
|
||||||
"Main loop failed after foreground service timing exception suppression: ${loopFailure.asLog()}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delegate(thread, loopFailure)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
runCatching { log(App.TAG, ERROR) { "UNCAUGHT EXCEPTION: ${throwable.asLog()}" } }
|
runCatching { log(App.TAG, ERROR) { "UNCAUGHT EXCEPTION: ${throwable.asLog()}" } }
|
||||||
delegate(thread, throwable)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shouldSuppress(thread: Thread, throwable: Throwable): Boolean {
|
|
||||||
val isMainThread = runCatching { thread === mainThreadProvider() }.getOrDefault(false)
|
|
||||||
return throwable.isForegroundServiceTimingException() &&
|
|
||||||
isMainThread &&
|
|
||||||
foregroundExceptionHandled.compareAndSet(false, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun delegate(thread: Thread, throwable: Throwable) {
|
|
||||||
runCatching { cancelBeforeDelegate(throwable) }
|
runCatching { cancelBeforeDelegate(throwable) }
|
||||||
previousHandler?.uncaughtException(thread, throwable) ?: exit(1)
|
previousHandler?.uncaughtException(thread, throwable) ?: exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun Throwable.isForegroundServiceTimingException(): Boolean {
|
|
||||||
var current: Throwable? = this
|
|
||||||
while (current != null) {
|
|
||||||
if (current.javaClass.simpleName == "ForegroundServiceDidNotStartInTimeException") return true
|
|
||||||
current = current.cause
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,67 +7,17 @@ import testhelpers.BaseTest
|
|||||||
class CapodUncaughtExceptionHandlerTest : BaseTest() {
|
class CapodUncaughtExceptionHandlerTest : BaseTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `suppresses first main thread foreground service timing exception`() {
|
fun `delegates foreground service timing exception`() {
|
||||||
val mainThread = Thread.currentThread()
|
val mainThread = Thread.currentThread()
|
||||||
val previousHandler = RecordingHandler()
|
val previousHandler = RecordingHandler()
|
||||||
val reports = mutableListOf<Throwable>()
|
|
||||||
var loopCalls = 0
|
|
||||||
val handler = CapodUncaughtExceptionHandler(
|
val handler = CapodUncaughtExceptionHandler(
|
||||||
previousHandler = previousHandler,
|
previousHandler = previousHandler,
|
||||||
mainThreadProvider = { mainThread },
|
|
||||||
loopMainThread = { loopCalls++ },
|
|
||||||
reportForegroundServiceTimingException = { reports += it },
|
|
||||||
exit = { throw AssertionError("exitProcess($it)") },
|
exit = { throw AssertionError("exitProcess($it)") },
|
||||||
)
|
)
|
||||||
val throwable = ForegroundServiceDidNotStartInTimeException()
|
val throwable = ForegroundServiceDidNotStartInTimeException()
|
||||||
|
|
||||||
handler.uncaughtException(mainThread, throwable)
|
handler.uncaughtException(mainThread, throwable)
|
||||||
|
|
||||||
loopCalls shouldBe 1
|
|
||||||
reports shouldBe listOf(throwable)
|
|
||||||
previousHandler.throwables shouldBe emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `delegates repeated main thread foreground service timing exception`() {
|
|
||||||
val mainThread = Thread.currentThread()
|
|
||||||
val previousHandler = RecordingHandler()
|
|
||||||
val reports = mutableListOf<Throwable>()
|
|
||||||
var loopCalls = 0
|
|
||||||
val handler = CapodUncaughtExceptionHandler(
|
|
||||||
previousHandler = previousHandler,
|
|
||||||
mainThreadProvider = { mainThread },
|
|
||||||
loopMainThread = { loopCalls++ },
|
|
||||||
reportForegroundServiceTimingException = { reports += it },
|
|
||||||
exit = { throw AssertionError("exitProcess($it)") },
|
|
||||||
)
|
|
||||||
val first = ForegroundServiceDidNotStartInTimeException()
|
|
||||||
val second = ForegroundServiceDidNotStartInTimeException()
|
|
||||||
|
|
||||||
handler.uncaughtException(mainThread, first)
|
|
||||||
handler.uncaughtException(mainThread, second)
|
|
||||||
|
|
||||||
loopCalls shouldBe 1
|
|
||||||
reports shouldBe listOf(first)
|
|
||||||
previousHandler.throwables shouldBe listOf(second)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `delegates foreground service timing exception from non-main thread`() {
|
|
||||||
val mainThread = Thread.currentThread()
|
|
||||||
val workerThread = Thread()
|
|
||||||
val previousHandler = RecordingHandler()
|
|
||||||
val handler = CapodUncaughtExceptionHandler(
|
|
||||||
previousHandler = previousHandler,
|
|
||||||
mainThreadProvider = { mainThread },
|
|
||||||
loopMainThread = { throw AssertionError("loopMainThread should not run") },
|
|
||||||
reportForegroundServiceTimingException = { throw AssertionError("report should not run") },
|
|
||||||
exit = { throw AssertionError("exitProcess($it)") },
|
|
||||||
)
|
|
||||||
val throwable = ForegroundServiceDidNotStartInTimeException()
|
|
||||||
|
|
||||||
handler.uncaughtException(workerThread, throwable)
|
|
||||||
|
|
||||||
previousHandler.throwables shouldBe listOf(throwable)
|
previousHandler.throwables shouldBe listOf(throwable)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,9 +27,6 @@ class CapodUncaughtExceptionHandlerTest : BaseTest() {
|
|||||||
val previousHandler = RecordingHandler()
|
val previousHandler = RecordingHandler()
|
||||||
val handler = CapodUncaughtExceptionHandler(
|
val handler = CapodUncaughtExceptionHandler(
|
||||||
previousHandler = previousHandler,
|
previousHandler = previousHandler,
|
||||||
mainThreadProvider = { mainThread },
|
|
||||||
loopMainThread = { throw AssertionError("loopMainThread should not run") },
|
|
||||||
reportForegroundServiceTimingException = { throw AssertionError("report should not run") },
|
|
||||||
exit = { throw AssertionError("exitProcess($it)") },
|
exit = { throw AssertionError("exitProcess($it)") },
|
||||||
)
|
)
|
||||||
val throwable = IllegalStateException("boom")
|
val throwable = IllegalStateException("boom")
|
||||||
@@ -101,9 +48,6 @@ class CapodUncaughtExceptionHandlerTest : BaseTest() {
|
|||||||
val throwable = IllegalStateException("boom")
|
val throwable = IllegalStateException("boom")
|
||||||
val handler = CapodUncaughtExceptionHandler(
|
val handler = CapodUncaughtExceptionHandler(
|
||||||
previousHandler = previousHandler,
|
previousHandler = previousHandler,
|
||||||
mainThreadProvider = { mainThread },
|
|
||||||
loopMainThread = { throw AssertionError("loopMainThread should not run") },
|
|
||||||
reportForegroundServiceTimingException = { throw AssertionError("report should not run") },
|
|
||||||
cancelBeforeDelegate = { events += "cancel" },
|
cancelBeforeDelegate = { events += "cancel" },
|
||||||
exit = { throw AssertionError("exitProcess($it)") },
|
exit = { throw AssertionError("exitProcess($it)") },
|
||||||
)
|
)
|
||||||
@@ -114,21 +58,19 @@ class CapodUncaughtExceptionHandlerTest : BaseTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `delegates loop failure after suppression`() {
|
fun `delegates even when cancelBeforeDelegate throws`() {
|
||||||
val mainThread = Thread.currentThread()
|
val mainThread = Thread.currentThread()
|
||||||
val previousHandler = RecordingHandler()
|
val previousHandler = RecordingHandler()
|
||||||
val loopFailure = IllegalStateException("loop failed")
|
val throwable = IllegalStateException("boom")
|
||||||
val handler = CapodUncaughtExceptionHandler(
|
val handler = CapodUncaughtExceptionHandler(
|
||||||
previousHandler = previousHandler,
|
previousHandler = previousHandler,
|
||||||
mainThreadProvider = { mainThread },
|
cancelBeforeDelegate = { throw IllegalStateException("shutdown failed") },
|
||||||
loopMainThread = { throw loopFailure },
|
|
||||||
reportForegroundServiceTimingException = {},
|
|
||||||
exit = { throw AssertionError("exitProcess($it)") },
|
exit = { throw AssertionError("exitProcess($it)") },
|
||||||
)
|
)
|
||||||
|
|
||||||
handler.uncaughtException(mainThread, ForegroundServiceDidNotStartInTimeException())
|
handler.uncaughtException(mainThread, throwable)
|
||||||
|
|
||||||
previousHandler.throwables shouldBe listOf(loopFailure)
|
previousHandler.throwables shouldBe listOf(throwable)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RecordingHandler : Thread.UncaughtExceptionHandler {
|
private class RecordingHandler : Thread.UncaughtExceptionHandler {
|
||||||
|
|||||||
Reference in New Issue
Block a user