Commit f7ab2ada authored by Tobias Sargeant's avatar Tobias Sargeant Committed by Commit Bot

[weblayer] Implement weblayer crash uploading.

This CL provides an API to the embedder for receiving crash notifications,
and triggering upload or deletion of crashes.

It is designed so that the notification can be used to trigger an upload
job to execute later (potentially in a different process) without fully
initializing weblayer.

A number of things in the minidump_uploader (chiefly MDUploadCallable ->
MinidumpUploader) have been forked. The intention is to unfork immediately
after this patch. Doing so will address existing TODOs in the component,
and address some existing separation of concerns issues.

Bug: 1027076
Test: run_weblayer_support_instrumentation_test_apk -f '*CrashReporterTest#*'
Change-Id: I86b9b37830014c468fd69cf99a67f6f00809f9fc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1940271
Auto-Submit: Tobias Sargeant <tobiasjs@chromium.org>
Commit-Queue: Tobias Sargeant <tobiasjs@chromium.org>
Reviewed-by: default avatarAlex Clarke <alexclarke@chromium.org>
Reviewed-by: default avatarColin Blundell <blundell@chromium.org>
Cr-Commit-Position: refs/heads/master@{#720728}
parent fc33e13d
...@@ -288,6 +288,7 @@ jumbo_static_library("weblayer_lib") { ...@@ -288,6 +288,7 @@ jumbo_static_library("weblayer_lib") {
"//android_webview:generate_aw_strings", "//android_webview:generate_aw_strings",
"//components/crash/android:crashpad_main", "//components/crash/android:crashpad_main",
"//components/metrics", "//components/metrics",
"//components/minidump_uploader",
"//components/safe_browsing", "//components/safe_browsing",
"//components/safe_browsing/android:remote_database_manager", "//components/safe_browsing/android:remote_database_manager",
"//components/safe_browsing/android:safe_browsing_api_handler", "//components/safe_browsing/android:safe_browsing_api_handler",
......
...@@ -9,6 +9,7 @@ android_library("weblayer_java_tests") { ...@@ -9,6 +9,7 @@ android_library("weblayer_java_tests") {
testonly = true testonly = true
java_files = [ java_files = [
"src/org/chromium/weblayer/test/BrowserFragmentLifecycleTest.java", "src/org/chromium/weblayer/test/BrowserFragmentLifecycleTest.java",
"src/org/chromium/weblayer/test/CrashReporterTest.java",
"src/org/chromium/weblayer/test/DataClearingTest.java", "src/org/chromium/weblayer/test/DataClearingTest.java",
"src/org/chromium/weblayer/test/DownloadCallbackTest.java", "src/org/chromium/weblayer/test/DownloadCallbackTest.java",
"src/org/chromium/weblayer/test/ErrorPageCallbackTest.java", "src/org/chromium/weblayer/test/ErrorPageCallbackTest.java",
......
// Copyright 2019 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.weblayer.test;
import android.os.Bundle;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.weblayer.CrashReporterCallback;
import org.chromium.weblayer.CrashReporterController;
import org.chromium.weblayer.shell.InstrumentationActivity;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
/**
* Tests for crash reporting in WebLayer.
*/
@RunWith(BaseJUnit4ClassRunner.class)
public class CrashReporterTest {
private static final String UUID = "032b90a6-836c-49bc-a9f4-aa210458eaf3";
private static final String LOCAL_ID = "aa210458eaf3";
@Rule
public InstrumentationActivityTestRule mActivityTestRule =
new InstrumentationActivityTestRule();
private File mCrashReport;
private File mCrashSidecar;
@Before
public void setUp() throws IOException {
File cacheDir =
InstrumentationRegistry.getInstrumentation().getTargetContext().getCacheDir();
File crashReportDir = new File(cacheDir, "weblayer/Crash Reports");
crashReportDir.mkdirs();
mCrashReport = new File(crashReportDir, UUID + ".dmp0.try0");
mCrashSidecar = new File(crashReportDir, UUID + ".json");
mCrashReport.createNewFile();
try (FileOutputStream out = new FileOutputStream(mCrashSidecar)) {
out.write("{\"foo\":\"bar\"}".getBytes());
}
}
@After
public void tearDown() throws IOException {
if (mCrashReport.exists()) mCrashReport.delete();
if (mCrashSidecar.exists()) mCrashSidecar.delete();
}
private static final class BundleCallbackHelper extends CallbackHelper {
private Bundle mResult;
public Bundle getResult() {
return mResult;
}
public void notifyCalled(Bundle result) {
mResult = result;
notifyCalled();
}
}
@Test
@SmallTest
public void testCrashReporterLoading() throws Exception {
BundleCallbackHelper callbackHelper = new BundleCallbackHelper();
CallbackHelper deleteHelper = new CallbackHelper();
InstrumentationActivity activity = mActivityTestRule.launchShell(new Bundle());
TestThreadUtils.runOnUiThreadBlocking(() -> {
CrashReporterController crashReporterController =
CrashReporterController.getInstance(activity);
// Set up a callback object that will fetch the crash keys for the crash with id
// LOCAL_ID and then delete it.
crashReporterController.registerCallback(new CrashReporterCallback() {
@Override
public void onPendingCrashReports(String[] localIds) {
if (!Arrays.asList(localIds).contains(LOCAL_ID)) {
callbackHelper.notifyFailed("localIds does not contain " + LOCAL_ID);
return;
}
Bundle crashKeys = crashReporterController.getCrashKeys(localIds[0]);
callbackHelper.notifyCalled(crashKeys);
crashReporterController.deleteCrash(localIds[0]);
}
@Override
public void onCrashDeleted(String localId) {
deleteHelper.notifyCalled();
}
});
// Check for crash reports ready to upload
crashReporterController.checkForPendingCrashReports();
});
// Expect that a Bundle containing { "foo": "bar" } is returned.
callbackHelper.waitForCallback(callbackHelper.getCallCount());
Bundle crashKeys = callbackHelper.getResult();
Assert.assertArrayEquals(crashKeys.keySet().toArray(new String[0]), new String[] {"foo"});
Assert.assertEquals(crashKeys.getString("foo"), "bar");
// Expect that the crash report and its sidecar are deleted.
deleteHelper.waitForCallback(deleteHelper.getCallCount());
Assert.assertFalse(mCrashReport.exists());
Assert.assertFalse(mCrashSidecar.exists());
}
}
...@@ -26,6 +26,8 @@ ...@@ -26,6 +26,8 @@
#include "weblayer/public/main.h" #include "weblayer/public/main.h"
#if defined(OS_ANDROID) #if defined(OS_ANDROID)
#include "components/crash/content/browser/child_exit_observer_android.h"
#include "components/crash/content/browser/child_process_crash_observer_android.h"
#include "net/android/network_change_notifier_factory_android.h" #include "net/android/network_change_notifier_factory_android.h"
#include "net/base/network_change_notifier.h" #include "net/base/network_change_notifier.h"
#include "weblayer/browser/android/metrics/uma_utils.h" #include "weblayer/browser/android/metrics/uma_utils.h"
...@@ -63,6 +65,17 @@ BrowserMainPartsImpl::BrowserMainPartsImpl( ...@@ -63,6 +65,17 @@ BrowserMainPartsImpl::BrowserMainPartsImpl(
BrowserMainPartsImpl::~BrowserMainPartsImpl() = default; BrowserMainPartsImpl::~BrowserMainPartsImpl() = default;
int BrowserMainPartsImpl::PreCreateThreads() {
#if defined(OS_ANDROID)
// The ChildExitObserver needs to be created before any child process is
// created because it needs to be notified during process creation.
crash_reporter::ChildExitObserver::Create();
crash_reporter::ChildExitObserver::GetInstance()->RegisterClient(
std::make_unique<crash_reporter::ChildProcessCrashObserver>());
#endif
return service_manager::RESULT_CODE_NORMAL_EXIT;
}
void BrowserMainPartsImpl::PreMainMessageLoopStart() { void BrowserMainPartsImpl::PreMainMessageLoopStart() {
#if defined(USE_AURA) && defined(USE_X11) #if defined(USE_AURA) && defined(USE_X11)
ui::TouchFactory::SetTouchDeviceListFromCommandLine(); ui::TouchFactory::SetTouchDeviceListFromCommandLine();
......
...@@ -23,6 +23,7 @@ class BrowserMainPartsImpl : public content::BrowserMainParts { ...@@ -23,6 +23,7 @@ class BrowserMainPartsImpl : public content::BrowserMainParts {
~BrowserMainPartsImpl() override; ~BrowserMainPartsImpl() override;
// BrowserMainParts overrides. // BrowserMainParts overrides.
int PreCreateThreads() override;
int PreEarlyInitialization() override; int PreEarlyInitialization() override;
void PreMainMessageLoopStart() override; void PreMainMessageLoopStart() override;
void PreMainMessageLoopRun() override; void PreMainMessageLoopRun() override;
......
...@@ -31,12 +31,14 @@ android_library("java") { ...@@ -31,12 +31,14 @@ android_library("java") {
"org/chromium/weblayer_private/ChildProcessServiceImpl.java", "org/chromium/weblayer_private/ChildProcessServiceImpl.java",
"org/chromium/weblayer_private/ContentView.java", "org/chromium/weblayer_private/ContentView.java",
"org/chromium/weblayer_private/ContentViewRenderView.java", "org/chromium/weblayer_private/ContentViewRenderView.java",
"org/chromium/weblayer_private/CrashReporterControllerImpl.java",
"org/chromium/weblayer_private/DownloadCallbackProxy.java", "org/chromium/weblayer_private/DownloadCallbackProxy.java",
"org/chromium/weblayer_private/ErrorPageCallbackProxy.java", "org/chromium/weblayer_private/ErrorPageCallbackProxy.java",
"org/chromium/weblayer_private/ExternalNavigationHandler.java", "org/chromium/weblayer_private/ExternalNavigationHandler.java",
"org/chromium/weblayer_private/ActionModeCallback.java", "org/chromium/weblayer_private/ActionModeCallback.java",
"org/chromium/weblayer_private/FullscreenCallbackProxy.java", "org/chromium/weblayer_private/FullscreenCallbackProxy.java",
"org/chromium/weblayer_private/LocaleChangedBroadcastReceiver.java", "org/chromium/weblayer_private/LocaleChangedBroadcastReceiver.java",
"org/chromium/weblayer_private/MinidumpUploader.java",
"org/chromium/weblayer_private/NavigationControllerImpl.java", "org/chromium/weblayer_private/NavigationControllerImpl.java",
"org/chromium/weblayer_private/NavigationImpl.java", "org/chromium/weblayer_private/NavigationImpl.java",
"org/chromium/weblayer_private/NewTabCallbackProxy.java", "org/chromium/weblayer_private/NewTabCallbackProxy.java",
...@@ -63,6 +65,7 @@ android_library("java") { ...@@ -63,6 +65,7 @@ android_library("java") {
"//components/crash/android:java", "//components/crash/android:java",
"//components/embedder_support/android:application_java", "//components/embedder_support/android:application_java",
"//components/embedder_support/android:web_contents_delegate_java", "//components/embedder_support/android:web_contents_delegate_java",
"//components/minidump_uploader:minidump_uploader_java",
"//components/version_info/android:version_constants_java", "//components/version_info/android:version_constants_java",
"//content/public/android:content_java", "//content/public/android:content_java",
"//third_party/android_deps:com_android_support_support_compat_java", "//third_party/android_deps:com_android_support_support_compat_java",
...@@ -154,6 +157,8 @@ android_aidl("aidl") { ...@@ -154,6 +157,8 @@ android_aidl("aidl") {
"org/chromium/weblayer_private/interfaces/IBrowserFragment.aidl", "org/chromium/weblayer_private/interfaces/IBrowserFragment.aidl",
"org/chromium/weblayer_private/interfaces/IChildProcessService.aidl", "org/chromium/weblayer_private/interfaces/IChildProcessService.aidl",
"org/chromium/weblayer_private/interfaces/IClientNavigation.aidl", "org/chromium/weblayer_private/interfaces/IClientNavigation.aidl",
"org/chromium/weblayer_private/interfaces/ICrashReporterController.aidl",
"org/chromium/weblayer_private/interfaces/ICrashReporterControllerClient.aidl",
"org/chromium/weblayer_private/interfaces/IDownloadCallbackClient.aidl", "org/chromium/weblayer_private/interfaces/IDownloadCallbackClient.aidl",
"org/chromium/weblayer_private/interfaces/IErrorPageCallbackClient.aidl", "org/chromium/weblayer_private/interfaces/IErrorPageCallbackClient.aidl",
"org/chromium/weblayer_private/interfaces/IFullscreenCallbackClient.aidl", "org/chromium/weblayer_private/interfaces/IFullscreenCallbackClient.aidl",
......
include_rules = [
"+components/crash/android/java",
"+components/minidump_uploader",
]
// Copyright 2019 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.weblayer_private;
import android.os.Bundle;
import android.os.RemoteException;
import android.util.AndroidRuntimeException;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import org.chromium.base.Log;
import org.chromium.base.PathUtils;
import org.chromium.base.task.AsyncTask;
import org.chromium.components.crash.browser.ChildProcessCrashObserver;
import org.chromium.components.minidump_uploader.CrashFileManager;
import org.chromium.weblayer_private.interfaces.ICrashReporterController;
import org.chromium.weblayer_private.interfaces.ICrashReporterControllerClient;
import org.chromium.weblayer_private.interfaces.StrictModeWorkaround;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
/**
* Provides the implementation of the API for managing captured crash reports.
*
* @see org.chromium.weblayer.CrashReporterController
*/
public final class CrashReporterControllerImpl extends ICrashReporterController.Stub {
private static final String TAG = "CrashReporter";
private static final int MAX_UPLOAD_RETRIES = 3;
@Nullable
private ICrashReporterControllerClient mClient;
private final CrashFileManager mCrashFileManager;
private static class Holder {
static CrashReporterControllerImpl sInstance = new CrashReporterControllerImpl();
}
private CrashReporterControllerImpl() {
mCrashFileManager = new CrashFileManager(new File(PathUtils.getCacheDirectory()));
ChildProcessCrashObserver.registerCrashCallback(
new ChildProcessCrashObserver.ChildCrashedCallback() {
@Override
public void childCrashed(int pid) {
processNewMinidumps();
}
});
}
public static CrashReporterControllerImpl getInstance() {
return Holder.sInstance;
}
@Override
public void deleteCrash(String localId) {
StrictModeWorkaround.apply();
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
deleteCrashOnBackgroundThread(localId);
try {
mClient.onCrashDeleted(localId);
} catch (RemoteException e) {
throw new AndroidRuntimeException(e);
}
});
}
@Override
public void uploadCrash(String localId) {
StrictModeWorkaround.apply();
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
File minidumpFile = mCrashFileManager.getCrashFileWithLocalId(localId);
MinidumpUploader.Result result = new MinidumpUploader().upload(minidumpFile);
if (result.mSuccess) {
CrashFileManager.markUploadSuccess(minidumpFile);
} else {
CrashFileManager.tryIncrementAttemptNumber(minidumpFile);
}
try {
if (result.mSuccess) {
mClient.onCrashUploadSucceeded(localId, result.mResult);
} else {
mClient.onCrashUploadFailed(localId, result.mResult);
}
} catch (RemoteException e) {
throw new AndroidRuntimeException(e);
}
});
}
@Override
public @Nullable Bundle getCrashKeys(String localId) {
StrictModeWorkaround.apply();
JSONObject data = readSidecar(localId);
if (data == null) {
return null;
}
Bundle result = new Bundle();
Iterator<String> iter = data.keys();
while (iter.hasNext()) {
String key = iter.next();
try {
result.putCharSequence(key, data.getString(key));
} catch (JSONException e) {
// Skip non-string values.
}
}
return result;
}
@Override
public void checkForPendingCrashReports() {
StrictModeWorkaround.apply();
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
try {
mClient.onPendingCrashReports(getPendingMinidumpsOnBackgroundThread());
} catch (RemoteException e) {
throw new AndroidRuntimeException(e);
}
});
}
@Override
public void setClient(ICrashReporterControllerClient client) {
StrictModeWorkaround.apply();
mClient = client;
processNewMinidumps();
}
/** Start an async task to import crashes, and notify if any are found. */
private void processNewMinidumps() {
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
String[] localIds = processNewMinidumpsOnBackgroundThread();
if (localIds.length > 0) {
try {
mClient.onPendingCrashReports(localIds);
} catch (RemoteException e) {
throw new AndroidRuntimeException(e);
}
}
});
}
/** Delete a crash report (and any sidecar file) given its local ID. */
private void deleteCrashOnBackgroundThread(String localId) {
File minidumpFile = mCrashFileManager.getCrashFileWithLocalId(localId);
File sidecarFile = sidecarFile(localId);
if (minidumpFile != null) {
CrashFileManager.deleteFile(minidumpFile);
}
if (sidecarFile != null) {
CrashFileManager.deleteFile(sidecarFile);
}
}
/**
* Determine the set of crashes that are currently ready to be uploaded.
*
* Clean out crashes that are too old, and return the any remaining crashes that have not
* exceeded their upload retry limit.
*
* @return An array of local IDs for crashes that are ready to be uploaded.
*/
private String[] getPendingMinidumpsOnBackgroundThread() {
mCrashFileManager.cleanOutAllNonFreshMinidumpFiles();
File[] pendingMinidumps = mCrashFileManager.getMinidumpsReadyForUpload(MAX_UPLOAD_RETRIES);
ArrayList<String> localIds = new ArrayList<>(pendingMinidumps.length);
for (File minidump : pendingMinidumps) {
localIds.add(CrashFileManager.getCrashLocalIdFromFileName(minidump.getName()));
}
return localIds.toArray(new String[0]);
}
/**
* Use the CrashFileManager to import crashes from crashpad.
*
* For each imported crash, a sidecar file (in JSON format) is written, containing the
* crash keys that were recorded at the time of the crash.
*
* @return An array of local IDs of the new crashes (may be empty).
*/
private String[] processNewMinidumpsOnBackgroundThread() {
Map<String, Map<String, String>> crashesInfoMap =
mCrashFileManager.importMinidumpsCrashKeys();
ArrayList<String> localIds = new ArrayList<>(crashesInfoMap.size());
for (Map.Entry<String, Map<String, String>> entry : crashesInfoMap.entrySet()) {
JSONObject crashKeysJson = new JSONObject(entry.getValue());
String uuid = entry.getKey();
// TODO(tobiasjs): the minidump uploader uses the last component of the uuid as
// the local ID. The ergonomics of this should be improved.
localIds.add(CrashFileManager.getCrashLocalIdFromFileName(uuid + ".dmp"));
writeSidecar(uuid, crashKeysJson);
}
for (File minidump : mCrashFileManager.getMinidumpsSansLogcat()) {
CrashFileManager.trySetReadyForUpload(minidump);
}
return localIds.toArray(new String[0]);
}
/**
* Generate a sidecar file path given a crash local ID.
*
* The sidecar file holds a JSON representation of the crash keys associated
* with the crash. All crash keys and values are strings.
*/
private @Nullable File sidecarFile(String localId) {
File minidumpFile = mCrashFileManager.getCrashFileWithLocalId(localId);
if (minidumpFile == null) {
return null;
}
String uuid = minidumpFile.getName().split("\\.")[0];
return new File(minidumpFile.getParent(), uuid + ".json");
}
/** Write JSON formatted crash key data to the sidecar file for a crash. */
private void writeSidecar(String localId, JSONObject data) {
File sidecar = sidecarFile(localId);
if (sidecar == null) {
return;
}
try (FileOutputStream out = new FileOutputStream(sidecar)) {
out.write(data.toString().getBytes("UTF-8"));
} catch (IOException e) {
Log.w(TAG, "Failed to write crash keys JSON for crash " + localId);
sidecar.delete();
}
}
/** Read JSON formatted crash key data previously written to a crash sidecar file. */
private @Nullable JSONObject readSidecar(String localId) {
File sidecar = sidecarFile(localId);
if (sidecar == null) {
return null;
}
try (FileInputStream in = new FileInputStream(sidecar)) {
byte[] data = new byte[(int) sidecar.length()];
int offset = 0;
while (offset < data.length) {
int count = in.read(data, offset, data.length - offset);
if (count <= 0) break;
offset += count;
}
return new JSONObject(new String(data, "UTF-8"));
} catch (IOException | JSONException e) {
return null;
}
}
}
// Copyright 2019 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.weblayer_private;
import org.chromium.components.minidump_uploader.util.HttpURLConnectionFactory;
import org.chromium.components.minidump_uploader.util.HttpURLConnectionFactoryImpl;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.util.zip.GZIPOutputStream;
/**
* This class tries to upload a minidump to the crash server.
*
* Minidumps are stored in multipart MIME format ready to form the body of a POST request. The MIME
* boundary forms the first line of the file.
*/
public class MinidumpUploader {
// TODO(crbug.com/1029724) unfork this class back to //components/minidump_uploader
private static final String CRASH_URL_STRING = "https://clients2.google.com/cr/report";
private static final String CONTENT_TYPE_TMPL = "multipart/form-data; boundary=%s";
private final HttpURLConnectionFactory mHttpURLConnectionFactory;
/* package */ static final class Result {
final boolean mSuccess;
final int mStatus;
final String mResult;
private Result(boolean success, int status, String result) {
mSuccess = success;
mStatus = status;
mResult = result;
}
static Result failure(String result) {
return new Result(false, -1, result);
}
static Result failure(int status, String result) {
return new Result(false, status, result);
}
static Result success(String result) {
return new Result(true, 0, result);
}
}
public MinidumpUploader() {
this(new HttpURLConnectionFactoryImpl());
}
public MinidumpUploader(HttpURLConnectionFactory httpURLConnectionFactory) {
mHttpURLConnectionFactory = httpURLConnectionFactory;
}
public Result upload(File fileToUpload) {
try {
if (fileToUpload == null || !fileToUpload.exists()) {
return Result.failure("Crash report does not exist");
}
HttpURLConnection connection =
mHttpURLConnectionFactory.createHttpURLConnection(CRASH_URL_STRING);
if (connection == null) {
return Result.failure("Failed to create connection");
}
configureConnectionForHttpPost(connection, readBoundary(fileToUpload));
try (InputStream minidumpInputStream = new FileInputStream(fileToUpload);
OutputStream requestBodyStream =
new GZIPOutputStream(connection.getOutputStream())) {
streamCopy(minidumpInputStream, requestBodyStream);
int responseCode = connection.getResponseCode();
if (isSuccessful(responseCode)) {
// The crash server returns the crash ID in the response body.
String responseContent = getResponseContentAsString(connection);
String uploadId = responseContent != null ? responseContent : "unknown";
return Result.success(uploadId);
} else {
// Return the remote error code and message.
return Result.failure(responseCode, connection.getResponseMessage());
}
} finally {
connection.disconnect();
}
} catch (IOException | RuntimeException e) {
return Result.failure(e.getMessage());
}
}
/**
* 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
* @param boundary the MIME boundary used in the request body
*/
private void configureConnectionForHttpPost(HttpURLConnection connection, String boundary) {
connection.setDoOutput(true);
connection.setRequestProperty("Connection", "Keep-Alive");
connection.setRequestProperty("Content-Encoding", "gzip");
connection.setRequestProperty("Content-Type", String.format(CONTENT_TYPE_TMPL, boundary));
}
/**
* 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(File fileToUpload) throws IOException {
try (FileReader fileReader = new FileReader(fileToUpload);
BufferedReader reader = new BufferedReader(fileReader)) {
String boundary = reader.readLine();
if (boundary == null || boundary.trim().isEmpty()) {
throw new RuntimeException(fileToUpload + " does not have a MIME boundary");
}
boundary = boundary.trim();
if (!boundary.startsWith("--") || boundary.length() < 10) {
throw new RuntimeException(fileToUpload + " does not have a MIME boundary");
}
// Note: The regex allows all alphanumeric characters, as well as dashes.
// This matches the code that generates minidumps boundaries:
// https://chromium.googlesource.com/crashpad/crashpad/+/0c322ecc3f711c34fbf85b2cbe69f38b8dbccf05/util/net/http_multipart_builder.cc#36
if (!boundary.matches("^[a-zA-Z0-9-]*$")) {
throw new RuntimeException(
fileToUpload.getName() + " has an illegal MIME boundary: " + boundary);
}
boundary = boundary.substring(2); // Remove the initial --
return boundary;
}
}
/**
* 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 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 String getResponseContentAsString(HttpURLConnection connection) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
streamCopy(connection.getInputStream(), baos);
if (baos.size() == 0) {
return null;
}
return baos.toString();
}
/**
* 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 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();
}
}
// Copyright 2019 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.weblayer_private.interfaces;
import android.os.Bundle;
import org.chromium.weblayer_private.interfaces.ICrashReporterControllerClient;
import org.chromium.weblayer_private.interfaces.IObjectWrapper;
interface ICrashReporterController {
void setClient(in ICrashReporterControllerClient client) = 0;
void checkForPendingCrashReports() = 1;
Bundle getCrashKeys(in String localId) = 2;
void deleteCrash(in String localId) = 3;
void uploadCrash(in String localId) = 4;
}
// Copyright 2019 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.weblayer_private.interfaces;
import android.os.Bundle;
import org.chromium.weblayer_private.interfaces.IObjectWrapper;
interface ICrashReporterControllerClient {
void onPendingCrashReports(in String[] localIds) = 0;
void onCrashDeleted(in String localId) = 1;
void onCrashUploadSucceeded(in String localId, in String reportId) = 2;
void onCrashUploadFailed(in String localId, in String failureMessage) = 3;
}
...@@ -9,4 +9,4 @@ package org.chromium.weblayer_private.interfaces; ...@@ -9,4 +9,4 @@ package org.chromium.weblayer_private.interfaces;
* *
* Whenever any AIDL file is changed, sVersionNumber must be incremented. * Whenever any AIDL file is changed, sVersionNumber must be incremented.
* */ * */
public final class WebLayerVersion { public static final int sVersionNumber = 18; } public final class WebLayerVersion { public static final int sVersionNumber = 19; }
...@@ -26,6 +26,8 @@ android_library("java") { ...@@ -26,6 +26,8 @@ android_library("java") {
"org/chromium/weblayer/BrowsingDataType.java", "org/chromium/weblayer/BrowsingDataType.java",
"org/chromium/weblayer/Callback.java", "org/chromium/weblayer/Callback.java",
"org/chromium/weblayer/ChildProcessService.java", "org/chromium/weblayer/ChildProcessService.java",
"org/chromium/weblayer/CrashReporterCallback.java",
"org/chromium/weblayer/CrashReporterController.java",
"org/chromium/weblayer/DownloadCallback.java", "org/chromium/weblayer/DownloadCallback.java",
"org/chromium/weblayer/ErrorPageCallback.java", "org/chromium/weblayer/ErrorPageCallback.java",
"org/chromium/weblayer/FullscreenCallback.java", "org/chromium/weblayer/FullscreenCallback.java",
......
// Copyright 2019 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.weblayer;
/**
* Callback object for results of asynchronous {@link CrashReporterController} operations.
*/
public abstract class CrashReporterCallback {
/**
* Called as a result of a new crash being detected, or with the result of {@link
* CrashReporterController#getPendingCrashes}
*
* @param localIds an array of crash report IDs available to be uploaded.
*/
public void onPendingCrashReports(String[] localIds) {}
/**
* Called when a crash has been deleted.
*
* @param localId the local identifier of the crash that was deleted.
*/
public void onCrashDeleted(String localId) {}
/**
* Called when a crash has been uploaded.
*
* @param localId the local identifier of the crash that was uploaded.
* @param reportId the remote identifier for the uploaded crash.
*/
public void onCrashUploadSucceeded(String localId, String reportId) {}
/**
* Called when a crash failed to upload.
*
* @param localId the local identifier of the crash that failed to upload.
* @param failureReason a free text string giving the failure reason.
*/
public void onCrashUploadFailed(String localId, String faulreReason) {}
}
// Copyright 2019 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.weblayer;
import android.content.Context;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.AndroidRuntimeException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.chromium.weblayer_private.interfaces.APICallException;
import org.chromium.weblayer_private.interfaces.ICrashReporterController;
import org.chromium.weblayer_private.interfaces.ICrashReporterControllerClient;
import org.chromium.weblayer_private.interfaces.StrictModeWorkaround;
/**
* Provides an API to allow WebLayer embedders to control the handling of crash reports.
*
* Creation of the CrashReporterController singleton bootstraps WebLayer code loading (but not full
* initialization) so that it can be used in a lightweight fashion from a scheduled task.
*
* Crash reports are identified by a unique string, with which is associated an opaque blob of data
* for upload, and a key: value dictionary of crash metadata. Given an identifier, callers can
* either trigger an upload attempt, or delete the crash report.
*
* After successful upload, local crash data is deleted and the crash can be referenced by the
* returned remote identifier string.
*
* An embedder should register a CrashReporterCallback to be alerted to the presence of crash
* reports via {@link CrashReporterCallback#onPendingCrashReports}. When a crash report is
* available, the embedder should decide whether the crash should be uploaded or deleted based on
* user preference. Knowing that a crash is available can be used as a signal to schedule upload
* work for a later point in time (or favourable power/network conditions).
*/
public final class CrashReporterController {
private ICrashReporterController mImpl;
private final ObserverList<CrashReporterCallback> mCallbacks;
private static final class Holder {
static CrashReporterController sInstance = new CrashReporterController();
}
private CrashReporterController() {
mCallbacks = new ObserverList<CrashReporterCallback>();
}
/** Retrieve the singleton CrashReporterController instance. */
public static CrashReporterController getInstance(@NonNull Context appContext) {
ThreadCheck.ensureOnUiThread();
return Holder.sInstance.connect(appContext.getApplicationContext());
}
/**
* Asynchronously fetch the set of crash reports ready to be uploaded.
*
* Results are returned via {@link CrashReporterCallback#onPendingCrashReports}.
*/
public void checkForPendingCrashReports() {
try {
mImpl.checkForPendingCrashReports();
} catch (RemoteException e) {
throw new APICallException(e);
}
}
/**
* Get the crash keys for a crash.
*
* Performs IO on this thread, so should not be called on the UI thread.
*
* @param localId a crash identifier.
* @return a Bundle containing crash information as key: value pairs.
*/
public @Nullable Bundle getCrashKeys(String localId) {
try {
return mImpl.getCrashKeys(localId);
} catch (RemoteException e) {
throw new APICallException(e);
}
}
/**
* Asynchronously delete a crash.
*
* Deletion is notified via {@link CrashReporterCallback#onCrashDeleted}
*
* @param localId a crash identifier.
*/
public void deleteCrash(String localId) {
try {
mImpl.deleteCrash(localId);
} catch (RemoteException e) {
throw new APICallException(e);
}
}
/**
* Asynchronously upload a crash.
*
* Success is notified via {@link CrashReporterCallback#onCrashUploadSucceeded},
* and the crash report is purged. On upload failure,
* {@link CrashReporterCallback#onCrashUploadFailed} is called. The crash report
* will be kept until it is deemed too old, or too many upload attempts have
* failed.
*
* @param localId a crash identifier.
*/
public void uploadCrash(String localId) {
try {
mImpl.uploadCrash(localId);
} catch (RemoteException e) {
throw new APICallException(e);
}
}
/** Register a {@link CrashReporterCallback} callback object. */
public void registerCallback(@NonNull CrashReporterCallback callback) {
ThreadCheck.ensureOnUiThread();
mCallbacks.addObserver(callback);
}
/** Unregister a previously registered {@link CrashReporterCallback} callback object. */
public void unregisterCallback(@NonNull CrashReporterCallback callback) {
ThreadCheck.ensureOnUiThread();
mCallbacks.removeObserver(callback);
}
private CrashReporterController connect(@NonNull Context appContext) {
if (mImpl != null) {
return this;
}
ClassLoader remoteClassLoader;
try {
remoteClassLoader = WebLayer.getOrCreateRemoteClassLoader(appContext);
Class crashReporterControllerClass = remoteClassLoader.loadClass(
"org.chromium.weblayer_private.CrashReporterControllerImpl");
mImpl = ICrashReporterController.Stub.asInterface(
(IBinder) crashReporterControllerClass.getMethod("getInstance").invoke(null));
mImpl.setClient(new CrashReporterControllerClientImpl());
} catch (Exception e) {
throw new AndroidRuntimeException(e);
}
return this;
}
private final class CrashReporterControllerClientImpl
extends ICrashReporterControllerClient.Stub {
@Override
public void onPendingCrashReports(String[] localIds) {
StrictModeWorkaround.apply();
for (CrashReporterCallback callback : mCallbacks) {
callback.onPendingCrashReports(localIds);
}
}
@Override
public void onCrashDeleted(String localId) {
StrictModeWorkaround.apply();
for (CrashReporterCallback callback : mCallbacks) {
callback.onCrashDeleted(localId);
}
}
@Override
public void onCrashUploadSucceeded(String localId, String reportId) {
StrictModeWorkaround.apply();
for (CrashReporterCallback callback : mCallbacks) {
callback.onCrashUploadSucceeded(localId, reportId);
}
}
@Override
public void onCrashUploadFailed(String localId, String failureReason) {
StrictModeWorkaround.apply();
for (CrashReporterCallback callback : mCallbacks) {
callback.onCrashUploadFailed(localId, failureReason);
}
}
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment