blob: 3f27b404b15c02e7f265f5fe5feb5d51d369681e [file] [log] [blame]
// 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.content.browser;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.text.TextUtils;
import android.view.Surface;
import org.chromium.base.CommandLine;
import org.chromium.base.CpuFeatures;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.UnguessableToken;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.library_loader.Linker;
import org.chromium.content.app.ChromiumLinkerParams;
import org.chromium.content.app.PrivilegedProcessService;
import org.chromium.content.app.SandboxedProcessService;
import org.chromium.content.common.ContentSwitches;
import org.chromium.content.common.FileDescriptorInfo;
import org.chromium.content.common.IChildProcessCallback;
import org.chromium.content.common.SurfaceWrapper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
/**
* This class provides the method to start/stop ChildProcess called by native.
*
* Note about threading. The threading here is complicated and not well documented.
* Code can run on these threads: UI, Launcher, async thread pool, binder, and one-off
* background threads.
*/
@JNINamespace("content")
public class ChildProcessLauncher {
private static final String TAG = "ChildProcLauncher";
static final int CALLBACK_FOR_UNKNOWN_PROCESS = 0;
static final int CALLBACK_FOR_GPU_PROCESS = 1;
static final int CALLBACK_FOR_RENDERER_PROCESS = 2;
static final int CALLBACK_FOR_UTILITY_PROCESS = 3;
private static class ChildConnectionAllocator {
// Connections to services. Indices of the array correspond to the service numbers.
private final ChildProcessConnection[] mChildProcessConnections;
// The list of free (not bound) service indices.
// SHOULD BE ACCESSED WITH mConnectionLock.
private final ArrayList<Integer> mFreeConnectionIndices;
private final Object mConnectionLock = new Object();
private final String mChildClassName;
private final boolean mInSandbox;
// Each Allocator keeps a queue for the pending spawn data. Once a connection is free, we
// dequeue the pending spawn data from the same allocator as the connection.
// SHOULD BE ACCESSED WITH mConnectionLock.
private final Queue<SpawnData> mPendingSpawnQueue = new LinkedList<>();
public ChildConnectionAllocator(boolean inSandbox, int numChildServices,
String serviceClassName) {
mChildProcessConnections = new ChildProcessConnectionImpl[numChildServices];
mFreeConnectionIndices = new ArrayList<Integer>(numChildServices);
for (int i = 0; i < numChildServices; i++) {
mFreeConnectionIndices.add(i);
}
mChildClassName = serviceClassName;
mInSandbox = inSandbox;
}
// Allocate or enqueue. If there are no free slots, return null and enqueue the spawn data.
public ChildProcessConnection allocate(SpawnData spawnData,
ChildProcessConnection.DeathCallback deathCallback,
Bundle childProcessCommonParameters, boolean alwaysInForeground) {
assert spawnData.inSandbox() == mInSandbox;
synchronized (mConnectionLock) {
if (mFreeConnectionIndices.isEmpty()) {
Log.d(TAG, "Ran out of services to allocate.");
if (!spawnData.forWarmUp()) {
mPendingSpawnQueue.add(spawnData);
}
return null;
}
int slot = mFreeConnectionIndices.remove(0);
assert mChildProcessConnections[slot] == null;
mChildProcessConnections[slot] =
new ChildProcessConnectionImpl(spawnData.context(), slot, mInSandbox,
deathCallback, mChildClassName, childProcessCommonParameters,
alwaysInForeground, spawnData.getCreationParams());
Log.d(TAG, "Allocator allocated a connection, sandbox: %b, slot: %d", mInSandbox,
slot);
return mChildProcessConnections[slot];
}
}
// Also return the first SpawnData in the pending queue, if any.
public SpawnData free(ChildProcessConnection connection) {
synchronized (mConnectionLock) {
int slot = connection.getServiceNumber();
if (mChildProcessConnections[slot] != connection) {
int occupier = mChildProcessConnections[slot] == null
? -1 : mChildProcessConnections[slot].getServiceNumber();
Log.e(TAG, "Unable to find connection to free in slot: %d "
+ "already occupied by service: %d", slot, occupier);
assert false;
} else {
mChildProcessConnections[slot] = null;
assert !mFreeConnectionIndices.contains(slot);
mFreeConnectionIndices.add(slot);
Log.d(TAG, "Allocator freed a connection, sandbox: %b, slot: %d", mInSandbox,
slot);
}
return mPendingSpawnQueue.poll();
}
}
public boolean isFreeConnectionAvailable() {
synchronized (mConnectionLock) {
return !mFreeConnectionIndices.isEmpty();
}
}
/** @return the count of connections managed by the allocator */
@VisibleForTesting
int allocatedConnectionsCountForTesting() {
return mChildProcessConnections.length - mFreeConnectionIndices.size();
}
@VisibleForTesting
ChildProcessConnection[] connectionArrayForTesting() {
return mChildProcessConnections;
}
@VisibleForTesting
void enqueuePendingQueueForTesting(SpawnData spawnData) {
synchronized (mConnectionLock) {
mPendingSpawnQueue.add(spawnData);
}
}
@VisibleForTesting
int pendingSpawnsCountForTesting() {
synchronized (mConnectionLock) {
return mPendingSpawnQueue.size();
}
}
}
private static class SpawnData {
private final boolean mForWarmup; // Do not queue up if failed.
private final Context mContext;
private final String[] mCommandLine;
private final int mChildProcessId;
private final FileDescriptorInfo[] mFilesToBeMapped;
private final long mClientContext;
private final int mCallbackType;
private final boolean mInSandbox;
private final ChildProcessCreationParams mCreationParams;
private SpawnData(boolean forWarmUp, Context context, String[] commandLine,
int childProcessId, FileDescriptorInfo[] filesToBeMapped, long clientContext,
int callbackType, boolean inSandbox, ChildProcessCreationParams creationParams) {
mForWarmup = forWarmUp;
mContext = context;
mCommandLine = commandLine;
mChildProcessId = childProcessId;
mFilesToBeMapped = filesToBeMapped;
mClientContext = clientContext;
mCallbackType = callbackType;
mInSandbox = inSandbox;
mCreationParams = creationParams;
}
private boolean forWarmUp() {
return mForWarmup;
}
private Context context() {
return mContext;
}
private String[] commandLine() {
return mCommandLine;
}
private int childProcessId() {
return mChildProcessId;
}
private FileDescriptorInfo[] filesToBeMapped() {
return mFilesToBeMapped;
}
private long clientContext() {
return mClientContext;
}
private int callbackType() {
return mCallbackType;
}
private boolean inSandbox() {
return mInSandbox;
}
private ChildProcessCreationParams getCreationParams() {
return mCreationParams;
}
}
// Service class for child process.
// Map from package name to ChildConnectionAllocator.
private static Map<String, ChildConnectionAllocator> sSandboxedChildConnectionAllocatorMap;
// As the default value it uses PrivilegedProcessService0.
private static ChildConnectionAllocator sPrivilegedChildConnectionAllocator;
private static final String NUM_SANDBOXED_SERVICES_KEY =
"org.chromium.content.browser.NUM_SANDBOXED_SERVICES";
private static final String NUM_PRIVILEGED_SERVICES_KEY =
"org.chromium.content.browser.NUM_PRIVILEGED_SERVICES";
private static final String SANDBOXED_SERVICES_NAME_KEY =
"org.chromium.content.browser.SANDBOXED_SERVICES_NAME";
// Overrides the number of available sandboxed services.
@VisibleForTesting
public static final String SWITCH_NUM_SANDBOXED_SERVICES_FOR_TESTING = "num-sandboxed-services";
public static final String SWITCH_SANDBOXED_SERVICES_NAME_FOR_TESTING =
"sandboxed-services-name";
private static int getNumberOfServices(Context context, boolean inSandbox, String packageName) {
int numServices = -1;
if (inSandbox
&& CommandLine.getInstance().hasSwitch(
SWITCH_NUM_SANDBOXED_SERVICES_FOR_TESTING)) {
String value = CommandLine.getInstance().getSwitchValue(
SWITCH_NUM_SANDBOXED_SERVICES_FOR_TESTING);
if (!TextUtils.isEmpty(value)) {
try {
numServices = Integer.parseInt(value);
} catch (NumberFormatException e) {
Log.w(TAG, "The value of --num-sandboxed-services is formatted wrongly: "
+ value);
}
}
} else {
try {
PackageManager packageManager = context.getPackageManager();
ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName,
PackageManager.GET_META_DATA);
if (appInfo.metaData != null) {
numServices = appInfo.metaData.getInt(inSandbox
? NUM_SANDBOXED_SERVICES_KEY : NUM_PRIVILEGED_SERVICES_KEY, -1);
}
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException("Could not get application info");
}
}
if (numServices < 0) {
throw new RuntimeException("Illegal meta data value for number of child services");
}
return numServices;
}
private static String getClassNameOfService(Context context, boolean inSandbox,
String packageName) {
if (!inSandbox) {
return PrivilegedProcessService.class.getName();
}
if (CommandLine.getInstance().hasSwitch(SWITCH_SANDBOXED_SERVICES_NAME_FOR_TESTING)) {
return CommandLine.getInstance().getSwitchValue(
SWITCH_SANDBOXED_SERVICES_NAME_FOR_TESTING);
}
String serviceName = null;
try {
PackageManager packageManager = context.getPackageManager();
ApplicationInfo appInfo =
packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
if (appInfo.metaData != null) {
serviceName = appInfo.metaData.getString(SANDBOXED_SERVICES_NAME_KEY);
}
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException("Could not get application info.");
}
if (serviceName != null) {
// Check that the service exists.
try {
PackageManager packageManager = context.getPackageManager();
// PackageManager#getServiceInfo() throws an exception if the service does not
// exist.
packageManager.getServiceInfo(new ComponentName(packageName, serviceName + "0"), 0);
return serviceName;
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(
"Illegal meta data value: the child service doesn't exist");
}
}
return SandboxedProcessService.class.getName();
}
private static void initConnectionAllocatorsIfNecessary(
Context context, boolean inSandbox, String packageName) {
// TODO(mariakhomenko): Uses an Object to lock the access.
synchronized (ChildProcessLauncher.class) {
if (inSandbox) {
if (sSandboxedChildConnectionAllocatorMap == null) {
sSandboxedChildConnectionAllocatorMap =
new ConcurrentHashMap<String, ChildConnectionAllocator>();
}
if (!sSandboxedChildConnectionAllocatorMap.containsKey(packageName)) {
Log.w(TAG, "Create a new ChildConnectionAllocator with package name = %s,"
+ " inSandbox = true",
packageName);
sSandboxedChildConnectionAllocatorMap.put(packageName,
new ChildConnectionAllocator(true,
getNumberOfServices(context, true, packageName),
getClassNameOfService(context, true, packageName)));
}
} else if (sPrivilegedChildConnectionAllocator == null) {
sPrivilegedChildConnectionAllocator = new ChildConnectionAllocator(false,
getNumberOfServices(context, false, packageName),
getClassNameOfService(context, false, packageName));
}
// TODO(pkotwicz|hanxi): Figure out when old allocators should be removed from
// {@code sSandboxedChildConnectionAllocatorMap}.
}
}
/**
* Note: please make sure that the Allocator has been initialized before calling this function.
* Otherwise, always calls {@link initConnectionAllocatorsIfNecessary} first.
*/
private static ChildConnectionAllocator getConnectionAllocator(
String packageName, boolean inSandbox) {
if (!inSandbox) {
return sPrivilegedChildConnectionAllocator;
}
return sSandboxedChildConnectionAllocatorMap.get(packageName);
}
private static ChildConnectionAllocator getAllocatorForTesting(
Context context, String packageName, boolean inSandbox) {
initConnectionAllocatorsIfNecessary(context, inSandbox, packageName);
return getConnectionAllocator(packageName, inSandbox);
}
private static ChildProcessConnection allocateConnection(
SpawnData spawnData, Bundle childProcessCommonParams, boolean alwaysInForeground) {
ChildProcessConnection.DeathCallback deathCallback =
new ChildProcessConnection.DeathCallback() {
@Override
public void onChildProcessDied(ChildProcessConnection connection) {
if (connection.getPid() != 0) {
stop(connection.getPid());
} else {
freeConnection(connection);
}
}
};
final ChildProcessCreationParams creationParams = spawnData.getCreationParams();
final Context context = spawnData.context();
final boolean inSandbox = spawnData.inSandbox();
String packageName =
creationParams != null ? creationParams.getPackageName() : context.getPackageName();
initConnectionAllocatorsIfNecessary(context, inSandbox, packageName);
return getConnectionAllocator(packageName, inSandbox)
.allocate(spawnData, deathCallback, childProcessCommonParams, alwaysInForeground);
}
private static boolean sLinkerInitialized;
private static long sLinkerLoadAddress;
private static ChromiumLinkerParams getLinkerParamsForNewConnection() {
if (!sLinkerInitialized) {
if (Linker.isUsed()) {
sLinkerLoadAddress = Linker.getInstance().getBaseLoadAddress();
if (sLinkerLoadAddress == 0) {
Log.i(TAG, "Shared RELRO support disabled!");
}
}
sLinkerInitialized = true;
}
if (sLinkerLoadAddress == 0) return null;
// Always wait for the shared RELROs in service processes.
final boolean waitForSharedRelros = true;
if (Linker.areTestsEnabled()) {
Linker linker = Linker.getInstance();
return new ChromiumLinkerParams(sLinkerLoadAddress,
waitForSharedRelros,
linker.getTestRunnerClassNameForTesting(),
linker.getImplementationForTesting());
} else {
return new ChromiumLinkerParams(sLinkerLoadAddress,
waitForSharedRelros);
}
}
private static ChildProcessConnection allocateBoundConnection(SpawnData spawnData,
boolean alwaysInForeground, ChildProcessConnection.StartCallback startCallback) {
final Context context = spawnData.context();
final boolean inSandbox = spawnData.inSandbox();
final ChildProcessCreationParams creationParams = spawnData.getCreationParams();
Bundle commonParams = new Bundle();
commonParams.putParcelable(
ChildProcessConstants.EXTRA_LINKER_PARAMS, getLinkerParamsForNewConnection());
ChildProcessConnection connection =
allocateConnection(spawnData, commonParams, alwaysInForeground);
if (connection != null) {
connection.start(startCallback);
String packageName = creationParams != null ? creationParams.getPackageName()
: context.getPackageName();
if (inSandbox
&& !getConnectionAllocator(packageName, inSandbox)
.isFreeConnectionAvailable()) {
// Proactively releases all the moderate bindings once all the sandboxed services
// are allocated, which will be very likely to have some of them killed by OOM
// killer.
sBindingManager.releaseAllModerateBindings();
}
}
return connection;
}
private static final long FREE_CONNECTION_DELAY_MILLIS = 1;
private static void freeConnection(ChildProcessConnection connection) {
synchronized (sSpareConnectionLock) {
if (connection.equals(sSpareSandboxedConnection)) sSpareSandboxedConnection = null;
}
// Freeing a service should be delayed. This is so that we avoid immediately reusing the
// freed service (see http://crbug.com/164069): the framework might keep a service process
// alive when it's been unbound for a short time. If a new connection to the same service
// is bound at that point, the process is reused and bad things happen (mostly static
// variables are set when we don't expect them to).
final ChildProcessConnection conn = connection;
ThreadUtils.postOnUiThreadDelayed(new Runnable() {
@Override
public void run() {
final SpawnData pendingSpawn = freeConnectionAndDequeuePending(conn);
if (pendingSpawn != null) {
new Thread(new Runnable() {
@Override
public void run() {
startInternal(pendingSpawn.context(), pendingSpawn.commandLine(),
pendingSpawn.childProcessId(), pendingSpawn.filesToBeMapped(),
pendingSpawn.clientContext(), pendingSpawn.callbackType(),
pendingSpawn.inSandbox(), pendingSpawn.getCreationParams());
}
}).start();
}
}
}, FREE_CONNECTION_DELAY_MILLIS);
}
private static SpawnData freeConnectionAndDequeuePending(ChildProcessConnection conn) {
ChildConnectionAllocator allocator = getConnectionAllocator(
conn.getPackageName(), conn.isInSandbox());
assert allocator != null;
return allocator.free(conn);
}
// Represents an invalid process handle; same as base/process/process.h kNullProcessHandle.
private static final int NULL_PROCESS_HANDLE = 0;
// Map from pid to ChildService connection.
private static Map<Integer, ChildProcessConnection> sServiceMap =
new ConcurrentHashMap<Integer, ChildProcessConnection>();
// Lock and monitor for these members {{{
private static final Object sSpareConnectionLock = new Object();
// A pre-allocated and pre-bound connection ready for connection setup, or null.
private static ChildProcessConnection sSpareSandboxedConnection;
// If sSpareSandboxedConnection is not null, this indicates whether the service is
// ready for connection setup. Wait on the monitor lock to be notified when this
// state changes. sSpareSandboxedConnection may be null after waiting, if starting
// the service failed.
private static boolean sSpareConnectionStarting;
// }}}
// Manages oom bindings used to bind chind services.
private static BindingManager sBindingManager = BindingManagerImpl.createBindingManager();
// Whether the main application is currently brought to the foreground.
private static boolean sApplicationInForeground = true;
@VisibleForTesting
public static void setBindingManagerForTesting(BindingManager manager) {
sBindingManager = manager;
}
/** @return true iff the child process is protected from out-of-memory killing */
@CalledByNative
private static boolean isOomProtected(int pid) {
return sBindingManager.isOomProtected(pid);
}
/**
* Sets the visibility of the child process when it changes or when it is determined for the
* first time.
*/
@CalledByNative
public static void setInForeground(int pid, boolean inForeground) {
sBindingManager.setInForeground(pid, inForeground);
}
/**
* Called when the renderer commits a navigation. This signals a time at which it is safe to
* rely on renderer visibility signalled through setInForeground. See http://crbug.com/421041.
*/
public static void determinedVisibility(int pid) {
sBindingManager.determinedVisibility(pid);
}
/**
* Called when the embedding application is sent to background.
*/
public static void onSentToBackground() {
sApplicationInForeground = false;
sBindingManager.onSentToBackground();
}
/**
* Starts moderate binding management.
* Note: WebAPKs and non WebAPKs share the same moderate binding pool, so the size of the
* shared moderate binding pool is always set based on the number of sandboxes processes
* used by Chrome.
* @param context Android's context.
* @param moderateBindingTillBackgrounded true if the BindingManager should add a moderate
* binding to a render process when it is created and remove the moderate binding when Chrome is
* sent to the background.
*/
public static void startModerateBindingManagement(
Context context, boolean moderateBindingTillBackgrounded) {
sBindingManager.startModerateBindingManagement(context,
getNumberOfServices(context, true, context.getPackageName()),
moderateBindingTillBackgrounded);
}
/**
* Called when the embedding application is brought to foreground.
*/
public static void onBroughtToForeground() {
sApplicationInForeground = true;
sBindingManager.onBroughtToForeground();
}
/**
* Returns whether the application is currently in the foreground.
*/
static boolean isApplicationInForeground() {
return sApplicationInForeground;
}
/**
* Should be called early in startup so the work needed to spawn the child process can be done
* in parallel to other startup work. Must not be called on the UI thread. Spare connection is
* created in sandboxed child process.
* @param context the application context used for the connection.
*/
public static void warmUp(Context context) {
synchronized (sSpareConnectionLock) {
assert !ThreadUtils.runningOnUiThread();
if (sSpareSandboxedConnection == null) {
ChildProcessCreationParams params = ChildProcessCreationParams.getDefault();
sSpareConnectionStarting = true;
ChildProcessConnection.StartCallback startCallback =
new ChildProcessConnection.StartCallback() {
@Override
public void onChildStarted() {
synchronized (sSpareConnectionLock) {
sSpareConnectionStarting = false;
sSpareConnectionLock.notify();
}
}
@Override
public void onChildStartFailed() {
Log.e(TAG, "Failed to warm up the spare sandbox service");
synchronized (sSpareConnectionLock) {
sSpareSandboxedConnection = null;
sSpareConnectionStarting = false;
sSpareConnectionLock.notify();
}
}
};
SpawnData spawnData = new SpawnData(true /* forWarmUp*/, context,
null /* commandLine */, -1 /* child process id */,
null /* filesToBeMapped */, 0 /* clientContext */,
CALLBACK_FOR_RENDERER_PROCESS, true /* inSandbox */, params);
sSpareSandboxedConnection = allocateBoundConnection(
spawnData, false /* alwaysInForeground */, startCallback);
}
}
}
@CalledByNative
private static FileDescriptorInfo makeFdInfo(
int id, int fd, boolean autoClose, long offset, long size) {
ParcelFileDescriptor pFd;
if (autoClose) {
// Adopt the FD, it will be closed when we close the ParcelFileDescriptor.
pFd = ParcelFileDescriptor.adoptFd(fd);
} else {
try {
pFd = ParcelFileDescriptor.fromFd(fd);
} catch (IOException e) {
Log.e(TAG, "Invalid FD provided for process connection, aborting connection.", e);
return null;
}
}
return new FileDescriptorInfo(id, pFd, offset, size);
}
/**
* Spawns and connects to a child process. May be called on any thread. It will not block, but
* will instead callback to {@link #nativeOnChildProcessStarted} when the connection is
* established. Note this callback will not necessarily be from the same thread (currently it
* always comes from the main thread).
*
* @param context Context used to obtain the application context.
* @param paramId Key used to retrieve ChildProcessCreationParams.
* @param commandLine The child process command line argv.
* @param filesToBeMapped File IDs, FDs, offsets, and lengths to pass through.
* @param clientContext Arbitrary parameter used by the client to distinguish this connection.
*/
// TODO(boliu): All tests should use this over startForTesting.
@VisibleForTesting
@CalledByNative
static void start(Context context, int paramId, final String[] commandLine, int childProcessId,
FileDescriptorInfo[] filesToBeMapped, long clientContext) {
int callbackType = CALLBACK_FOR_UNKNOWN_PROCESS;
boolean inSandbox = true;
String processType =
ContentSwitches.getSwitchValue(commandLine, ContentSwitches.SWITCH_PROCESS_TYPE);
ChildProcessCreationParams params = ChildProcessCreationParams.get(paramId);
if (paramId != ChildProcessCreationParams.DEFAULT_ID && params == null) {
throw new RuntimeException("CreationParams id " + paramId + " not found");
}
if (ContentSwitches.SWITCH_RENDERER_PROCESS.equals(processType)) {
callbackType = CALLBACK_FOR_RENDERER_PROCESS;
} else {
if (params != null && !params.getPackageName().equals(context.getPackageName())) {
// WebViews and WebAPKs have renderer processes running in their applications.
// When launching these renderer processes, {@link ChildProcessConnectionImpl}
// requires the package name of the application which holds the renderer process.
// Therefore, the package name in ChildProcessCreationParams could be the package
// name of WebViews, WebAPKs, or Chrome, depending on the host application.
// Except renderer process, all other child processes should use Chrome's package
// name. In WebAPK, ChildProcessCreationParams are initialized with WebAPK's
// package name. Make a copy of the WebAPK's params, but replace the package with
// Chrome's package to use when initializing a non-renderer processes.
// TODO(boliu): Should fold into |paramId|. Investigate why this is needed.
params = new ChildProcessCreationParams(context.getPackageName(),
params.getIsExternalService(), params.getLibraryProcessType());
}
if (ContentSwitches.SWITCH_GPU_PROCESS.equals(processType)) {
callbackType = CALLBACK_FOR_GPU_PROCESS;
inSandbox = false;
} else if (ContentSwitches.SWITCH_UTILITY_PROCESS.equals(processType)) {
// We only support sandboxed right now.
callbackType = CALLBACK_FOR_UTILITY_PROCESS;
} else {
assert false;
}
}
startInternal(context, commandLine, childProcessId, filesToBeMapped, clientContext,
callbackType, inSandbox, params);
}
private static ChildProcessConnection startInternal(
final Context context,
final String[] commandLine,
final int childProcessId,
final FileDescriptorInfo[] filesToBeMapped,
final long clientContext,
final int callbackType,
final boolean inSandbox,
final ChildProcessCreationParams creationParams) {
try {
TraceEvent.begin("ChildProcessLauncher.startInternal");
ChildProcessConnection allocatedConnection = null;
String packageName = creationParams != null ? creationParams.getPackageName()
: context.getPackageName();
synchronized (sSpareConnectionLock) {
if (inSandbox && sSpareSandboxedConnection != null
&& sSpareSandboxedConnection.getPackageName().equals(packageName)
// Object identity check for getDefault should be enough. The default is
// not supposed to change once set.
&& creationParams == ChildProcessCreationParams.getDefault()) {
while (sSpareConnectionStarting) {
try {
sSpareConnectionLock.wait();
} catch (InterruptedException ex) {
}
}
allocatedConnection = sSpareSandboxedConnection;
sSpareSandboxedConnection = null;
}
}
if (allocatedConnection == null) {
boolean alwaysInForeground = false;
if (callbackType == CALLBACK_FOR_GPU_PROCESS) alwaysInForeground = true;
ChildProcessConnection.StartCallback startCallback =
new ChildProcessConnection.StartCallback() {
@Override
public void onChildStarted() {}
@Override
public void onChildStartFailed() {
Log.e(TAG, "ChildProcessConnection.start failed, trying again");
AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
// The child process may already be bound to another client
// (this can happen if multi-process WebView is used in more
// than one process), so try starting the process again.
// This connection that failed to start has not been freed,
// so a new bound connection will be allocated.
startInternal(context, commandLine, childProcessId,
filesToBeMapped, clientContext, callbackType,
inSandbox, creationParams);
}
});
}
};
SpawnData spawnData = new SpawnData(false /* forWarmUp */, context, commandLine,
childProcessId, filesToBeMapped, clientContext, callbackType, inSandbox,
creationParams);
allocatedConnection =
allocateBoundConnection(spawnData, alwaysInForeground, startCallback);
if (allocatedConnection == null) {
return null;
}
}
Log.d(TAG, "Setting up connection to process: slot=%d",
allocatedConnection.getServiceNumber());
triggerConnectionSetup(allocatedConnection, commandLine, childProcessId,
filesToBeMapped, callbackType, clientContext);
return allocatedConnection;
} finally {
TraceEvent.end("ChildProcessLauncher.startInternal");
}
}
/**
* Create the common bundle to be passed to child processes.
* @param context Application context.
* @param commandLine Command line params to be passed to the service.
* @param linkerParams Linker params to start the service.
*/
protected static Bundle createsServiceBundle(
String[] commandLine, FileDescriptorInfo[] filesToBeMapped, Bundle sharedRelros) {
Bundle bundle = new Bundle();
bundle.putStringArray(ChildProcessConstants.EXTRA_COMMAND_LINE, commandLine);
bundle.putParcelableArray(ChildProcessConstants.EXTRA_FILES, filesToBeMapped);
bundle.putInt(ChildProcessConstants.EXTRA_CPU_COUNT, CpuFeatures.getCount());
bundle.putLong(ChildProcessConstants.EXTRA_CPU_FEATURES, CpuFeatures.getMask());
bundle.putBundle(Linker.EXTRA_LINKER_SHARED_RELROS, sharedRelros);
return bundle;
}
@VisibleForTesting
static void triggerConnectionSetup(
final ChildProcessConnection connection,
String[] commandLine,
int childProcessId,
FileDescriptorInfo[] filesToBeMapped,
final int callbackType,
final long clientContext) {
ChildProcessConnection.ConnectionCallback connectionCallback =
new ChildProcessConnection.ConnectionCallback() {
@Override
public void onConnected(int pid) {
Log.d(TAG, "on connect callback, pid=%d context=%d callbackType=%d",
pid, clientContext, callbackType);
if (pid != NULL_PROCESS_HANDLE) {
sBindingManager.addNewConnection(pid, connection);
sServiceMap.put(pid, connection);
}
// If the connection fails and pid == 0, the Java-side cleanup was already
// handled by DeathCallback. We still have to call back to native for
// cleanup there.
if (clientContext != 0) { // Will be 0 in Java instrumentation tests.
nativeOnChildProcessStarted(clientContext, pid);
}
}
};
assert callbackType != CALLBACK_FOR_UNKNOWN_PROCESS;
connection.setupConnection(commandLine,
filesToBeMapped,
createCallback(childProcessId, callbackType),
connectionCallback,
Linker.getInstance().getSharedRelros());
}
/**
* Terminates a child process. This may be called from any thread.
*
* @param pid The pid (process handle) of the service connection obtained from {@link #start}.
*/
@CalledByNative
static void stop(int pid) {
Log.d(TAG, "stopping child connection: pid=%d", pid);
ChildProcessConnection connection = sServiceMap.remove(pid);
if (connection == null) {
logPidWarning(pid, "Tried to stop non-existent connection");
return;
}
sBindingManager.clearConnection(pid);
connection.stop();
freeConnection(connection);
}
/**
* This implementation is used to receive callbacks from the remote service.
*/
private static IChildProcessCallback createCallback(
final int childProcessId, final int callbackType) {
return new IChildProcessCallback.Stub() {
@Override
public void forwardSurfaceForSurfaceRequest(
UnguessableToken requestToken, Surface surface) {
// Do not allow a malicious renderer to connect to a producer. This is only used
// from stream textures managed by the GPU process.
if (callbackType != CALLBACK_FOR_GPU_PROCESS) {
Log.e(TAG, "Illegal callback for non-GPU process.");
return;
}
nativeCompleteScopedSurfaceRequest(requestToken, surface);
}
@Override
public SurfaceWrapper getViewSurface(int surfaceId) {
// Do not allow a malicious renderer to get to our view surface.
if (callbackType != CALLBACK_FOR_GPU_PROCESS) {
Log.e(TAG, "Illegal callback for non-GPU process.");
return null;
}
Surface surface = ChildProcessLauncher.nativeGetViewSurface(surfaceId);
if (surface == null) {
return null;
}
return new SurfaceWrapper(surface);
}
};
}
static void logPidWarning(int pid, String message) {
// This class is effectively a no-op in single process mode, so don't log warnings there.
if (pid > 0 && !nativeIsSingleProcess()) {
Log.w(TAG, "%s, pid=%d", message, pid);
}
}
@VisibleForTesting
public static ChildProcessConnection startForTesting(Context context, String[] commandLine,
FileDescriptorInfo[] filesToMap, ChildProcessCreationParams params) {
return startInternal(context, commandLine, 0, filesToMap, 0,
CALLBACK_FOR_RENDERER_PROCESS, true, params);
}
@VisibleForTesting
static ChildProcessConnection allocateBoundConnectionForTesting(Context context,
ChildProcessCreationParams creationParams) {
return allocateBoundConnection(
new SpawnData(false /* forWarmUp */, context, null /* commandLine */,
0 /* childProcessId */, null /* filesToBeMapped */, 0 /* clientContext */,
CALLBACK_FOR_RENDERER_PROCESS, true /* inSandbox */, creationParams),
false /* alwaysInForeground */, null);
}
@VisibleForTesting
static ChildProcessConnection allocateConnectionForTesting(Context context,
ChildProcessCreationParams creationParams) {
Bundle commonParams = new Bundle();
commonParams.putParcelable(
ChildProcessConstants.EXTRA_LINKER_PARAMS, getLinkerParamsForNewConnection());
return allocateConnection(
new SpawnData(false /* forWarmUp */, context, null /* commandLine */,
0 /* childProcessId */, null /* filesToBeMapped */, 0 /* clientContext */,
CALLBACK_FOR_RENDERER_PROCESS, true /* inSandbox */, creationParams),
commonParams, false);
}
/**
* Queue up a spawn requests for testing.
*/
@VisibleForTesting
static void enqueuePendingSpawnForTesting(Context context, String[] commandLine,
ChildProcessCreationParams creationParams, boolean inSandbox) {
String packageName = creationParams != null ? creationParams.getPackageName()
: context.getPackageName();
ChildConnectionAllocator allocator =
getAllocatorForTesting(context, packageName, inSandbox);
allocator.enqueuePendingQueueForTesting(new SpawnData(false /* forWarmUp*/, context,
commandLine, 1, new FileDescriptorInfo[0], 0, CALLBACK_FOR_RENDERER_PROCESS, true,
creationParams));
}
/**
* @return the number of sandboxed connections of given {@link packageName} managed by the
* allocator.
*/
@VisibleForTesting
static int allocatedSandboxedConnectionsCountForTesting(Context context, String packageName) {
initConnectionAllocatorsIfNecessary(context, true, packageName);
return sSandboxedChildConnectionAllocatorMap.get(packageName)
.allocatedConnectionsCountForTesting();
}
/**
* @return gets the service connection array for a specific package name.
*/
@VisibleForTesting
static ChildProcessConnection[] getSandboxedConnectionArrayForTesting(String packageName) {
return sSandboxedChildConnectionAllocatorMap.get(packageName).connectionArrayForTesting();
}
/** @return the count of services set up and working */
@VisibleForTesting
static int connectedServicesCountForTesting() {
return sServiceMap.size();
}
/**
* @param context The context.
* @param packageName The package name of the {@link ChildProcessAlocator}.
* @param inSandbox Whether the connection is sandboxed.
* @return the count of pending spawns in the queue.
*/
@VisibleForTesting
static int pendingSpawnsCountForTesting(
Context context, String packageName, boolean inSandbox) {
ChildConnectionAllocator allocator =
getAllocatorForTesting(context, packageName, inSandbox);
return allocator.pendingSpawnsCountForTesting();
}
/**
* Kills the child process for testing.
* @return true iff the process was killed as expected
*/
@VisibleForTesting
public static boolean crashProcessForTesting(int pid) {
if (sServiceMap.get(pid) == null) return false;
try {
((ChildProcessConnectionImpl) sServiceMap.get(pid)).crashServiceForTesting();
} catch (RemoteException ex) {
return false;
}
return true;
}
private static native void nativeOnChildProcessStarted(long clientContext, int pid);
private static native void nativeCompleteScopedSurfaceRequest(
UnguessableToken requestToken, Surface surface);
private static native boolean nativeIsSingleProcess();
private static native Surface nativeGetViewSurface(int surfaceId);
}