| // Copyright 2017 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 org.chromium.base.Log; |
| import org.chromium.components.minidump_uploader.CrashReportMimeWriter; |
| |
| import java.io.BufferedReader; |
| import java.io.ByteArrayInputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStream; |
| import java.io.SequenceInputStream; |
| import java.net.HttpURLConnection; |
| import java.net.URL; |
| import java.nio.charset.Charset; |
| import java.nio.charset.UnsupportedCharsetException; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.Future; |
| import java.util.concurrent.ScheduledExecutorService; |
| 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. |
| * |
| * <p>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 = "cr_CastCrashUploader"; |
| private static final String CRASH_REPORT_HOST = "clients2.google.com"; |
| private static final String CAST_SHELL_USER_AGENT = android.os.Build.MODEL + "/CastShell"; |
| // Multipart dump filename has format "[random string].dmp[pid]", e.g. |
| // 20597a65-b822-008e-31f8fc8e-02bb45c0.dmp18169 |
| private static final String DUMP_FILE_REGEX = ".*\\.dmp\\d*"; |
| |
| private final ScheduledExecutorService mExecutorService; |
| private final ElidedLogcatProvider mLogcatProvider; |
| private final String mCrashDumpPath; |
| private final String mCrashReportPath; |
| private final String mCrashReportUploadUrl; |
| private final String mUuid; |
| private final String mApplicationFeedback; |
| private final Runnable mQueueAllCrashDumpUploadsRunnable = () -> checkForCrashDumps(); |
| |
| public CastCrashUploader(ScheduledExecutorService executorService, |
| ElidedLogcatProvider logcatProvider, String crashDumpPath, String crashReportPath, |
| String uuid, String applicationFeedback, boolean uploadCrashToStaging) { |
| mExecutorService = executorService; |
| mLogcatProvider = logcatProvider; |
| mCrashDumpPath = crashDumpPath; |
| mCrashReportPath = crashReportPath; |
| mUuid = uuid; |
| mApplicationFeedback = applicationFeedback; |
| mCrashReportUploadUrl = uploadCrashToStaging |
| ? "https://clients2.google.com/cr/staging_report" |
| : "https://clients2.google.com/cr/report"; |
| } |
| |
| @SuppressWarnings("FutureReturnValueIgnored") |
| public void uploadOnce() { |
| mExecutorService.schedule(mQueueAllCrashDumpUploadsRunnable, 0, TimeUnit.MINUTES); |
| } |
| |
| public void removeCrashDumps() { |
| Log.i(TAG, "Remove crash dumps"); |
| File crashReportDirectory = new File(mCrashReportPath); |
| for (File potentialDump : crashReportDirectory.listFiles()) { |
| if (potentialDump.getName().matches(DUMP_FILE_REGEX)) { |
| potentialDump.delete(); |
| } |
| } |
| } |
| |
| /** |
| * Searches for files matching the given regex in the crash dump folder, queueing each one for |
| * upload. |
| * |
| * @param synchronous Whether or not this function should block on queued uploads |
| * @param log Log to include, if any |
| */ |
| private void checkForCrashDumps() { |
| if (mCrashDumpPath == null) return; |
| |
| Log.i(TAG, "Checking for crash dumps"); |
| File crashDumpDirectory = new File(mCrashDumpPath); |
| File crashReportDirectory = new File(mCrashReportPath); |
| |
| if (!crashDumpDirectory.isDirectory() || !crashReportDirectory.isDirectory()) { |
| return; |
| } |
| |
| CrashReportMimeWriter.rewriteMinidumpsAsMIMEs(crashDumpDirectory, crashReportDirectory); |
| |
| int numCrashDumps = crashReportDirectory.listFiles().length; |
| if (numCrashDumps > 0) { |
| Log.i(TAG, numCrashDumps + " crash dumps found"); |
| mLogcatProvider.getElidedLogcat( |
| (String logs) -> queueAllCrashDumpUploadsWithLogs(crashReportDirectory, logs)); |
| } |
| } |
| |
| private void queueAllCrashDumpUploadsWithLogs(File crashDumpDirectory, String logs) { |
| for (final File potentialDump : crashDumpDirectory.listFiles()) { |
| String dumpName = potentialDump.getName(); |
| if (dumpName.matches(DUMP_FILE_REGEX)) { |
| mExecutorService.submit(() -> uploadCrashDump(potentialDump, logs)); |
| } |
| } |
| } |
| |
| /** Enqueues a background task to upload a single crash dump file. */ |
| private void uploadCrashDump(final File dumpFile, final String log) { |
| Log.i(TAG, "Uploading dump crash log: %s", dumpFile.getName()); |
| |
| try { |
| InputStream uploadCrashDumpStream = new FileInputStream(dumpFile); |
| // 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); |
| |
| if (!log.equals("")) { |
| Log.i(TAG, "Including log file"); |
| StringBuilder logHeader = new StringBuilder(); |
| logHeader.append(dumpFirstLine); |
| logHeader.append("\n"); |
| logHeader.append( |
| "Content-Disposition: form-data; name=\"log.txt\"; filename=\"log.txt\"\n"); |
| logHeader.append("Content-Type: text/plain\n\n"); |
| logHeader.append(log); |
| logHeader.append("\n"); |
| InputStream logHeaderStream = new ByteArrayInputStream( |
| logHeader.toString().getBytes(Charset.forName("UTF-8"))); |
| // Upload: prepend the log file for uploading |
| uploadCrashDumpStream = |
| new SequenceInputStream(logHeaderStream, uploadCrashDumpStream); |
| } |
| |
| Log.d(TAG, "UUID: " + mUuid); |
| if (!mUuid.equals("")) { |
| StringBuilder uuidBuilder = new StringBuilder(); |
| uuidBuilder.append(dumpFirstLine); |
| uuidBuilder.append("\n"); |
| uuidBuilder.append("Content-Disposition: form-data; name=\"comments\"\n"); |
| uuidBuilder.append("Content-Type: text/plain\n\n"); |
| uuidBuilder.append(mUuid); |
| uuidBuilder.append("\n"); |
| uploadCrashDumpStream = new SequenceInputStream( |
| new ByteArrayInputStream( |
| uuidBuilder.toString().getBytes(Charset.forName("UTF-8"))), |
| uploadCrashDumpStream); |
| } else { |
| Log.d(TAG, "No UUID"); |
| } |
| |
| if (!mApplicationFeedback.equals("")) { |
| Log.i(TAG, "Including feedback"); |
| StringBuilder feedbackHeader = new StringBuilder(); |
| feedbackHeader.append(dumpFirstLine); |
| feedbackHeader.append("\n"); |
| feedbackHeader.append( |
| "Content-Disposition: form-data; name=\"application_feedback.txt\"; filename=\"application.txt\"\n"); |
| feedbackHeader.append("Content-Type: text/plain\n\n"); |
| feedbackHeader.append(mApplicationFeedback); |
| feedbackHeader.append("\n"); |
| InputStream feedbackHeaderStream = new ByteArrayInputStream( |
| feedbackHeader.toString().getBytes(Charset.forName("UTF-8"))); |
| // Upload: prepend the log file for uploading |
| uploadCrashDumpStream = |
| new SequenceInputStream(feedbackHeaderStream, uploadCrashDumpStream); |
| } else { |
| Log.d(TAG, "No Feedback"); |
| } |
| |
| HttpURLConnection connection = |
| (HttpURLConnection) new URL(mCrashReportUploadUrl).openConnection(); |
| |
| // Expect a report ID as the entire response |
| try { |
| connection.setDoOutput(true); |
| connection.setRequestProperty( |
| "Content-Type", "multipart/form-data; boundary=" + mimeBoundary); |
| |
| streamCopy(uploadCrashDumpStream, connection.getOutputStream()); |
| |
| String responseLine = getFirstLine(connection.getInputStream()); |
| |
| int responseCode = connection.getResponseCode(); |
| if (responseCode == HttpURLConnection.HTTP_OK |
| || responseCode == HttpURLConnection.HTTP_CREATED |
| || responseCode == HttpURLConnection.HTTP_ACCEPTED) { |
| Log.i(TAG, "Successfully uploaded to %s, report ID: %s", mCrashReportUploadUrl, |
| responseLine); |
| } else { |
| Log.e(TAG, "Failed response (%d): %s", responseCode, |
| connection.getResponseMessage()); |
| |
| // 400 Bad Request is returned if the dump file is malformed. If request |
| // is not malformed, short-circuit before cleanup to avoid deletion and |
| // retry later, otherwise pass through and delete malformed file. |
| if (responseCode != HttpURLConnection.HTTP_BAD_REQUEST) { |
| return; |
| } |
| } |
| } catch (FileNotFoundException fnfe) { |
| // Android's HttpURLConnection implementation fires FNFE on some errors. |
| Log.e(TAG, "Failed response: " + connection.getResponseCode(), fnfe); |
| } catch (UnsupportedCharsetException e) { |
| Log.wtf(TAG, "UTF-8 not supported"); |
| } finally { |
| connection.disconnect(); |
| dumpFileStream.close(); |
| } |
| |
| // Delete the file so we don't re-upload it next time. |
| dumpFile.delete(); |
| } catch (IOException e) { |
| Log.e(TAG, "Error occurred trying to upload crash dump", e); |
| } |
| } |
| |
| /** |
| * 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(); |
| } |
| |
| /** |
| * Gets the first line from an input stream |
| * |
| * @return First line of the input stream. |
| * @throws IOException |
| */ |
| private String getFirstLine(InputStream inputStream) throws IOException { |
| try (InputStreamReader streamReader = new InputStreamReader(inputStream, "UTF-8"); |
| BufferedReader reader = new BufferedReader(streamReader)) { |
| return reader.readLine(); |
| } catch (UnsupportedCharsetException e) { |
| Log.wtf(TAG, "UTF-8 not supported"); |
| return ""; |
| } |
| } |
| |
| /** |
| * Waits until Future is propagated |
| * |
| * @return Whether thread should continue waiting |
| */ |
| private boolean waitOnTask(Future task) { |
| try { |
| task.get(); |
| return true; |
| } catch (InterruptedException e) { |
| // Was interrupted while waiting, tell caller to cancel waiting |
| return false; |
| } catch (ExecutionException e) { |
| // Task execution may have failed, but this is fine as long as it finished |
| // executing |
| return true; |
| } |
| } |
| } |