Skip to content

Update Crashlytics file system to support multi-process apps #3715

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 2 commits into from
May 13, 2022
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 @@ -14,11 +14,14 @@

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

import static com.google.firebase.crashlytics.internal.persistence.FileStore.sanitizeName;

import com.google.firebase.crashlytics.internal.CrashlyticsTestCase;
import java.io.File;
import java.util.Arrays;
import java.util.List;

@SuppressWarnings("ResultOfMethodCallIgnored") // Convenient use of files.
public class FileStoreTest extends CrashlyticsTestCase {
FileStore fileStore;

Expand All @@ -31,6 +34,7 @@ protected void setUp() throws Exception {
public void testGetCommonFile() {
File commonFile = fileStore.getCommonFile("testCommonFile");
assertFalse(commonFile.exists());
assertNotNull(commonFile.getParentFile());
assertTrue(commonFile.getParentFile().exists());
}

Expand All @@ -39,6 +43,7 @@ public void testGetSessionFile() {
String filename = "testSessionFile";
File sessionFile = fileStore.getSessionFile(sessionId, filename);
assertFalse(sessionFile.exists());
assertNotNull(sessionFile.getParentFile());
assertTrue(sessionFile.getParentFile().exists());
assertEquals(sessionFile.getParentFile().getName(), sessionId);

Expand Down Expand Up @@ -122,4 +127,10 @@ public void testGetReports() throws Exception {
nativeReport.delete();
assertEquals(0, fileStore.getNativeReports().size());
}

public void testSanitizeName() {
assertEquals(
"com.google.my.awesome.app_big.stuff.Happens_Here123___",
sanitizeName("com.google.my.awesome.app:big.stuff.Happens_Here123$%^"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,7 @@ public List<CrashlyticsReportWithSessionId> loadFinalizedReports() {
}

private SortedSet<String> capAndGetOpenSessions(@Nullable String currentSessionId) {

// Fixes b/195664514
fileStore.cleanupLegacyFiles();
fileStore.cleanupPreviousFileSystems();

SortedSet<String> openSessionIds = getOpenSessionIds();
if (currentSessionId != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@

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

import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.firebase.crashlytics.internal.Logger;
Expand All @@ -31,7 +35,7 @@
*
* <ul>
* <li>"Common" files, that exist independent of a specific session id.
* <li>"Open session" files, which contain a varierty of temporary files specific ot a Crashlytics
* <li>"Open session" files, which contain a variety of temporary files specific ot a Crashlytics
* session. These files may or may not eventually be combined into a Crashlytics crash report
* file.
* <li>"Report" files, which are processed reports, ready to be uploaded to Crashlytics servers.
Expand All @@ -48,46 +52,54 @@
* convention, any use of new File(...) or similar outside of this class is a code smell.
*/
public class FileStore {

private static final String FILES_PATH = ".com.google.firebase.crashlytics.files.v1";
private static final String CRASHLYTICS_PATH_V1 = ".com.google.firebase.crashlytics.files.v1";
private static final String CRASHLYTICS_PATH_V2 = ".com.google.firebase.crashlytics.files.v2";
private static final String SESSIONS_PATH = "open-sessions";
private static final String NATIVE_SESSION_SUBDIR = "native";
private static final String REPORTS_PATH = "reports";
private static final String PRIORITY_REPORTS_PATH = "priority-reports";
private static final String NATIVE_REPORTS_PATH = "native-reports";

private final File rootDir;
private final File filesDir;
private final File crashlyticsDir;
private final File sessionsDir;
private final File reportsDir;
private final File priorityReportsDir;
private final File nativeReportsDir;

public FileStore(Context context) {
rootDir = prepareBaseDir(new File(context.getFilesDir(), FILES_PATH));
sessionsDir = prepareBaseDir(new File(rootDir, SESSIONS_PATH));
reportsDir = prepareBaseDir(new File(rootDir, REPORTS_PATH));
priorityReportsDir = prepareBaseDir(new File(rootDir, PRIORITY_REPORTS_PATH));
nativeReportsDir = prepareBaseDir(new File(rootDir, NATIVE_REPORTS_PATH));
filesDir = context.getFilesDir();
String crashlyticsPath =
useV2FileSystem()
? CRASHLYTICS_PATH_V2 + File.pathSeparator + sanitizeName(Application.getProcessName())
: CRASHLYTICS_PATH_V1;
crashlyticsDir = prepareBaseDir(new File(filesDir, crashlyticsPath));
sessionsDir = prepareBaseDir(new File(crashlyticsDir, SESSIONS_PATH));
reportsDir = prepareBaseDir(new File(crashlyticsDir, REPORTS_PATH));
priorityReportsDir = prepareBaseDir(new File(crashlyticsDir, PRIORITY_REPORTS_PATH));
nativeReportsDir = prepareBaseDir(new File(crashlyticsDir, NATIVE_REPORTS_PATH));
}

@VisibleForTesting
public void deleteAllCrashlyticsFiles() {
recursiveDelete(rootDir);
recursiveDelete(crashlyticsDir);
}

public void cleanupLegacyFiles() {
// Fixes b/195664514
// :TODO: consider removing this method in mid 2023, to give all clients time to upgrade
File[] legacyDirs =
new File[] {
new File(rootDir.getParent(), ".com.google.firebase.crashlytics"),
new File(rootDir.getParent(), ".com.google.firebase.crashlytics-ndk")
};
/** Clean up files from previous file systems. */
public void cleanupPreviousFileSystems() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this implementation. Very clean. :)

// Clean up pre-versioned file systems.
cleanupDir(new File(filesDir, ".com.google.firebase.crashlytics"));
cleanupDir(new File(filesDir, ".com.google.firebase.crashlytics-ndk"));

for (File legacyDir : legacyDirs) {
if (legacyDir.exists() && recursiveDelete(legacyDir)) {
Logger.getLogger().d("Deleted legacy Crashlytics files from " + legacyDir.getPath());
}
// Clean up v1 file system.
if (useV2FileSystem()) {
cleanupDir(new File(filesDir, CRASHLYTICS_PATH_V1));
}
}

private void cleanupDir(File dir) {
if (dir.exists() && recursiveDelete(dir)) {
Logger.getLogger().d("Deleted previous Crashlytics file system: " + dir.getPath());
}
}

Expand All @@ -103,12 +115,12 @@ static boolean recursiveDelete(File fileOrDirectory) {

/** @return internal File used by Crashlytics, that is not specific to a session */
public File getCommonFile(String filename) {
return new File(rootDir, filename);
return new File(crashlyticsDir, filename);
}

/** @return all common (non session specific) files matching the given filter. */
public List<File> getCommonFiles(FilenameFilter filter) {
return safeArrayToList(rootDir.listFiles(filter));
return safeArrayToList(crashlyticsDir.listFiles(filter));
}

private File getSessionDir(String sessionId) {
Expand Down Expand Up @@ -193,4 +205,15 @@ private static synchronized File prepareBaseDir(File file) {
private static <T> List<T> safeArrayToList(@Nullable T[] array) {
return (array == null) ? Collections.emptyList() : Arrays.asList(array);
}

@SuppressLint("AnnotateVersionCheck")
private static boolean useV2FileSystem() {
return VERSION.SDK_INT >= VERSION_CODES.P;
}

/** Replace potentially unsafe chars with underscores to make a safe file name. */
@VisibleForTesting
static String sanitizeName(String filename) {
return filename.replaceAll("[^a-zA-Z0-9.]", "_");
}
}