blob: 6fbc3ed6a12953831fd75b05c7bdacd6ba9cb186 [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.Context;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import org.chromium.base.CpuFeatures;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.library_loader.Linker;
import org.chromium.base.process_launcher.ChildProcessCreationParams;
import org.chromium.base.process_launcher.FileDescriptorInfo;
import org.chromium.content.app.ChromiumLinkerParams;
import org.chromium.content.common.ContentSwitches;
import java.util.Map;
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.
*/
public class ChildProcessLauncher {
private static final String TAG = "ChildProcLauncher";
/**
* Implemented by ChildProcessLauncherHelper.
*/
public interface LaunchCallback { void onChildProcessStarted(int pid); }
private static final boolean SPARE_CONNECTION_ALWAYS_IN_FOREGROUND = false;
private static ChildProcessConnection allocateConnection(
ChildSpawnData spawnData, Bundle childProcessCommonParams, boolean forWarmUp) {
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.getContext();
final boolean inSandbox = spawnData.isInSandbox();
String packageName =
creationParams != null ? creationParams.getPackageName() : context.getPackageName();
return ChildConnectionAllocator.getAllocator(context, packageName, inSandbox)
.allocate(spawnData, deathCallback, childProcessCommonParams, !forWarmUp);
}
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 Bundle createCommonParamsBundle(ChildProcessCreationParams params) {
Bundle commonParams = new Bundle();
commonParams.putParcelable(
ChildProcessConstants.EXTRA_LINKER_PARAMS, getLinkerParamsForNewConnection());
final boolean bindToCallerCheck = params == null ? false : params.getBindToCallerCheck();
commonParams.putBoolean(ChildProcessConstants.EXTRA_BIND_TO_CALLER, bindToCallerCheck);
return commonParams;
}
private static ChildProcessConnection allocateBoundConnection(ChildSpawnData spawnData,
ChildProcessConnection.StartCallback startCallback, boolean forWarmUp) {
final Context context = spawnData.getContext();
final boolean inSandbox = spawnData.isInSandbox();
final ChildProcessCreationParams creationParams = spawnData.getCreationParams();
ChildProcessConnection connection = allocateConnection(
spawnData, createCommonParamsBundle(spawnData.getCreationParams()), forWarmUp);
if (connection != null) {
connection.start(startCallback);
String packageName = creationParams != null ? creationParams.getPackageName()
: context.getPackageName();
if (inSandbox
&& !ChildConnectionAllocator.getAllocator(context, 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 ChildSpawnData pendingSpawn = freeConnectionAndDequeuePending(conn);
if (pendingSpawn != null) {
LauncherThread.post(new Runnable() {
@Override
public void run() {
startInternal(pendingSpawn.getContext(), pendingSpawn.getCommandLine(),
pendingSpawn.getChildProcessId(),
pendingSpawn.getFilesToBeMapped(),
pendingSpawn.getLaunchCallback(),
pendingSpawn.getChildProcessCallback(),
pendingSpawn.isInSandbox(), pendingSpawn.isAlwaysInForeground(),
pendingSpawn.getCreationParams());
}
});
}
}
}, FREE_CONNECTION_DELAY_MILLIS);
}
private static ChildSpawnData freeConnectionAndDequeuePending(ChildProcessConnection conn) {
// TODO(jcivelli): it should be safe to pass a null Context here as it is used to initialize
// the ChildConnectionAllocator object and if we are freeing a connection, we must have
// allocated one previously guaranteeing it is already initialized.
// When we consolidate ChildProcessLauncher and ChildProcessLauncherHelper, we'll have a
// context around that we can pass in there.
ChildConnectionAllocator allocator = ChildConnectionAllocator.getAllocator(
null /* context */, 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;
// TODO(boliu): This should be internal to content.
public static BindingManager getBindingManager() {
return sBindingManager;
}
@VisibleForTesting
public static void setBindingManagerForTesting(BindingManager manager) {
sBindingManager = manager;
}
/**
* 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) {
sBindingManager.startModerateBindingManagement(context,
ChildConnectionAllocator.getNumberOfServices(
context, true, context.getPackageName()));
}
/**
* 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. Spare connection is created in sandboxed child process.
* @param context the application context used for the connection.
*/
public static void warmUp(final Context context) {
assert ThreadUtils.runningOnUiThread();
LauncherThread.post(new Runnable() {
@Override
public void run() {
synchronized (sSpareConnectionLock) {
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();
}
}
};
ChildSpawnData spawnData = new ChildSpawnData(context,
null /* commandLine */, -1 /* child process id */,
null /* filesToBeMapped */, null /* launchCallback */,
null /* child process callback */, true /* inSandbox */,
SPARE_CONNECTION_ALWAYS_IN_FOREGROUND, params);
sSpareSandboxedConnection = allocateBoundConnection(
spawnData, startCallback, true /* forWarmUp */);
}
}
}
});
}
/**
* 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.
*/
// TODO(boliu): All tests should use this over startForTesting.
static void start(Context context, int paramId, final String[] commandLine, int childProcessId,
FileDescriptorInfo[] filesToBeMapped, LaunchCallback launchCallback) {
IBinder childProcessCallback = null;
boolean inSandbox = true;
boolean alwaysInForeground = false;
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)) {
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(),
params.getBindToCallerCheck());
}
if (ContentSwitches.SWITCH_GPU_PROCESS.equals(processType)) {
childProcessCallback = new GpuProcessCallback();
inSandbox = false;
alwaysInForeground = true;
} else {
// We only support sandboxed utility processes now.
assert ContentSwitches.SWITCH_UTILITY_PROCESS.equals(processType);
}
}
startInternal(context, commandLine, childProcessId, filesToBeMapped, launchCallback,
childProcessCallback, inSandbox, alwaysInForeground, params);
}
private static ChildProcessConnection startInternal(final Context context,
final String[] commandLine, final int childProcessId,
final FileDescriptorInfo[] filesToBeMapped, final LaunchCallback launchCallback,
final IBinder childProcessCallback, final boolean inSandbox,
final boolean alwaysInForeground, 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
&& SPARE_CONNECTION_ALWAYS_IN_FOREGROUND == alwaysInForeground
&& 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) {
ChildProcessConnection.StartCallback startCallback =
new ChildProcessConnection.StartCallback() {
@Override
public void onChildStarted() {}
@Override
public void onChildStartFailed() {
Log.e(TAG, "ChildProcessConnection.start failed, trying again");
LauncherThread.post(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, launchCallback,
childProcessCallback, inSandbox, alwaysInForeground,
creationParams);
}
});
}
};
ChildSpawnData spawnData = new ChildSpawnData(context, commandLine, childProcessId,
filesToBeMapped, launchCallback, childProcessCallback, inSandbox,
alwaysInForeground, creationParams);
allocatedConnection =
allocateBoundConnection(spawnData, startCallback, false /* forWarmUp */);
if (allocatedConnection == null) {
return null;
}
}
Log.d(TAG, "Setting up connection to process: slot=%d",
allocatedConnection.getServiceNumber());
triggerConnectionSetup(allocatedConnection, commandLine, childProcessId,
filesToBeMapped, childProcessCallback, launchCallback);
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 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, Linker.getInstance().getSharedRelros());
return bundle;
}
@VisibleForTesting
static void triggerConnectionSetup(final ChildProcessConnection connection,
String[] commandLine, int childProcessId, FileDescriptorInfo[] filesToBeMapped,
final IBinder childProcessCallback, final LaunchCallback launchCallback) {
ChildProcessConnection.ConnectionCallback connectionCallback =
new ChildProcessConnection.ConnectionCallback() {
@Override
public void onConnected(int pid) {
Log.d(TAG, "on connect callback, pid=%d", pid);
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 (launchCallback != null) { // Will be null in Java instrumentation tests.
launchCallback.onChildProcessStarted(pid);
}
}
};
connection.setupConnection(
commandLine, filesToBeMapped, childProcessCallback, connectionCallback);
}
/**
* 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}.
*/
static void stop(int pid) {
Log.d(TAG, "stopping child connection: pid=%d", pid);
ChildProcessConnection connection = sServiceMap.remove(pid);
if (connection == null) {
// Can happen for single process.
return;
}
sBindingManager.clearConnection(pid);
connection.stop();
freeConnection(connection);
}
@VisibleForTesting
public static ChildProcessConnection startForTesting(Context context, String[] commandLine,
FileDescriptorInfo[] filesToMap, ChildProcessCreationParams params) {
return startInternal(context, commandLine, 0 /* childProcessId */, filesToMap,
null /* launchCallback */, null /* childProcessCallback */, true /* inSandbox */,
false /* alwaysInForeground */, params);
}
@VisibleForTesting
static ChildProcessConnection allocateBoundConnectionForTesting(Context context,
ChildProcessCreationParams creationParams) {
return allocateBoundConnection(
new ChildSpawnData(context, null /* commandLine */, 0 /* childProcessId */,
null /* filesToBeMapped */, null /* LaunchCallback */,
null /* childProcessCallback */, true /* inSandbox */,
false /* alwaysInForeground */, creationParams),
null /* startCallback */, false /* forWarmUp */);
}
@VisibleForTesting
static ChildProcessConnection allocateConnectionForTesting(
Context context, ChildProcessCreationParams creationParams) {
return allocateConnection(
new ChildSpawnData(context, null /* commandLine */, 0 /* childProcessId */,
null /* filesToBeMapped */, null /* launchCallback */,
null /* childProcessCallback */, true /* inSandbox */,
false /* alwaysInForeground */, creationParams),
createCommonParamsBundle(creationParams), false /* forWarmUp */);
}
/**
* 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 =
ChildConnectionAllocator.getAllocator(context, packageName, inSandbox);
allocator.enqueuePendingQueueForTesting(new ChildSpawnData(context, commandLine,
1 /* childProcessId */, new FileDescriptorInfo[0], null /* launchCallback */,
null /* childProcessCallback */, true /* inSandbox */,
false /* alwaysInForeground */, creationParams));
}
/**
* @return the number of sandboxed connections of given {@link packageName} managed by the
* allocator.
*/
@VisibleForTesting
static int allocatedSandboxedConnectionsCountForTesting(Context context, String packageName) {
return ChildConnectionAllocator.getAllocator(context, packageName, true /*isSandboxed */)
.allocatedConnectionsCountForTesting();
}
/**
* @return gets the service connection array for a specific package name.
*/
@VisibleForTesting
static ChildProcessConnection[] getSandboxedConnectionArrayForTesting(
Context context, String packageName) {
return ChildConnectionAllocator.getAllocator(context, packageName, true /*isSandboxed */)
.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) {
return ChildConnectionAllocator.getAllocator(context, packageName, inSandbox)
.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;
}
}