Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ import com.datadog.android.core.feature.event.JvmCrash
import com.datadog.android.core.feature.event.ThreadDump
import com.datadog.android.core.internal.thread.waitToIdle
import com.datadog.android.core.internal.utils.triggerUploadWorker
import com.datadog.android.internal.utils.AllStackTracesProvider
import com.datadog.android.internal.utils.asString
import com.datadog.android.internal.utils.loggableStackTrace
import com.datadog.android.internal.utils.safeGetAllStackTraces
import java.util.concurrent.ThreadPoolExecutor

internal class DatadogExceptionHandler(
private val sdkCore: FeatureSdkCore,
private val appContext: Context
private val appContext: Context,
private val allStackTracesProvider: AllStackTracesProvider = ::safeGetAllStackTraces
) : Thread.UncaughtExceptionHandler {

@Volatile
Expand Down Expand Up @@ -123,7 +126,7 @@ internal class DatadogExceptionHandler(
state = crashedThread.state.asString(),
stack = e.loggableStackTrace()
)
) + safeGetAllStacktraces()
) + captureAllStackTraces()
.filterKeys { it != crashedThread }
.filterValues { it.isNotEmpty() }
.map { (thread, stackTrace) ->
Expand All @@ -136,18 +139,15 @@ internal class DatadogExceptionHandler(
}
}

private fun safeGetAllStacktraces(): Map<Thread, Array<StackTraceElement>> {
return try {
Thread.getAllStackTraces()
} catch (@Suppress("TooGenericExceptionCaught") t: Throwable) {
// coroutines machinery can throw errors here
private fun captureAllStackTraces(): Map<Thread, Array<StackTraceElement>> {
return allStackTracesProvider { message, throwable ->
sdkCore.internalLogger.log(
InternalLogger.Level.ERROR,
InternalLogger.Target.MAINTAINER,
{ "Failed to get all threads dump" },
t
{ message },
throwable,
onlyOnce = true
)
emptyMap()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,36 @@ internal class DatadogExceptionHandlerTest {
}
}

@Test
fun `M log capture failure once W uncaughtException() { stack capture fails }`(
@StringForgery fakeCaptureMessage: String
) {
// Given
val captureError = IllegalStateException(fakeCaptureMessage)
testedHandler = DatadogExceptionHandler(
sdkCore = mockSdkCore,
appContext = appContext.mockInstance,
allStackTracesProvider = { onError ->
onError(fakeCaptureMessage, captureError)
emptyMap()
}
)
testedHandler.register()
val currentThread = Thread.currentThread()

// When
testedHandler.uncaughtException(currentThread, fakeThrowable)

// Then
mockInternalLogger.verifyLog(
InternalLogger.Level.ERROR,
InternalLogger.Target.MAINTAINER,
fakeCaptureMessage,
captureError,
onlyOnce = true
)
}

// endregion

private fun Forge.aThrowableWithoutMessage(): Throwable {
Expand Down
3 changes: 3 additions & 0 deletions dd-sdk-android-internal/api/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,9 @@ fun android.view.View.isValidTapTarget(): Boolean
fun Thread.safeGetThreadId(): Long
fun Thread.State.asString(): String
fun Array<StackTraceElement>.loggableStackTrace(): String
typealias StackTraceErrorHandler = (String, Throwable) -> Unit
typealias AllStackTracesProvider = (StackTraceErrorHandler) -> Map<Thread, Array<StackTraceElement>>
fun safeGetAllStackTraces(StackTraceErrorHandler): Map<Thread, Array<StackTraceElement>>
fun Throwable.loggableStackTrace(): String
fun Throwable.loggableMessage(): String?
fun formatIsoUtc(Long): String
Expand Down
4 changes: 4 additions & 0 deletions dd-sdk-android-internal/api/dd-sdk-android-internal.api
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,10 @@ public final class com/datadog/android/internal/utils/ThreadExtKt {
public static final fun safeGetThreadId (Ljava/lang/Thread;)J
}

public final class com/datadog/android/internal/utils/ThreadStackTracesKt {
public static final fun safeGetAllStackTraces (Lkotlin/jvm/functions/Function2;)Ljava/util/Map;
}

public final class com/datadog/android/internal/utils/ThrowableExtKt {
public static final fun loggableMessage (Ljava/lang/Throwable;)Ljava/lang/String;
public static final fun loggableStackTrace (Ljava/lang/Throwable;)Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.internal.utils

/**
* Receives a message and its cause when a stack trace capture fails.
*/
typealias StackTraceErrorHandler = (message: String, throwable: Throwable) -> Unit

/**
* Captures the stack traces of all live threads, given a [StackTraceErrorHandler].
*/
typealias AllStackTracesProvider = (onError: StackTraceErrorHandler) -> Map<Thread, Array<StackTraceElement>>

/**
* Captures the stack traces of all live threads.
*
* Uses [Thread.getAllStackTraces], which fails entirely if a single thread fails. In that case,
* falls back to capturing each live thread separately. In the fallback, recoverable runtime
* failures (a [SecurityException] or any other [RuntimeException] thrown while capturing a
* single thread) are treated as capture failures: they are reported once via [onError], with a
* stable message (so callers can safely use `onlyOnce`) together with the first error
* encountered, and the affected threads are skipped. Fatal [Error]s (such as
* [OutOfMemoryError]) are never caught and propagate. This fallback is not an atomic snapshot:
* threads are captured one at a time.
*
* @param onError called with a message and the cause of each failure.
* @return the stack traces per thread; empty if nothing could be captured.
*/
fun safeGetAllStackTraces(onError: StackTraceErrorHandler): Map<Thread, Array<StackTraceElement>> {
return safeGetAllStackTraces(
onError = onError,
bulkStackTracesProvider = {
@Suppress("UnsafeThirdPartyFunctionCall")
Thread.getAllStackTraces()
},
liveThreadsProvider = ::enumerateLiveThreads,
threadStackTraceProvider = { it.stackTrace }
)
}

internal fun safeGetAllStackTraces(
onError: StackTraceErrorHandler,
bulkStackTracesProvider: () -> Map<Thread, Array<StackTraceElement>>,
liveThreadsProvider: () -> List<Thread>,
threadStackTraceProvider: (Thread) -> Array<StackTraceElement>
): Map<Thread, Array<StackTraceElement>> {
return try {
bulkStackTracesProvider()
} catch (@Suppress("TooGenericExceptionCaught") t: Throwable) {
// coroutines machinery can throw errors here
onError(BULK_CAPTURE_FAILED, t)
perThreadStackTraces(onError, liveThreadsProvider, threadStackTraceProvider)
}
}

private fun perThreadStackTraces(
onError: StackTraceErrorHandler,
liveThreadsProvider: () -> List<Thread>,
threadStackTraceProvider: (Thread) -> Array<StackTraceElement>
): Map<Thread, Array<StackTraceElement>> {
val threads = try {
liveThreadsProvider()
} catch (e: SecurityException) {
onError(ENUMERATE_FAILED, e)
return emptyMap()
}
val result = LinkedHashMap<Thread, Array<StackTraceElement>>()
var firstFailure: RuntimeException? = null
for (thread in threads) {
try {
result[thread] = threadStackTraceProvider(thread)
} catch (
// recoverable per-thread runtime failures are skipped and aggregated; fatal Errors
// (e.g. OutOfMemoryError) are not caught and must propagate
@Suppress("TooGenericExceptionCaught")
e: RuntimeException
) {
if (firstFailure == null) firstFailure = e
}
}
firstFailure?.let { onError(THREAD_CAPTURE_FAILED, it) }
return result
}

internal fun enumerateLiveThreads(): List<Thread> {
var root = Thread.currentThread().threadGroup ?: return emptyList()
while (true) root = root.parent ?: break
return enumerateLiveThreads(root)
}

@Suppress("UnsafeThirdPartyFunctionCall") // SecurityException is handled by perThreadStackTraces
internal fun enumerateLiveThreads(root: ThreadGroup): List<Thread> {
var buffer = arrayOfNulls<Thread>(root.activeCount() + THREAD_COUNT_MARGIN)
var count = root.enumerate(buffer, true)
var attempts = 1
// ponytail: after MAX_ENUMERATE_ATTEMPTS the list may be truncated; fine for a best-effort dump
while (count == buffer.size && attempts < MAX_ENUMERATE_ATTEMPTS) {
buffer = arrayOfNulls(buffer.size * 2)
count = root.enumerate(buffer, true)
attempts++
}
return buffer.take(count).filterNotNull()
}

private const val THREAD_COUNT_MARGIN = 16
private const val MAX_ENUMERATE_ATTEMPTS = 3
internal const val BULK_CAPTURE_FAILED = "Failed to get all stack traces, falling back to per-thread capture."
internal const val ENUMERATE_FAILED = "Failed to enumerate live threads."
internal const val THREAD_CAPTURE_FAILED =
"Failed to get the stack trace of some thread(s), skipping them."
Loading
Loading