Skip to content

Don't send fatal uncaught exception if an NDK crash exists #3020

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 1, 2021
Merged
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 @@ -49,6 +49,7 @@ private interface SignalHandlerInstaller {
}

private boolean installHandlerDuringPrepareSession;
private String currentSessionId;
private SignalHandlerInstaller signalHandlerInstaller;

FirebaseCrashlyticsNdk(
Expand All @@ -57,6 +58,11 @@ private interface SignalHandlerInstaller {
this.installHandlerDuringPrepareSession = installHandlerDuringPrepareSession;
}

@Override
public boolean hasCrashDataForCurrentSession() {
return currentSessionId != null && hasCrashDataForSession(currentSessionId);
}

@Override
public boolean hasCrashDataForSession(@NonNull String sessionId) {
return controller.hasCrashDataForSession(sessionId);
Expand All @@ -74,6 +80,7 @@ public synchronized void prepareNativeSession(
long startedAtSeconds,
@NonNull StaticSessionData sessionData) {

currentSessionId = sessionId;
signalHandlerInstaller =
() -> {
Logger.getLogger().d("Initializing native session: " + sessionId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

public interface CrashlyticsNativeComponent {

boolean hasCrashDataForCurrentSession();

boolean hasCrashDataForSession(@NonNull String sessionId);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ public CrashlyticsNativeComponentDeferredProxy(
});
}

@Override
public boolean hasCrashDataForCurrentSession() {
CrashlyticsNativeComponent component = availableNativeComponent.get();
return component != null && component.hasCrashDataForCurrentSession();
}

@Override
public boolean hasCrashDataForSession(@NonNull String sessionId) {
CrashlyticsNativeComponent component = availableNativeComponent.get();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ public void onUncaughtException(
}
};
crashHandler =
new CrashlyticsUncaughtExceptionHandler(crashListener, settingsProvider, defaultHandler);
new CrashlyticsUncaughtExceptionHandler(
crashListener, settingsProvider, defaultHandler, nativeComponent);
Thread.setDefaultUncaughtExceptionHandler(crashHandler);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,44 +14,47 @@

package com.google.firebase.crashlytics.internal.common;

import com.google.firebase.crashlytics.internal.CrashlyticsNativeComponent;
import com.google.firebase.crashlytics.internal.Logger;
import com.google.firebase.crashlytics.internal.settings.SettingsDataProvider;
import java.util.concurrent.atomic.AtomicBoolean;

class CrashlyticsUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {

interface CrashListener {

void onUncaughtException(
SettingsDataProvider settingsDataProvider, Thread thread, Throwable ex);
}

private final CrashListener crashListener;
private final SettingsDataProvider settingsDataProvider;
private final Thread.UncaughtExceptionHandler defaultHandler;
private final CrashlyticsNativeComponent nativeComponent;

// use AtomicBoolean because value is accessible from other threads.
private final AtomicBoolean isHandlingException;

public CrashlyticsUncaughtExceptionHandler(
CrashListener crashListener,
SettingsDataProvider settingsProvider,
Thread.UncaughtExceptionHandler defaultHandler) {
Thread.UncaughtExceptionHandler defaultHandler,
CrashlyticsNativeComponent nativeComponent) {
this.crashListener = crashListener;
this.settingsDataProvider = settingsProvider;
this.defaultHandler = defaultHandler;
this.isHandlingException = new AtomicBoolean(false);
this.nativeComponent = nativeComponent;
}

@Override
public void uncaughtException(Thread thread, Throwable ex) {
isHandlingException.set(true);
try {
if (thread == null) {
Logger.getLogger().e("Could not handle uncaught exception; null thread");
} else if (ex == null) {
Logger.getLogger().e("Could not handle uncaught exception; null throwable");
} else {
if (shouldRecordUncaughtException(thread, ex)) {
crashListener.onUncaughtException(settingsDataProvider, thread, ex);
} else {
Logger.getLogger().d("Uncaught exception will not be recorded by Crashlytics.");
}
} catch (Exception e) {
Logger.getLogger().e("An error occurred in the uncaught exception handler", e);
Expand All @@ -65,4 +68,31 @@ public void uncaughtException(Thread thread, Throwable ex) {
boolean isHandlingException() {
return isHandlingException.get();
}

/**
* Returns true if Crashlytics should record this exception. The decision to record is different
* than the decision to report the exception to Crashlytics servers, which is handled by the
* {@link DataCollectionArbiter}
*
* @return false if the thread or exception is null, or if a native crash already exists for this
* session.
*/
private boolean shouldRecordUncaughtException(Thread thread, Throwable ex) {
if (thread == null) {
Logger.getLogger().e("Crashlytics will not record uncaught exception; null thread");
return false;
}
if (ex == null) {
Logger.getLogger().e("Crashlytics will not record uncaught exception; null throwable");
return false;
}
// We should only report at most one fatal event for a session. If a native fatal already exists
// for this session, ignore the uncaught exception
if (nativeComponent.hasCrashDataForCurrentSession()) {
Logger.getLogger()
.d("Crashlytics will not record uncaught exception; native crash exists for session.");
return false;
}
return true;
}
}