1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.chromecast.shell;
7 import android.net.http.AndroidHttpClient;
8 import android.util.Log;
10 import org.apache.http.HttpHost;
11 import org.apache.http.HttpResponse;
12 import org.apache.http.HttpStatus;
13 import org.apache.http.client.methods.HttpPost;
14 import org.apache.http.entity.InputStreamEntity;
16 import java.io.BufferedReader;
17 import java.io.ByteArrayInputStream;
19 import java.io.FileInputStream;
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.io.InputStreamReader;
23 import java.io.SequenceInputStream;
24 import java.util.concurrent.ExecutorService;
25 import java.util.concurrent.Executors;
26 import java.util.concurrent.TimeUnit;
29 * Crash crashdump uploader. Scans the crash dump location provided by CastCrashReporterClient
30 * for dump files, attempting to upload all crash dumps to the crash server.
32 * Uploading is intended to happen in a background thread, and this method will likely be called
33 * on startup, looking for crash dumps from previous runs, since Chromium's crash code
34 * explicitly blocks any post-dump hooks or uploading for Android builds.
36 public final class CastCrashUploader {
37 private static final String TAG = "CastCrashUploader";
38 private static final String CRASH_REPORT_HOST = "clients2.google.com";
39 private static final String CAST_SHELL_USER_AGENT = android.os.Build.MODEL + "/CastShell";
41 private final ExecutorService mExecutorService;
42 private final String mCrashDumpPath;
43 private final String mCrashReportUploadUrl;
45 public CastCrashUploader(String crashDumpPath, boolean isDebugBuild) {
46 this.mCrashDumpPath = crashDumpPath;
47 mCrashReportUploadUrl = isDebugBuild ?
48 "http://clients2.google.com/cr/staging_report" :
49 "http://clients2.google.com/cr/report";
50 mExecutorService = Executors.newFixedThreadPool(1);
53 public void removeCrashDumpsSync() {
54 mExecutorService.submit(new Runnable() {
57 File crashDumpDirectory = new File(mCrashDumpPath);
58 for (File potentialDump : crashDumpDirectory.listFiles()) {
59 if (potentialDump.getName().matches(".*\\.dmp\\d*")) {
60 potentialDump.delete();
65 waitForTasksToFinish();
69 * Synchronously uploads the crash dump from the current process, along with an attached
71 * @param logFilePath Full path to the log file for the current process.
73 public void uploadCurrentProcessDumpSync(String logFilePath) {
74 int pid = android.os.Process.myPid();
75 Log.d(TAG, "Immediately attempting a crash upload with logs, looking for: .dmp" + pid);
77 queueAllCrashDumpUpload(".*\\.dmp" + pid, new File(logFilePath));
78 waitForTasksToFinish();
81 private void waitForTasksToFinish() {
83 mExecutorService.shutdown();
84 boolean finished = mExecutorService.awaitTermination(60, TimeUnit.SECONDS);
86 Log.d(TAG, "Crash dump handling did not finish executing in time. Exiting.");
88 } catch (InterruptedException e) {
89 Log.e(TAG, "Interrupted waiting for asynchronous execution", e);
94 * Scans the given location for crash dump files and queues background
95 * uploads of each file.
97 public void uploadRecentCrashesAsync() {
98 mExecutorService.submit(new Runnable() {
101 // Multipart dump filename has format "[random string].dmp[pid]", e.g.
102 // 20597a65-b822-008e-31f8fc8e-02bb45c0.dmp18169
103 queueAllCrashDumpUpload(".*\\.dmp\\d*", null);
109 * Searches for files matching the given regex in the crash dump folder, queueing each
111 * @param dumpFileRegex Regex to test against the name of each file in the crash dump folder.
112 * @param logFile Log file to include, if any.
114 private void queueAllCrashDumpUpload(String dumpFileRegex, File logFile) {
115 if (mCrashDumpPath == null) return;
116 final AndroidHttpClient httpClient = AndroidHttpClient.newInstance(CAST_SHELL_USER_AGENT);
117 File crashDumpDirectory = new File(mCrashDumpPath);
118 for (File potentialDump : crashDumpDirectory.listFiles()) {
119 if (potentialDump.getName().matches(dumpFileRegex)) {
120 queueCrashDumpUpload(httpClient, potentialDump, logFile);
124 // When finished uploading all of the queued dumps, release httpClient
125 mExecutorService.submit(new Runnable() {
134 * Enqueues a background task to upload a single crash dump file.
136 private void queueCrashDumpUpload(final AndroidHttpClient httpClient, final File dumpFile,
137 final File logFile) {
138 mExecutorService.submit(new Runnable() {
141 Log.d(TAG, "Uploading dump crash log: " + dumpFile.getName());
144 // Dump file is already in multipart MIME format and has a boundary throughout.
145 // Scrape the first line, remove two dashes, call that the "boundary" and add it
146 // to the content-type.
147 FileInputStream dumpFileStream = new FileInputStream(dumpFile);
148 String dumpFirstLine = getFirstLine(dumpFileStream);
149 String mimeBoundary = dumpFirstLine.substring(2);
151 InputStream uploadCrashDumpStream = new FileInputStream(dumpFile);
152 InputStream logFileStream = null;
154 if (logFile != null) {
155 Log.d(TAG, "Including log file: " + logFile.getName());
156 StringBuffer logHeader = new StringBuffer();
157 logHeader.append(dumpFirstLine);
158 logHeader.append("\n");
160 "Content-Disposition: form-data; name=\"log\"; filename=\"log\"\n");
161 logHeader.append("Content-Type: text/plain\n\n");
162 InputStream logHeaderStream =
163 new ByteArrayInputStream(logHeader.toString().getBytes());
164 logFileStream = new FileInputStream(logFile);
166 // Upload: prepend the log file for uploading
167 uploadCrashDumpStream = new SequenceInputStream(
168 new SequenceInputStream(logHeaderStream, logFileStream),
169 uploadCrashDumpStream);
172 InputStreamEntity entity = new InputStreamEntity(uploadCrashDumpStream, -1);
173 entity.setContentType("multipart/form-data; boundary=" + mimeBoundary);
175 HttpPost uploadRequest = new HttpPost(mCrashReportUploadUrl);
176 uploadRequest.setEntity(entity);
177 HttpResponse response =
178 httpClient.execute(new HttpHost(CRASH_REPORT_HOST), uploadRequest);
180 // Expect a report ID as the entire response
181 String responseLine = getFirstLine(response.getEntity().getContent());
183 int statusCode = response.getStatusLine().getStatusCode();
184 if (statusCode != HttpStatus.SC_OK) {
185 Log.e(TAG, "Failed response (" + statusCode + "): " + responseLine);
187 // 400 Bad Request is returned if the dump file is malformed. Delete file
188 // to prevent re-attempting later.
189 if (statusCode == HttpStatus.SC_BAD_REQUEST) {
195 Log.d(TAG, "Successfully uploaded as report ID: " + responseLine);
197 // Delete the file so we don't re-upload it next time.
198 dumpFileStream.close();
200 } catch (IOException e) {
201 Log.e(TAG, "File I/O error trying to upload crash dump", e);
208 * Gets the first line from an input stream, opening and closing readers to do so.
209 * We're targeting Java 6, so this is still tedious to do.
210 * @return First line of the input stream.
211 * @throws IOException
213 private String getFirstLine(InputStream inputStream) throws IOException {
214 InputStreamReader streamReader = null;
215 BufferedReader reader = null;
217 streamReader = new InputStreamReader(inputStream);
218 reader = new BufferedReader(streamReader);
219 return reader.readLine();
221 if (reader != null) {
224 if (streamReader != null) {
225 streamReader.close();