blob: 8d1e2601d9039dbde110ac32c36c3495dc92fcec [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.content.browser;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import org.chromium.base.Log;
import org.chromium.base.ObserverList;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.process_launcher.ChildProcessCreationParams;
import java.util.ArrayList;
import java.util.Arrays;
/**
* This class is responsible for allocating and managing connections to child
* process services. These connections are in a pool (the services are defined
* in the AndroidManifest.xml).
*/
public class ChildConnectionAllocator {
private static final String TAG = "ChildConnAllocator";
/** Listener that clients can use to get notified when connections get allocated/freed. */
public interface Listener {
/** Called when a connection has been allocated, before it gets bound. */
void onConnectionAllocated(
ChildConnectionAllocator allocator, ChildProcessConnection connection);
/** Called when a connection has been freed. */
void onConnectionFreed(
ChildConnectionAllocator allocator, ChildProcessConnection connection);
}
/** Factory interface. Used by tests to specialize created connections. */
@VisibleForTesting
protected interface ConnectionFactory {
ChildProcessConnection createConnection(Context context, ComponentName serviceName,
boolean bindAsExternalService, Bundle serviceBundle,
ChildProcessCreationParams creationParams);
}
/** Default implementation of the ConnectionFactory that creates actual connections. */
private static class ConnectionFactoryImpl implements ConnectionFactory {
@Override
public ChildProcessConnection createConnection(Context context, ComponentName serviceName,
boolean bindAsExternalService, Bundle serviceBundle,
ChildProcessCreationParams creationParams) {
return new ChildProcessConnection(
context, serviceName, bindAsExternalService, serviceBundle, creationParams);
}
}
// Delay between the call to freeConnection and the connection actually beeing freed.
private static final long FREE_CONNECTION_DELAY_MILLIS = 1;
// Connections to services. Indices of the array correspond to the service numbers.
private final ChildProcessConnection[] mChildProcessConnections;
private final String mPackageName;
private final String mServiceClassName;
private final boolean mBindAsExternalService;
private final boolean mUseStrongBinding;
private final ChildProcessCreationParams mCreationParams;
// The list of free (not bound) service indices.
private final ArrayList<Integer> mFreeConnectionIndices;
private final ObserverList<Listener> mListeners = new ObserverList<>();
private ConnectionFactory mConnectionFactory = new ConnectionFactoryImpl();
/**
* Factory method that retrieves the service name and number of service from the
* AndroidManifest.xml.
*/
public static ChildConnectionAllocator create(Context context,
ChildProcessCreationParams creationParams, String packageName,
String serviceClassNameManifestKey, String numChildServicesManifestKey,
boolean bindAsExternalService, boolean useStrongBinding) {
String serviceClassName = null;
int numServices = -1;
PackageManager packageManager = context.getPackageManager();
try {
ApplicationInfo appInfo =
packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
if (appInfo.metaData != null) {
serviceClassName = appInfo.metaData.getString(serviceClassNameManifestKey);
numServices = appInfo.metaData.getInt(numChildServicesManifestKey, -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");
}
// Check that the service exists.
try {
// PackageManager#getServiceInfo() throws an exception if the service does not
// exist.
packageManager.getServiceInfo(
new ComponentName(packageName, serviceClassName + "0"), 0);
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException("Illegal meta data value: the child service doesn't exist");
}
return new ChildConnectionAllocator(creationParams, packageName, serviceClassName,
bindAsExternalService, useStrongBinding, numServices);
}
// TODO(jcivelli): remove this method once crbug.com/693484 has been addressed.
static int getNumberOfServices(
Context context, String packageName, String numChildServicesManifestKey) {
int numServices = -1;
try {
PackageManager packageManager = context.getPackageManager();
ApplicationInfo appInfo =
packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
if (appInfo.metaData != null) {
numServices = appInfo.metaData.getInt(numChildServicesManifestKey, -1);
}
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException("Could not get application info", e);
}
if (numServices < 0) {
throw new RuntimeException("Illegal meta data value for number of child services");
}
return numServices;
}
/**
* Factory method used with some tests to create an allocator with values passed in directly
* instead of being retrieved from the AndroidManifest.xml.
*/
@VisibleForTesting
public static ChildConnectionAllocator createForTest(ChildProcessCreationParams creationParams,
String packageName, String serviceClassName, int serviceCount,
boolean bindAsExternalService, boolean useStrongBinding) {
return new ChildConnectionAllocator(creationParams, packageName, serviceClassName,
bindAsExternalService, useStrongBinding, serviceCount);
}
private ChildConnectionAllocator(ChildProcessCreationParams creationParams, String packageName,
String serviceClassName, boolean bindAsExternalService, boolean useStrongBinding,
int numChildServices) {
mCreationParams = creationParams;
mPackageName = packageName;
mServiceClassName = serviceClassName;
mBindAsExternalService = bindAsExternalService;
mUseStrongBinding = useStrongBinding;
mChildProcessConnections = new ChildProcessConnection[numChildServices];
mFreeConnectionIndices = new ArrayList<Integer>(numChildServices);
for (int i = 0; i < numChildServices; i++) {
mFreeConnectionIndices.add(i);
}
}
/** @return a bound connection, or null if there are no free slots. */
public ChildProcessConnection allocate(Context context, Bundle serviceBundle,
final ChildProcessConnection.ServiceCallback serviceCallback) {
assert LauncherThread.runningOnLauncherThread();
if (mFreeConnectionIndices.isEmpty()) {
Log.d(TAG, "Ran out of services to allocate.");
return null;
}
int slot = mFreeConnectionIndices.remove(0);
assert mChildProcessConnections[slot] == null;
ComponentName serviceName = new ComponentName(mPackageName, mServiceClassName + slot);
// Wrap the service callbacks so that:
// - we can intercept onChildProcessDied and clean-up connections
// - the callbacks are actually posted so that this method will return before the callbacks
// are called (so that the caller may set any reference to the returned connection before
// any callback logic potentially tries to access that connection).
ChildProcessConnection.ServiceCallback serviceCallbackWrapper =
new ChildProcessConnection.ServiceCallback() {
@Override
public void onChildStarted() {
assert LauncherThread.runningOnLauncherThread();
if (serviceCallback != null) {
LauncherThread.post(new Runnable() {
@Override
public void run() {
serviceCallback.onChildStarted();
}
});
}
}
@Override
public void onChildStartFailed() {
assert LauncherThread.runningOnLauncherThread();
if (serviceCallback != null) {
LauncherThread.post(new Runnable() {
@Override
public void run() {
serviceCallback.onChildStartFailed();
}
});
}
}
@Override
public void onChildProcessDied(final ChildProcessConnection connection) {
assert LauncherThread.runningOnLauncherThread();
if (serviceCallback != null) {
LauncherThread.post(new Runnable() {
@Override
public void run() {
serviceCallback.onChildProcessDied(connection);
}
});
}
// 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).
LauncherThread.postDelayed(new Runnable() {
@Override
public void run() {
free(connection);
}
}, FREE_CONNECTION_DELAY_MILLIS);
}
};
ChildProcessConnection connection = mConnectionFactory.createConnection(
context, serviceName, mBindAsExternalService, serviceBundle, mCreationParams);
mChildProcessConnections[slot] = connection;
for (Listener listener : mListeners) {
listener.onConnectionAllocated(this, connection);
}
connection.start(mUseStrongBinding, serviceCallbackWrapper);
Log.d(TAG, "Allocator allocated and bound a connection, name: %s, slot: %d",
mServiceClassName, slot);
return connection;
}
/** Frees a connection and notifies listeners. */
private void free(ChildProcessConnection connection) {
assert LauncherThread.runningOnLauncherThread();
// mChildProcessConnections is relatively short (20 items at max at this point).
// We are better of iterating than caching in a map.
int slot = Arrays.asList(mChildProcessConnections).indexOf(connection);
if (slot == -1) {
Log.e(TAG, "Unable to find connection to free.");
assert false;
} else {
mChildProcessConnections[slot] = null;
assert !mFreeConnectionIndices.contains(slot);
mFreeConnectionIndices.add(slot);
Log.d(TAG, "Allocator freed a connection, name: %s, slot: %d", mServiceClassName, slot);
}
for (Listener listener : mListeners) {
listener.onConnectionFreed(this, connection);
}
}
public String getPackageName() {
return mPackageName;
}
public boolean anyConnectionAllocated() {
return mFreeConnectionIndices.size() < mChildProcessConnections.length;
}
public boolean isFreeConnectionAvailable() {
assert LauncherThread.runningOnLauncherThread();
return !mFreeConnectionIndices.isEmpty();
}
public int getNumberOfServices() {
return mChildProcessConnections.length;
}
public void addListener(Listener listener) {
assert !mListeners.hasObserver(listener);
mListeners.addObserver(listener);
}
public void removeListener(Listener listener) {
boolean removed = mListeners.removeObserver(listener);
assert removed;
}
@VisibleForTesting
void setConnectionFactoryForTesting(ConnectionFactory connectionFactory) {
mConnectionFactory = connectionFactory;
}
/** @return the count of connections managed by the allocator */
@VisibleForTesting
int allocatedConnectionsCountForTesting() {
assert LauncherThread.runningOnLauncherThread();
return mChildProcessConnections.length - mFreeConnectionIndices.size();
}
@VisibleForTesting
ChildProcessConnection getChildProcessConnectionAtSlotForTesting(int slotNumber) {
return mChildProcessConnections[slotNumber];
}
}