7
7
8
8
package io.bitdrift.capture.events.lifecycle
9
9
10
+ import android.annotation.SuppressLint
10
11
import android.annotation.TargetApi
11
12
import android.app.ActivityManager
12
13
import android.app.ActivityManager.RunningAppProcessInfo
@@ -21,10 +22,13 @@ import io.bitdrift.capture.LoggerImpl
21
22
import io.bitdrift.capture.common.ErrorHandler
22
23
import io.bitdrift.capture.common.Runtime
23
24
import io.bitdrift.capture.common.RuntimeFeature
25
+ import io.bitdrift.capture.providers.FieldValue
24
26
import io.bitdrift.capture.providers.toFields
25
27
import io.bitdrift.capture.utils.BuildVersionChecker
26
28
import java.lang.reflect.InvocationTargetException
27
29
import java.nio.charset.StandardCharsets
30
+ import java.util.regex.Matcher
31
+ import java.util.regex.Pattern
28
32
29
33
internal class AppExitLogger (
30
34
private val logger : LoggerImpl ,
@@ -37,6 +41,15 @@ internal class AppExitLogger(
37
41
38
42
companion object {
39
43
const val APP_EXIT_EVENT_NAME = " AppExit"
44
+
45
+ // TODO Refactor to trace parser class
46
+ val THREAD_HEADER_PATTERN =
47
+ Pattern .compile(" .*----- pid (?<pid>.\\ d+) at (?<timeCreated>\\ d{4}-\\ d{2}-\\ d{2}[T ]{0,}[0-9:.-]+) -----(?<body>.*$)" )
48
+ val TRACE_THREADS_PATTERN = Pattern .compile(
49
+ " .*DALVIK THREADS \\ ((?<threadCnt>\\ d+)\\ ):\\ s(.*)----- end (\\ d+) -----" ,
50
+ Pattern .MULTILINE
51
+ )
52
+ val THREAD_ID_PATTERN = Pattern .compile(" ^\" (?<threadName>.*)\" (.*)prio=(\\ d+).*$" )
40
53
}
41
54
42
55
fun installAppExitLogger () {
@@ -98,6 +111,13 @@ internal class AppExitLogger(
98
111
lastExitInfo.toFields(),
99
112
attributesOverrides = LogAttributesOverrides (sessionId, timestampMs),
100
113
) { APP_EXIT_EVENT_NAME }
114
+
115
+ /* * FIXME
116
+ * AEI records are maintained in a ring buffer that is updated when ART sees fit,
117
+ * So the _last_ AEI record for this PID may appear over several launches.
118
+ * To avoid over reporting of exit reasons, we need to maintain a history of
119
+ * AEI records visited
120
+ */
101
121
}
102
122
103
123
fun logCrash (thread : Thread , throwable : Throwable ) {
@@ -141,7 +161,8 @@ internal class AppExitLogger(
141
161
@TargetApi(Build .VERSION_CODES .R )
142
162
private fun ApplicationExitInfo.toFields (): InternalFieldsMap {
143
163
// https://developer.android.com/reference/kotlin/android/app/ApplicationExitInfo
144
- return mapOf (
164
+
165
+ val appExitProps = mapOf (
145
166
" _app_exit_source" to " ApplicationExitInfo" ,
146
167
" _app_exit_process_name" to this .processName,
147
168
" _app_exit_reason" to this .reason.toReasonText(),
@@ -150,8 +171,11 @@ internal class AppExitLogger(
150
171
" _app_exit_pss" to this .pss.toString(),
151
172
" _app_exit_rss" to this .rss.toString(),
152
173
" _app_exit_description" to this .description.orEmpty(),
153
- // TODO(murki): Extract getTraceInputStream() for REASON_ANR or REASON_CRASH_NATIVE
154
- ).toFields()
174
+ )
175
+
176
+ appExitProps.plus(decomposeSystemTraceToFieldValues(this ))
177
+
178
+ return appExitProps.toFields()
155
179
}
156
180
157
181
private fun Int.toReasonText (): String {
@@ -202,4 +226,67 @@ internal class AppExitLogger(
202
226
else -> LogLevel .INFO
203
227
}
204
228
}
229
+
230
+ /* *
231
+ * For AEI reason types that support it (ANR and NATIVE_CRASH), read the options trace file provided through
232
+ * the getTraceInputStream() method. Parse out the interesting bits from the sys trace
233
+ *
234
+ * @param appExitInfo ApplicationExitInfo record provided by ART
235
+ * @return InternalFieldsMap containing any harvested data
236
+ */
237
+ @SuppressLint(" SwitchIntDef" )
238
+ @TargetApi(Build .VERSION_CODES .R )
239
+ @VisibleForTesting
240
+ fun decomposeSystemTraceToFieldValues (appExitInfo : ApplicationExitInfo ): InternalFieldsMap {
241
+ val traceFields = buildMap<String , FieldValue > { }.toMutableMap()
242
+
243
+ when (appExitInfo.reason) {
244
+ ApplicationExitInfo .REASON_CRASH_NATIVE ,
245
+ ApplicationExitInfo .REASON_ANR -> {
246
+ var sysTrace =
247
+ appExitInfo.traceInputStream?.bufferedReader().use { it?.readText() ? : " " }
248
+
249
+ if (sysTrace.isNotBlank()) {
250
+ // replace newlines with tabs to parse the entire trace as a tab delimited string
251
+ sysTrace = sysTrace.trim().replace(' \n ' , ' \t ' )
252
+
253
+ // ----- pid 4473 at 2024-02-15 23:37:45.593138790-0800 -----
254
+ val headerMatcher: Matcher =
255
+ THREAD_HEADER_PATTERN .matcher(sysTrace)
256
+ if (headerMatcher.matches()) {
257
+ traceFields[" _app_pid" ] =
258
+ FieldValue .StringField (headerMatcher.group(1 )!! .trim())
259
+ traceFields[" _app_timestamp" ] =
260
+ FieldValue .StringField (headerMatcher.group(2 )!! .trim())
261
+ }
262
+
263
+ // DALVIK THREADS (<nThreads>):\n<thread 0>\n<thread 1>...\n<thread n>\n----- end (<pid>) -----
264
+ val threadsMatcher: Matcher = TRACE_THREADS_PATTERN .matcher(sysTrace)
265
+ if (threadsMatcher.matches()) {
266
+ val threadData = threadsMatcher.group(2 )!! .trim()
267
+ traceFields[" _app_threads" ] =
268
+ FieldValue .StringField (parseThreadData(threadData).toString())
269
+ }
270
+
271
+ // TODO Look for additional context info: GC, JNI/JIT, ART internal metrics?
272
+ }
273
+ }
274
+ }
275
+
276
+ return traceFields
277
+ }
278
+
279
+ @VisibleForTesting
280
+ fun parseThreadData (threadData : String? ): List <String > {
281
+ if (! threadData.isNullOrBlank()) {
282
+ val threads = threadData.split(" \t\t " )
283
+ .filter { THREAD_ID_PATTERN .matcher(it).matches() }
284
+ .map { it.replace(" \t " , " \n " ).plus(" \n\n " ) }
285
+
286
+ return threads
287
+ }
288
+
289
+ return arrayListOf ()
290
+ }
291
+
205
292
}
0 commit comments