blob: 0300b04aa8c801f46475e973350cd1b7a89d73a1 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.crash;
import android.annotation.SuppressLint;
import android.app.job.JobInfo;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Build;
import android.os.PersistableBundle;
import android.os.Process;
import androidx.annotation.StringDef;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ApplicationState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ApplicationStatus.ApplicationStateListener;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.StreamUtil;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
import org.chromium.chrome.browser.privacy.settings.PrivacyPreferencesManagerImpl;
import org.chromium.components.background_task_scheduler.TaskIds;
import org.chromium.components.crash.browser.ProcessExitReasonFromSystem;
import org.chromium.components.minidump_uploader.CrashFileManager;
import org.chromium.components.minidump_uploader.MinidumpUploadCallable;
import org.chromium.components.minidump_uploader.MinidumpUploadCallable.MinidumpUploadStatus;
import org.chromium.components.minidump_uploader.MinidumpUploadJobService;
import org.chromium.components.minidump_uploader.util.CrashReportingPermissionManager;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Service that is responsible for uploading crash minidumps to the Google crash server.
*/
public class MinidumpUploadServiceImpl extends MinidumpUploadService.Impl {
private static final String TAG = "MinidmpUploadService";
// Intent actions
@VisibleForTesting
static final String ACTION_UPLOAD = "com.google.android.apps.chrome.crash.ACTION_UPLOAD";
// Intent bundle keys
@VisibleForTesting
static final String FILE_TO_UPLOAD_KEY = "minidump_file";
static final String UPLOAD_LOG_KEY = "upload_log";
/**
* The number of times we will try to upload a crash.
*/
public static final int MAX_TRIES_ALLOWED = 3;
/**
* Histogram related constants.
*/
private static final String HISTOGRAM_NAME_PREFIX = "Tab.AndroidCrashUpload_";
private static final int HISTOGRAM_MAX = 2;
private static final int FAILURE = 0;
private static final int SUCCESS = 1;
private static AtomicBoolean sBrowserCrashMetricsInitialized = new AtomicBoolean();
private static AtomicBoolean sDidBrowserCrashRecently = new AtomicBoolean();
@StringDef({ProcessType.BROWSER, ProcessType.RENDERER, ProcessType.GPU, ProcessType.OTHER})
@Retention(RetentionPolicy.SOURCE)
public @interface ProcessType {
String BROWSER = "Browser";
String RENDERER = "Renderer";
String GPU = "GPU";
String OTHER = "Other";
}
static final String[] TYPES = {
ProcessType.BROWSER, ProcessType.RENDERER, ProcessType.GPU, ProcessType.OTHER};
@Override
protected void onServiceSet() {
getService().setIntentRedelivery(true);
}
/**
* @return Whether to use the JobSchduler API to upload crash reports, rather than directly
* creating a service for uploading.
*/
public static boolean shouldUseJobSchedulerForUploads() {
// The JobScheduler API is only usable as of Android M.
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
/**
* Schedules uploading of all pending minidumps, using the JobScheduler API.
*/
@SuppressLint("NewApi")
public static void scheduleUploadJob() {
assert shouldUseJobSchedulerForUploads();
CrashReportingPermissionManager permissionManager =
PrivacyPreferencesManagerImpl.getInstance();
PersistableBundle permissions = new PersistableBundle();
permissions.putBoolean(ChromeMinidumpUploaderDelegate.IS_CLIENT_IN_METRICS_SAMPLE,
permissionManager.isClientInMetricsSample());
permissions.putBoolean(ChromeMinidumpUploaderDelegate.IS_UPLOAD_ENABLED_FOR_TESTS,
permissionManager.isUploadEnabledForTests());
JobInfo.Builder builder =
new JobInfo
.Builder(TaskIds.CHROME_MINIDUMP_UPLOADING_JOB_ID,
new ComponentName(ContextUtils.getApplicationContext(),
ChromeMinidumpUploadJobService.class))
.setExtras(permissions);
MinidumpUploadJobService.scheduleUpload(builder);
}
private static ApplicationStateListener createApplicationStateListener() {
return newState -> {
SharedPreferencesManager.getInstance().writeInt(
ChromePreferenceKeys.LAST_SESSION_APPLICATION_STATE, newState);
};
}
/**
* Stores the successes and failures from uploading crash to UMA,
*/
public static void storeBreakpadUploadStatsInUma(CrashUploadCountStore pref) {
sBrowserCrashMetricsInitialized.set(true);
SharedPreferencesManager sharedPrefs = SharedPreferencesManager.getInstance();
int previousPid = sharedPrefs.readInt(ChromePreferenceKeys.LAST_SESSION_BROWSER_PID);
@ApplicationState
int applicationExitState =
sharedPrefs.readInt(ChromePreferenceKeys.LAST_SESSION_APPLICATION_STATE);
String umaSuffix;
if (applicationExitState == ApplicationState.HAS_RUNNING_ACTIVITIES) {
umaSuffix = "Foreground2";
} else {
umaSuffix = "Background2";
}
sharedPrefs.writeInt(ChromePreferenceKeys.LAST_SESSION_BROWSER_PID, Process.myPid());
ApplicationStateListener appStateListener = createApplicationStateListener();
appStateListener.onApplicationStateChange(ApplicationStatus.getStateForApplication());
ApplicationStatus.registerApplicationStateListener(appStateListener);
if (previousPid != 0) {
int reason = ProcessExitReasonFromSystem.getExitReason(previousPid);
ProcessExitReasonFromSystem.recordAsEnumHistogram(
"Stability.Android.SystemExitReason.Browser", reason);
ProcessExitReasonFromSystem.recordAsEnumHistogram(
"Stability.Android.SystemExitReason.Browser." + umaSuffix, reason);
}
for (String type : TYPES) {
for (int success = pref.getCrashSuccessUploadCount(type); success > 0; success--) {
RecordHistogram.recordEnumeratedHistogram(
HISTOGRAM_NAME_PREFIX + type, SUCCESS, HISTOGRAM_MAX);
if (ProcessType.BROWSER.equals(type)) sDidBrowserCrashRecently.set(true);
}
for (int fail = pref.getCrashFailureUploadCount(type); fail > 0; fail--) {
RecordHistogram.recordEnumeratedHistogram(
HISTOGRAM_NAME_PREFIX + type, FAILURE, HISTOGRAM_MAX);
if (ProcessType.BROWSER.equals(type)) sDidBrowserCrashRecently.set(true);
}
pref.resetCrashUploadCounts(type);
}
}
/**
* Returns true if the initial breakpad upload stats have been recorded.
*/
@CalledByNative
private static boolean browserCrashMetricsInitialized() {
return sBrowserCrashMetricsInitialized.get();
}
/**
* Returns if browser crash dumps were found for recent browser crashes.
*
* We detect if the browser crash dump was uploaded in last session (for a previous session) or
* if a crash dump was seen in current session. Detection of a crash from earlier session is
* valid right from the point where minidump service was initialized
* (sBrowserCrashMetricsInitialized() returns true). But, the detection of a crash in previous
* session is only valid after background minidump upload job is finished, depending on the job
* scheduler. So, calling this function at startup can return false even if browser crashed in
* previous session.
*/
@CalledByNative
private static boolean didBrowserCrashRecently() {
assert browserCrashMetricsInitialized();
return sDidBrowserCrashRecently.get();
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent == null) return;
if (!ACTION_UPLOAD.equals(intent.getAction())) {
Log.w(TAG, "Got unknown action from intent: " + intent.getAction());
return;
}
String minidumpFileName = intent.getStringExtra(FILE_TO_UPLOAD_KEY);
if (minidumpFileName == null || minidumpFileName.isEmpty()) {
Log.w(TAG, "Cannot upload crash data since minidump is absent.");
return;
}
File minidumpFile = new File(minidumpFileName);
if (!minidumpFile.isFile()) {
Log.w(TAG,
"Cannot upload crash data since specified minidump " + minidumpFileName
+ " is not present.");
return;
}
int tries = CrashFileManager.readAttemptNumber(minidumpFileName);
// Since we do not rename a file after reaching max number of tries,
// files that have maxed out tries will NOT reach this.
if (tries >= MAX_TRIES_ALLOWED || tries < 0) {
// Reachable only if the file naming is incorrect by current standard.
// Thus we log an error instead of recording failure to UMA.
Log.e(TAG,
"Giving up on trying to upload " + minidumpFileName
+ " after failing to read a valid attempt number.");
return;
}
String logfileName = intent.getStringExtra(UPLOAD_LOG_KEY);
File logfile = new File(logfileName);
// Try to upload minidump
MinidumpUploadCallable minidumpUploadCallable =
createMinidumpUploadCallable(minidumpFile, logfile);
@MinidumpUploadStatus
int uploadStatus = minidumpUploadCallable.call();
if (uploadStatus == MinidumpUploadStatus.SUCCESS) {
// Only update UMA stats if an intended and successful upload.
incrementCrashSuccessUploadCount(minidumpFileName);
} else if (uploadStatus == MinidumpUploadStatus.FAILURE) {
// Unable to upload minidump. Incrementing try number and restarting.
++tries;
if (tries == MAX_TRIES_ALLOWED) {
// Only record failure to UMA after we have maxed out the allotted tries.
incrementCrashFailureUploadCount(minidumpFileName);
}
// Only create another attempt if we have successfully renamed the file.
String newName = CrashFileManager.tryIncrementAttemptNumber(minidumpFile);
if (newName != null) {
if (tries < MAX_TRIES_ALLOWED) {
// TODO(nyquist): Do this as an exponential backoff.
MinidumpUploadRetry.scheduleRetry(ContextUtils.getApplicationContext(),
getCrashReportingPermissionManager());
} else {
Log.d(TAG,
"Giving up on trying to upload " + minidumpFileName + "after " + tries
+ " number of tries.");
}
} else {
Log.w(TAG, "Failed to rename minidump " + minidumpFileName);
}
}
}
/**
* Get the permission manager, can be overridden for testing.
*/
CrashReportingPermissionManager getCrashReportingPermissionManager() {
return PrivacyPreferencesManagerImpl.getInstance();
}
private static String getNewNameAfterSuccessfulUpload(String fileName) {
return fileName.replace("dmp", "up").replace("forced", "up");
}
@ProcessType
@VisibleForTesting
protected static String getCrashType(String fileName) {
// Read file and get the line containing name="ptype".
BufferedReader fileReader = null;
try {
fileReader = new BufferedReader(new FileReader(fileName));
String line;
while ((line = fileReader.readLine()) != null) {
if (line.equals("Content-Disposition: form-data; name=\"ptype\"")) {
// Crash type is on the line after the next line.
fileReader.readLine();
String crashType = fileReader.readLine();
if (crashType == null) return ProcessType.OTHER;
if (crashType.equals("browser")) return ProcessType.BROWSER;
if (crashType.equals("renderer")) return ProcessType.RENDERER;
if (crashType.equals("gpu-process")) return ProcessType.GPU;
return ProcessType.OTHER;
}
}
} catch (IOException e) {
Log.w(TAG, "Error while reading crash file %s: %s", fileName, e.toString());
} finally {
StreamUtil.closeQuietly(fileReader);
}
return ProcessType.OTHER;
}
/**
* Increments the count of successful uploads by 1. Distinguishes between different types of
* crashes by looking into the file contents. Because this code can execute in a context when
* the main Chrome activity is no longer running, the counts are stored in shared preferences;
* they are later read and recorded as metrics by the main Chrome activity.
* NOTE: This method should be called *after* renaming the file, since renaming occurs as a
* side-effect of a successful upload.
* @param originalFilename The name of the successfully uploaded minidump, *prior* to uploading.
*/
public static void incrementCrashSuccessUploadCount(String originalFilename) {
final @ProcessType String process_type =
getCrashType(getNewNameAfterSuccessfulUpload(originalFilename));
if (ProcessType.BROWSER.equals(process_type)) {
sDidBrowserCrashRecently.set(true);
}
CrashUploadCountStore.getInstance().incrementCrashSuccessUploadCount(process_type);
}
/**
* Increments the count of failed uploads by 1. Distinguishes between different types of crashes
* by looking into the file contents. Because this code can execute in a context when the main
* Chrome activity is no longer running, the counts are stored in shared preferences; they are
* later read and recorded as metrics by the main Chrome activity.
* NOTE: This method should be called *prior* to renaming the file.
* @param originalFilename The name of the successfully uploaded minidump, *prior* to uploading.
*/
public static void incrementCrashFailureUploadCount(String originalFilename) {
final @ProcessType String process_type = getCrashType(originalFilename);
if (ProcessType.BROWSER.equals(process_type)) {
sDidBrowserCrashRecently.set(true);
}
CrashUploadCountStore.getInstance().incrementCrashFailureUploadCount(process_type);
}
/**
* Factory method for creating minidump callables.
*
* This may be overridden for tests.
*
* @param minidumpFile the File to upload.
* @param logfile the Log file to write to upon successful uploads.
* @return a new MinidumpUploadCallable.
*/
@VisibleForTesting
MinidumpUploadCallable createMinidumpUploadCallable(File minidumpFile, File logfile) {
return new MinidumpUploadCallable(
minidumpFile, logfile, getCrashReportingPermissionManager());
}
/**
* Attempts to upload the specified {@param minidumpFile}.
*
* Note that this method is asynchronous. All that is guaranteed is that an upload attempt will
* be enqueued.
*/
public static void tryUploadCrashDump(File minidumpFile) {
assert !shouldUseJobSchedulerForUploads();
tryUploadCrashDumpNow(minidumpFile);
}
/**
* Attempts to upload the specified {@param minidumpFile} directly even when JobScheduler is
* available. This function is same as |tryUploadCrashDump| without the JobScheduler check.
*
* Note that the preferred way to upload minidump is through JobScheduler when possible, use
* this function if want to upload ASAP.
*/
static void tryUploadCrashDumpNow(File minidumpFile) {
CrashFileManager fileManager =
new CrashFileManager(ContextUtils.getApplicationContext().getCacheDir());
Intent intent =
new Intent(ContextUtils.getApplicationContext(), MinidumpUploadService.class);
intent.setAction(ACTION_UPLOAD);
intent.putExtra(FILE_TO_UPLOAD_KEY, minidumpFile.getAbsolutePath());
intent.putExtra(UPLOAD_LOG_KEY, fileManager.getCrashUploadLogFile().getAbsolutePath());
ContextUtils.getApplicationContext().startService(intent);
}
/**
* Attempts to upload all minidump files using the given {@link android.content.Context}.
*
* Note that this method is asynchronous. All that is guaranteed is that
* upload attempts will be enqueued.
*
* This method is safe to call from the UI thread.
*
*/
public static void tryUploadAllCrashDumps() {
assert !shouldUseJobSchedulerForUploads();
CrashFileManager fileManager =
new CrashFileManager(ContextUtils.getApplicationContext().getCacheDir());
File[] minidumps = fileManager.getMinidumpsReadyForUpload(MAX_TRIES_ALLOWED);
Log.i(TAG, "Attempting to upload accumulated crash dumps.");
for (File minidump : minidumps) {
tryUploadCrashDump(minidump);
}
}
/**
* Attempts to upload the crash report with the given local ID.
*
* Note that this method is asynchronous. All that is guaranteed is that
* upload attempts will be enqueued.
*
* This method is safe to call from the UI thread.
*
* @param localId The local ID of the crash report.
*/
@CalledByNative
public static void tryUploadCrashDumpWithLocalId(String localId) {
if (localId == null || localId.isEmpty()) {
Log.w(TAG, "Cannot force crash upload since local crash id is absent.");
return;
}
CrashFileManager fileManager =
new CrashFileManager(ContextUtils.getApplicationContext().getCacheDir());
File minidumpFile = fileManager.getCrashFileWithLocalId(localId);
if (minidumpFile == null) {
Log.w(TAG, "Could not find a crash dump with local ID " + localId);
return;
}
File renamedMinidumpFile = CrashFileManager.trySetForcedUpload(minidumpFile);
if (renamedMinidumpFile == null) {
Log.w(TAG, "Could not rename the file " + minidumpFile.getName() + " for re-upload");
return;
}
if (shouldUseJobSchedulerForUploads()) {
scheduleUploadJob();
} else {
tryUploadCrashDump(renamedMinidumpFile);
}
}
}