Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1560)

Unified Diff: chrome/android/java_staging/src/org/chromium/chrome/browser/crash/MinidumpUploadCallable.java

Issue 1141283003: Upstream oodles of Chrome for Android code into Chromium. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: final patch? Created 5 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/android/java_staging/src/org/chromium/chrome/browser/crash/MinidumpUploadCallable.java
diff --git a/chrome/android/java_staging/src/org/chromium/chrome/browser/crash/MinidumpUploadCallable.java b/chrome/android/java_staging/src/org/chromium/chrome/browser/crash/MinidumpUploadCallable.java
new file mode 100644
index 0000000000000000000000000000000000000000..7e9c938a2ef44c4c06fbfbbe1fa0eafc5932ea04
--- /dev/null
+++ b/chrome/android/java_staging/src/org/chromium/chrome/browser/crash/MinidumpUploadCallable.java
@@ -0,0 +1,330 @@
+// 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.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import org.chromium.base.VisibleForTesting;
+import org.chromium.chrome.browser.preferences.privacy.CrashReportingPermissionManager;
+import org.chromium.chrome.browser.preferences.privacy.PrivacyPreferencesManager;
+import org.chromium.chrome.browser.util.HttpURLConnectionFactory;
+import org.chromium.chrome.browser.util.HttpURLConnectionFactoryImpl;
+import org.chromium.chrome.browser.util.StreamUtil;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.concurrent.Callable;
+
+/**
+ * This class tries to upload a minidump to the crash server.
+ *
+ * It is implemented as a Callable<Boolean> and returns true on successful uploads,
+ * and false otherwise.
+ */
+public class MinidumpUploadCallable implements Callable<Boolean> {
+ private static final String TAG = "MinidumpUploadCallable";
+ @VisibleForTesting protected static final int LOG_SIZE_LIMIT_BYTES = 1024 * 1024; // 1MB
+ @VisibleForTesting protected static final int LOG_UPLOAD_LIMIT_PER_DAY = 5;
+
+ @VisibleForTesting
+ protected static final String PREF_LAST_UPLOAD_DAY = "crash_dump_last_upload_day";
+ @VisibleForTesting protected static final String PREF_UPLOAD_COUNT = "crash_dump_upload_count";
+
+ @VisibleForTesting
+ protected static final String CRASH_URL_STRING = "https://clients2.google.com/cr/report";
+
+ @VisibleForTesting
+ protected static final String CONTENT_TYPE_TMPL = "multipart/form-data; boundary=%s";
+
+ private final File mFileToUpload;
+ private final File mLogfile;
+ private final HttpURLConnectionFactory mHttpURLConnectionFactory;
+ private final CrashReportingPermissionManager mPermManager;
+ private final SharedPreferences mSharedPreferences;
+
+ public MinidumpUploadCallable(File fileToUpload, File logfile, Context context) {
+ this(fileToUpload, logfile, new HttpURLConnectionFactoryImpl(),
+ PrivacyPreferencesManager.getInstance(context),
+ PreferenceManager.getDefaultSharedPreferences(context));
+ }
+
+ public MinidumpUploadCallable(File fileToUpload, File logfile,
+ HttpURLConnectionFactory httpURLConnectionFactory,
+ CrashReportingPermissionManager permManager, SharedPreferences sharedPreferences) {
+ mFileToUpload = fileToUpload;
+ mLogfile = logfile;
+ mHttpURLConnectionFactory = httpURLConnectionFactory;
+ mPermManager = permManager;
+ mSharedPreferences = sharedPreferences;
+ }
+
+ @Override
+ public Boolean call() {
+ if (!mPermManager.isUploadPermitted()) {
+ Log.i(TAG, "Minidump upload is not permitted");
+ return false;
+ }
+
+ boolean isLimited = mPermManager.isUploadLimited();
+ if (isLimited && !isUploadSizeAndFrequencyAllowed()) {
+ Log.i(TAG, "Minidump cannot currently be uploaded due to constraints");
+ return false;
+ }
+
+ HttpURLConnection connection =
+ mHttpURLConnectionFactory.createHttpURLConnection(CRASH_URL_STRING);
+ if (connection == null) {
+ return false;
+ }
+
+ FileInputStream minidumpInputStream = null;
+ try {
+ if (!configureConnectionForHttpPost(connection)) {
+ return false;
+ }
+ minidumpInputStream = new FileInputStream(mFileToUpload);
+ streamCopy(minidumpInputStream, connection.getOutputStream());
+ boolean status = handleExecutionResponse(connection);
+
+ if (isLimited) updateUploadPrefs();
+ return status;
+ } catch (IOException e) {
+ // For now just log the stack trace.
+ Log.w(TAG, "Error while uploading " + mFileToUpload.getName(), e);
+ return false;
+ } finally {
+ connection.disconnect();
+
+ if (minidumpInputStream != null) {
+ StreamUtil.closeQuietly(minidumpInputStream);
+ }
+ }
+ }
+
+ /**
+ * Configures a HttpURLConnection to send a HTTP POST request for uploading the minidump.
+ *
+ * This also reads the content-type from the minidump file.
+ *
+ * @param connection the HttpURLConnection to configure
+ * @return true if successful.
+ * @throws IOException
+ */
+ private boolean configureConnectionForHttpPost(HttpURLConnection connection)
+ throws IOException {
+ // Read the boundary which we need for the content type.
+ String boundary = readBoundary();
+ if (boundary == null) {
+ return false;
+ }
+
+ connection.setDoOutput(true);
+ connection.setRequestProperty("Connection", "Keep-Alive");
+ connection.setRequestProperty("Content-Type", String.format(CONTENT_TYPE_TMPL, boundary));
+ return true;
+ }
+
+ /**
+ * Reads the HTTP response and cleans up successful uploads.
+ *
+ * @param connection the connection to read the response from
+ * @return true if the upload was successful, false otherwise.
+ * @throws IOException
+ */
+ private Boolean handleExecutionResponse(HttpURLConnection connection) throws IOException {
+ int responseCode = connection.getResponseCode();
+ if (isSuccessful(responseCode)) {
+ String responseContent = getResponseContentAsString(connection);
+ // The crash server returns the crash ID.
+ String id = responseContent != null ? responseContent : "unknown";
+ Log.i(TAG, "Minidump " + mFileToUpload.getName() + " uploaded successfully, id: " + id);
+
+ // TODO(acleung): MinidumpUploadService is in charge of renaming while this class is
+ // in charge of deleting. We should move all the file system operations into
+ // MinidumpUploadService instead.
+ cleanupMinidumpFile();
+
+ try {
+ appendUploadedEntryToLog(id);
+ } catch (IOException ioe) {
+ Log.e(TAG, "Fail to write uploaded entry to log file");
+ }
+ return true;
+ } else {
+ // Log the results of the upload. Note that periodic upload failures aren't bad
+ // because we will need to throttle uploads in the future anyway.
+ String msg = String.format(Locale.US,
+ "Failed to upload %s with code: %d (%s).",
+ mFileToUpload.getName(), responseCode, connection.getResponseMessage());
+ Log.i(TAG, msg);
+
+ // TODO(acleung): The return status informs us about why an upload might be
+ // rejected. The next logical step is to put the reasons in an UMA histogram.
+ return false;
+ }
+ }
+
+ /**
+ * Records the upload entry to a log file
+ * similar to what is done in chrome/app/breakpad_linux.cc
+ *
+ * @param id The crash ID return from the server.
+ */
+ private void appendUploadedEntryToLog(String id) throws IOException {
+ FileWriter writer = new FileWriter(mLogfile, /* Appending */ true);
+
+ // The log entries are formated like so:
+ // seconds_since_epoch,crash_id
+ StringBuilder sb = new StringBuilder();
+ sb.append(System.currentTimeMillis() / 1000);
+ sb.append(",");
+ sb.append(id);
+ sb.append('\n');
+
+ try {
+ // Since we are writing one line at a time, lets forget about BufferWriters.
+ writer.write(sb.toString());
+ } finally {
+ writer.close();
+ }
+ }
+
+ /**
+ * Get the boundary from the file, we need it for the content-type.
+ *
+ * @return the boundary if found, else null.
+ * @throws IOException
+ */
+ private String readBoundary() throws IOException {
+ BufferedReader reader = new BufferedReader(new FileReader(mFileToUpload));
+ String boundary = reader.readLine();
+ reader.close();
+ if (boundary == null || boundary.trim().isEmpty()) {
+ Log.e(TAG, "Ignoring invalid crash dump: '" + mFileToUpload + "'");
+ return null;
+ }
+ boundary = boundary.trim();
+ if (!boundary.startsWith("--") || boundary.length() < 10) {
+ Log.e(TAG, "Ignoring invalidly bound crash dump: '" + mFileToUpload + "'");
+ return null;
+ }
+ boundary = boundary.substring(2); // Remove the initial --
+ return boundary;
+ }
+
+ /**
+ * Mark file we just uploaded for cleanup later.
+ *
+ * We do not immediately delete the file for testing reasons,
+ * but if marking the file fails, we do delete it right away.
+ */
+ private void cleanupMinidumpFile() {
+ if (!CrashFileManager.tryMarkAsUploaded(mFileToUpload)) {
+ Log.w(TAG, "Unable to mark " + mFileToUpload + " as uploaded.");
+ if (!mFileToUpload.delete()) {
+ Log.w(TAG, "Cannot delete " + mFileToUpload);
+ }
+ }
+ }
+
+ /**
+ * Checks whether crash upload satisfies the size and frequency constraints.
+ *
+ * @return whether crash upload satisfies the size and frequency constraints.
+ */
+ private boolean isUploadSizeAndFrequencyAllowed() {
+ // Check upload size constraint.
+ if (mFileToUpload.length() > LOG_SIZE_LIMIT_BYTES) return false;
+
+ // Check upload frequency constraint.
+ // If pref doesn't exist then in both cases default value 0 will be returned and comparison
+ // always would be true.
+ if (mSharedPreferences.getInt(PREF_LAST_UPLOAD_DAY, 0) != getCurrentDay()) return true;
+ return mSharedPreferences.getInt(PREF_UPLOAD_COUNT, 0) < LOG_UPLOAD_LIMIT_PER_DAY;
+ }
+
+ /**
+ * Updates preferences used for determining crash upload constraints.
+ */
+ private void updateUploadPrefs() {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+
+ int day = getCurrentDay();
+ int prevCount = mSharedPreferences.getInt(PREF_UPLOAD_COUNT, 0);
+ if (mSharedPreferences.getInt(PREF_LAST_UPLOAD_DAY, 0) != day) {
+ prevCount = 0;
+ }
+ editor.putInt(PREF_LAST_UPLOAD_DAY, day).putInt(PREF_UPLOAD_COUNT, prevCount + 1).apply();
+ }
+
+ /**
+ * Returns number of current day in a year starting from 1. Overridden in tests.
+ */
+ protected int getCurrentDay() {
+ return Calendar.getInstance().get(Calendar.YEAR) * 365
+ + Calendar.getInstance().get(Calendar.DAY_OF_YEAR);
+ }
+
+ /**
+ * Returns whether the response code indicates a successful HTTP request.
+ *
+ * @param responseCode the response code
+ * @return true if response code indicates success, false otherwise.
+ */
+ private static boolean isSuccessful(int responseCode) {
+ return responseCode == 200 || responseCode == 201 || responseCode == 202;
+ }
+
+ /**
+ * Reads the response from |connection| as a String.
+ *
+ * @param connection the connection to read the response from.
+ * @return the content of the response.
+ * @throws IOException
+ */
+ private static String getResponseContentAsString(HttpURLConnection connection)
+ throws IOException {
+ String responseContent = null;
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ streamCopy(connection.getInputStream(), baos);
+ if (baos.size() > 0) {
+ responseContent = baos.toString();
+ }
+ return responseContent;
+ }
+
+ /**
+ * Copies all available data from |inStream| to |outStream|. Closes both
+ * streams when done.
+ *
+ * @param inStream the stream to read
+ * @param outStream the stream to write to
+ * @throws IOException
+ */
+ private static void streamCopy(InputStream inStream,
+ OutputStream outStream) throws IOException {
+ byte[] temp = new byte[4096];
+ int bytesRead = inStream.read(temp);
+ while (bytesRead >= 0) {
+ outStream.write(temp, 0, bytesRead);
+ bytesRead = inStream.read(temp);
+ }
+ inStream.close();
+ outStream.close();
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698