// 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.app.IntentService; import android.content.Context; import android.content.Intent; import android.support.annotation.StringDef; import org.chromium.base.Log; import org.chromium.base.StreamUtil; import org.chromium.base.VisibleForTesting; import org.chromium.base.annotations.CalledByNative; import org.chromium.base.metrics.RecordHistogram; import org.chromium.chrome.browser.preferences.ChromePreferenceManager; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; /** * Service that is responsible for uploading crash minidumps to the Google crash server. */ public class MinidumpUploadService extends IntentService { private static final String TAG = "MinidmpUploadService"; // Intent actions private static final String ACTION_FIND_LAST = "com.google.android.apps.chrome.crash.ACTION_FIND_LAST"; @VisibleForTesting static final String ACTION_FIND_ALL = "com.google.android.apps.chrome.crash.ACTION_FIND_ALL"; @VisibleForTesting static final String ACTION_UPLOAD = "com.google.android.apps.chrome.crash.ACTION_UPLOAD"; @VisibleForTesting static final String ACTION_FORCE_UPLOAD = "com.google.android.apps.chrome.crash.ACTION_FORCE_UPLOAD"; // Intent bundle keys @VisibleForTesting static final String FILE_TO_UPLOAD_KEY = "minidump_file"; static final String UPLOAD_LOG_KEY = "upload_log"; static final String FINISHED_LOGCAT_EXTRACTION_KEY = "upload_extraction_completed"; static final String LOCAL_CRASH_ID_KEY = "local_id"; /** * The number of times we will try to upload a crash. */ @VisibleForTesting 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; @StringDef({BROWSER, RENDERER, GPU, OTHER}) public @interface ProcessType {} static final String BROWSER = "Browser"; static final String RENDERER = "Renderer"; static final String GPU = "GPU"; static final String OTHER = "Other"; static final String[] TYPES = {BROWSER, RENDERER, GPU, OTHER}; public MinidumpUploadService() { super(TAG); setIntentRedelivery(true); } /** * Attempts to populate logcat dumps to be associated with the minidumps * if they do not already exists. */ private void tryPopulateLogcat(Intent redirectAction) { redirectAction.putExtra(FINISHED_LOGCAT_EXTRACTION_KEY, true); Context context = getApplicationContext(); CrashFileManager fileManager = new CrashFileManager(context.getCacheDir()); File[] dumps = fileManager.getMinidumpWithoutLogcat(); if (dumps.length == 0) { onHandleIntent(redirectAction); return; } context.startService(LogcatExtractionService.createLogcatExtractionTask( context, dumps, redirectAction)); } @Override protected void onHandleIntent(Intent intent) { if (intent == null) return; if (!intent.getBooleanExtra(FINISHED_LOGCAT_EXTRACTION_KEY, false)) { // The current intent was sent before a chance to gather some // logcat information. tryPopulateLogcat will re-send the // same action once it has a go at gather logcat. tryPopulateLogcat(intent); } else if (ACTION_FIND_LAST.equals(intent.getAction())) { handleFindAndUploadLastCrash(intent); } else if (ACTION_FIND_ALL.equals(intent.getAction())) { handleFindAndUploadAllCrashes(); } else if (ACTION_UPLOAD.equals(intent.getAction())) { handleUploadCrash(intent); } else if (ACTION_FORCE_UPLOAD.equals(intent.getAction())) { handleForceUploadCrash(intent); } else { Log.w(TAG, "Got unknown action from intent: " + intent.getAction()); } } /** * Creates an intent that when started will find the last created or * updated minidump, and try to upload it. * * @param context the context to use for the intent. * @return an Intent to use to start the service. */ public static Intent createFindAndUploadLastCrashIntent(Context context) { Intent intent = new Intent(context, MinidumpUploadService.class); intent.setAction(ACTION_FIND_LAST); return intent; } /** * Stores the successes and failures from uploading crash to UMA, */ public static void storeBreakpadUploadStatsInUma(ChromePreferenceManager pref) { for (String type : TYPES) { for (int success = pref.getCrashSuccessUploadCount(type); success > 0; success--) { RecordHistogram.recordEnumeratedHistogram( HISTOGRAM_NAME_PREFIX + type, SUCCESS, HISTOGRAM_MAX); } for (int fail = pref.getCrashFailureUploadCount(type); fail > 0; fail--) { RecordHistogram.recordEnumeratedHistogram( HISTOGRAM_NAME_PREFIX + type, FAILURE, HISTOGRAM_MAX); } pref.setCrashSuccessUploadCount(type, 0); pref.setCrashFailureUploadCount(type, 0); } } private void handleFindAndUploadLastCrash(Intent intent) { CrashFileManager fileManager = new CrashFileManager(getApplicationContext().getCacheDir()); File[] minidumpFiles = fileManager.getAllMinidumpFilesSorted(); if (minidumpFiles.length == 0) { // Try again later. Maybe the minidump hasn't finished being written. Log.d(TAG, "Could not find any crash dumps to upload"); return; } File minidumpFile = minidumpFiles[0]; File logfile = fileManager.getCrashUploadLogFile(); Intent uploadIntent = createUploadIntent(getApplicationContext(), minidumpFile, logfile); // We should have at least one chance to secure logcat to the minidump now. uploadIntent.putExtra(FINISHED_LOGCAT_EXTRACTION_KEY, true); startService(uploadIntent); } /** * Creates an intent that when started will find all minidumps, and try to upload them. * * @param context the context to use for the intent. * @return an Intent to use to start the service. */ @VisibleForTesting static Intent createFindAndUploadAllCrashesIntent(Context context) { Intent intent = new Intent(context, MinidumpUploadService.class); intent.setAction(ACTION_FIND_ALL); return intent; } private void handleFindAndUploadAllCrashes() { CrashFileManager fileManager = new CrashFileManager(getApplicationContext().getCacheDir()); File[] minidumps = fileManager.getAllMinidumpFiles(); File logfile = fileManager.getCrashUploadLogFile(); Log.i(TAG, "Attempting to upload accumulated crash dumps."); for (File minidump : minidumps) { Intent uploadIntent = createUploadIntent(getApplicationContext(), minidump, logfile); startService(uploadIntent); } } /** * Creates an intent that when started will find all minidumps, and try to upload them. * * @param minidumpFile the minidump file to upload. * @return an Intent to use to start the service. */ @VisibleForTesting public static Intent createUploadIntent(Context context, File minidumpFile, File logfile) { Intent intent = new Intent(context, MinidumpUploadService.class); intent.setAction(ACTION_UPLOAD); intent.putExtra(FILE_TO_UPLOAD_KEY, minidumpFile.getAbsolutePath()); intent.putExtra(UPLOAD_LOG_KEY, logfile.getAbsolutePath()); return intent; } private void handleUploadCrash(Intent intent) { 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); @MinidumpUploadCallable.MinidumpUploadStatus int uploadStatus = minidumpUploadCallable.call(); if (uploadStatus == MinidumpUploadCallable.UPLOAD_SUCCESS) { // Only update UMA stats if an intended and successful upload. incrementCrashSuccessUploadCount(getNewNameAfterSuccessfulUpload(minidumpFileName)); } else if (uploadStatus == MinidumpUploadCallable.UPLOAD_FAILURE) { // Unable to upload minidump. Incrementing try number and restarting. // 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(getApplicationContext()); } else { // Only record failure to UMA after we have maxed out the allotted tries. incrementCrashFailureUploadCount(newName); Log.d(TAG, "Giving up on trying to upload " + minidumpFileName + "after " + tries + " number of tries."); } } else { Log.w(TAG, "Failed to rename minidump " + minidumpFileName); } } } private static String getNewNameAfterSuccessfulUpload(String fileName) { return fileName.replace("dmp", "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 OTHER; } if (crashType.equals("browser")) { return BROWSER; } if (crashType.equals("renderer")) { return RENDERER; } if (crashType.equals("gpu-process")) { return GPU; } return OTHER; } } } catch (IOException e) { Log.w(TAG, "Error while reading crash file.", e.toString()); } finally { StreamUtil.closeQuietly(fileReader); } return OTHER; } /** * Increment the count of success/failure by 1 and distinguish between different types of * crashes by looking into the file. * @param fileName is the name of a minidump file that contains the type of crash. */ private void incrementCrashSuccessUploadCount(String fileName) { ChromePreferenceManager.getInstance(this) .incrementCrashSuccessUploadCount(getCrashType(fileName)); } private void incrementCrashFailureUploadCount(String fileName) { ChromePreferenceManager.getInstance(this) .incrementCrashFailureUploadCount(getCrashType(fileName)); } /** * 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, getApplicationContext()); } /** * 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. * * @param context Context of the application. */ public static void tryUploadAllCrashDumps(Context context) { Intent findAndUploadAllCrashesIntent = createFindAndUploadAllCrashesIntent(context); context.startService(findAndUploadAllCrashesIntent); } /** * 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 context the context to use for the intent. * @param localId The local ID of the crash report. */ @CalledByNative public static void tryUploadCrashDumpWithLocalId(Context context, String localId) { Intent intent = new Intent(context, MinidumpUploadService.class); intent.setAction(ACTION_FORCE_UPLOAD); intent.putExtra(LOCAL_CRASH_ID_KEY, localId); context.startService(intent); } private void handleForceUploadCrash(Intent intent) { String localId = intent.getStringExtra(LOCAL_CRASH_ID_KEY); if (localId == null || localId.isEmpty()) { Log.w(TAG, "Cannot force crash upload since local crash id is absent."); return; } Context context = getApplicationContext(); CrashFileManager fileManager = new CrashFileManager(context.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 = fileManager.trySetForcedUpload(minidumpFile); if (renamedMinidumpFile == null) { Log.w(TAG, "Could not rename the file " + minidumpFile.getName() + " for re-upload"); return; } File logfile = fileManager.getCrashUploadLogFile(); Intent uploadIntent = createUploadIntent(context, renamedMinidumpFile, logfile); // This method is intended to be used for manually triggering an attempt to upload an // already existing crash dump. Such a crash dump should already have had a chance to attach // the logcat to the minidump. Moreover, it's almost certainly too late to try to extract // the logcat now, since typically some time has passed between the crash and the user's // manual upload attempt. uploadIntent.putExtra(FINISHED_LOGCAT_EXTRACTION_KEY, true); startService(uploadIntent); } }