| // 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.annotation.SuppressLint; |
| import android.content.Context; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.text.TextUtils; |
| |
| import org.chromium.base.Log; |
| import org.chromium.base.ThreadUtils; |
| import org.chromium.base.TraceEvent; |
| import org.chromium.base.VisibleForTesting; |
| import org.chromium.base.annotations.SuppressFBWarnings; |
| import org.chromium.base.process_launcher.ChildProcessCreationParams; |
| import org.chromium.content.app.SandboxedProcessService; |
| |
| import java.util.HashMap; |
| 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"; |
| |
| private static final String NUM_SANDBOXED_SERVICES_KEY = |
| "org.chromium.content.browser.NUM_SANDBOXED_SERVICES"; |
| private static final String SANDBOXED_SERVICES_NAME_KEY = |
| "org.chromium.content.browser.SANDBOXED_SERVICES_NAME"; |
| private static final String NUM_PRIVILEGED_SERVICES_KEY = |
| "org.chromium.content.browser.NUM_PRIVILEGED_SERVICES"; |
| private static final String PRIVILEGED_SERVICES_NAME_KEY = |
| "org.chromium.content.browser.PRIVILEGED_SERVICES_NAME"; |
| |
| /** |
| * Implemented by ChildProcessLauncherHelper. |
| */ |
| public interface LaunchCallback { |
| void onChildProcessStarted(ChildProcessConnection connection); |
| } |
| |
| private static final boolean SPARE_CONNECTION_ALWAYS_IN_FOREGROUND = false; |
| |
| // Map from package name to ChildConnectionAllocator. |
| private static final Map<String, ChildConnectionAllocator> |
| sSandboxedChildConnectionAllocatorMap = new HashMap<>(); |
| |
| // Map from a connection to its ChildConnectionAllocator. |
| private static final Map<ChildProcessConnection, ChildConnectionAllocator> |
| sConnectionsToAllocatorMap = new HashMap<>(); |
| |
| // Allocator used for non-sandboxed services. |
| private static ChildConnectionAllocator sPrivilegedChildConnectionAllocator; |
| |
| // Used by tests to override the default sandboxed service allocator settings. |
| private static ChildConnectionAllocator.ConnectionFactory sSandboxedServiceFactoryForTesting; |
| private static int sSandboxedServicesCountForTesting = -1; |
| private static String sSandboxedServicesNameForTesting; |
| |
| @SuppressFBWarnings("LI_LAZY_INIT_STATIC") // Method is single thread. |
| public static ChildConnectionAllocator getConnectionAllocator( |
| Context context, String packageName, boolean sandboxed) { |
| assert LauncherThread.runningOnLauncherThread(); |
| if (!sandboxed) { |
| if (sPrivilegedChildConnectionAllocator == null) { |
| sPrivilegedChildConnectionAllocator = ChildConnectionAllocator.create(context, |
| packageName, PRIVILEGED_SERVICES_NAME_KEY, NUM_PRIVILEGED_SERVICES_KEY); |
| } |
| return sPrivilegedChildConnectionAllocator; |
| } |
| |
| if (!sSandboxedChildConnectionAllocatorMap.containsKey(packageName)) { |
| Log.w(TAG, |
| "Create a new ChildConnectionAllocator with package name = %s," |
| + " inSandbox = true", |
| packageName); |
| ChildConnectionAllocator connectionAllocator = null; |
| if (sSandboxedServicesCountForTesting != -1) { |
| // Testing case where allocator settings are overriden. |
| String serviceName = !TextUtils.isEmpty(sSandboxedServicesNameForTesting) |
| ? sSandboxedServicesNameForTesting |
| : SandboxedProcessService.class.getName(); |
| connectionAllocator = ChildConnectionAllocator.createForTest( |
| packageName, serviceName, sSandboxedServicesCountForTesting); |
| } else { |
| connectionAllocator = ChildConnectionAllocator.create(context, packageName, |
| SANDBOXED_SERVICES_NAME_KEY, NUM_SANDBOXED_SERVICES_KEY); |
| } |
| if (sSandboxedServiceFactoryForTesting != null) { |
| connectionAllocator.setConnectionFactoryForTesting( |
| sSandboxedServiceFactoryForTesting); |
| } |
| sSandboxedChildConnectionAllocatorMap.put(packageName, connectionAllocator); |
| } |
| return sSandboxedChildConnectionAllocatorMap.get(packageName); |
| // TODO(pkotwicz|hanxi): Figure out when old allocators should be removed from |
| // {@code sSandboxedChildConnectionAllocatorMap}. |
| } |
| |
| @VisibleForTesting |
| static ChildProcessConnection allocateConnection(ChildSpawnData spawnData, boolean forWarmUp) { |
| assert LauncherThread.runningOnLauncherThread(); |
| ChildProcessConnection.DeathCallback deathCallback = |
| new ChildProcessConnection.DeathCallback() { |
| @Override |
| public void onChildProcessDied(ChildProcessConnection connection) { |
| assert LauncherThread.runningOnLauncherThread(); |
| 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(); |
| ChildConnectionAllocator allocator = |
| getConnectionAllocator(context, packageName, inSandbox); |
| ChildProcessConnection connection = |
| allocator.allocate(spawnData, deathCallback, !forWarmUp); |
| sConnectionsToAllocatorMap.put(connection, allocator); |
| return connection; |
| } |
| |
| @VisibleForTesting |
| static ChildProcessConnection allocateBoundConnection(ChildSpawnData spawnData, |
| ChildProcessConnection.StartCallback startCallback, boolean forWarmUp) { |
| assert LauncherThread.runningOnLauncherThread(); |
| final Context context = spawnData.getContext(); |
| final boolean inSandbox = spawnData.isInSandbox(); |
| final ChildProcessCreationParams creationParams = spawnData.getCreationParams(); |
| |
| ChildProcessConnection connection = allocateConnection(spawnData, forWarmUp); |
| if (connection != null) { |
| // Non sandboxed processes are privileged processes that should be strongly bound. |
| boolean useStrongBinding = !inSandbox; |
| connection.start(useStrongBinding, startCallback); |
| |
| String packageName = creationParams != null ? creationParams.getPackageName() |
| : context.getPackageName(); |
| if (inSandbox |
| && !getConnectionAllocator(context, packageName, true /* sandboxed */) |
| .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. |
| getBindingManager().releaseAllModerateBindings(); |
| } |
| } |
| return connection; |
| } |
| |
| private static final long FREE_CONNECTION_DELAY_MILLIS = 1; |
| |
| private static void freeConnection(ChildProcessConnection connection) { |
| assert LauncherThread.runningOnLauncherThread(); |
| if (connection == sSpareSandboxedConnection) clearSpareConnection(); |
| |
| // 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; |
| LauncherThread.postDelayed(new Runnable() { |
| @Override |
| public void run() { |
| ChildConnectionAllocator allocator = sConnectionsToAllocatorMap.remove(conn); |
| assert allocator != null; |
| final ChildSpawnData pendingSpawn = allocator.free(conn); |
| if (pendingSpawn != null) { |
| LauncherThread.post(new Runnable() { |
| @Override |
| public void run() { |
| start(pendingSpawn.getContext(), pendingSpawn.getServiceBundle(), |
| pendingSpawn.getConnectionBundle(), |
| pendingSpawn.getLaunchCallback(), |
| pendingSpawn.getChildProcessCallback(), |
| pendingSpawn.isInSandbox(), pendingSpawn.isAlwaysInForeground(), |
| pendingSpawn.getCreationParams()); |
| } |
| }); |
| } |
| } |
| }, FREE_CONNECTION_DELAY_MILLIS); |
| } |
| |
| // Map from pid to ChildService connection. |
| private static Map<Integer, ChildProcessConnection> sServiceMap = new ConcurrentHashMap<>(); |
| |
| // These variables are used for the warm up sandboxed connection. |
| // |sSpareSandboxedConnection| is non-null when there is a pending connection. Note it's cleared |
| // to null again after the connection is used for a real child process. |
| // |sSpareConnectionStarting| is true if ChildProcessConnection.StartCallback has not fired. |
| // This is used for a child process allocation to determine if StartCallback should be chained. |
| // |sSpareConnectionStartCallback| is the chained StartCallback. This is also used to determine |
| // if there is already a child process launch that's used this this connection. |
| @SuppressLint("StaticFieldLeak") |
| private static ChildProcessConnection sSpareSandboxedConnection; |
| private static boolean sSpareConnectionStarting; |
| private static ChildProcessConnection.StartCallback sSpareConnectionStartCallback; |
| |
| // Manages oom bindings used to bind chind services. Lazily initialized by getBindingManager() |
| private static BindingManager sBindingManager; |
| |
| // Whether the main application is currently brought to the foreground. |
| private static boolean sApplicationInForeground = true; |
| |
| // Lazy initialize sBindingManager |
| // TODO(boliu): This should be internal to content. |
| @SuppressFBWarnings("LI_LAZY_INIT_STATIC") // Method is single thread. |
| public static BindingManager getBindingManager() { |
| assert LauncherThread.runningOnLauncherThread(); |
| if (sBindingManager == null) { |
| sBindingManager = BindingManagerImpl.createBindingManager(); |
| } |
| return sBindingManager; |
| } |
| |
| @VisibleForTesting |
| public static void setBindingManagerForTesting(BindingManager manager) { |
| sBindingManager = manager; |
| } |
| |
| /** |
| * Called when the embedding application is sent to background. |
| */ |
| public static void onSentToBackground() { |
| assert ThreadUtils.runningOnUiThread(); |
| sApplicationInForeground = false; |
| LauncherThread.post(new Runnable() { |
| @Override |
| public void run() { |
| getBindingManager().onSentToBackground(); |
| } |
| }); |
| } |
| |
| /** |
| * Called when the embedding application is brought to foreground. |
| */ |
| public static void onBroughtToForeground() { |
| assert ThreadUtils.runningOnUiThread(); |
| sApplicationInForeground = true; |
| LauncherThread.post(new Runnable() { |
| @Override |
| public void run() { |
| getBindingManager().onBroughtToForeground(); |
| } |
| }); |
| } |
| |
| /** |
| * Returns whether the application is currently in the foreground. |
| */ |
| static boolean isApplicationInForeground() { |
| return sApplicationInForeground; |
| } |
| |
| /** |
| * 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(final Context context) { |
| assert ThreadUtils.runningOnUiThread(); |
| LauncherThread.post(new Runnable() { |
| @Override |
| public void run() { |
| ChildConnectionAllocator allocator = getConnectionAllocator( |
| context, context.getPackageName(), true /* sandboxed */); |
| getBindingManager().startModerateBindingManagement( |
| context, allocator.getNumberOfServices()); |
| } |
| }); |
| } |
| |
| /** |
| * 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() { |
| if (sSpareSandboxedConnection != null) return; |
| ChildProcessCreationParams params = ChildProcessCreationParams.getDefault(); |
| |
| ChildProcessConnection.StartCallback startCallback = |
| new ChildProcessConnection.StartCallback() { |
| @Override |
| public void onChildStarted() { |
| assert LauncherThread.runningOnLauncherThread(); |
| sSpareConnectionStarting = false; |
| if (sSpareConnectionStartCallback != null) { |
| sSpareConnectionStartCallback.onChildStarted(); |
| clearSpareConnection(); |
| } |
| // If there is no chained callback, that means nothing has tried to |
| // use the spare connection yet. It will be cleared when it is used |
| // for an actual child process launch. |
| } |
| |
| @Override |
| public void onChildStartFailed() { |
| assert LauncherThread.runningOnLauncherThread(); |
| Log.e(TAG, "Failed to warm up the spare sandbox service"); |
| if (sSpareConnectionStartCallback != null) { |
| sSpareConnectionStartCallback.onChildStartFailed(); |
| } |
| clearSpareConnection(); |
| } |
| }; |
| boolean bindToCallerCheck = params == null ? false : params.getBindToCallerCheck(); |
| ChildSpawnData spawnData = new ChildSpawnData(context, |
| ChildProcessLauncherHelper.createServiceBundle(bindToCallerCheck), |
| null /* connectionBundle */, null /* launchCallback */, |
| null /* child process callback */, true /* inSandbox */, |
| SPARE_CONNECTION_ALWAYS_IN_FOREGROUND, params); |
| sSpareSandboxedConnection = |
| allocateBoundConnection(spawnData, startCallback, true /* forWarmUp */); |
| sSpareConnectionStarting = sSpareSandboxedConnection != null; |
| } |
| }); |
| } |
| |
| private static void clearSpareConnection() { |
| assert LauncherThread.runningOnLauncherThread(); |
| sSpareSandboxedConnection = null; |
| sSpareConnectionStarting = false; |
| sSpareConnectionStartCallback = null; |
| } |
| |
| /** |
| * Spawns and connects to a child process. It will not block, but will instead callback to |
| * {@link #LaunchCallback} on the launcher thread when the connection is established on. |
| * |
| * @param context Context used to obtain the application context. |
| * @param paramId Key used to retrieve ChildProcessCreationParams. |
| * @param serviceBundle The Bundle passed in the intent used to bind to the service. |
| * @param connectionBundle The Bundle passed in setupConnection call. |
| * @param launchCallback Callback invoked when the connection is established. |
| * @param childProcessCallback IBinder callback passed to the service. |
| */ |
| @VisibleForTesting |
| public static boolean start(final Context context, final Bundle serviceBundle, |
| final Bundle connectionBundle, final LaunchCallback launchCallback, |
| final IBinder childProcessCallback, final boolean inSandbox, |
| final boolean alwaysInForeground, final ChildProcessCreationParams creationParams) { |
| assert LauncherThread.runningOnLauncherThread(); |
| try { |
| TraceEvent.begin("ChildProcessLauncher.start"); |
| |
| ChildProcessConnection allocatedConnection = null; |
| String packageName = creationParams != null ? creationParams.getPackageName() |
| : context.getPackageName(); |
| ChildProcessConnection.StartCallback startCallback = |
| new ChildProcessConnection.StartCallback() { |
| @Override |
| public void onChildStarted() {} |
| |
| @Override |
| public void onChildStartFailed() { |
| assert LauncherThread.runningOnLauncherThread(); |
| 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. |
| start(context, serviceBundle, connectionBundle, launchCallback, |
| childProcessCallback, inSandbox, alwaysInForeground, |
| creationParams); |
| } |
| }); |
| } |
| }; |
| |
| if (inSandbox && sSpareSandboxedConnection != null |
| && sSpareConnectionStartCallback == 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()) { |
| allocatedConnection = sSpareSandboxedConnection; |
| if (sSpareConnectionStarting) { |
| sSpareConnectionStartCallback = startCallback; |
| } else { |
| clearSpareConnection(); |
| } |
| } |
| if (allocatedConnection == null) { |
| ChildSpawnData spawnData = new ChildSpawnData(context, serviceBundle, |
| connectionBundle, launchCallback, childProcessCallback, inSandbox, |
| alwaysInForeground, creationParams); |
| allocatedConnection = |
| allocateBoundConnection(spawnData, startCallback, false /* forWarmUp */); |
| if (allocatedConnection == null) { |
| return false; |
| } |
| } |
| boolean addToBindingmanager = inSandbox; |
| triggerConnectionSetup(allocatedConnection, connectionBundle, childProcessCallback, |
| launchCallback, addToBindingmanager); |
| return true; |
| } finally { |
| TraceEvent.end("ChildProcessLauncher.start"); |
| } |
| } |
| |
| @VisibleForTesting |
| static void triggerConnectionSetup(final ChildProcessConnection connection, |
| Bundle connectionBundle, final IBinder childProcessCallback, |
| final LaunchCallback launchCallback, final boolean addToBindingmanager) { |
| assert LauncherThread.runningOnLauncherThread(); |
| Log.d(TAG, "Setting up connection to process, connection name=%s", |
| connection.getServiceName()); |
| ChildProcessConnection.ConnectionCallback connectionCallback = |
| new ChildProcessConnection.ConnectionCallback() { |
| @Override |
| public void onConnected(ChildProcessConnection connection) { |
| assert LauncherThread.runningOnLauncherThread(); |
| if (connection != null) { |
| int pid = connection.getPid(); |
| Log.d(TAG, "on connect callback, pid=%d", pid); |
| if (addToBindingmanager) { |
| getBindingManager().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(connection); |
| } |
| } |
| }; |
| |
| connection.setupConnection(connectionBundle, 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) { |
| assert LauncherThread.runningOnLauncherThread(); |
| Log.d(TAG, "stopping child connection: pid=%d", pid); |
| ChildProcessConnection connection = sServiceMap.remove(pid); |
| if (connection == null) { |
| // Can happen for single process. |
| return; |
| } |
| getBindingManager().removeConnection(pid); |
| connection.stop(); |
| freeConnection(connection); |
| } |
| |
| public static int getNumberOfSandboxedServices(Context context, String packageName) { |
| assert ThreadUtils.runningOnUiThread(); |
| if (sSandboxedServicesCountForTesting != -1) { |
| return sSandboxedServicesCountForTesting; |
| } |
| return ChildConnectionAllocator.getNumberOfServices( |
| context, packageName, NUM_SANDBOXED_SERVICES_KEY); |
| } |
| |
| /** @return the count of services set up and working */ |
| @VisibleForTesting |
| static int connectedServicesCountForTesting() { |
| return sServiceMap.size(); |
| } |
| |
| @VisibleForTesting |
| public static void setSandboxServicesSettingsForTesting( |
| ChildConnectionAllocator.ConnectionFactory factory, int serviceCount, |
| String serviceName) { |
| sSandboxedServiceFactoryForTesting = factory; |
| sSandboxedServicesCountForTesting = serviceCount; |
| sSandboxedServicesNameForTesting = serviceName; |
| } |
| |
| /** |
| * 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 { |
| sServiceMap.get(pid).crashServiceForTesting(); |
| } catch (RemoteException ex) { |
| return false; |
| } |
| |
| return true; |
| } |
| } |