| Index: chromecast/shell/android/apk/src/org/chromium/chromecast/shell/CastCrashUploader.java
|
| diff --git a/chromecast/shell/android/apk/src/org/chromium/chromecast/shell/CastCrashUploader.java b/chromecast/shell/android/apk/src/org/chromium/chromecast/shell/CastCrashUploader.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..bc1631ccb62f75e036e15d9eb2b0889aed9abebd
|
| --- /dev/null
|
| +++ b/chromecast/shell/android/apk/src/org/chromium/chromecast/shell/CastCrashUploader.java
|
| @@ -0,0 +1,223 @@
|
| +// Copyright 2014 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.chromecast.shell;
|
| +
|
| +import android.net.http.AndroidHttpClient;
|
| +import android.util.Log;
|
| +
|
| +import org.apache.http.HttpHost;
|
| +import org.apache.http.HttpResponse;
|
| +import org.apache.http.HttpStatus;
|
| +import org.apache.http.client.methods.HttpPost;
|
| +import org.apache.http.entity.InputStreamEntity;
|
| +
|
| +import java.io.BufferedReader;
|
| +import java.io.ByteArrayInputStream;
|
| +import java.io.File;
|
| +import java.io.FileInputStream;
|
| +import java.io.IOException;
|
| +import java.io.InputStream;
|
| +import java.io.InputStreamReader;
|
| +import java.io.SequenceInputStream;
|
| +import java.util.concurrent.ExecutorService;
|
| +import java.util.concurrent.Executors;
|
| +import java.util.concurrent.TimeUnit;
|
| +
|
| +/**
|
| + * Crash crashdump uploader. Scans the crash dump location provided by CastCrashReporterClient
|
| + * for dump files, attempting to upload all crash dumps to the crash server.
|
| + *
|
| + * Uploading is intended to happen in a background thread, and this method will likely be called
|
| + * on startup, looking for crash dumps from previous runs, since Chromium's crash code
|
| + * explicitly blocks any post-dump hooks or uploading for Android builds.
|
| + */
|
| +public final class CastCrashUploader {
|
| + private static final String TAG = "CastCrashUploader";
|
| + private static final String CRASH_REPORT_HOST = "clients2.google.com";
|
| + private static final String CAST_SHELL_USER_AGENT = android.os.Build.MODEL + "/CastShell";
|
| +
|
| + private final ExecutorService mExecutorService;
|
| + private final String mCrashDumpPath;
|
| + private final String mCrashReportUploadUrl;
|
| +
|
| + public CastCrashUploader(String crashDumpPath, boolean isDebugBuild) {
|
| + this.mCrashDumpPath = crashDumpPath;
|
| + mCrashReportUploadUrl = isDebugBuild ?
|
| + "http://clients2.google.com/cr/staging_report" :
|
| + "http://clients2.google.com/cr/report";
|
| + mExecutorService = Executors.newFixedThreadPool(1);
|
| + }
|
| +
|
| + public void removeCrashDumpsSync() {
|
| + mExecutorService.submit(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + File crashDumpDirectory = new File(mCrashDumpPath);
|
| + for (File potentialDump : crashDumpDirectory.listFiles()) {
|
| + if (potentialDump.getName().matches(".*\\.dmp\\d*")) {
|
| + potentialDump.delete();
|
| + }
|
| + }
|
| + }
|
| + });
|
| + waitForTasksToFinish();
|
| + }
|
| +
|
| + /**
|
| + * Synchronously uploads the crash dump from the current process, along with an attached
|
| + * log file.
|
| + * @param logFilePath Full path to the log file for the current process.
|
| + */
|
| + public void uploadCurrentProcessDumpSync(String logFilePath) {
|
| + int pid = android.os.Process.myPid();
|
| + Log.d(TAG, "Immediately attempting a crash upload with logs, looking for: .dmp" + pid);
|
| +
|
| + queueAllCrashDumpUpload(".*\\.dmp" + pid, new File(logFilePath));
|
| + waitForTasksToFinish();
|
| + }
|
| +
|
| + private void waitForTasksToFinish() {
|
| + try {
|
| + mExecutorService.shutdown();
|
| + boolean finished = mExecutorService.awaitTermination(60, TimeUnit.SECONDS);
|
| + if (!finished) {
|
| + Log.d(TAG, "Crash dump handling did not finish executing in time. Exiting.");
|
| + }
|
| + } catch (InterruptedException e) {
|
| + Log.e(TAG, "Interrupted waiting for asynchronous execution", e);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Scans the given location for crash dump files and queues background
|
| + * uploads of each file.
|
| + */
|
| + public void uploadRecentCrashesAsync() {
|
| + mExecutorService.submit(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + // Multipart dump filename has format "[random string].dmp[pid]", e.g.
|
| + // 20597a65-b822-008e-31f8fc8e-02bb45c0.dmp18169
|
| + queueAllCrashDumpUpload(".*\\.dmp\\d+", null);
|
| + }
|
| + });
|
| + }
|
| +
|
| + /**
|
| + * Searches for files matching the given regex in the crash dump folder, queueing each
|
| + * one for upload.
|
| + * @param dumpFileRegex Regex to test against the name of each file in the crash dump folder.
|
| + * @param logFile Log file to include, if any.
|
| + */
|
| + private void queueAllCrashDumpUpload(String dumpFileRegex, File logFile) {
|
| + if (mCrashDumpPath == null) return;
|
| + final AndroidHttpClient httpClient = AndroidHttpClient.newInstance(CAST_SHELL_USER_AGENT);
|
| + File crashDumpDirectory = new File(mCrashDumpPath);
|
| + for (File potentialDump : crashDumpDirectory.listFiles()) {
|
| + if (potentialDump.getName().matches(dumpFileRegex)) {
|
| + queueCrashDumpUpload(httpClient, potentialDump, logFile);
|
| + }
|
| + }
|
| +
|
| + // When finished uploading all of the queued dumps, release httpClient
|
| + mExecutorService.submit(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + httpClient.close();
|
| + }
|
| + });
|
| + }
|
| +
|
| + /**
|
| + * Enqueues a background task to upload a single crash dump file.
|
| + */
|
| + private void queueCrashDumpUpload(final AndroidHttpClient httpClient, final File dumpFile,
|
| + final File logFile) {
|
| + mExecutorService.submit(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + Log.d(TAG, "Uploading dump crash log: " + dumpFile.getName());
|
| +
|
| + try {
|
| + // Dump file is already in multipart MIME format and has a boundary throughout.
|
| + // Scrape the first line, remove two dashes, call that the "boundary" and add it
|
| + // to the content-type.
|
| + FileInputStream dumpFileStream = new FileInputStream(dumpFile);
|
| + String dumpFirstLine = getFirstLine(dumpFileStream);
|
| + String mimeBoundary = dumpFirstLine.substring(2);
|
| +
|
| + InputStream uploadCrashDumpStream = new FileInputStream(dumpFile);
|
| + InputStream logFileStream = null;
|
| +
|
| + if (logFile != null) {
|
| + Log.d(TAG, "Including log file: " + logFile.getName());
|
| + StringBuffer logHeader = new StringBuffer();
|
| + logHeader.append(dumpFirstLine);
|
| + logHeader.append("\n");
|
| + logHeader.append(
|
| + "Content-Disposition: form-data; name=\"log\"; filename=\"log\"\n");
|
| + logHeader.append("Content-Type: text/plain\n\n");
|
| + InputStream logHeaderStream =
|
| + new ByteArrayInputStream(logHeader.toString().getBytes());
|
| + logFileStream = new FileInputStream(logFile);
|
| +
|
| + // Upload: prepend the log file for uploading
|
| + uploadCrashDumpStream = new SequenceInputStream(
|
| + new SequenceInputStream(logHeaderStream, logFileStream),
|
| + uploadCrashDumpStream);
|
| + }
|
| +
|
| + InputStreamEntity entity = new InputStreamEntity(uploadCrashDumpStream, -1);
|
| + entity.setContentType("multipart/form-data; boundary=" + mimeBoundary);
|
| +
|
| + HttpPost uploadRequest = new HttpPost(mCrashReportUploadUrl);
|
| + uploadRequest.setEntity(entity);
|
| + HttpResponse response =
|
| + httpClient.execute(new HttpHost(CRASH_REPORT_HOST), uploadRequest);
|
| +
|
| + // Expect a report ID as the entire response
|
| + String responseLine = getFirstLine(response.getEntity().getContent());
|
| +
|
| + int statusCode = response.getStatusLine().getStatusCode();
|
| + if (statusCode != HttpStatus.SC_OK) {
|
| + Log.e(TAG, "Failed response (" + statusCode + "): " + responseLine);
|
| + return;
|
| + }
|
| +
|
| + Log.d(TAG, "Successfully uploaded as report ID: " + responseLine);
|
| +
|
| + // Delete the file so we don't re-upload it next time.
|
| + dumpFileStream.close();
|
| + dumpFile.delete();
|
| + } catch (IOException e) {
|
| + Log.e(TAG, "File I/O error trying to upload crash dump", e);
|
| + }
|
| + }
|
| + });
|
| + }
|
| +
|
| + /**
|
| + * Gets the first line from an input stream, opening and closing readers to do so.
|
| + * We're targeting Java 6, so this is still tedious to do.
|
| + * @return First line of the input stream.
|
| + * @throws IOException
|
| + */
|
| + private String getFirstLine(InputStream inputStream) throws IOException {
|
| + InputStreamReader streamReader = null;
|
| + BufferedReader reader = null;
|
| + try {
|
| + streamReader = new InputStreamReader(inputStream);
|
| + reader = new BufferedReader(streamReader);
|
| + return reader.readLine();
|
| + } finally {
|
| + if (reader != null) {
|
| + reader.close();
|
| + }
|
| + if (streamReader != null) {
|
| + streamReader.close();
|
| + }
|
| + }
|
| + }
|
| +}
|
|
|