blob: 330d61f63ea9bd31f377783ff53cdd3af901c312 [file] [log] [blame]
// Copyright 2016 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.android_webview.services;
import android.app.Service;
import android.app.job.JobInfo;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.components.background_task_scheduler.TaskIds;
import org.chromium.components.minidump_uploader.CrashFileManager;
import org.chromium.components.minidump_uploader.MinidumpUploadJobService;
import java.io.File;
import java.io.IOException;
/**
* Service that is responsible for receiving crash dumps from an application, for upload.
*/
public class CrashReceiverService extends Service {
private static final String TAG = "CrashReceiverService";
private static final String WEBVIEW_CRASH_DIR = "WebView_Crashes";
private static final String WEBVIEW_TMP_CRASH_DIR = "WebView_Crashes_Tmp";
private final Object mCopyingLock = new Object();
private boolean mIsCopying;
@Override
@SuppressWarnings("NoContextGetApplicationContext")
public void onCreate() {
super.onCreate();
ServiceInit.init(getApplicationContext());
}
private final ICrashReceiverService.Stub mBinder = new ICrashReceiverService.Stub() {
@Override
public void transmitCrashes(ParcelFileDescriptor[] fileDescriptors) {
int uid = Binder.getCallingUid();
performMinidumpCopyingSerially(uid, fileDescriptors, true /* scheduleUploads */);
}
};
/**
* Copies minidumps in a synchronized way, waiting for any already started copying operations to
* finish before copying the current dumps.
* @param scheduleUploads whether to ask JobScheduler to schedule an upload-job (avoid this
* during testing).
*/
@VisibleForTesting
public void performMinidumpCopyingSerially(
int uid, ParcelFileDescriptor[] fileDescriptors, boolean scheduleUploads) {
if (!waitUntilWeCanCopy()) {
Log.e(TAG, "something went wrong when waiting to copy minidumps, bailing!");
return;
}
try {
boolean copySucceeded = copyMinidumps(uid, fileDescriptors);
if (copySucceeded && scheduleUploads) {
// Only schedule a new job if there actually are any files to upload.
scheduleNewJob();
}
} finally {
synchronized (mCopyingLock) {
mIsCopying = false;
mCopyingLock.notifyAll();
}
}
}
/**
* Wait until we are allowed to copy minidumps.
* @return whether we are actually allowed to copy the files - if false we should just bail.
*/
private boolean waitUntilWeCanCopy() {
synchronized (mCopyingLock) {
while (mIsCopying) {
try {
mCopyingLock.wait();
} catch (InterruptedException e) {
Log.e(TAG, "Was interrupted when waiting to copy minidumps", e);
return false;
}
}
mIsCopying = true;
return true;
}
}
private void scheduleNewJob() {
JobInfo.Builder builder = new JobInfo.Builder(TaskIds.WEBVIEW_MINIDUMP_UPLOADING_JOB_ID,
new ComponentName(this, AwMinidumpUploadJobService.class));
MinidumpUploadJobService.scheduleUpload(builder);
}
/**
* 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.
* @return whether any minidump was copied.
*/
@VisibleForTesting
public static boolean copyMinidumps(int uid, ParcelFileDescriptor[] fileDescriptors) {
CrashFileManager crashFileManager = new CrashFileManager(getOrCreateWebViewCrashDir());
boolean copiedAnything = false;
if (fileDescriptors != null) {
for (ParcelFileDescriptor fd : fileDescriptors) {
if (fd == null) continue;
try {
File copiedFile = crashFileManager.copyMinidumpFromFD(
fd.getFileDescriptor(), getWebViewTmpCrashDir(), uid);
if (copiedFile == null) {
Log.w(TAG, "failed to copy minidump from " + fd.toString());
// TODO(gsennton): add UMA metric to ensure we aren't losing too many
// minidumps here.
} else {
copiedAnything = true;
}
} catch (IOException e) {
Log.w(TAG, "failed to copy minidump from " + fd.toString() + ": "
+ e.getMessage());
} finally {
deleteFilesInWebViewTmpDirIfExists();
}
}
}
return copiedAnything;
}
/**
* Delete all files in the directory where temporary files from this Service are stored.
*/
@VisibleForTesting
public static void deleteFilesInWebViewTmpDirIfExists() {
deleteFilesInDirIfExists(getWebViewTmpCrashDir());
}
private static void deleteFilesInDirIfExists(File directory) {
if (directory.isDirectory()) {
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (!file.delete()) {
Log.w(TAG, "Couldn't delete file " + file.getAbsolutePath());
}
}
}
}
}
/**
* 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
* minidump handling won't clash in cases where both Chrome and WebView are provided by the
* same app (Monochrome).
* @return a reference to the created directory, or null if the creation failed.
*/
@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;
}
/**
* Fetch the crash directory where WebView stores its minidumps.
* @return a File pointing to the crash directory.
*/
@VisibleForTesting
public static File getWebViewCrashDir() {
return new File(ContextUtils.getApplicationContext().getCacheDir(), WEBVIEW_CRASH_DIR);
}
/**
* Directory where we store files temporarily when copying from an app process.
*/
@VisibleForTesting
public static File getWebViewTmpCrashDir() {
return new File(ContextUtils.getApplicationContext().getCacheDir(), WEBVIEW_TMP_CRASH_DIR);
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}