Commit 84082b98 authored by Hazem Ashmawy's avatar Hazem Ashmawy Committed by Commit Bot

[AW] Log WebView crash info in WebView's crash dir

- Log WebView crashs' info keys ("app-package-name" and "variations") as
  well as crash-local-id and capture-time to log files under WebView's
  crash dir. Each crash has its unique log file with a JSON object that
  contains crash info.

- Move CrashInfoLoader.java to a separate build target to be shared
  between the UI and crash service code.

Bug: 958340

Change-Id: I446204cc400bc05e6bac4cf40001469ed0ccc7fe
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1593375
Commit-Queue: Hazem Ashmawy <hazems@chromium.org>
Reviewed-by: default avatarTobias Sargeant <tobiasjs@chromium.org>
Cr-Commit-Position: refs/heads/master@{#679573}
parent 283b92c7
......@@ -927,11 +927,11 @@ android_library("android_webview_java") {
"java/src/org/chromium/android_webview/permission/AwGeolocationCallback.java",
"java/src/org/chromium/android_webview/permission/AwPermissionRequest.java",
"java/src/org/chromium/android_webview/policy/AwPolicyProvider.java",
"java/src/org/chromium/android_webview/ui/util/CrashInfoLoader.java",
"java/src/org/chromium/android_webview/ui/util/UploadedCrashesInfoLoader.java",
]
deps = [
":android_webview_commandline_java",
":android_webview_crash_info_java",
":android_webview_platform_services_java",
":android_webview_services_java",
":android_webview_variations_utils_java",
......@@ -1079,6 +1079,15 @@ android_library("platform_service_bridge_upstream_implementation_java") {
]
}
android_library("android_webview_crash_info_java") {
java_files =
[ "java/src/org/chromium/android_webview/ui/util/CrashInfoLoader.java" ]
deps = [
"//base:base_java",
]
}
android_library("android_webview_commandline_java") {
java_files = [
"java/src/org/chromium/android_webview/command_line/CommandLineUtil.java",
......@@ -1102,6 +1111,7 @@ android_library("android_webview_services_java") {
]
deps = [
":android_webview_commandline_java",
":android_webview_crash_info_java",
":android_webview_platform_services_java",
":android_webview_variations_utils_java",
":system_webview_manifest",
......
......@@ -12,6 +12,7 @@ import android.os.Binder;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import org.chromium.android_webview.ui.util.CrashInfoLoader.CrashInfo;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
......@@ -20,8 +21,11 @@ import org.chromium.components.minidump_uploader.CrashFileManager;
import org.chromium.components.minidump_uploader.MinidumpUploadJobService;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* Service that is responsible for receiving crash dumps from an application, for upload.
......@@ -31,6 +35,8 @@ public class CrashReceiverService extends Service {
private static final String WEBVIEW_CRASH_DIR = "WebView_Crashes";
private static final String WEBVIEW_TMP_CRASH_DIR = "WebView_Crashes_Tmp";
private static final String WEBVIEW_CRASH_LOG_DIR = "crash_logs";
private static final String WEBVIEW_CRASH_LOG_SUFFIX = "_log.json";
private final Object mCopyingLock = new Object();
private boolean mIsCopying;
......@@ -39,7 +45,11 @@ public class CrashReceiverService extends Service {
@Override
public void transmitCrashes(ParcelFileDescriptor[] fileDescriptors, List crashInfo) {
int uid = Binder.getCallingUid();
performMinidumpCopyingSerially(uid, fileDescriptors, true /* scheduleUploads */);
if (crashInfo != null) {
assert crashInfo.size() == fileDescriptors.length;
}
performMinidumpCopyingSerially(
uid, fileDescriptors, crashInfo, true /* scheduleUploads */);
}
};
......@@ -50,15 +60,15 @@ public class CrashReceiverService extends Service {
* during testing).
*/
@VisibleForTesting
public void performMinidumpCopyingSerially(
int uid, ParcelFileDescriptor[] fileDescriptors, boolean scheduleUploads) {
public void performMinidumpCopyingSerially(int uid, ParcelFileDescriptor[] fileDescriptors,
List<Map<String, String>> crashesInfo, boolean scheduleUploads) {
if (!waitUntilWeCanCopy()) {
Log.e(TAG, "something went wrong when waiting to copy minidumps, bailing!");
return;
}
try {
boolean copySucceeded = copyMinidumps(uid, fileDescriptors);
boolean copySucceeded = copyMinidumps(uid, fileDescriptors, crashesInfo);
if (copySucceeded && scheduleUploads) {
// Only schedule a new job if there actually are any files to upload.
scheduleNewJob();
......@@ -98,16 +108,20 @@ public class CrashReceiverService extends Service {
/**
* Copy minidumps from the {@param fileDescriptors} to the directory where WebView stores its
* minidump files. {@param context} is used to look up the directory in which the files will be
* saved.
* minidump files. Also writes a new log file for each mindump, the log file contains a JSON
* object with info from {@param crashesInfo}. The log file name is: <copied-file-name> +
* {@code "_log.json"} suffix.
*
* @return whether any minidump was copied.
*/
@VisibleForTesting
public static boolean copyMinidumps(int uid, ParcelFileDescriptor[] fileDescriptors) {
public static boolean copyMinidumps(int uid, ParcelFileDescriptor[] fileDescriptors,
List<Map<String, String>> crashesInfo) {
CrashFileManager crashFileManager = new CrashFileManager(getOrCreateWebViewCrashDir());
boolean copiedAnything = false;
if (fileDescriptors != null) {
for (ParcelFileDescriptor fd : fileDescriptors) {
for (int i = 0; i < fileDescriptors.length; i++) {
ParcelFileDescriptor fd = fileDescriptors[i];
if (fd == null) continue;
try {
File copiedFile = crashFileManager.copyMinidumpFromFD(
......@@ -118,6 +132,12 @@ public class CrashReceiverService extends Service {
// minidumps here.
} else {
copiedAnything = true;
if (crashesInfo != null) {
Map<String, String> crashInfo = crashesInfo.get(i);
File logFile = new File(getOrCreateWebViewCrashLogDir(),
copiedFile.getName() + WEBVIEW_CRASH_LOG_SUFFIX);
writeCrashInfoToLogFile(logFile, copiedFile, crashInfo);
}
}
} catch (IOException e) {
Log.w(TAG, "failed to copy minidump from " + fd.toString() + ": "
......@@ -130,6 +150,38 @@ public class CrashReceiverService extends Service {
return copiedAnything;
}
/**
* Writes info about crash in a separate log file for each crash as a JSON Object.
*/
@VisibleForTesting
public static boolean writeCrashInfoToLogFile(
File logFile, File crashFile, Map<String, String> crashInfoMap) {
try {
CrashInfo crashInfo = new CrashInfo();
crashInfo.localId = CrashFileManager.getCrashLocalIdFromFileName(crashFile.getName());
if (crashInfo.localId == null) return false;
crashInfo.captureTime = crashFile.lastModified();
if (crashInfoMap == null) return false;
crashInfo.packageName = crashInfoMap.get("app-package-name");
if (crashInfoMap.containsKey("variations")) {
crashInfo.variations = Arrays.asList(crashInfoMap.get("variations").split(","));
}
FileWriter writer = new FileWriter(logFile);
try {
writer.write(crashInfo.serializeToJson());
} finally {
writer.close();
}
return true;
} catch (IOException e) {
Log.w(TAG, "failed to write JSON log entry for crash", e);
}
return false;
}
/**
* Delete all files in the directory where temporary files from this Service are stored.
*/
......@@ -151,6 +203,15 @@ public class CrashReceiverService extends Service {
}
}
private static File getOrCreateDir(File dir) {
// Call mkdir before isDirectory to ensure that if another thread created the directory
// just before the call to mkdir, the current thread fails mkdir, but passes isDirectory.
if (dir.mkdir() || dir.isDirectory()) {
return dir;
}
return null;
}
/**
* Create the directory in which WebView will store its minidumps.
* WebView needs a crash directory different from Chrome's to ensure Chrome's and WebView's
......@@ -160,13 +221,7 @@ public class CrashReceiverService extends Service {
*/
@VisibleForTesting
public static File getOrCreateWebViewCrashDir() {
File dir = getWebViewCrashDir();
// Call mkdir before isDirectory to ensure that if another thread created the directory
// just before the call to mkdir, the current thread fails mkdir, but passes isDirectory.
if (dir.mkdir() || dir.isDirectory()) {
return dir;
}
return null;
return getOrCreateDir(getWebViewCrashDir());
}
/**
......@@ -178,6 +233,23 @@ public class CrashReceiverService extends Service {
return new File(ContextUtils.getApplicationContext().getCacheDir(), WEBVIEW_CRASH_DIR);
}
/**
* Create the directory in which WebView will log crashes info.
* @return a reference to the created directory, or null if the creation failed.
*/
private static File getOrCreateWebViewCrashLogDir() {
File dir = new File(getOrCreateWebViewCrashDir(), WEBVIEW_CRASH_LOG_DIR);
return getOrCreateDir(dir);
}
/**
* Create the directory in which WebView will log crashes info.
* @return a reference to the created directory, or null if the creation failed.
*/
public static File getWebViewCrashLogDir() {
return new File(getWebViewCrashDir(), WEBVIEW_CRASH_LOG_DIR);
}
/**
* Directory where we store files temporarily when copying from an app process.
*/
......
......@@ -3,6 +3,11 @@
// found in the LICENSE file.
package org.chromium.android_webview.ui.util;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
/**
......@@ -36,6 +41,84 @@ public abstract class CrashInfoLoader {
// These fields are only valid when |uploadState| == Uploaded.
public String uploadId;
public long uploadTime = -1;
private static final String CRASH_LOCAL_ID_KEY = "crash-local-id";
private static final String CRASH_CAPTURE_TIME_KEY = "crash-capture-time";
private static final String CRASH_PACKAGE_NAME_KEY = "app-package-name";
private static final String CRASH_VARIATIONS_KEY = "variations";
private static final String CRASH_UPLOAD_ID_KEY = "crash-upload-id";
private static final String CRASH_UPLOAD_TIME_KEY = "crash-upload-time";
/**
* Serialize {@code CrashInfo} object into a JSON object string.
*
* @return serialized string for the object.
*/
public String serializeToJson() {
try {
JSONObject jsonObj = new JSONObject();
if (localId != null) {
jsonObj.put(CRASH_LOCAL_ID_KEY, localId);
}
if (captureTime != -1) {
jsonObj.put(CRASH_CAPTURE_TIME_KEY, captureTime);
}
if (packageName != null) {
jsonObj.put(CRASH_PACKAGE_NAME_KEY, packageName);
}
if (variations != null && !variations.isEmpty()) {
jsonObj.put(CRASH_VARIATIONS_KEY, new JSONArray(variations));
}
if (uploadId != null) {
jsonObj.put(CRASH_UPLOAD_ID_KEY, uploadId);
}
if (uploadTime != -1) {
jsonObj.put(CRASH_UPLOAD_TIME_KEY, uploadTime);
}
return jsonObj.toString();
} catch (JSONException e) {
return null;
}
}
/**
* Load {@code CrashInfo} from a JSON string.
*
* @param jsonString JSON string to load {@code CrashInfo} from.
* @return {@code CrashInfo} loaded from the serialized JSON object string.
* @throws JSONException if it's a malformatted string.
*/
public static CrashInfo readFromJsonString(String jsonString) throws JSONException {
JSONObject jsonObj = new JSONObject(jsonString);
CrashInfo crashInfo = new CrashInfo();
if (jsonObj.has(CRASH_LOCAL_ID_KEY)) {
crashInfo.localId = jsonObj.getString(CRASH_LOCAL_ID_KEY);
}
if (jsonObj.has(CRASH_CAPTURE_TIME_KEY)) {
crashInfo.captureTime = jsonObj.getLong(CRASH_CAPTURE_TIME_KEY);
}
if (jsonObj.has(CRASH_PACKAGE_NAME_KEY)) {
crashInfo.packageName = jsonObj.getString(CRASH_PACKAGE_NAME_KEY);
}
if (jsonObj.has(CRASH_VARIATIONS_KEY)) {
JSONArray variationsJSONArr = jsonObj.getJSONArray(CRASH_VARIATIONS_KEY);
if (variationsJSONArr != null) {
crashInfo.variations = new ArrayList<>();
for (int i = 0; i < variationsJSONArr.length(); i++) {
crashInfo.variations.add(variationsJSONArr.getString(i));
}
}
}
if (jsonObj.has(CRASH_UPLOAD_ID_KEY)) {
crashInfo.uploadId = jsonObj.getString(CRASH_UPLOAD_ID_KEY);
}
if (jsonObj.has(CRASH_UPLOAD_TIME_KEY)) {
crashInfo.uploadTime = jsonObj.getLong(CRASH_UPLOAD_TIME_KEY);
}
return crashInfo;
}
}
/**
......
......@@ -9,16 +9,23 @@ import static org.chromium.android_webview.test.OnlyRunIn.ProcessMode.SINGLE_PRO
import android.os.ParcelFileDescriptor;
import android.support.test.filters.MediumTest;
import org.json.JSONException;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.chromium.android_webview.services.CrashReceiverService;
import org.chromium.android_webview.test.AwJUnit4ClassRunner;
import org.chromium.android_webview.test.OnlyRunIn;
import org.chromium.android_webview.ui.util.CrashInfoLoader.CrashInfo;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* Instrumentation tests for CrashReceiverService.
......@@ -26,17 +33,25 @@ import java.io.IOException;
@RunWith(AwJUnit4ClassRunner.class)
@OnlyRunIn(SINGLE_PROCESS)
public class CrashReceiverServiceTest {
private static final String TEST_CRASH_LOCAL_ID = "abc1234";
private static final String TEST_CRASH_FILE_NAME =
"pkg-process-" + TEST_CRASH_LOCAL_ID + ".dmp";
private static final String TEST_CRASH_PACKAGE = "org.test.package";
@Rule
public TemporaryFolder mTempTestDir = new TemporaryFolder();
/**
* Ensure that the minidump copying doesn't trigger when we pass it invalid file descriptors.
*/
@Test
@MediumTest
public void testCopyingAbortsForInvalidFds() throws IOException {
Assert.assertFalse(CrashReceiverService.copyMinidumps(0 /* uid */, null));
Assert.assertFalse(CrashReceiverService.copyMinidumps(0 /* uid */, null, null));
Assert.assertFalse(CrashReceiverService.copyMinidumps(
0 /* uid */, new ParcelFileDescriptor[] {null, null}));
0 /* uid */, new ParcelFileDescriptor[] {null, null}, null));
Assert.assertFalse(
CrashReceiverService.copyMinidumps(0 /* uid */, new ParcelFileDescriptor[0]));
CrashReceiverService.copyMinidumps(0 /* uid */, new ParcelFileDescriptor[0], null));
}
/**
......@@ -59,4 +74,34 @@ public class CrashReceiverServiceTest {
Assert.assertFalse(testFile1.exists());
Assert.assertFalse(testFile2.exists());
}
/**
* Test writing crash info to crash logs works correctly and logs the correct info.
*/
@Test
@MediumTest
public void testWriteToLogFile() throws IOException, JSONException {
File testLogFile = mTempTestDir.newFile("test_log_file");
// No need to actually create it since nothing is written to it.
File testCrashFile = mTempTestDir.newFile(TEST_CRASH_FILE_NAME);
Map<String, String> crashInfoMap = new HashMap<String, String>();
crashInfoMap.put("app-package-name", TEST_CRASH_PACKAGE);
CrashReceiverService.writeCrashInfoToLogFile(testLogFile, testCrashFile, crashInfoMap);
CrashInfo crashInfo = CrashInfo.readFromJsonString(readEntireFile(testLogFile));
// Asserting some fields just to make sure that contents are valid.
Assert.assertEquals(TEST_CRASH_LOCAL_ID, crashInfo.localId);
Assert.assertEquals(testCrashFile.lastModified(), crashInfo.captureTime);
Assert.assertEquals(TEST_CRASH_PACKAGE, crashInfo.packageName);
}
private static String readEntireFile(File file) throws IOException {
try (FileInputStream fileInputStream = new FileInputStream(file)) {
byte[] data = new byte[(int) file.length()];
fileInputStream.read(data);
return new String(data);
}
}
}
......@@ -293,7 +293,7 @@ public class MinidumpUploaderTest {
Assert.assertTrue(currentMinidumps[m].delete());
}
crashReceiverService.performMinidumpCopyingSerially(
uids[n] /* uid */, fileDescriptors[n], false /* scheduleUploads */);
uids[n] /* uid */, fileDescriptors[n], null, false /* scheduleUploads */);
}
final CrashReportingPermissionManager permManager =
......
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