Skip to content

Commit

Permalink
feat(ExitInfo)add exit infos at first run
Browse files Browse the repository at this point in the history
  • Loading branch information
YYChen01988 committed Nov 5, 2024
1 parent afac6be commit cb823c5
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 32 deletions.
1 change: 1 addition & 0 deletions bugsnag-plugin-android-exitinfo/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<ID>MaxLineLength:TraceParserNativeStackframeTest.kt$TraceParserNativeStackframeTest.Companion$"void* std::__1::__thread_proxy&lt;std::__1::tuple&lt;std::__1::unique_ptr&lt;std::__1::__thread_struct, std::__1::default_delete&lt;std::__1::__thread_struct> >, android::uirenderer::CommonPool::CommonPool()::\$_0> >(void*) (.__uniq.99815402873434996937524029735804459536)"</ID>
<ID>MaxLineLength:TraceParserTest.kt$TraceParserTest$"void* std::__1::__thread_proxy&lt;std::__1::tuple&lt;std::__1::unique_ptr&lt;std::__1::__thread_struct, std::__1::default_delete&lt;std::__1::__thread_struct> >, void (android::AsyncWorker::*)(), android::AsyncWorker*> >(void*)"</ID>
<ID>NestedBlockDepth:TraceParser.kt$TraceParser$private fun parseThreadAttributes(line: String)</ID>
<ID>ReturnCount:EventSynthesizer.kt$EventSynthesizer$fun createEventWithExitInfo(appExitInfo: ApplicationExitInfo): Event?</ID>
<ID>ReturnCount:TraceParser.kt$TraceParser$@VisibleForTesting internal fun parseNativeFrame(line: String): Stackframe?</ID>
<ID>SwallowedException:BugsnagExitInfoPlugin.kt$BugsnagExitInfoPlugin$e: Exception</ID>
<ID>SwallowedException:ExitInfoCallback.kt$ExitInfoCallback$exc: Throwable</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.bugsnag.android.internal.ImmutableConfig
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Test
import java.io.File
Expand Down Expand Up @@ -71,7 +71,7 @@ internal class BugsnagExitInfoPluginStoreTest {
fun writableFileWithEmptyExitInfo() {
exitInfoPluginStore.persist(12345, emptySet())
val firstPid = exitInfoPluginStore.load().first
assertNull(firstPid)
assertNotNull(firstPid)
exitInfoPluginStore = ExitInfoPluginStore(immutableConfig)
val (storedPid, storageExitInfoKeys) = exitInfoPluginStore.load()
assertEquals(12345, storedPid)
Expand All @@ -85,8 +85,8 @@ internal class BugsnagExitInfoPluginStoreTest {
exitInfoPluginStore.persist(expectedPid, expectedExitInfoKeys)

val (storedPid, storageExitInfoKeys) = exitInfoPluginStore.load()
assertNull(storedPid)
assertEquals(emptySet<ExitInfoKey>(), storageExitInfoKeys)
assertNotNull(storedPid)
assertEquals(expectedExitInfoKeys, storageExitInfoKeys)

exitInfoPluginStore = ExitInfoPluginStore(immutableConfig)
val (storedPid2, storageExitInfoKeys2) = exitInfoPluginStore.load()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ class BugsnagExitInfoPlugin @JvmOverloads constructor(

private val configuration = configuration.copy()

private var reportUnmatchedAnrs: Boolean = true
private var reportUnmatchedNativeCrashes: Boolean = true

@SuppressLint("VisibleForTests")
override fun load(client: Client) {
if (!configuration.disableProcessStateSummaryOverride) {
Expand All @@ -38,6 +41,7 @@ class BugsnagExitInfoPlugin @JvmOverloads constructor(

val exitInfoPluginStore =
ExitInfoPluginStore(client.immutableConfig)
addAllExitInfoAtFirstRun(client, exitInfoPluginStore)
val (oldPid, exitInfoKeys) = exitInfoPluginStore.load()
exitInfoPluginStore.persist(android.os.Process.myPid(), exitInfoKeys)

Expand All @@ -59,6 +63,22 @@ class BugsnagExitInfoPlugin @JvmOverloads constructor(
client.addOnSend(exitInfoCallback)
}

private fun addAllExitInfoAtFirstRun(
client: Client,
exitInfoPluginStore: ExitInfoPluginStore
) {
if (exitInfoPluginStore.isFirstRun || exitInfoPluginStore.legacyStore) {
val am: ActivityManager =
client.appContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val allExitInfo: List<ApplicationExitInfo> =
am.getHistoricalProcessExitReasons(client.appContext.packageName, 0, 100)

allExitInfo.forEach { exitInfo ->
exitInfoPluginStore.addExitInfoKey(ExitInfoKey(exitInfo.pid, exitInfo.timestamp))
}
}
}

private fun createExitInfoCallback(
client: Client,
oldPid: Int?,
Expand All @@ -82,7 +102,9 @@ class BugsnagExitInfoPlugin @JvmOverloads constructor(
val eventSynthesizer = EventSynthesizer(
traceEventEnhancer,
tombstoneEventEnhancer,
exitInfoPluginStore
exitInfoPluginStore,
reportUnmatchedAnrs,
reportUnmatchedNativeCrashes
)
val context = client.appContext
val am: ActivityManager = context.safeGetActivityManager() ?: return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@ import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE
import android.app.ActivityManager.RunningAppProcessInfo.REASON_PROVIDER_IN_USE
import android.app.ActivityManager.RunningAppProcessInfo.REASON_SERVICE_IN_USE
import android.app.ApplicationExitInfo
import android.app.ApplicationExitInfo.REASON_ANR
import android.app.ApplicationExitInfo.REASON_CRASH_NATIVE
import android.os.Build
import androidx.annotation.RequiresApi

@RequiresApi(Build.VERSION_CODES.R)
internal class EventSynthesizer(
private val anrEventEnhancer: (Event, ApplicationExitInfo) -> Unit,
private val nativeEnhancer: (Event, ApplicationExitInfo) -> Unit,
private val exitInfoPluginStore: ExitInfoPluginStore
private val exitInfoPluginStore: ExitInfoPluginStore,
private val reportUnmatchedAnrs: Boolean,
private val reportUnmatchedNativeCrashes: Boolean
) {
fun createEventWithExitInfo(appExitInfo: ApplicationExitInfo): Event? {
val (_, knownExitInfoKeys) = exitInfoPluginStore.load()
Expand All @@ -31,31 +35,37 @@ internal class EventSynthesizer(
)

when (appExitInfo.reason) {
ApplicationExitInfo.REASON_ANR
REASON_ANR
-> {
val newAnrEvent = InternalHooks.createEmptyANR(exitInfoKey.timestamp)
addExitInfoMetadata(newAnrEvent, appExitInfo)
anrEventEnhancer(newAnrEvent, appExitInfo)
val thread =
newAnrEvent.threads.find { it.name == "main" }
?: newAnrEvent.threads.firstOrNull()
val error = newAnrEvent.addError("ANR", appExitInfo.description)
thread?.let { error.stacktrace.addAll(it.stacktrace) }
if (reportUnmatchedAnrs) {
val newAnrEvent = InternalHooks.createEmptyANR(exitInfoKey.timestamp)
addExitInfoMetadata(newAnrEvent, appExitInfo)
anrEventEnhancer(newAnrEvent, appExitInfo)
val thread =
newAnrEvent.threads.find { it.name == "main" }
?: newAnrEvent.threads.firstOrNull()
val error = newAnrEvent.addError("ANR", appExitInfo.description)
thread?.let { error.stacktrace.addAll(it.stacktrace) }

return newAnrEvent
return newAnrEvent
}
return null
}

ApplicationExitInfo.REASON_CRASH_NATIVE
REASON_CRASH_NATIVE
-> {
val newNativeEvent = InternalHooks.createEmptyCrash(exitInfoKey.timestamp)
addExitInfoMetadata(newNativeEvent, appExitInfo)
nativeEnhancer(newNativeEvent, appExitInfo)
val thread =
newNativeEvent.threads.find { it.name == "main" }
?: newNativeEvent.threads.firstOrNull()
val error = newNativeEvent.addError("Native", appExitInfo.description)
thread?.let { error.stacktrace.addAll(it.stacktrace) }
return newNativeEvent
if (reportUnmatchedNativeCrashes) {
val newNativeEvent = InternalHooks.createEmptyCrash(exitInfoKey.timestamp)
addExitInfoMetadata(newNativeEvent, appExitInfo)
nativeEnhancer(newNativeEvent, appExitInfo)
val thread =
newNativeEvent.threads.find { it.name == "main" }
?: newNativeEvent.threads.firstOrNull()
val error = newNativeEvent.addError("Native", appExitInfo.description)
thread?.let { error.stacktrace.addAll(it.stacktrace) }
return newNativeEvent
}
return null
}

else -> return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ internal class ExitInfoCallback(
exitInfo.reason == ApplicationExitInfo.REASON_SIGNALED
) {
nativeEnhancer(event, exitInfo)
exitInfoPluginStore?.addExitInfoKey(ExitInfoKey(exitInfo))
} else if (exitInfo.reason == ApplicationExitInfo.REASON_ANR) {
anrEventEnhancer(event, exitInfo)
exitInfoPluginStore?.addExitInfoKey(ExitInfoKey(exitInfo))
}
} catch (exc: Throwable) {
return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ internal class ExitInfoPluginStore(config: ImmutableConfig) {
private val file: File = File(config.persistenceDirectory.value, "bugsnag-exit-reasons")
private val logger: Logger = config.logger
private val lock = ReentrantReadWriteLock()
private val isFirstRun: Boolean = !file.exists()
internal val isFirstRun: Boolean = !file.exists()

internal var legacyStore: Boolean = false
private set

fun persist(currentPid: Int, exitInfoKeys: Set<ExitInfoKey>) {
lock.writeLock().withLock {
Expand All @@ -31,9 +34,6 @@ internal class ExitInfoPluginStore(config: ImmutableConfig) {
}

fun load(): Pair<Int?, Set<ExitInfoKey>> {
if (isFirstRun) {
return null to emptySet()
}
return tryLoadJson() ?: (tryLoadLegacy() to emptySet())
}

Expand Down Expand Up @@ -62,6 +62,7 @@ internal class ExitInfoPluginStore(config: ImmutableConfig) {
private fun tryLoadLegacy(): Int? {
try {
val content = file.readText()
legacyStore = true
if (content.isEmpty()) {
logger.w("PID is empty")
return null
Expand All @@ -75,7 +76,7 @@ internal class ExitInfoPluginStore(config: ImmutableConfig) {

fun addExitInfoKey(exitInfoKey: ExitInfoKey) {
val (oldPid, exitInfoKeys) = load()
val newExitInfoKeys = exitInfoKeys.toMutableSet().plus(exitInfoKey)
val newExitInfoKeys = exitInfoKeys + exitInfoKey
oldPid?.let { persist(it, newExitInfoKeys) }
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
package com.example.bugsnag.android

import android.app.ActivityManager
import android.app.Application
import android.app.ApplicationExitInfo
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import com.bugsnag.android.Bugsnag
import com.bugsnag.android.BugsnagExitInfoPlugin
import com.bugsnag.android.Configuration
import com.bugsnag.android.okhttp.BugsnagOkHttpPlugin
import okhttp3.OkHttpClient
import java.io.File

@RequiresApi(Build.VERSION_CODES.R)
class ExampleApplication : Application() {

private val bugsnagOkHttpPlugin = BugsnagOkHttpPlugin()
private val exitInfoPlugin = BugsnagExitInfoPlugin()
val httpClient = OkHttpClient.Builder()
.eventListener(bugsnagOkHttpPlugin)
.build()
Expand All @@ -36,6 +44,36 @@ class ExampleApplication : Application() {
config.setUser("123456", "joebloggs@example.com", "Joe Bloggs")
config.addMetadata("user", "age", 31)
config.addPlugin(bugsnagOkHttpPlugin)
config.addPlugin(exitInfoPlugin)


if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
(getSystemService(ACTIVITY_SERVICE) as ActivityManager)
.getHistoricalProcessExitReasons(packageName, 0, 1000).forEach { r ->
val reason = when(r.reason) {
ApplicationExitInfo.REASON_UNKNOWN -> "unknown reason (${r.reason})"
ApplicationExitInfo.REASON_EXIT_SELF -> "exit self"
ApplicationExitInfo.REASON_SIGNALED -> "signaled"
ApplicationExitInfo.REASON_LOW_MEMORY -> "low memory"
ApplicationExitInfo.REASON_CRASH -> "crash"
ApplicationExitInfo.REASON_CRASH_NATIVE -> "crash native"
ApplicationExitInfo.REASON_ANR -> "ANR"
ApplicationExitInfo.REASON_INITIALIZATION_FAILURE -> "initialization failure"
ApplicationExitInfo.REASON_PERMISSION_CHANGE -> "permission change"
ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE -> "excessive resource usage"
ApplicationExitInfo.REASON_USER_REQUESTED -> "user requested"
ApplicationExitInfo.REASON_USER_STOPPED -> "user stopped"
ApplicationExitInfo.REASON_DEPENDENCY_DIED -> "dependency died"
ApplicationExitInfo.REASON_OTHER -> "other"
ApplicationExitInfo.REASON_FREEZER -> "freezer"
ApplicationExitInfo.REASON_PACKAGE_STATE_CHANGE -> "package state change"
ApplicationExitInfo.REASON_PACKAGE_UPDATED -> "package updated"
else -> "unknown reason (${r.reason})"
}
Log.i("ExitReason", "${r.description} / $reason")
}}



// Configure the persistence directory when running MultiProcessActivity in a separate
// process to ensure the two Bugsnag clients are independent
Expand All @@ -50,4 +88,4 @@ class ExampleApplication : Application() {
performNativeBugsnagSetup()
}

}
}
10 changes: 10 additions & 0 deletions features/smoke_tests/04_unhandled.feature
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,16 @@ Feature: Unhandled smoke tests
And the event "metaData.opaque.array.1" equals "b"
And the event "metaData.opaque.array.2" equals "c"


@skip_below_android_12
Scenario: Signal raised with exit info
When I set the screen orientation to portrait
And I run "CXXSignalSmokeScenario" and relaunch the crashed app
And I configure Bugsnag for "CXXSignalSmokeScenario"
And I wait to receive an error
And the event "metaData.Open FileDescriptors" is not null
And the event "metaData.app.exitReason" equals "crash native"

@debug-safe
Scenario: C++ exception thrown with overwritten config
When I set the screen orientation to portrait
Expand Down
4 changes: 4 additions & 0 deletions features/support/env.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
skip_this_scenario("Skipping scenario") if Maze.config.os_version < 11
end

Before('@skip_below_android_12') do |scenario|
skip_this_scenario("Skipping scenario") if Maze.config.os_version < 12
end

Before('@skip_below_android_9') do |scenario|
skip_this_scenario("Skipping scenario") if Maze.config.os_version < 9
end
Expand Down

0 comments on commit cb823c5

Please sign in to comment.