summaryrefslogtreecommitdiff
path: root/base/android/java/src/org/chromium/base/process_launcher/ChildProcessService.java
diff options
context:
space:
mode:
Diffstat (limited to 'base/android/java/src/org/chromium/base/process_launcher/ChildProcessService.java')
-rw-r--r--base/android/java/src/org/chromium/base/process_launcher/ChildProcessService.java346
1 files changed, 346 insertions, 0 deletions
diff --git a/base/android/java/src/org/chromium/base/process_launcher/ChildProcessService.java b/base/android/java/src/org/chromium/base/process_launcher/ChildProcessService.java
new file mode 100644
index 0000000000..876ebbb175
--- /dev/null
+++ b/base/android/java/src/org/chromium/base/process_launcher/ChildProcessService.java
@@ -0,0 +1,346 @@
+// Copyright 2017 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.base.process_launcher;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Parcelable;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.SparseArray;
+
+import org.chromium.base.BaseSwitches;
+import org.chromium.base.CommandLine;
+import org.chromium.base.ContextUtils;
+import org.chromium.base.Log;
+import org.chromium.base.MemoryPressureLevel;
+import org.chromium.base.ThreadUtils;
+import org.chromium.base.annotations.JNINamespace;
+import org.chromium.base.annotations.MainDex;
+import org.chromium.base.memory.MemoryPressureMonitor;
+
+import java.util.List;
+import java.util.concurrent.Semaphore;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * This is the base class for child services; the embedding application should contain
+ * ProcessService0, 1.. etc subclasses that provide the concrete service entry points, so it can
+ * connect to more than one distinct process (i.e. one process per service number, up to limit of
+ * N).
+ * The embedding application must declare these service instances in the application section
+ * of its AndroidManifest.xml, first with some meta-data describing the services:
+ * <meta-data android:name="org.chromium.test_app.SERVICES_NAME"
+ * android:value="org.chromium.test_app.ProcessService"/>
+ * and then N entries of the form:
+ * <service android:name="org.chromium.test_app.ProcessServiceX"
+ * android:process=":processX" />
+ *
+ * Subclasses must also provide a delegate in this class constructor. That delegate is responsible
+ * for loading native libraries and running the main entry point of the service.
+ */
+@JNINamespace("base::android")
+@MainDex
+public abstract class ChildProcessService extends Service {
+ private static final String MAIN_THREAD_NAME = "ChildProcessMain";
+ private static final String TAG = "ChildProcessService";
+
+ // Only for a check that create is only called once.
+ private static boolean sCreateCalled;
+
+ private final ChildProcessServiceDelegate mDelegate;
+
+ private final Object mBinderLock = new Object();
+ private final Object mLibraryInitializedLock = new Object();
+
+ // True if we should enforce that bindToCaller() is called before setupConnection().
+ // Only set once in bind(), does not require synchronization.
+ private boolean mBindToCallerCheck;
+
+ // PID of the client of this service, set in bindToCaller(), if mBindToCallerCheck is true.
+ @GuardedBy("mBinderLock")
+ private int mBoundCallingPid;
+
+ // This is the native "Main" thread for the renderer / utility process.
+ private Thread mMainThread;
+
+ // Parameters received via IPC, only accessed while holding the mMainThread monitor.
+ private String[] mCommandLineParams;
+
+ // File descriptors that should be registered natively.
+ private FileDescriptorInfo[] mFdInfos;
+
+ @GuardedBy("mLibraryInitializedLock")
+ private boolean mLibraryInitialized;
+
+ // Called once the service is bound and all service related member variables have been set.
+ // Only set once in bind(), does not require synchronization.
+ private boolean mServiceBound;
+
+ private final Semaphore mActivitySemaphore = new Semaphore(1);
+
+ public ChildProcessService(ChildProcessServiceDelegate delegate) {
+ mDelegate = delegate;
+ }
+
+ // Binder object used by clients for this service.
+ private final IChildProcessService.Stub mBinder = new IChildProcessService.Stub() {
+ // NOTE: Implement any IChildProcessService methods here.
+ @Override
+ public boolean bindToCaller() {
+ assert mBindToCallerCheck;
+ assert mServiceBound;
+ synchronized (mBinderLock) {
+ int callingPid = Binder.getCallingPid();
+ if (mBoundCallingPid == 0) {
+ mBoundCallingPid = callingPid;
+ } else if (mBoundCallingPid != callingPid) {
+ Log.e(TAG, "Service is already bound by pid %d, cannot bind for pid %d",
+ mBoundCallingPid, callingPid);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void setupConnection(Bundle args, ICallbackInt pidCallback, List<IBinder> callbacks)
+ throws RemoteException {
+ assert mServiceBound;
+ synchronized (mBinderLock) {
+ if (mBindToCallerCheck && mBoundCallingPid == 0) {
+ Log.e(TAG, "Service has not been bound with bindToCaller()");
+ pidCallback.call(-1);
+ return;
+ }
+ }
+
+ pidCallback.call(Process.myPid());
+ processConnectionBundle(args, callbacks);
+ }
+
+ @Override
+ public void forceKill() {
+ assert mServiceBound;
+ Process.killProcess(Process.myPid());
+ }
+
+ @Override
+ public void onMemoryPressure(@MemoryPressureLevel int pressure) {
+ // This method is called by the host process when the host process reports pressure
+ // to its native side. The key difference between the host process and its services is
+ // that the host process polls memory pressure when it gets CRITICAL, and periodically
+ // invokes pressure listeners until pressure subsides. (See MemoryPressureMonitor for
+ // more info.)
+ //
+ // Services don't poll, so this side-channel is used to notify services about memory
+ // pressure from the host process's POV.
+ //
+ // However, since both host process and services listen to ComponentCallbacks2, we
+ // can't be sure that the host process won't get better signals than their services.
+ // I.e. we need to watch out for a situation where a service gets CRITICAL, but the
+ // host process gets MODERATE - in this case we need to ignore MODERATE.
+ //
+ // So we're ignoring pressure from the host process if it's better than the last
+ // reported pressure. I.e. the host process can drive pressure up, but it'll go
+ // down only when we the service get a signal through ComponentCallbacks2.
+ ThreadUtils.postOnUiThread(() -> {
+ if (pressure >= MemoryPressureMonitor.INSTANCE.getLastReportedPressure()) {
+ MemoryPressureMonitor.INSTANCE.notifyPressure(pressure);
+ }
+ });
+ }
+ };
+
+ /**
+ * Loads Chrome's native libraries and initializes a ChildProcessService.
+ */
+ // For sCreateCalled check.
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.i(TAG, "Creating new ChildProcessService pid=%d", Process.myPid());
+ if (sCreateCalled) {
+ throw new RuntimeException("Illegal child process reuse.");
+ }
+ sCreateCalled = true;
+
+ // Initialize the context for the application that owns this ChildProcessService object.
+ ContextUtils.initApplicationContext(getApplicationContext());
+
+ mDelegate.onServiceCreated();
+
+ mMainThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // CommandLine must be initialized before everything else.
+ synchronized (mMainThread) {
+ while (mCommandLineParams == null) {
+ mMainThread.wait();
+ }
+ }
+ assert mServiceBound;
+ CommandLine.init(mCommandLineParams);
+
+ if (CommandLine.getInstance().hasSwitch(
+ BaseSwitches.RENDERER_WAIT_FOR_JAVA_DEBUGGER)) {
+ android.os.Debug.waitForDebugger();
+ }
+
+ boolean nativeLibraryLoaded = false;
+ try {
+ nativeLibraryLoaded = mDelegate.loadNativeLibrary(getApplicationContext());
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to load native library.", e);
+ }
+ if (!nativeLibraryLoaded) {
+ System.exit(-1);
+ }
+
+ synchronized (mLibraryInitializedLock) {
+ mLibraryInitialized = true;
+ mLibraryInitializedLock.notifyAll();
+ }
+ synchronized (mMainThread) {
+ mMainThread.notifyAll();
+ while (mFdInfos == null) {
+ mMainThread.wait();
+ }
+ }
+
+ SparseArray<String> idsToKeys = mDelegate.getFileDescriptorsIdsToKeys();
+
+ int[] fileIds = new int[mFdInfos.length];
+ String[] keys = new String[mFdInfos.length];
+ int[] fds = new int[mFdInfos.length];
+ long[] regionOffsets = new long[mFdInfos.length];
+ long[] regionSizes = new long[mFdInfos.length];
+ for (int i = 0; i < mFdInfos.length; i++) {
+ FileDescriptorInfo fdInfo = mFdInfos[i];
+ String key = idsToKeys != null ? idsToKeys.get(fdInfo.id) : null;
+ if (key != null) {
+ keys[i] = key;
+ } else {
+ fileIds[i] = fdInfo.id;
+ }
+ fds[i] = fdInfo.fd.detachFd();
+ regionOffsets[i] = fdInfo.offset;
+ regionSizes[i] = fdInfo.size;
+ }
+ nativeRegisterFileDescriptors(keys, fileIds, fds, regionOffsets, regionSizes);
+
+ mDelegate.onBeforeMain();
+ if (mActivitySemaphore.tryAcquire()) {
+ mDelegate.runMain();
+ nativeExitChildProcess();
+ }
+ } catch (InterruptedException e) {
+ Log.w(TAG, "%s startup failed: %s", MAIN_THREAD_NAME, e);
+ }
+ }
+ }, MAIN_THREAD_NAME);
+ mMainThread.start();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ Log.i(TAG, "Destroying ChildProcessService pid=%d", Process.myPid());
+ if (mActivitySemaphore.tryAcquire()) {
+ // TODO(crbug.com/457406): This is a bit hacky, but there is no known better solution
+ // as this service will get reused (at least if not sandboxed).
+ // In fact, we might really want to always exit() from onDestroy(), not just from
+ // the early return here.
+ System.exit(0);
+ return;
+ }
+ synchronized (mLibraryInitializedLock) {
+ try {
+ while (!mLibraryInitialized) {
+ // Avoid a potential race in calling through to native code before the library
+ // has loaded.
+ mLibraryInitializedLock.wait();
+ }
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+ mDelegate.onDestroy();
+ }
+
+ /*
+ * Returns the communication channel to the service. Note that even if multiple clients were to
+ * connect, we should only get one call to this method. So there is no need to synchronize
+ * member variables that are only set in this method and accessed from binder methods, as binder
+ * methods can't be called until this method returns.
+ * @param intent The intent that was used to bind to the service.
+ * @return the binder used by the client to setup the connection.
+ */
+ @Override
+ public IBinder onBind(Intent intent) {
+ assert !mServiceBound;
+
+ // We call stopSelf() to request that this service be stopped as soon as the client unbinds.
+ // Otherwise the system may keep it around and available for a reconnect. The child
+ // processes do not currently support reconnect; they must be initialized from scratch every
+ // time.
+ stopSelf();
+
+ mBindToCallerCheck =
+ intent.getBooleanExtra(ChildProcessConstants.EXTRA_BIND_TO_CALLER, false);
+ mServiceBound = true;
+ mDelegate.onServiceBound(intent);
+ // Don't block bind() with any extra work, post it to the application thread instead.
+ new Handler(Looper.getMainLooper())
+ .post(() -> mDelegate.preloadNativeLibrary(getApplicationContext()));
+ return mBinder;
+ }
+
+ private void processConnectionBundle(Bundle bundle, List<IBinder> clientInterfaces) {
+ // Required to unparcel FileDescriptorInfo.
+ ClassLoader classLoader = getApplicationContext().getClassLoader();
+ bundle.setClassLoader(classLoader);
+ synchronized (mMainThread) {
+ if (mCommandLineParams == null) {
+ mCommandLineParams =
+ bundle.getStringArray(ChildProcessConstants.EXTRA_COMMAND_LINE);
+ mMainThread.notifyAll();
+ }
+ // We must have received the command line by now
+ assert mCommandLineParams != null;
+ Parcelable[] fdInfosAsParcelable =
+ bundle.getParcelableArray(ChildProcessConstants.EXTRA_FILES);
+ if (fdInfosAsParcelable != null) {
+ // For why this arraycopy is necessary:
+ // http://stackoverflow.com/questions/8745893/i-dont-get-why-this-classcastexception-occurs
+ mFdInfos = new FileDescriptorInfo[fdInfosAsParcelable.length];
+ System.arraycopy(fdInfosAsParcelable, 0, mFdInfos, 0, fdInfosAsParcelable.length);
+ }
+ mDelegate.onConnectionSetup(bundle, clientInterfaces);
+ mMainThread.notifyAll();
+ }
+ }
+
+ /**
+ * Helper for registering FileDescriptorInfo objects with GlobalFileDescriptors or
+ * FileDescriptorStore.
+ * This includes the IPC channel, the crash dump signals and resource related
+ * files.
+ */
+ private static native void nativeRegisterFileDescriptors(
+ String[] keys, int[] id, int[] fd, long[] offset, long[] size);
+
+ /**
+ * Force the child process to exit.
+ */
+ private static native void nativeExitChildProcess();
+}