// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.minidump_uploader;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.Log;
import org.chromium.components.crash.anr.AnrCollector;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Scanner;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The CrashFileManager is responsible for managing the "Crash Reports" directory containing
* minidump files and shepherding them through a state machine represented by the file names.
* 1. Minidumps are read from Crashpad's CrashReportDatabase and re-written as MIME files in the
* "Crash Reports" directory as foo.dmpNNNNN where NNNNN is the PID (process id) of the
* crashing process.
* 2. foo.dmpNNNNN.try0 is a minidump file with recent logcat output attached to it; or a file for
* which logcat output has been intentionally omitted. Notably, Webview-generated minidumps do
* not include logcat output.
* 3. foo.dmpNNNNN.tryM for M > 0 is a minidump file that's been attempted to be uploaded to the
* crash server, but for which M upload attempts have failed.
* 4. foo.upNNNNN.tryM names a successfully uploaded file.
* 5. foo.skippedNNNNN.tryM names for a file whose upload was skipped. An upload may be skipped,
* for example, if the user has not consented to uploading crash reports. These files are marked
* as skipped rather than deleted immediately to allow the user to manually initiate an upload.
* 6. foo.forcedNNNNN.tryM names a file that the user has manually requested to upload.
* 7. foo.tmp is a temporary file.
*/
public class CrashFileManager {
private static final String TAG = "CrashFileManager";
/** The name of the crash directory. */
public static final String CRASH_DUMP_DIR = "Crash Reports";
private static final String CRASHPAD_DIR = "Crashpad";
private static final String ANR_DIR = "ANRs";
// This should mirror the C++ CrashUploadList::kReporterLogFilename variable.
@VisibleForTesting public static final String CRASH_DUMP_LOGFILE = "uploads.log";
// Local ID is the segment after the last hyphen and before the extensions part. It's usually an
// alphanumeric value but there is not restriction of having other characters like `_`. So we
// define the id to be a sequence of non separator characters {`-`, `,` or `.`}
private static final Pattern CRASH_LOCAL_ID_PATTERN = Pattern.compile("^[^.]+-([^-,]+?)\\.");
// Unlike the MINIDUMP_ALL_READY_FOR_UPLOAD_PATTERN below, this pattern omits a ".tryN" suffix.
private static final Pattern MINIDUMP_SANS_LOGCAT_PATTERN =
Pattern.compile("\\.dmp([0-9]*)\\z");
// Minidumps that are ready for uploading including forced uploads.
private static final Pattern MINIDUMP_ALL_READY_FOR_UPLOAD_PATTERN =
Pattern.compile("\\.(dmp|forced)([0-9]*)(\\.try([0-9]+))\\z");
// Minidumps that are ready for uploading excluding forced uploads.
private static final Pattern MINIDUMP_READY_FOR_UPLOAD_PATTERN =
Pattern.compile("\\.(dmp)([0-9]*)(\\.try([0-9]+))\\z");
private static final Pattern UPLOADED_MINIDUMP_PATTERN =
Pattern.compile("\\.up([0-9]*)(\\.try([0-9]+))\\z");
private static final Pattern MINIDUMP_FORCED_UPLOAD_PATTERN =
Pattern.compile("\\.forced([0-9]*)(\\.try([0-9]+))\\z");
private static final Pattern MINIDUMP_SKIPPED_UPLOAD_PATTERN =
Pattern.compile("\\.skipped([0-9]*)(\\.try([0-9]+))\\z");
private static final String NOT_YET_UPLOADED_MINIDUMP_SUFFIX = ".dmp";
private static final String UPLOADED_MINIDUMP_SUFFIX = ".up";
private static final String UPLOAD_SKIPPED_MINIDUMP_SUFFIX = ".skipped";
private static final String UPLOAD_FORCED_MINIDUMP_SUFFIX = ".forced";
private static final String UPLOAD_ATTEMPT_DELIMITER = ".try";
// The suffix used when a minidump is first ready for upload: ".try0".
public static final String READY_FOR_UPLOAD_SUFFIX = UPLOAD_ATTEMPT_DELIMITER + "0";
// A delimiter between uid and the rest of a minidump filename. Only used for WebView minidumps.
private static final String UID_DELIMITER = "_";
@VisibleForTesting protected static final String TMP_SUFFIX = ".tmp";
private static final Pattern TMP_PATTERN = Pattern.compile("\\.tmp\\z");
// The maximum number of non-uploaded crashes that may be kept in the crash reports directory.
// Chosen to attempt to balance between keeping a generous number of crashes, and not using up
// too much filesystem storage space for obsolete crash reports.
@VisibleForTesting protected static final int MAX_CRASH_REPORTS_TO_KEEP = 10;
// The maximum age, in days, considered acceptable for a crash report. Reports older than this
// age will be removed. The constant is chosen to be quite conservative, while still allowing
// users to eventually reclaim filesystem storage space from obsolete crash reports.
private static final int MAX_CRASH_REPORT_AGE_IN_DAYS = 30;
// The maximum number of non-uploaded crashes to copy to the crash reports directory. The
// difference between this value and MAX_CRASH_REPORTS_TO_KEEP is that TO_KEEP is only checked
// when we clean out the crash directory - the TO_UPLOAD value is checked every time we try to
// copy a minidump - to ensure we don't store too many minidumps before they are cleaned out
// after being uploaded.
@VisibleForTesting static final int MAX_CRASH_REPORTS_TO_UPLOAD = MAX_CRASH_REPORTS_TO_KEEP * 2;
// Same as above except this value is enforced per UID, so that one single app can't hog all
// storage/uploading resources.
@VisibleForTesting
static final int MAX_CRASH_REPORTS_TO_UPLOAD_PER_UID = MAX_CRASH_REPORTS_TO_KEEP;
/**
* Comparator used for sorting files by modification date.
* @return Comparator for prioritizing the more recently modified file
*/
@VisibleForTesting
protected static final Comparator<File> sFileComparator =
new Comparator<File>() {
@Override
public int compare(File lhs, File rhs) {
if (lhs.lastModified() == rhs.lastModified()) {
return lhs.compareTo(rhs);
} else if (lhs.lastModified() < rhs.lastModified()) {
return 1;
} else {
return -1;
}
}
};
/** Delete the file {@param fileToDelete}. */
public static boolean deleteFile(File fileToDelete) {
boolean isSuccess = fileToDelete.delete();
if (!isSuccess) {
Log.w(TAG, "Unable to delete " + fileToDelete.getAbsolutePath());
}
return isSuccess;
}
/**
* Returns whether a minidump file definitely lacks logcat output. Note: This method does not
* provide an "if and only if" test: it may return false for a path that lacks logcat output, if
* logcat output has been intentionally skipped for that minidump. However, a return value of
* true means that the file definitely lacks logcat output.
* @param path The minidump pathname to test.
* @return Whether the given path corresponds to a minidump file that definitely lacks logcat
* output.
*/
public static boolean isMinidumpSansLogcat(String path) {
return MINIDUMP_SANS_LOGCAT_PATTERN.matcher(path).find();
}
public static String tryIncrementAttemptNumber(File mFileToUpload) {
String newName = filenameWithIncrementedAttemptNumber(mFileToUpload.getPath());
return mFileToUpload.renameTo(new File(newName)) ? newName : null;
}
/** @return The file name to rename to after an addition attempt to upload */
@VisibleForTesting
public static String filenameWithIncrementedAttemptNumber(String filename) {
int numTried = readAttemptNumberInternal(filename);
if (numTried >= 0) {
int newCount = numTried + 1;
return filename.replace(
UPLOAD_ATTEMPT_DELIMITER + numTried, UPLOAD_ATTEMPT_DELIMITER + newCount);
} else {
// readAttemptNumberInternal returning -1 means there is no UPLOAD_ATTEMPT_DELIMITER in
// the file name (or that there is a delimiter but no attempt number). So, we have to
// add the delimiter and attempt number ourselves.
return filename + UPLOAD_ATTEMPT_DELIMITER + "1";
}
}
/**
* Attempts to rename the given file to mark it as ready for upload. This should be done when
* logcat extraction fails or is otherwise intentionally skipped. An equivalent operation is
* done when extraction succeeds; but since the logcat output needs to be included in the
* uploaded data, more than a simple rename is needed.
*
* @return The renamed file, or null if renaming failed.
*/
public static File trySetReadyForUpload(File fileToUpload) {
assert CrashFileManager.isMinidumpSansLogcat(fileToUpload.getName());
File renamedFile = new File(fileToUpload.getPath() + READY_FOR_UPLOAD_SUFFIX);
return fileToUpload.renameTo(renamedFile) ? renamedFile : null;
}
/** @return True iff the provided File was ready be uploaded for the first time. */
public static boolean isReadyUploadForFirstTime(File fileToUpload) {
return fileToUpload.getName().contains(READY_FOR_UPLOAD_SUFFIX);
}
/**
* Attempts to rename the given file to mark it as a forced upload. This is useful for allowing
* users to manually initiate previously skipped uploads.
*
* @return The renamed file, or null if renaming failed.
*/
public static File trySetForcedUpload(File fileToUpload) {
if (fileToUpload.getName().contains(UPLOADED_MINIDUMP_SUFFIX)) {
Log.w(
TAG,
"Refusing to reset upload attempt state for a file that has already been "
+ "successfully uploaded: "
+ fileToUpload.getName());
return null;
}
File renamedFile = new File(filenameWithForcedUploadState(fileToUpload.getPath()));
return fileToUpload.renameTo(renamedFile) ? renamedFile : null;
}
/** @return True iff the provided File was manually forced (by the user) to be uploaded. */
public static boolean isForcedUpload(File fileToUpload) {
return fileToUpload.getName().contains(UPLOAD_FORCED_MINIDUMP_SUFFIX);
}
/**
* @return The filename to rename to so as to manually force an upload (including clearing any
* previous upload attempt history).
*/
@VisibleForTesting
protected static String filenameWithForcedUploadState(String filename) {
int numTried = readAttemptNumber(filename);
if (numTried > 0) {
filename =
filename.replace(
UPLOAD_ATTEMPT_DELIMITER + numTried, UPLOAD_ATTEMPT_DELIMITER + 0);
}
filename = filename.replace(UPLOAD_SKIPPED_MINIDUMP_SUFFIX, UPLOAD_FORCED_MINIDUMP_SUFFIX);
return filename.replace(NOT_YET_UPLOADED_MINIDUMP_SUFFIX, UPLOAD_FORCED_MINIDUMP_SUFFIX);
}
/**
* Returns how many times we've tried to upload a certain minidump file.
* @return The number of attempts to upload the given minidump file, parsed from its filename.
* Returns 0 if an attempt number cannot be parsed from the filename.
*/
public static int readAttemptNumber(String filename) {
int numTries = readAttemptNumberInternal(filename);
return numTries >= 0 ? numTries : 0;
}
/**
* Returns how many times we've tried to upload a certain minidump file.
* @return The number of attempts to upload the given minidump file, parsed from its filename,
* Returns -1 if an attempt number cannot be parsed from the filename.
*/
@VisibleForTesting
static int readAttemptNumberInternal(String filename) {
int tryIndex = filename.lastIndexOf(UPLOAD_ATTEMPT_DELIMITER);
if (tryIndex >= 0) {
tryIndex += UPLOAD_ATTEMPT_DELIMITER.length();
String numTriesString = filename.substring(tryIndex);
Scanner numTriesScanner = new Scanner(numTriesString).useDelimiter("[^0-9]+");
try {
int nextInt = numTriesScanner.nextInt();
// Only return the number if it occurs just after the UPLOAD_ATTEMPT_DELIMITER.
return numTriesString.indexOf(Integer.toString(nextInt)) == 0 ? nextInt : -1;
} catch (NoSuchElementException e) {
return -1;
}
}
return -1;
}
/**
* Marks a crash dump file as successfully uploaded, by renaming the file.
*
* Does not immediately delete the file, for testing reasons. However, if renaming fails,
* attempts to delete the file immediately.
*/
public static void markUploadSuccess(File crashDumpFile) {
CrashFileManager.renameCrashDumpFollowingUpload(crashDumpFile, UPLOADED_MINIDUMP_SUFFIX);
}
/**
* Marks a crash dump file's upload being skipped. An upload might be skipped due to lack of
* user consent, or due to this client being excluded from the sample of clients reporting
* crashes.
*
* Renames the file rather than deleting it, so that the user can manually upload the file later
* (via chrome://crashes). However, if renaming fails, attempts to delete the file immediately.
*/
public static void markUploadSkipped(File crashDumpFile) {
CrashFileManager.renameCrashDumpFollowingUpload(
crashDumpFile, UPLOAD_SKIPPED_MINIDUMP_SUFFIX);
}
/**
* Renames a crash dump file. However, if renaming fails, attempts to delete the file
* immediately.
*/
private static void renameCrashDumpFollowingUpload(File crashDumpFile, String suffix) {
// The pre-upload filename might have been either "foo.dmpN.tryM" or "foo.forcedN.tryM".
String newName =
crashDumpFile
.getPath()
.replace(NOT_YET_UPLOADED_MINIDUMP_SUFFIX, suffix)
.replace(UPLOAD_FORCED_MINIDUMP_SUFFIX, suffix);
boolean renamed = crashDumpFile.renameTo(new File(newName));
if (!renamed) {
Log.w(TAG, "Failed to rename " + crashDumpFile);
if (!crashDumpFile.delete()) {
Log.w(TAG, "Failed to delete " + crashDumpFile);
}
}
}
private final File mCacheDir;
public CrashFileManager(File cacheDir) {
if (cacheDir == null) {
throw new NullPointerException("Specified context cannot be null.");
} else if (!cacheDir.isDirectory()) {
throw new IllegalArgumentException(cacheDir.getAbsolutePath() + " is not a directory.");
}
mCacheDir = cacheDir;
}
/**
* Create the crash directory for this file manager unless it exists already.
* @return true iff the crash directory exists when this method returns.
*/
private boolean ensureCrashDirExists() {
File crashDir = getCrashDirectory();
// Call mkdir before isDirectory to ensure that if another thread created the directory
// just before the call to mkdir, the current thread fails mkdir, but passes isDirectory.
return crashDir.mkdir() || crashDir.isDirectory();
}
/** @return whether the crash directory already exists. */
public boolean crashDirectoryExists() {
return getCrashDirectory().isDirectory();
}
/**
* Collects ANRs from Android, then writes them as MIME files in the appropriate directory for
* crash to automatically upload.
*/
public void collectAndWriteAnrs() {
if (ensureCrashDirExists()) {
File anrDir = new File(getCrashDirectory(), ANR_DIR);
anrDir.mkdir();
List<String> anrs = AnrCollector.collectAndWriteAnrs(anrDir);
if (anrs.isEmpty()) {
return;
}
File crashDir = getCrashDirectory();
CrashReportMimeWriter.rewriteAnrsAsMIMEs(anrs, crashDir);
}
}
/**
* Imports minidumps from Crashpad's database to the Crash Reports directory, converting them to
* MIME files.
**/
private void importCrashpadMinidumps() {
File crashpadDir = getCrashpadDirectory();
if (crashpadDir.exists() && ensureCrashDirExists()) {
File crashDir = getCrashDirectory();
CrashReportMimeWriter.rewriteMinidumpsAsMIMEs(crashpadDir, crashDir);
}
}
/**
* Imports minidumps from Crashpad's database to the Crash Reports directory, converting them to
* MIME files and returning crash info as key-value pairs.
*
* @return a Map for crash report uuid to this crash info key-value pairs.
*/
public Map<String, Map<String, String>> importMinidumpsCrashKeys() {
File crashpadDir = getCrashpadDirectory();
if (!crashpadDir.exists() || !ensureCrashDirExists()) {
return null;
}
File crashDir = getCrashDirectory();
return CrashReportMimeWriter.rewriteMinidumpsAsMIMEsAndGetCrashKeys(crashpadDir, crashDir);
}
/**
* Returns the most recent minidump without a logcat for a given pid, or null if no such
* minidump exists. This method begins by reading all minidumps from Crashpad's database and
* rewriting them as MIME files in the Crash Reports directory.
*/
public File getMinidumpSansLogcatForPid(int pid) {
importCrashpadMinidumps();
File[] foundFiles =
listCrashFiles(Pattern.compile("\\.dmp" + Integer.toString(pid) + "\\z"));
return foundFiles.length > 0 ? foundFiles[0] : null;
}
/**
* Returns all minidump files that definitely do not have logcat output, sorted by modification
* time stamp. This method begins by reading all minidumps from Crashpad's database and
* rewriting them as MIME files in the Crash Reports directory. Note: This method does not
* provide an "if and only if" test: it may return some files that lack logcat output, if logcat
* output has been intentionally skipped for those minidumps. However, any files returned
* definitely lack logcat output.
*/
public File[] getMinidumpsSansLogcat() {
importCrashpadMinidumps();
return listCrashFiles(MINIDUMP_SANS_LOGCAT_PATTERN);
}
/**
* Returns all minidump files currently in the Crash Reports directory that definitely do not
* have logcat output, sorted by modification time stamp. Note: This method does not provide an
* "if and only if" test: it may return some files that lack logcat output, if logcat outpuy has
* been intentionally skipped for those minidumps. However, any files returned definitely lack
* logcat output.
*/
public File[] getCurrentMinidumpsSansLogcat() {
return listCrashFiles(MINIDUMP_SANS_LOGCAT_PATTERN);
}
/**
* Returns all minidump files that could still be uploaded, sorted by modification time stamp.
* Only returns files that we have tried to upload less than {@param maxTries} number of times.
*/
public File[] getMinidumpsReadyForUpload(int maxTries) {
return getFilesBelowMaxTries(
listCrashFiles(MINIDUMP_ALL_READY_FOR_UPLOAD_PATTERN), maxTries);
}
/**
* Returns minidump files that could still be uploaded excluding forced uploads,
* sorted by modification time stamp.
*/
public File[] getMinidumpsNotForcedReadyForUpload() {
return listCrashFiles(MINIDUMP_READY_FOR_UPLOAD_PATTERN);
}
/** Returns all minidump files that could still be uploaded, sorted by modification time stamp. */
public File[] getMinidumpsSkippedUpload() {
return listCrashFiles(MINIDUMP_SKIPPED_UPLOAD_PATTERN);
}
/**
* Returns minidump files that are forced to be uploaded by the user, sorted by modification
* time stamp.
*/
public File[] getMinidumpsForcedUpload() {
return listCrashFiles(MINIDUMP_FORCED_UPLOAD_PATTERN);
}
/** Returns all minidump files with the uid {@param uid} from {@param minidumpFiles}. */
public static List<File> filterMinidumpFilesOnUid(File[] minidumpFiles, int uid) {
List<File> uidMinidumps = new ArrayList<>();
for (File minidump : minidumpFiles) {
if (belongsToUid(minidump, uid)) {
uidMinidumps.add(minidump);
}
}
return uidMinidumps;
}
public void cleanOutAllNonFreshMinidumpFiles() {
for (File f : getAllUploadedFiles()) {
deleteFile(f);
}
for (File f : getAllTempFiles()) {
deleteFile(f);
}
int numSavedCrashes = 0;
for (File f : listCrashFiles(null)) {
// The uploads.log file should always be preserved, as it stores the metadata that
// powers the chrome://crashes UI.
if (f.getName().equals(CRASH_DUMP_LOGFILE)) {
continue;
}
// Delete any crash reports that are especially old.
long ageInMillis = new Date().getTime() - f.lastModified();
long ageInDays = TimeUnit.DAYS.convert(ageInMillis, TimeUnit.MILLISECONDS);
if (ageInDays > MAX_CRASH_REPORT_AGE_IN_DAYS) {
deleteFile(f);
continue;
}
// Delete the oldest crash reports that exceed the cap on the number of allowed reports.
if (numSavedCrashes < MAX_CRASH_REPORTS_TO_KEEP) {
// Note that /not/ deleting the file is a no-op, so all that's needed is to mark
// that one more file has been kept.
++numSavedCrashes;
} else {
deleteFile(f);
}
}
}
/**
* Filters a set of files to keep the ones we have tried to upload only a few times.
* Given a set of files {@param unfilteredFiles}, returns only the files in that set which we
* have tried to upload less than {@param maxTries} times.
*/
@VisibleForTesting
static File[] getFilesBelowMaxTries(File[] unfilteredFiles, int maxTries) {
List<File> filesBelowMaxTries = new ArrayList<>();
for (File file : unfilteredFiles) {
if (readAttemptNumber(file.getName()) < maxTries) {
filesBelowMaxTries.add(file);
}
}
return filesBelowMaxTries.toArray(new File[filesBelowMaxTries.size()]);
}
/** Returns a sorted and filtered list of files within the crash directory. */
@VisibleForTesting
File[] listCrashFiles(@Nullable final Pattern pattern) {
File crashDir = getCrashDirectory();
FilenameFilter filter = null;
if (pattern != null) {
filter =
new FilenameFilter() {
@Override
public boolean accept(File dir, String filename) {
return pattern.matcher(filename).find();
}
};
}
File[] foundFiles = crashDir.listFiles(filter);
if (foundFiles == null) {
Log.w(TAG, crashDir.getAbsolutePath() + " does not exist or is not a directory");
return new File[] {};
}
Arrays.sort(foundFiles, sFileComparator);
return foundFiles;
}
@VisibleForTesting
public File[] getAllUploadedFiles() {
return listCrashFiles(UPLOADED_MINIDUMP_PATTERN);
}
@VisibleForTesting
public File getCrashDirectory() {
return new File(mCacheDir, CRASH_DUMP_DIR);
}
private File getCrashpadDirectory() {
return new File(mCacheDir, CRASHPAD_DIR);
}
public File createNewTempFile(String name) throws IOException {
File f = new File(getCrashDirectory(), name);
if (f.exists()) {
if (f.delete()) {
f = new File(getCrashDirectory(), name);
} else {
Log.w(TAG, "Unable to delete previous logfile" + f.getAbsolutePath());
}
}
return f;
}
/** @return the crash file named {@param filename}. */
public File getCrashFile(String filename) {
return new File(getCrashDirectory(), filename);
}
/**
* Returns the minidump file with the given local ID, or null if no minidump file has the given
* local ID.
* NOTE: Crash files that have already been successfully uploaded are not included.
*
* @param localId The local ID of the crash report.
* @return The matching File, or null if no matching file is found.
*/
public File getCrashFileWithLocalId(String localId) {
for (File f : listCrashFiles(null)) {
// Only match non-uploaded or previously skipped files. In particular, do not match
// successfully uploaded files; nor files which are not minidump files, such as logcat
// files.
if (!f.getName().contains(NOT_YET_UPLOADED_MINIDUMP_SUFFIX)
&& !f.getName().contains(UPLOAD_SKIPPED_MINIDUMP_SUFFIX)
&& !f.getName().contains(UPLOAD_FORCED_MINIDUMP_SUFFIX)) {
continue;
}
String filenameSansExtension = f.getName().split("\\.")[0];
if (filenameSansExtension.endsWith(localId)) {
return f;
}
}
return null;
}
/**
* Extracts crash local ID from crash file name.
*
* ID is the last part of the file name. e.g. {@code
* chromium-renderer-minidump-f297dbcba7a2d0bb.dump.try2} has local ID of {@code
* f297dbcba7a2d0bb}.
*
* @param fileName Crash File name.
* @return Local ID string or null if not found.
*/
public static String getCrashLocalIdFromFileName(String fileName) {
Matcher matcher = CRASH_LOCAL_ID_PATTERN.matcher(fileName);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
/** @return the file used for logging crash upload events. */
public File getCrashUploadLogFile() {
return new File(getCrashDirectory(), CRASH_DUMP_LOGFILE);
}
@VisibleForTesting
File[] getAllTempFiles() {
return listCrashFiles(TMP_PATTERN);
}
/**
* Delete the oldest minidump if we have reached our threshold on the number of minidumps to
* store (either per-app, or globally).
* @param uid The uid of the app to check the minidump limit for.
*/
private void enforceMinidumpStorageRestrictions(int uid) {
File[] allMinidumpFiles = listCrashFiles(MINIDUMP_ALL_READY_FOR_UPLOAD_PATTERN);
List<File> minidumpFilesWithCurrentUid = filterMinidumpFilesOnUid(allMinidumpFiles, uid);
// If we have exceeded our cap per uid, delete the oldest minidump of the same uid
if (minidumpFilesWithCurrentUid.size() >= MAX_CRASH_REPORTS_TO_UPLOAD_PER_UID) {
// Minidumps are sorted from newest to oldest.
File oldestFile =
minidumpFilesWithCurrentUid.get(minidumpFilesWithCurrentUid.size() - 1);
if (!oldestFile.delete()) {
// Note that we will still try to copy the new file if this deletion fails.
Log.w(TAG, "Couldn't delete old minidump " + oldestFile.getAbsolutePath());
}
return;
}
// If we have exceeded our minidump cap, delete the oldest minidump.
if (allMinidumpFiles.length >= MAX_CRASH_REPORTS_TO_UPLOAD) {
// Minidumps are sorted from newest to oldest.
File oldestFile = allMinidumpFiles[allMinidumpFiles.length - 1];
if (!oldestFile.delete()) {
// Note that we will still try to copy the new file if this deletion fails.
Log.w(TAG, "Couldn't delete old minidump " + oldestFile.getAbsolutePath());
}
}
}
/**
* Copy a minidump from the File Descriptor {@param fd}.
* Use {@param tmpDir} as an intermediate location to store temporary files.
* @return The new minidump file copied with the contents of the File Descriptor, or null if the
* copying failed.
*/
public File copyMinidumpFromFD(FileDescriptor fd, File tmpDir, int uid) throws IOException {
File crashDirectory = getCrashDirectory();
if (!ensureCrashDirExists()) {
Log.e(TAG, "Crash directory doesn't exist");
return null;
}
// Only threads copying minidumps will be touching this tmp-directory. Since these threads
// are synchronized to avoid copying several minidumps simultaneously we don't need
// synchronization explicitly for creating this tmp-directory.
if (!tmpDir.isDirectory() && !tmpDir.mkdir()) {
Log.e(TAG, "Couldn't create " + tmpDir.getAbsolutePath());
return null;
}
if (tmpDir.getCanonicalPath().equals(crashDirectory.getCanonicalPath())) {
// Cause a hard failure since this should never happen in the wild.
throw new RuntimeException("The tmp-dir and the crash dir can't have the same paths.");
}
enforceMinidumpStorageRestrictions(uid);
// Make sure the temp file doesn't overwrite an existing file.
File tmpFile = createMinidumpTmpFile(tmpDir);
FileInputStream in = null;
FileOutputStream out = null;
// TODO(gsennton): ensure that the copied file is indeed a minidump.
try {
in = new FileInputStream(fd);
out = new FileOutputStream(tmpFile);
final int bufSize = 4096;
byte[] buf = new byte[bufSize];
final int maxSize = 1024 * 1024; // 1MB maximum size
int curCount = in.read(buf);
int totalCount = curCount;
while ((curCount != -1) && (totalCount < maxSize)) {
out.write(buf, 0, curCount);
curCount = in.read(buf);
totalCount += curCount;
}
if (curCount != -1) {
// We are trying to keep on reading beyond our maximum threshold (1MB) - bail!
Log.w(TAG, "Tried to copy a file of size > 1MB, deleting the file and bailing!");
if (!tmpFile.delete()) {
Log.w(TAG, "Couldn't delete file " + tmpFile.getAbsolutePath());
}
return null;
}
} finally {
try {
if (out != null) out.close();
} catch (IOException e) {
Log.w(TAG, "Couldn't close minidump output stream ", e);
}
try {
if (in != null) in.close();
} catch (IOException e) {
Log.w(TAG, "Couldn't close minidump input stream ", e);
}
}
File minidumpFile = new File(crashDirectory, createUniqueMinidumpNameForUid(uid));
if (tmpFile.renameTo(minidumpFile)) {
return minidumpFile;
}
return null;
}
/** Returns whether the {@param minidump} belongs to the uid {@param uid}. */
private static boolean belongsToUid(File minidump, int uid) {
return minidump.getName().startsWith(uid + UID_DELIMITER);
}
/**
* Returns a unique minidump name based on {@param uid} to differentiate between minidumps from
* different packages.
* The 'uniqueness' of the file name lies in it being created from a UUID. A UUID is a
* Universally Unique ID - it is simply a 128-bit value that can be used to uniquely identify
* some entity. A uid, on the other hand, is a unique identifier for Android packages.
*/
private static String createUniqueMinidumpNameForUid(int uid) {
return uid
+ UID_DELIMITER
+ UUID.randomUUID()
+ NOT_YET_UPLOADED_MINIDUMP_SUFFIX
+ READY_FOR_UPLOAD_SUFFIX;
}
/**
* Create a temporary file to store a minidump in before renaming it with a real minidump name.
* @return a new temporary file with prefix {@param prefix} stored in the directory
* {@param directory}.
*
*/
private static File createMinidumpTmpFile(File directory) throws IOException {
return File.createTempFile("webview_minidump", TMP_SUFFIX, directory);
}
}