blob: f63408c9cc6d2351dc20842c43ec22da73f7a280 [file] [log] [blame]
// Copyright 2013 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.base.process_launcher;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import org.chromium.base.ChildBindingState;
import org.chromium.base.Log;
import org.chromium.base.MemoryPressureLevel;
import org.chromium.base.MemoryPressureListener;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.memory.MemoryPressureCallback;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
/**
* Manages a connection between the browser activity and a child service.
*/
public class ChildProcessConnection {
private static final String TAG = "ChildProcessConn";
private static final int NUM_BINDING_STATES = ChildBindingState.MAX_VALUE + 1;
/**
* Used to notify the consumer about the process start. These callbacks will be invoked before
* the ConnectionCallbacks.
*/
public interface ServiceCallback {
/**
* Called when the child process has successfully started and is ready for connection
* setup.
*/
void onChildStarted();
/**
* Called when the child process failed to start. This can happen if the process is already
* in use by another client. The client will not receive any other callbacks after this one.
*/
void onChildStartFailed(ChildProcessConnection connection);
/**
* Called when the service has been disconnected. whether it was stopped by the client or
* if it stopped unexpectedly (process crash).
* This is the last callback from this interface that a client will receive for a specific
* connection.
*/
void onChildProcessDied(ChildProcessConnection connection);
}
/**
* Used to notify the consumer about the connection being established.
*/
public interface ConnectionCallback {
/**
* Called when the connection to the service is established.
* @param connection the connection object to the child process
*/
void onConnected(ChildProcessConnection connection);
}
/**
* Delegate that ChildServiceConnection should call when the service connects/disconnects.
* These callbacks are expected to happen on a background thread.
*/
@VisibleForTesting
protected interface ChildServiceConnectionDelegate {
void onServiceConnected(IBinder service);
void onServiceDisconnected();
}
@VisibleForTesting
protected interface ChildServiceConnectionFactory {
ChildServiceConnection createConnection(
Intent bindIntent, int bindFlags, ChildServiceConnectionDelegate delegate);
}
/** Interface representing a connection to the Android service. Can be mocked in unit-tests. */
@VisibleForTesting
protected interface ChildServiceConnection {
boolean bind();
void unbind();
boolean isBound();
}
/** Implementation of ChildServiceConnection that does connect to a service. */
private static class ChildServiceConnectionImpl
implements ChildServiceConnection, ServiceConnection {
private final Context mContext;
private final Intent mBindIntent;
private final int mBindFlags;
private final ChildServiceConnectionDelegate mDelegate;
private boolean mBound;
private ChildServiceConnectionImpl(Context context, Intent bindIntent, int bindFlags,
ChildServiceConnectionDelegate delegate) {
mContext = context;
mBindIntent = bindIntent;
mBindFlags = bindFlags;
mDelegate = delegate;
}
@Override
public boolean bind() {
if (!mBound) {
try {
TraceEvent.begin("ChildProcessConnection.ChildServiceConnectionImpl.bind");
mBound = mContext.bindService(mBindIntent, this, mBindFlags);
} finally {
TraceEvent.end("ChildProcessConnection.ChildServiceConnectionImpl.bind");
}
}
return mBound;
}
@Override
public void unbind() {
if (mBound) {
mContext.unbindService(this);
mBound = false;
}
}
@Override
public boolean isBound() {
return mBound;
}
@Override
public void onServiceConnected(ComponentName className, final IBinder service) {
mDelegate.onServiceConnected(service);
}
// Called on the main thread to notify that the child service did not disconnect gracefully.
@Override
public void onServiceDisconnected(ComponentName className) {
mDelegate.onServiceDisconnected();
}
}
// Global lock to protect all the fields that can be accessed outside launcher thread.
private static final Object sBindingStateLock = new Object();
@GuardedBy("sBindingStateLock")
private static final int[] sAllBindingStateCounts = new int[NUM_BINDING_STATES];
@VisibleForTesting
static void resetBindingStateCountsForTesting() {
synchronized (sBindingStateLock) {
for (int i = 0; i < NUM_BINDING_STATES; ++i) {
sAllBindingStateCounts[i] = 0;
}
}
}
private final Handler mLauncherHandler;
private final ComponentName mServiceName;
// Parameters passed to the child process through the service binding intent.
// If the service gets recreated by the framework the intent will be reused, so these parameters
// should be common to all processes of that type.
private final Bundle mServiceBundle;
// Whether bindToCaller should be called on the service after setup to check that only one
// process is bound to the service.
private final boolean mBindToCaller;
private static class ConnectionParams {
final Bundle mConnectionBundle;
final List<IBinder> mClientInterfaces;
ConnectionParams(Bundle connectionBundle, List<IBinder> clientInterfaces) {
mConnectionBundle = connectionBundle;
mClientInterfaces = clientInterfaces;
}
}
// This is set in start() and is used in onServiceConnected().
private ServiceCallback mServiceCallback;
// This is set in setupConnection() and is later used in doConnectionSetup(), after which the
// variable is cleared. Therefore this is only valid while the connection is being set up.
private ConnectionParams mConnectionParams;
// Callback provided in setupConnection() that will communicate the result to the caller. This
// has to be called exactly once after setupConnection(), even if setup fails, so that the
// caller can free up resources associated with the setup attempt. This is set to null after the
// call.
private ConnectionCallback mConnectionCallback;
private IChildProcessService mService;
// Set to true when the service connection callback runs. This differs from
// mServiceConnectComplete, which tracks that the connection completed successfully.
private boolean mDidOnServiceConnected;
// Set to true when the service connected successfully.
private boolean mServiceConnectComplete;
// Set to true when the service disconnects, as opposed to being properly closed. This happens
// when the process crashes or gets killed by the system out-of-memory killer.
private boolean mServiceDisconnected;
// Process ID of the corresponding child process.
private int mPid;
// Strong binding will make the service priority equal to the priority of the activity.
private final ChildServiceConnection mStrongBinding;
// Moderate binding will make the service priority equal to the priority of a visible process
// while the app is in the foreground.
// This is also used as the initial binding before any priorities are set.
private final ChildServiceConnection mModerateBinding;
// Low priority binding maintained in the entire lifetime of the connection, i.e. between calls
// to start() and stop().
private final ChildServiceConnection mWaivedBinding;
// Refcount of bindings.
private int mStrongBindingCount;
private int mModerateBindingCount;
// Set to true once unbind() was called.
private boolean mUnbound;
// Binding state of this connection.
@GuardedBy("sBindingStateLock")
private @ChildBindingState int mBindingState;
// Same as above except it no longer updates after |unbind()|.
@GuardedBy("sBindingStateLock")
private @ChildBindingState int mBindingStateCurrentOrWhenDied;
// Indicate |kill()| was called to intentionally kill this process.
@GuardedBy("sBindingStateLock")
private boolean mKilledByUs;
// Copy of |sAllBindingStateCounts| at the time this is unbound.
@GuardedBy("sBindingStateLock")
private int[] mAllBindingStateCountsWhenDied;
private MemoryPressureCallback mMemoryPressureCallback;
public ChildProcessConnection(Context context, ComponentName serviceName, boolean bindToCaller,
boolean bindAsExternalService, Bundle serviceBundle) {
this(context, serviceName, bindToCaller, bindAsExternalService, serviceBundle,
null /* connectionFactory */);
}
@VisibleForTesting
public ChildProcessConnection(final Context context, ComponentName serviceName,
boolean bindToCaller, boolean bindAsExternalService, Bundle serviceBundle,
ChildServiceConnectionFactory connectionFactory) {
mLauncherHandler = new Handler();
assert isRunningOnLauncherThread();
mServiceName = serviceName;
mServiceBundle = serviceBundle != null ? serviceBundle : new Bundle();
mServiceBundle.putBoolean(ChildProcessConstants.EXTRA_BIND_TO_CALLER, bindToCaller);
mBindToCaller = bindToCaller;
if (connectionFactory == null) {
connectionFactory = new ChildServiceConnectionFactory() {
@Override
public ChildServiceConnection createConnection(
Intent bindIntent, int bindFlags, ChildServiceConnectionDelegate delegate) {
return new ChildServiceConnectionImpl(context, bindIntent, bindFlags, delegate);
}
};
}
ChildServiceConnectionDelegate delegate = new ChildServiceConnectionDelegate() {
@Override
public void onServiceConnected(final IBinder service) {
mLauncherHandler.post(new Runnable() {
@Override
public void run() {
onServiceConnectedOnLauncherThread(service);
}
});
}
@Override
public void onServiceDisconnected() {
mLauncherHandler.post(new Runnable() {
@Override
public void run() {
onServiceDisconnectedOnLauncherThread();
}
});
}
};
Intent intent = new Intent();
intent.setComponent(serviceName);
if (serviceBundle != null) {
intent.putExtras(serviceBundle);
}
int defaultFlags = Context.BIND_AUTO_CREATE
| (bindAsExternalService ? Context.BIND_EXTERNAL_SERVICE : 0);
mModerateBinding = connectionFactory.createConnection(intent, defaultFlags, delegate);
mStrongBinding = connectionFactory.createConnection(
intent, defaultFlags | Context.BIND_IMPORTANT, delegate);
mWaivedBinding = connectionFactory.createConnection(
intent, defaultFlags | Context.BIND_WAIVE_PRIORITY, delegate);
}
public final IChildProcessService getService() {
assert isRunningOnLauncherThread();
return mService;
}
public final ComponentName getServiceName() {
assert isRunningOnLauncherThread();
return mServiceName;
}
public boolean isConnected() {
return mService != null;
}
/**
* @return the connection pid, or 0 if not yet connected
*/
public int getPid() {
assert isRunningOnLauncherThread();
return mPid;
}
/**
* Starts a connection to an IChildProcessService. This must be followed by a call to
* setupConnection() to setup the connection parameters. start() and setupConnection() are
* separate to allow to pass whatever parameters are available in start(), and complete the
* remainder addStrongBinding while reducing the connection setup latency.
* @param useStrongBinding whether a strong binding should be bound by default. If false, an
* initial moderate binding is used.
* @param serviceCallback (optional) callbacks invoked when the child process starts or fails to
* start and when the service stops.
*/
public void start(boolean useStrongBinding, ServiceCallback serviceCallback) {
try {
TraceEvent.begin("ChildProcessConnection.start");
assert isRunningOnLauncherThread();
assert mConnectionParams
== null : "setupConnection() called before start() in ChildProcessConnection.";
mServiceCallback = serviceCallback;
if (!bind(useStrongBinding)) {
Log.e(TAG, "Failed to establish the service connection.");
// We have to notify the caller so that they can free-up associated resources.
// TODO(ppi): Can we hard-fail here?
notifyChildProcessDied();
}
} finally {
TraceEvent.end("ChildProcessConnection.start");
}
}
/**
* Sets-up the connection after it was started with start().
* @param connectionBundle a bundle passed to the service that can be used to pass various
* parameters to the service
* @param clientInterfaces optional client specified interfaces that the child can use to
* communicate with the parent process
* @param connectionCallback will be called exactly once after the connection is set up or the
* setup fails
*/
public void setupConnection(Bundle connectionBundle, @Nullable List<IBinder> clientInterfaces,
ConnectionCallback connectionCallback) {
assert isRunningOnLauncherThread();
assert mConnectionParams == null;
if (mServiceDisconnected) {
Log.w(TAG, "Tried to setup a connection that already disconnected.");
connectionCallback.onConnected(null);
return;
}
try {
TraceEvent.begin("ChildProcessConnection.setupConnection");
mConnectionCallback = connectionCallback;
mConnectionParams = new ConnectionParams(connectionBundle, clientInterfaces);
// Run the setup if the service is already connected. If not, doConnectionSetup() will
// be called from onServiceConnected().
if (mServiceConnectComplete) {
doConnectionSetup();
}
} finally {
TraceEvent.end("ChildProcessConnection.setupConnection");
}
}
/**
* Terminates the connection to IChildProcessService, closing all bindings. It is safe to call
* this multiple times.
*/
public void stop() {
assert isRunningOnLauncherThread();
unbind();
notifyChildProcessDied();
}
public void kill() {
assert isRunningOnLauncherThread();
IChildProcessService service = mService;
unbind();
try {
if (service != null) service.forceKill();
} catch (RemoteException e) {
// Intentionally ignore since we are killing it anyway.
}
synchronized (sBindingStateLock) {
mKilledByUs = true;
}
notifyChildProcessDied();
}
@VisibleForTesting
protected void onServiceConnectedOnLauncherThread(IBinder service) {
assert isRunningOnLauncherThread();
// A flag from the parent class ensures we run the post-connection logic only once
// (instead of once per each ChildServiceConnection).
if (mDidOnServiceConnected) {
return;
}
try {
TraceEvent.begin("ChildProcessConnection.ChildServiceConnection.onServiceConnected");
mDidOnServiceConnected = true;
mService = IChildProcessService.Stub.asInterface(service);
if (mBindToCaller) {
try {
if (!mService.bindToCaller()) {
if (mServiceCallback != null) {
mServiceCallback.onChildStartFailed(this);
}
unbind();
return;
}
} catch (RemoteException ex) {
// Do not trigger the StartCallback here, since the service is already
// dead and the onChildStopped callback will run from onServiceDisconnected().
Log.e(TAG, "Failed to bind service to connection.", ex);
return;
}
}
if (mServiceCallback != null) {
mServiceCallback.onChildStarted();
}
mServiceConnectComplete = true;
if (mMemoryPressureCallback == null) {
final MemoryPressureCallback callback = this ::onMemoryPressure;
ThreadUtils.postOnUiThread(() -> MemoryPressureListener.addCallback(callback));
mMemoryPressureCallback = callback;
}
// Run the setup if the connection parameters have already been provided. If
// not, doConnectionSetup() will be called from setupConnection().
if (mConnectionParams != null) {
doConnectionSetup();
}
} finally {
TraceEvent.end("ChildProcessConnection.ChildServiceConnection.onServiceConnected");
}
}
@VisibleForTesting
protected void onServiceDisconnectedOnLauncherThread() {
assert isRunningOnLauncherThread();
// Ensure that the disconnection logic runs only once (instead of once per each
// ChildServiceConnection).
if (mServiceDisconnected) {
return;
}
mServiceDisconnected = true;
Log.w(TAG, "onServiceDisconnected (crash or killed by oom): pid=%d", mPid);
stop(); // We don't want to auto-restart on crash. Let the browser do that.
// If we have a pending connection callback, we need to communicate the failure to
// the caller.
if (mConnectionCallback != null) {
mConnectionCallback.onConnected(null);
mConnectionCallback = null;
}
}
private void onSetupConnectionResult(int pid) {
mPid = pid;
assert mPid != 0 : "Child service claims to be run by a process of pid=0.";
if (mConnectionCallback != null) {
mConnectionCallback.onConnected(this);
}
mConnectionCallback = null;
}
/**
* Called after the connection parameters have been set (in setupConnection()) *and* a
* connection has been established (as signaled by onServiceConnected()). These two events can
* happen in any order.
*/
private void doConnectionSetup() {
try {
TraceEvent.begin("ChildProcessConnection.doConnectionSetup");
assert mServiceConnectComplete && mService != null;
assert mConnectionParams != null;
ICallbackInt pidCallback = new ICallbackInt.Stub() {
@Override
public void call(final int pid) {
mLauncherHandler.post(new Runnable() {
@Override
public void run() {
onSetupConnectionResult(pid);
}
});
}
};
try {
mService.setupConnection(mConnectionParams.mConnectionBundle, pidCallback,
mConnectionParams.mClientInterfaces);
} catch (RemoteException re) {
Log.e(TAG, "Failed to setup connection.", re);
}
mConnectionParams = null;
} finally {
TraceEvent.end("ChildProcessConnection.doConnectionSetup");
}
}
private boolean bind(boolean useStrongBinding) {
assert isRunningOnLauncherThread();
assert !mUnbound;
boolean success;
if (useStrongBinding) {
success = mStrongBinding.bind();
} else {
mModerateBindingCount++;
success = mModerateBinding.bind();
}
if (!success) return false;
mWaivedBinding.bind();
updateBindingState();
return true;
}
@VisibleForTesting
protected void unbind() {
assert isRunningOnLauncherThread();
mService = null;
mConnectionParams = null;
mUnbound = true;
mStrongBinding.unbind();
mWaivedBinding.unbind();
mModerateBinding.unbind();
updateBindingState();
synchronized (sBindingStateLock) {
mAllBindingStateCountsWhenDied =
Arrays.copyOf(sAllBindingStateCounts, NUM_BINDING_STATES);
}
if (mMemoryPressureCallback != null) {
final MemoryPressureCallback callback = mMemoryPressureCallback;
ThreadUtils.postOnUiThread(() -> MemoryPressureListener.removeCallback(callback));
mMemoryPressureCallback = null;
}
}
public boolean isStrongBindingBound() {
assert isRunningOnLauncherThread();
return mStrongBinding.isBound();
}
public void addStrongBinding() {
assert isRunningOnLauncherThread();
if (!isConnected()) {
Log.w(TAG, "The connection is not bound for %d", getPid());
return;
}
if (mStrongBindingCount == 0) {
mStrongBinding.bind();
updateBindingState();
}
mStrongBindingCount++;
}
public void removeStrongBinding() {
assert isRunningOnLauncherThread();
if (!isConnected()) {
Log.w(TAG, "The connection is not bound for %d", getPid());
return;
}
assert mStrongBindingCount > 0;
mStrongBindingCount--;
if (mStrongBindingCount == 0) {
mStrongBinding.unbind();
updateBindingState();
}
}
public boolean isModerateBindingBound() {
assert isRunningOnLauncherThread();
return mModerateBinding.isBound();
}
public void addModerateBinding() {
assert isRunningOnLauncherThread();
if (!isConnected()) {
Log.w(TAG, "The connection is not bound for %d", getPid());
return;
}
if (mModerateBindingCount == 0) {
mModerateBinding.bind();
updateBindingState();
}
mModerateBindingCount++;
}
public void removeModerateBinding() {
assert isRunningOnLauncherThread();
if (!isConnected()) {
Log.w(TAG, "The connection is not bound for %d", getPid());
return;
}
assert mModerateBindingCount > 0;
mModerateBindingCount--;
if (mModerateBindingCount == 0) {
mModerateBinding.unbind();
updateBindingState();
}
}
/**
* @return true if the connection is bound and only bound with the waived binding or if the
* connection is unbound and was only bound with the waived binding when it disconnected.
*/
public @ChildBindingState int bindingStateCurrentOrWhenDied() {
// WARNING: this method can be called from a thread other than the launcher thread.
// Note that it returns the current waived bound only state and is racy. This not really
// preventable without changing the caller's API, short of blocking.
synchronized (sBindingStateLock) {
return mBindingStateCurrentOrWhenDied;
}
}
/**
* @return true if the connection is intentionally killed by calling kill().
*/
public boolean isKilledByUs() {
// WARNING: this method can be called from a thread other than the launcher thread.
// Note that it returns the current waived bound only state and is racy. This not really
// preventable without changing the caller's API, short of blocking.
synchronized (sBindingStateLock) {
return mKilledByUs;
}
}
/**
* Returns the binding state of remaining processes, excluding the current connection.
*
* If the current process is dead then returns the binding state of all processes when it died.
* Otherwise returns current state.
*/
public int[] remainingBindingStateCountsCurrentOrWhenDied() {
// WARNING: this method can be called from a thread other than the launcher thread.
// Note that it returns the current waived bound only state and is racy. This not really
// preventable without changing the caller's API, short of blocking.
synchronized (sBindingStateLock) {
if (mAllBindingStateCountsWhenDied != null) {
return Arrays.copyOf(mAllBindingStateCountsWhenDied, NUM_BINDING_STATES);
}
int[] counts = Arrays.copyOf(sAllBindingStateCounts, NUM_BINDING_STATES);
// If current process is still bound then remove it from the counts.
if (mBindingState != ChildBindingState.UNBOUND) {
assert counts[mBindingState] > 0;
counts[mBindingState]--;
}
return counts;
}
}
// Should be called any binding is bound or unbound.
private void updateBindingState() {
int newBindingState;
if (mUnbound) {
newBindingState = ChildBindingState.UNBOUND;
} else if (mStrongBinding.isBound()) {
newBindingState = ChildBindingState.STRONG;
} else if (mModerateBinding.isBound()) {
newBindingState = ChildBindingState.MODERATE;
} else {
assert mWaivedBinding.isBound();
newBindingState = ChildBindingState.WAIVED;
}
synchronized (sBindingStateLock) {
if (newBindingState != mBindingState) {
if (mBindingState != ChildBindingState.UNBOUND) {
assert sAllBindingStateCounts[mBindingState] > 0;
sAllBindingStateCounts[mBindingState]--;
}
if (newBindingState != ChildBindingState.UNBOUND) {
sAllBindingStateCounts[newBindingState]++;
}
}
mBindingState = newBindingState;
if (!mUnbound) {
mBindingStateCurrentOrWhenDied = mBindingState;
}
}
}
private void notifyChildProcessDied() {
if (mServiceCallback != null) {
// Guard against nested calls to this method.
ServiceCallback serviceCallback = mServiceCallback;
mServiceCallback = null;
serviceCallback.onChildProcessDied(this);
}
}
private boolean isRunningOnLauncherThread() {
return mLauncherHandler.getLooper() == Looper.myLooper();
}
@VisibleForTesting
public void crashServiceForTesting() {
try {
mService.forceKill();
} catch (RemoteException e) {
// Expected. Ignore.
}
}
@VisibleForTesting
public boolean didOnServiceConnectedForTesting() {
return mDidOnServiceConnected;
}
@VisibleForTesting
protected Handler getLauncherHandler() {
return mLauncherHandler;
}
private void onMemoryPressure(@MemoryPressureLevel int pressure) {
mLauncherHandler.post(() -> onMemoryPressureOnLauncherThread(pressure));
}
private void onMemoryPressureOnLauncherThread(@MemoryPressureLevel int pressure) {
if (mService == null) return;
try {
mService.onMemoryPressure(pressure);
} catch (RemoteException ex) {
// Ignore
}
}
}