Skip to content
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
4 changes: 2 additions & 2 deletions firebase-crashlytics-ndk/firebase-crashlytics-ndk.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ android {
}

ndkVersion "25.1.8937393"
compileSdkVersion project.targetSdkVersion
compileSdkVersion 33
defaultConfig {
minSdkVersion 16
targetSdkVersion project.targetSdkVersion
targetSdkVersion 33
versionName version

externalNativeBuild {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,39 @@

package com.google.firebase.crashlytics.ndk;

import android.app.ActivityManager;
import android.app.ApplicationExitInfo;
import android.content.Context;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import com.google.firebase.crashlytics.internal.Logger;
import com.google.firebase.crashlytics.internal.common.CommonUtils;
import com.google.firebase.crashlytics.internal.model.CrashlyticsReport;
import com.google.firebase.crashlytics.internal.model.StaticSessionData;
import com.google.firebase.crashlytics.internal.persistence.FileStore;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.zip.GZIPOutputStream;

public class CrashpadController {

@SuppressWarnings("CharsetObjectCanBeUsed") // StandardCharsets requires API level 19.
private static final Charset UTF_8 = Charset.forName("UTF-8");

private static final String SESSION_START_TIMESTAMP_FILE_NAME = "start-time";

private static final String SESSION_METADATA_FILE = "session.json";
private static final String APP_METADATA_FILE = "app.json";
private static final String DEVICE_METADATA_FILE = "device.json";
Expand Down Expand Up @@ -70,8 +84,8 @@ public boolean initialize(
}

public boolean hasCrashDataForSession(String sessionId) {
File crashFile = getFilesForSession(sessionId).minidump;
return crashFile != null && crashFile.exists();
SessionFiles files = getFilesForSession(sessionId);
return files.nativeCore != null && files.nativeCore.hasCore();
}

@NonNull
Expand All @@ -94,7 +108,7 @@ public SessionFiles getFilesForSession(String sessionId) {
&& sessionFileDirectory.exists()
&& sessionFileDirectoryForMinidump.exists()) {
builder
.minidumpFile(getSingleFileWithExtension(sessionFileDirectoryForMinidump, ".dmp"))
.nativeCore(getNativeCore(sessionId, sessionFileDirectoryForMinidump))
.metadataFile(getSingleFileWithExtension(sessionFileDirectory, ".device_info"))
.sessionFile(new File(sessionFileDirectory, SESSION_METADATA_FILE))
.appFile(new File(sessionFileDirectory, APP_METADATA_FILE))
Expand All @@ -104,6 +118,52 @@ public SessionFiles getFilesForSession(String sessionId) {
return builder.build();
}

private SessionFiles.NativeCore getNativeCore(
String sessionId, File sessionFileDirectoryForMinidump) {
File minidump = getSingleFileWithExtension(sessionFileDirectoryForMinidump, ".dmp");
CrashlyticsReport.ApplicationExitInfo applicationExitInfo = getApplicationExitInfo(sessionId);
return new SessionFiles.NativeCore(minidump, applicationExitInfo);
}

private CrashlyticsReport.ApplicationExitInfo getApplicationExitInfo(String sessionId) {
return android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
? getNativeCrashApplicationExitInfo(sessionId)
: null;
}

@RequiresApi(api = Build.VERSION_CODES.S)
private CrashlyticsReport.ApplicationExitInfo getNativeCrashApplicationExitInfo(
String sessionId) {
ActivityManager activityManager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ApplicationExitInfo> applicationExitInfoList =
activityManager.getHistoricalProcessExitReasons(null, 0, 0);

File sessionStartFile = fileStore.getSessionFile(sessionId, SESSION_START_TIMESTAMP_FILE_NAME);
long sessionTime =
sessionStartFile == null ? System.currentTimeMillis() : sessionStartFile.lastModified();

return getRelevantApplicationExitInfo(sessionTime, applicationExitInfoList);
}

@RequiresApi(api = Build.VERSION_CODES.S)
private CrashlyticsReport.ApplicationExitInfo getRelevantApplicationExitInfo(
long sessionTime, List<ApplicationExitInfo> applicationExitInfoList) {
List<ApplicationExitInfo> filtered = new ArrayList<>();
for (ApplicationExitInfo applicationExitInfo : applicationExitInfoList) {
if (applicationExitInfo.getReason() != ApplicationExitInfo.REASON_CRASH_NATIVE
|| applicationExitInfo.getTimestamp() < sessionTime) {
continue;
}

filtered.add(applicationExitInfo);
}

// For GWP-ASan and MTE, there can only be one tombstone, even in the case of non-crashy
// GWP-ASan.
return filtered.isEmpty() ? null : convertApplicationExitInfoToModel(filtered.get(0));
}

public void writeBeginSession(String sessionId, String generator, long startedAtSeconds) {
final String json =
SessionMetadataJsonSerializer.serializeBeginSession(sessionId, generator, startedAtSeconds);
Expand Down Expand Up @@ -181,4 +241,59 @@ private static File getSingleFileWithExtension(File directory, String extension)

return null;
}

@RequiresApi(api = Build.VERSION_CODES.S)
private static CrashlyticsReport.ApplicationExitInfo convertApplicationExitInfoToModel(
ApplicationExitInfo applicationExitInfo) {
return CrashlyticsReport.ApplicationExitInfo.builder()
.setImportance(applicationExitInfo.getImportance())
.setProcessName(applicationExitInfo.getProcessName())
.setReasonCode(applicationExitInfo.getReason())
.setTimestamp(applicationExitInfo.getTimestamp())
.setPid(applicationExitInfo.getPid())
.setPss(applicationExitInfo.getPss())
.setRss(applicationExitInfo.getRss())
.setTraceFile(getTraceFileFromApplicationExitInfo(applicationExitInfo))
.build();
}

@RequiresApi(api = Build.VERSION_CODES.S)
private static String getTraceFileFromApplicationExitInfo(
ApplicationExitInfo applicationExitInfo) {
try {
return convertInputStreamToString(applicationExitInfo.getTraceInputStream());
} catch (IOException e) {
Logger.getLogger().w("Failed to get input stream from ApplicationExitInfo");
}

return null;
}

@VisibleForTesting
@RequiresApi(api = Build.VERSION_CODES.S)
public static String convertInputStreamToString(InputStream inputStream) throws IOException {
if (inputStream == null) {
return null;
}

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] bytes = new byte[8192];
int length;
while ((length = inputStream.read(bytes)) != -1) {
byteArrayOutputStream.write(bytes, 0, length);
}

return zipAndEncode(byteArrayOutputStream.toByteArray());
}

@RequiresApi(api = Build.VERSION_CODES.S)
private static String zipAndEncode(byte[] bytes) throws IOException {
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPOutputStream gzip = new GZIPOutputStream(out)) {
gzip.write(bytes);
gzip.finish();

return Base64.getEncoder().encodeToString(out.toByteArray());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,47 @@

package com.google.firebase.crashlytics.ndk;

import androidx.annotation.Nullable;
import com.google.firebase.crashlytics.internal.model.CrashlyticsReport;
import java.io.File;

final class SessionFiles {
/**
* Starting with Android S, it is possible to collect the tombstone upon an application restart.
* In most cases, both, the tombstone and the minidump will be available.
*/
static final class NativeCore {
@Nullable public final File minidump;

@Nullable public final CrashlyticsReport.ApplicationExitInfo applicationExitInfo;

NativeCore(
@Nullable File minidump,
@Nullable CrashlyticsReport.ApplicationExitInfo applicationExitInfo) {
this.minidump = minidump;
this.applicationExitInfo = applicationExitInfo;
}

boolean hasCore() {
// The classic case is that a crash occurred and a minidump was successfully captured. There
// are two new cases to handle, however. First, a crash occurred and a minidump was not
// captured - this is ok; check for a tombstone and use that instead. Second, a crash did not
// occur because of a non-crashy GWP-ASan tombstone - this ok; capture just the tombstone.
return (minidump != null && minidump.exists()) || applicationExitInfo != null;
}
}

static final class Builder {
private File minidump;
private NativeCore nativeCore;
private File binaryImages;
private File metadata;
private File session;
private File app;
private File device;
private File os;

Builder minidumpFile(File minidump) {
this.minidump = minidump;
Builder nativeCore(NativeCore nativeCore) {
this.nativeCore = nativeCore;
return this;
}

Expand Down Expand Up @@ -67,7 +93,7 @@ SessionFiles build() {
}
}

public final File minidump;
public final NativeCore nativeCore;
public final File binaryImages;
public final File metadata;
public final File session;
Expand All @@ -76,7 +102,7 @@ SessionFiles build() {
public final File os;

private SessionFiles(Builder builder) {
minidump = builder.minidump;
nativeCore = builder.nativeCore;
binaryImages = builder.binaryImages;
metadata = builder.metadata;
session = builder.session;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package com.google.firebase.crashlytics.ndk;

import com.google.firebase.crashlytics.internal.NativeSessionFileProvider;
import com.google.firebase.crashlytics.internal.model.CrashlyticsReport;
import java.io.File;

class SessionFilesProvider implements NativeSessionFileProvider {
Expand All @@ -27,7 +28,12 @@ class SessionFilesProvider implements NativeSessionFileProvider {

@Override
public File getMinidumpFile() {
return sessionFiles.minidump;
return sessionFiles.nativeCore.minidump;
}

@Override
public CrashlyticsReport.ApplicationExitInfo getApplicationExitInto() {
return sessionFiles.nativeCore != null ? sessionFiles.nativeCore.applicationExitInfo : null;
}

@Override
Expand Down
4 changes: 2 additions & 2 deletions firebase-crashlytics/firebase-crashlytics.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ android {
timeOutInMs 60 * 1000
}

compileSdkVersion 30
compileSdkVersion 33
testOptions.unitTests.includeAndroidResources = true
defaultConfig {
minSdkVersion 16
targetSdkVersion 30
targetSdkVersion 33
versionName version

multiDexEnabled true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import com.google.firebase.crashlytics.internal.NativeSessionFileProvider;
import com.google.firebase.crashlytics.internal.analytics.AnalyticsEventLogger;
import com.google.firebase.crashlytics.internal.metadata.LogFileManager;
import com.google.firebase.crashlytics.internal.model.CrashlyticsReport;
import com.google.firebase.crashlytics.internal.persistence.FileStore;
import com.google.firebase.crashlytics.internal.settings.Settings;
import com.google.firebase.crashlytics.internal.settings.SettingsProvider;
Expand Down Expand Up @@ -239,6 +240,11 @@ public File getMinidumpFile() {
return minidump;
}

@Override
public CrashlyticsReport.ApplicationExitInfo getApplicationExitInto() {
return null;
}

@Override
public File getBinaryImagesFile() {
return null;
Expand Down Expand Up @@ -279,9 +285,9 @@ public File getOsFile() {

controller.finalizeSessions(testSettingsProvider);
verify(mockSessionReportingCoordinator)
.finalizeSessionWithNativeEvent(eq(previousSessionId), any());
.finalizeSessionWithNativeEvent(eq(previousSessionId), any(), any());
verify(mockSessionReportingCoordinator, never())
.finalizeSessionWithNativeEvent(eq(sessionId), any());
.finalizeSessionWithNativeEvent(eq(sessionId), any(), any());
}

public void testMissingNativeComponentCausesNoReports() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -397,11 +397,12 @@ public void testFinalizeSessionWithNativeEvent_createsCrashlyticsReportWithNativ
String byteBackedSessionName = "byte";
BytesBackedNativeSessionFile byteSession =
new BytesBackedNativeSessionFile(byteBackedSessionName, "not_applicable", testBytes);
reportingCoordinator.finalizeSessionWithNativeEvent("id", Arrays.asList(byteSession));
reportingCoordinator.finalizeSessionWithNativeEvent("id", Arrays.asList(byteSession), null);

ArgumentCaptor<CrashlyticsReport.FilesPayload> filesPayload =
ArgumentCaptor.forClass(CrashlyticsReport.FilesPayload.class);
verify(reportPersistence).finalizeSessionWithNativeEvent(eq("id"), filesPayload.capture());
verify(reportPersistence)
.finalizeSessionWithNativeEvent(eq("id"), filesPayload.capture(), any());
CrashlyticsReport.FilesPayload ndkPayloadFinalized = filesPayload.getValue();
assertEquals(1, ndkPayloadFinalized.getFiles().size());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,11 +350,11 @@ public void testFinalizeReports_prioritizesNativeAndNonnativeFatals() throws IOE
fileStore, createSettingsProviderMock(4, VERY_LARGE_UPPER_LIMIT));

persistReportWithEvent(reportPersistence, "testSession1", true);
reportPersistence.finalizeSessionWithNativeEvent("testSession1", filesPayload);
reportPersistence.finalizeSessionWithNativeEvent("testSession1", filesPayload, null);
persistReportWithEvent(reportPersistence, "testSession2low", false);
persistReportWithEvent(reportPersistence, "testSession3low", false);
persistReportWithEvent(reportPersistence, "testSession4", true);
reportPersistence.finalizeSessionWithNativeEvent("testSession4", filesPayload);
reportPersistence.finalizeSessionWithNativeEvent("testSession4", filesPayload, null);
reportPersistence.finalizeReports("skippedSession", 0L);

List<CrashlyticsReportWithSessionId> finalizedReports =
Expand Down Expand Up @@ -453,7 +453,7 @@ public void testFinalizeSessionWithNativeEvent_writesNativeSessions() {

assertEquals(0, finalizedReports.size());

reportPersistence.finalizeSessionWithNativeEvent("sessionId", filesPayload);
reportPersistence.finalizeSessionWithNativeEvent("sessionId", filesPayload, null);

finalizedReports = reportPersistence.loadFinalizedReports();
assertEquals(1, finalizedReports.size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package com.google.firebase.crashlytics.internal;

import androidx.annotation.NonNull;
import com.google.firebase.crashlytics.internal.model.CrashlyticsReport;
import com.google.firebase.crashlytics.internal.model.StaticSessionData;
import com.google.firebase.inject.Deferred;
import java.io.File;
Expand Down Expand Up @@ -85,6 +86,11 @@ public File getMinidumpFile() {
return null;
}

@Override
public CrashlyticsReport.ApplicationExitInfo getApplicationExitInto() {
return null;
}

@Override
public File getBinaryImagesFile() {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package com.google.firebase.crashlytics.internal;

import androidx.annotation.Nullable;
import com.google.firebase.crashlytics.internal.model.CrashlyticsReport;
import java.io.File;

/** Provides references to the files that make up a native crash report. */
Expand All @@ -25,6 +26,9 @@ public interface NativeSessionFileProvider {
@Nullable
File getMinidumpFile();

@Nullable
CrashlyticsReport.ApplicationExitInfo getApplicationExitInto();

@Nullable
File getBinaryImagesFile();

Expand Down
Loading