blob: 9bc7d3bfef1f7ff428cb9d0481b05ae0c150e1a5 [file] [log] [blame]
// Copyright 2017 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.base.process_launcher;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.os.Binder;
import android.os.Build;
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.os.SystemClock;
import android.text.TextUtils;
import android.util.SparseArray;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;
import org.chromium.base.BaseSwitches;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.EarlyTraceEvent;
import org.chromium.base.Log;
import org.chromium.base.MemoryPressureLevel;
import org.chromium.base.ThreadUtils;
import org.chromium.base.compat.ApiHelperForN;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.memory.MemoryPressureMonitor;
import org.chromium.base.metrics.RecordHistogram;
import java.util.List;
import javax.annotation.concurrent.GuardedBy;
/**
* This is the base class for child services.
* Pre-Q, and for privileged 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" />
*
* Q added bindIsolatedService which supports creating multiple instances from a single manifest
* declaration for isolated services. In this case, only need to declare instance 0 in the manifest.
*
* 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.
*
* This class does not directly inherit from Service because the logic may be used by a Service
* implementation which cannot directly inherit from this class (e.g. for WebLayer child services).
*/
@JNINamespace("base::android")
public class ChildProcessService {
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 static int sZygotePid;
private static long sZygoteStartupTimeMillis;
private final ChildProcessServiceDelegate mDelegate;
private final Service mService;
private final Context mApplicationContext;
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;
@GuardedBy("mBinderLock")
private String mBoundCallingClazz;
// 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;
// Interface to send notifications to the parent process.
private IParentProcess mParentProcess;
public ChildProcessService(
ChildProcessServiceDelegate delegate, Service service, Context applicationContext) {
mDelegate = delegate;
mService = service;
mApplicationContext = applicationContext;
}
// 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(String clazz) {
assert mBindToCallerCheck;
assert mServiceBound;
synchronized (mBinderLock) {
int callingPid = Binder.getCallingPid();
if (mBoundCallingPid == 0 && mBoundCallingClazz == null) {
mBoundCallingPid = callingPid;
mBoundCallingClazz = clazz;
} else if (mBoundCallingPid != callingPid) {
Log.e(
TAG,
"Service is already bound by pid %d, cannot bind for pid %d",
mBoundCallingPid,
callingPid);
return false;
} else if (!TextUtils.equals(mBoundCallingClazz, clazz)) {
Log.w(
TAG,
"Service is already bound by %s, cannot bind for %s",
mBoundCallingClazz,
clazz);
return false;
}
}
return true;
}
@Override
public ApplicationInfo getAppInfo() {
return mApplicationContext.getApplicationInfo();
}
@Override
public void setupConnection(
Bundle args, IParentProcess parentProcess, List<IBinder> callbacks)
throws RemoteException {
assert mServiceBound;
synchronized (mBinderLock) {
if (mBindToCallerCheck && mBoundCallingPid == 0) {
Log.e(TAG, "Service has not been bound with bindToCaller()");
parentProcess.finishSetupConnection(-1, 0, 0, null);
return;
}
}
int pid = Process.myPid();
int zygotePid = 0;
long startupTimeMillis = -1;
Bundle relroBundle = null;
if (LibraryLoader.getInstance().isLoadedByZygote()) {
zygotePid = sZygotePid;
startupTimeMillis = sZygoteStartupTimeMillis;
LibraryLoader.MultiProcessMediator m =
LibraryLoader.getInstance().getMediator();
m.initInChildProcess();
// In a number of cases the app zygote decides not to produce a RELRO FD.
// The bundle will tell the receiver to silently ignore it.
relroBundle = new Bundle();
m.putSharedRelrosToBundle(relroBundle);
}
// After finishSetupConnection() the parent process will stop accepting
// |relroBundle| from this process to ensure that another FD to shared memory
// is not sent later.
parentProcess.finishSetupConnection(
pid, zygotePid, startupTimeMillis, relroBundle);
mParentProcess = parentProcess;
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);
}
});
}
@Override
public void dumpProcessStack() {
assert mServiceBound;
synchronized (mLibraryInitializedLock) {
if (!mLibraryInitialized) {
Log.e(TAG, "Cannot dump process stack before native is loaded");
return;
}
}
ChildProcessServiceJni.get().dumpProcessStack();
}
@Override
public void consumeRelroBundle(Bundle bundle) {
mDelegate.consumeRelroBundle(bundle);
}
};
/** Loads Chrome's native libraries and initializes a ChildProcessService. */
// For sCreateCalled check.
public void 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();
// Unlike desktop Linux, on Android we leave the main looper thread to handle Android
// lifecycle events, and create a separate thread to serve as the main renderer. This
// affects the thread stack size: instead of getting the kernel default we get the Java
// default, which can be much smaller. So, explicitly set up a larger stack here.
long stackSize = ContextUtils.isProcess64Bit() ? 8 * 1024 * 1024 : 4 * 1024 * 1024;
mMainThread =
new Thread(
/* threadGroup= */ null, this::mainThreadMain, MAIN_THREAD_NAME, stackSize);
mMainThread.start();
}
private void mainThreadMain() {
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();
}
EarlyTraceEvent.onCommandLineAvailableInChildProcess();
mDelegate.loadNativeLibrary(getApplicationContext());
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;
}
ChildProcessServiceJni.get()
.registerFileDescriptors(keys, fileIds, fds, regionOffsets, regionSizes);
mDelegate.onBeforeMain();
} catch (Throwable e) {
try {
mParentProcess.reportExceptionInInit(
ChildProcessService.class.getName()
+ "\n"
+ android.util.Log.getStackTraceString(e));
} catch (RemoteException re) {
Log.e(TAG, "Failed to call reportExceptionInInit.", re);
}
throw new RuntimeException(e);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Record process startup time histograms.
long startTime = SystemClock.uptimeMillis() - ApiHelperForN.getStartUptimeMillis();
String baseHistogramName = "Android.ChildProcessStartTimeV2";
String suffix = ContextUtils.isIsolatedProcess() ? ".Isolated" : ".NotIsolated";
RecordHistogram.recordMediumTimesHistogram(baseHistogramName + ".All", startTime);
RecordHistogram.recordMediumTimesHistogram(baseHistogramName + suffix, startTime);
}
mDelegate.runMain();
try {
mParentProcess.reportCleanExit();
} catch (RemoteException e) {
Log.e(TAG, "Failed to call clean exit callback.", e);
}
ChildProcessServiceJni.get().exitChildProcess();
}
@SuppressWarnings("checkstyle:SystemExitCheck") // Allowed due to http://crbug.com/928521#c16.
public void onDestroy() {
Log.i(TAG, "Destroying ChildProcessService pid=%d", Process.myPid());
System.exit(0);
}
/*
* 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.
*/
public IBinder onBind(Intent intent) {
if (mServiceBound) return mBinder;
// 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.
mService.stopSelf();
mBindToCallerCheck =
intent.getBooleanExtra(ChildProcessConstants.EXTRA_BIND_TO_CALLER, false);
mServiceBound = true;
mDelegate.onServiceBound(intent);
String packageName =
intent.getStringExtra(ChildProcessConstants.EXTRA_BROWSER_PACKAGE_NAME);
if (packageName == null) {
packageName = getApplicationContext().getApplicationInfo().packageName;
}
// Don't block bind() with any extra work, post it to the application thread instead.
final String preloadPackageName = packageName;
new Handler(Looper.getMainLooper())
.post(() -> mDelegate.preloadNativeLibrary(preloadPackageName));
return mBinder;
}
/** This will be called from the zygote on startup. */
public static void setZygoteInfo(int zygotePid, long zygoteStartupTimeMillis) {
sZygotePid = zygotePid;
sZygoteStartupTimeMillis = zygoteStartupTimeMillis;
}
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();
}
}
private Context getApplicationContext() {
return mApplicationContext;
}
@NativeMethods
interface Natives {
/**
* Helper for registering FileDescriptorInfo objects with GlobalFileDescriptors or
* FileDescriptorStore.
* This includes the IPC channel, the crash dump signals and resource related
* files.
*/
void registerFileDescriptors(String[] keys, int[] id, int[] fd, long[] offset, long[] size);
/** Force the child process to exit. */
void exitChildProcess();
/** Dumps the child process stack without crashing it. */
void dumpProcessStack();
}
}