chromium/android_webview/nonembedded/java/src/org/chromium/android_webview/services/CrashReceiverService.java

// Copyright 2016 The Chromium Authors
// 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.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;

import androidx.annotation.VisibleForTesting;

import org.chromium.android_webview.common.services.ICrashReceiverService;
import org.chromium.android_webview.nonembedded.crash.CrashUploadUtil;
import org.chromium.android_webview.nonembedded.crash.SystemWideCrashDirectories;
import org.chromium.base.Log;
import org.chromium.components.minidump_uploader.CrashFileManager;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;

import javax.annotation.concurrent.GuardedBy;

/** 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 final Object mCopyingLock = new Object();

    @GuardedBy("mCopyingLock")
    private boolean mIsCopying;

    private final ICrashReceiverService.Stub mBinder =
            new ICrashReceiverService.Stub() {
                @Override
                public void transmitCrashes(
                        ParcelFileDescriptor[] fileDescriptors, List crashInfo) {
                    int uid = Binder.getCallingUid();
                    if (crashInfo != null) {
                        assert crashInfo.size() == fileDescriptors.length;
                    }
                    performMinidumpCopyingSerially(
                            uid, fileDescriptors, crashInfo, /* scheduleUploads= */ true);
                }
            };

    /**
     * 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,
            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, crashesInfo);
            if (copySucceeded && scheduleUploads) {
                // Only schedule a new job if there actually are any files to upload.
                CrashUploadUtil.scheduleNewJob(this);
            }
        } 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;
        }
    }

    /**
     * Copy minidumps from the {@param fileDescriptors} to the directory where WebView stores its
     * 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,
            List<Map<String, String>> crashesInfo) {
        CrashFileManager crashFileManager =
                new CrashFileManager(SystemWideCrashDirectories.getOrCreateWebViewCrashDir());
        boolean copiedAnything = false;
        for (int i = 0; i < fileDescriptors.length; i++) {
            ParcelFileDescriptor fd = fileDescriptors[i];
            Map<String, String> crashInfo = crashesInfo.get(i);
            if (fd == null) continue;
            try {
                File copiedFile =
                        crashFileManager.copyMinidumpFromFD(
                                fd.getFileDescriptor(),
                                SystemWideCrashDirectories.getWebViewTmpCrashDir(),
                                uid);
                if (copiedFile == null) {
                    Log.w(TAG, "failed to copy minidump from " + fd);
                    // TODO(gsennton): add UMA metric to ensure we aren't losing too many
                    // minidumps here.
                } else {
                    copiedAnything = true;
                    File logFile =
                            SystemWideCrashDirectories.createCrashJsonLogFile(copiedFile.getName());
                    CrashLoggingUtils.writeCrashInfoToLogFile(logFile, copiedFile, crashInfo);
                }
            } catch (IOException e) {
                Log.w(TAG, "failed to copy minidump from " + fd, e);
            } finally {
                deleteFilesInWebViewTmpDirIfExists();
            }
        }
        return copiedAnything;
    }

    /** Delete all files in the directory where temporary files from this Service are stored. */
    @VisibleForTesting
    public static void deleteFilesInWebViewTmpDirIfExists() {
        deleteFilesInDirIfExists(SystemWideCrashDirectories.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());
                    }
                }
            }
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}