| // Copyright 2012 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; |
| |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.ServiceConnection; |
| import android.os.Build; |
| import android.os.IBinder; |
| import android.os.ParcelFileDescriptor; |
| import android.os.RemoteException; |
| import android.os.StrictMode; |
| |
| import org.chromium.android_webview.command_line.CommandLineUtil; |
| import org.chromium.android_webview.policy.AwPolicyProvider; |
| import org.chromium.android_webview.services.CrashReceiverService; |
| import org.chromium.android_webview.services.ICrashReceiverService; |
| import org.chromium.base.CommandLine; |
| import org.chromium.base.ContextUtils; |
| import org.chromium.base.Log; |
| import org.chromium.base.PathUtils; |
| import org.chromium.base.ThreadUtils; |
| import org.chromium.base.annotations.CalledByNative; |
| import org.chromium.base.annotations.JNINamespace; |
| import org.chromium.base.library_loader.LibraryLoader; |
| import org.chromium.base.library_loader.LibraryProcessType; |
| import org.chromium.base.library_loader.ProcessInitException; |
| import org.chromium.base.task.PostTask; |
| import org.chromium.base.task.TaskRunner; |
| import org.chromium.base.task.TaskTraits; |
| import org.chromium.components.minidump_uploader.CrashFileManager; |
| import org.chromium.content_public.browser.BrowserStartupController; |
| import org.chromium.content_public.browser.ChildProcessCreationParams; |
| import org.chromium.content_public.browser.ChildProcessLauncherHelper; |
| import org.chromium.policy.CombinedPolicyProvider; |
| |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.RandomAccessFile; |
| import java.nio.channels.FileLock; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Wrapper for the steps needed to initialize the java and native sides of webview chromium. |
| */ |
| @JNINamespace("android_webview") |
| public final class AwBrowserProcess { |
| private static final String TAG = "AwBrowserProcess"; |
| |
| private static final String WEBVIEW_DIR_BASENAME = "webview"; |
| private static final String EXCLUSIVE_LOCK_FILE = "webview_data.lock"; |
| |
| // To avoid any potential synchronization issues we post all minidump-copying actions to |
| // the same sequence to be run serially. |
| private static final TaskRunner sSequencedTaskRunner = |
| PostTask.createSequencedTaskRunner(TaskTraits.BEST_EFFORT_MAY_BLOCK); |
| |
| private static RandomAccessFile sLockFile; |
| private static FileLock sExclusiveFileLock; |
| private static String sWebViewPackageName; |
| |
| /** |
| * Loads the native library, and performs basic static construction of objects needed |
| * to run webview in this process. Does not create threads; safe to call from zygote. |
| * Note: it is up to the caller to ensure this is only called once. |
| * |
| * @param processDataDirSuffix The suffix to use when setting the data directory for this |
| * process; null to use no suffix. |
| */ |
| public static void loadLibrary(String processDataDirSuffix) { |
| if (processDataDirSuffix == null) { |
| PathUtils.setPrivateDataDirectorySuffix(WEBVIEW_DIR_BASENAME, null); |
| } else { |
| String processDataDirName = WEBVIEW_DIR_BASENAME + "_" + processDataDirSuffix; |
| PathUtils.setPrivateDataDirectorySuffix(processDataDirName, processDataDirName); |
| } |
| StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); |
| try { |
| LibraryLoader.getInstance().loadNow(); |
| // Switch the command line implementation from Java to native. |
| // It's okay for the WebView to do this before initialization because we have |
| // setup the JNI bindings by this point. |
| LibraryLoader.getInstance().switchCommandLineForWebView(); |
| } catch (ProcessInitException e) { |
| throw new RuntimeException("Cannot load WebView", e); |
| } finally { |
| StrictMode.setThreadPolicy(oldPolicy); |
| } |
| } |
| |
| /** |
| * Configures child process launcher. This is required only if child services are used in |
| * WebView. |
| */ |
| public static void configureChildProcessLauncher() { |
| final boolean isExternalService = true; |
| final boolean bindToCaller = true; |
| final boolean ignoreVisibilityForImportance = true; |
| ChildProcessCreationParams.set(getWebViewPackageName(), isExternalService, |
| LibraryProcessType.PROCESS_WEBVIEW_CHILD, bindToCaller, |
| ignoreVisibilityForImportance); |
| } |
| |
| /** |
| * Starts the chromium browser process running within this process. Creates threads |
| * and performs other per-app resource allocations; must not be called from zygote. |
| * Note: it is up to the caller to ensure this is only called once. |
| */ |
| public static void start() { |
| try (ScopedSysTraceEvent e1 = ScopedSysTraceEvent.scoped("AwBrowserProcess.start")) { |
| final Context appContext = ContextUtils.getApplicationContext(); |
| tryObtainingDataDirLock(appContext); |
| // We must post to the UI thread to cover the case that the user |
| // has invoked Chromium startup by using the (thread-safe) |
| // CookieManager rather than creating a WebView. |
| ThreadUtils.runOnUiThreadBlocking(() -> { |
| boolean multiProcess = |
| CommandLine.getInstance().hasSwitch(AwSwitches.WEBVIEW_SANDBOXED_RENDERER); |
| if (multiProcess) { |
| ChildProcessLauncherHelper.warmUp(appContext, true); |
| } |
| // The policies are used by browser startup, so we need to register the policy |
| // providers before starting the browser process. This only registers java objects |
| // and doesn't need the native library. |
| CombinedPolicyProvider.get().registerProvider(new AwPolicyProvider(appContext)); |
| |
| // Check android settings but only when safebrowsing is enabled. |
| try (ScopedSysTraceEvent e2 = |
| ScopedSysTraceEvent.scoped("AwBrowserProcess.maybeEnable")) { |
| AwSafeBrowsingConfigHelper.maybeEnableSafeBrowsingFromManifest(appContext); |
| } |
| |
| try (ScopedSysTraceEvent e2 = ScopedSysTraceEvent.scoped( |
| "AwBrowserProcess.startBrowserProcessesSync")) { |
| BrowserStartupController.get(LibraryProcessType.PROCESS_WEBVIEW) |
| .startBrowserProcessesSync(!multiProcess); |
| } catch (ProcessInitException e) { |
| throw new RuntimeException("Cannot initialize WebView", e); |
| } |
| }); |
| } |
| } |
| |
| private static void tryObtainingDataDirLock(final Context appContext) { |
| try (ScopedSysTraceEvent e1 = |
| ScopedSysTraceEvent.scoped("AwBrowserProcess.tryObtainingDataDirLock")) { |
| // Many existing apps rely on this even though it's known to be unsafe. |
| // Make it fatal when on P for apps that target P or higher |
| boolean dieOnFailure = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P |
| && appContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P; |
| |
| StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); |
| try { |
| String dataPath = PathUtils.getDataDirectory(); |
| File lockFile = new File(dataPath, EXCLUSIVE_LOCK_FILE); |
| boolean success = false; |
| try { |
| // Note that the file is kept open intentionally. |
| sLockFile = new RandomAccessFile(lockFile, "rw"); |
| sExclusiveFileLock = sLockFile.getChannel().tryLock(); |
| success = sExclusiveFileLock != null; |
| } catch (IOException e) { |
| Log.w(TAG, "Failed to create lock file " + lockFile, e); |
| } |
| if (!success) { |
| final String error = |
| "Using WebView from more than one process at once with the " |
| + "same data directory is not supported. https://crbug.com/558377"; |
| if (dieOnFailure) { |
| throw new RuntimeException(error); |
| } else { |
| Log.w(TAG, error); |
| } |
| } |
| } finally { |
| StrictMode.setThreadPolicy(oldPolicy); |
| } |
| } |
| } |
| |
| public static void setWebViewPackageName(String webViewPackageName) { |
| assert sWebViewPackageName == null || sWebViewPackageName.equals(webViewPackageName); |
| sWebViewPackageName = webViewPackageName; |
| } |
| |
| public static String getWebViewPackageName() { |
| if (sWebViewPackageName == null) return ""; // May be null in testing. |
| return sWebViewPackageName; |
| } |
| |
| /** |
| * Trigger minidump copying, which in turn triggers minidump uploading. |
| */ |
| @CalledByNative |
| private static void triggerMinidumpUploading() { |
| handleMinidumpsAndSetMetricsConsent(false /* updateMetricsConsent */); |
| } |
| |
| /** |
| * Trigger minidump uploading, and optionaly also update the metrics-consent value depending on |
| * whether the Android Checkbox is toggled on. |
| * @param updateMetricsConsent whether to update the metrics-consent value to represent the |
| * Android Checkbox toggle. |
| */ |
| public static void handleMinidumpsAndSetMetricsConsent(final boolean updateMetricsConsent) { |
| try (ScopedSysTraceEvent e1 = ScopedSysTraceEvent.scoped( |
| "AwBrowserProcess.handleMinidumpsAndSetMetricsConsent")) { |
| final boolean enableMinidumpUploadingForTesting = CommandLine.getInstance().hasSwitch( |
| CommandLineUtil.CRASH_UPLOADS_ENABLED_FOR_TESTING_SWITCH); |
| if (enableMinidumpUploadingForTesting) { |
| handleMinidumps(true /* enabled */); |
| } |
| |
| PlatformServiceBridge.getInstance().queryMetricsSetting(enabled -> { |
| ThreadUtils.assertOnUiThread(); |
| if (updateMetricsConsent) { |
| AwMetricsServiceClient.setConsentSetting( |
| ContextUtils.getApplicationContext(), enabled); |
| } |
| |
| if (!enableMinidumpUploadingForTesting) { |
| handleMinidumps(enabled); |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Make a list of crash key-value pairs for in the same order as minidump file array. |
| * These crash key-value pairs are passed from native-code while generating the minidump files. |
| * This is basically reordering the crash-key maps in @code{crashesInfo} in the same order as |
| * minidump files, ignoring crashkeys for files that are not in the @code{minidumps} array and |
| * null for minidumps that don't have crash key-value maps in @code{crashesInfo}. |
| * |
| * @param minidumps array of minidump files to get crash-keys for. |
| * @param crashesInfo crash key-value pairs grouped/mapped by crash report uuid. |
| * @return list of crash key-value pairs map corresponding for each minidumps file. |
| */ |
| private static List<Map<String, String>> getCrashKeysForCrashFiles( |
| File[] minidumps, Map<String, Map<String, String>> crashesInfo) { |
| List<Map<String, String>> crashesInfoList = new ArrayList<>(minidumps.length); |
| for (int i = 0; i < minidumps.length; i++) { |
| String fileName = minidumps[i].getName(); |
| // crash report uuid is the minidump file name without any extensions. |
| int firstDotIndex = fileName.indexOf('.'); |
| if (firstDotIndex == -1) { |
| firstDotIndex = fileName.length(); |
| } |
| String crashUuid = fileName.substring(0, firstDotIndex); |
| if (crashesInfo == null) { |
| crashesInfoList.add(null); |
| } else { |
| crashesInfoList.add(crashesInfo.get(crashUuid)); |
| } |
| } |
| return crashesInfoList; |
| } |
| |
| /** |
| * Pass Minidumps to a separate Service declared in the WebView provider package. |
| * That Service will copy the Minidumps to its own data directory - at which point we can delete |
| * our copies in the app directory. |
| * @param userApproved whether we have user consent to upload crash data - if we do, copy the |
| * minidumps, if we don't, delete them. |
| */ |
| public static void handleMinidumps(final boolean userApproved) { |
| sSequencedTaskRunner.postTask(() -> { |
| final Context appContext = ContextUtils.getApplicationContext(); |
| final File crashSpoolDir = new File(appContext.getCacheDir().getPath(), "WebView"); |
| if (!crashSpoolDir.isDirectory()) return; |
| final CrashFileManager crashFileManager = new CrashFileManager(crashSpoolDir); |
| |
| // The lifecycle of a minidump in the app directory is very simple: foo.dmpNNNNN -- |
| // where NNNNN is a Process ID (PID) -- gets created, and is either deleted or |
| // copied over to the shared crash directory for all WebView-using apps. |
| Map<String, Map<String, String>> crashesInfoMap = |
| crashFileManager.importMinidumpsCrashKeys(); |
| final File[] minidumpFiles = crashFileManager.getCurrentMinidumpsSansLogcat(); |
| if (minidumpFiles.length == 0) return; |
| |
| // Delete the minidumps if the user doesn't allow crash data uploading. |
| if (!userApproved) { |
| for (File minidump : minidumpFiles) { |
| if (!minidump.delete()) { |
| Log.w(TAG, "Couldn't delete file " + minidump.getAbsolutePath()); |
| } |
| } |
| return; |
| } |
| |
| final Intent intent = new Intent(); |
| intent.setClassName(getWebViewPackageName(), CrashReceiverService.class.getName()); |
| |
| ServiceConnection connection = new ServiceConnection() { |
| @Override |
| public void onServiceConnected(ComponentName className, IBinder service) { |
| // Pass file descriptors, pointing to our minidumps, to the minidump-copying |
| // service so that the contents of the minidumps will be copied to WebView's |
| // data directory. Delete our direct File-references to the minidumps after |
| // creating the file-descriptors to resign from retrying to copy the |
| // minidumps if anything goes wrong - this makes sense given that a failure |
| // to copy a file usually means that retrying won't succeed either, e.g. the |
| // disk being full, or the file system being corrupted. |
| final ParcelFileDescriptor[] minidumpFds = |
| new ParcelFileDescriptor[minidumpFiles.length]; |
| try { |
| for (int i = 0; i < minidumpFiles.length; ++i) { |
| try { |
| minidumpFds[i] = ParcelFileDescriptor.open( |
| minidumpFiles[i], ParcelFileDescriptor.MODE_READ_ONLY); |
| } catch (FileNotFoundException e) { |
| minidumpFds[i] = null; // This is slightly ugly :) |
| } |
| if (!minidumpFiles[i].delete()) { |
| Log.w(TAG, |
| "Couldn't delete file " |
| + minidumpFiles[i].getAbsolutePath()); |
| } |
| } |
| try { |
| List<Map<String, String>> crashesInfoList = |
| getCrashKeysForCrashFiles(minidumpFiles, crashesInfoMap); |
| ICrashReceiverService.Stub.asInterface(service).transmitCrashes( |
| minidumpFds, crashesInfoList); |
| } catch (RemoteException e) { |
| // TODO(gsennton): add a UMA metric here to ensure we aren't losing |
| // too many minidumps because of this. |
| } |
| } finally { |
| // Close FDs |
| for (int i = 0; i < minidumpFds.length; ++i) { |
| try { |
| if (minidumpFds[i] != null) minidumpFds[i].close(); |
| } catch (IOException e) { |
| } |
| } |
| appContext.unbindService(this); |
| } |
| } |
| |
| @Override |
| public void onServiceDisconnected(ComponentName className) {} |
| }; |
| if (!appContext.bindService(intent, connection, Context.BIND_AUTO_CREATE)) { |
| Log.w(TAG, "Could not bind to Minidump-copying Service " + intent); |
| } |
| }); |
| } |
| |
| // Do not instantiate this class. |
| private AwBrowserProcess() {} |
| } |