blob: 4fdcd1cb2f1c0d54e02897e5d63b0233c6c68ccc [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.content.browser;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.DeadObjectException;
import android.os.IBinder;
import android.os.RemoteException;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.process_launcher.ChildProcessCreationParams;
import org.chromium.content.common.FileDescriptorInfo;
import org.chromium.content.common.IChildProcessCallback;
import org.chromium.content.common.IChildProcessService;
import java.io.IOException;
/**
* Manages a connection between the browser activity and a child service.
*/
public class ChildProcessConnectionImpl implements ChildProcessConnection {
private final Context mContext;
private final int mServiceNumber;
private final boolean mInSandbox;
private final ChildProcessConnection.DeathCallback mDeathCallback;
private final ComponentName mServiceName;
// Synchronization: While most internal flow occurs on the UI thread, the public API
// (specifically start and stop) may be called from any thread, hence all entry point methods
// into the class are synchronized on the lock to protect access to these members.
private final Object mLock = new Object();
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;
// When the service disconnects (i.e. mServiceDisconnected is set to true), the status of the
// oom bindings is stashed here for future inspection.
private boolean mWasOomProtected;
private int mPid; // Process ID of the corresponding child process.
// Initial binding protects the newly spawned process from being killed before it is put to use,
// it is maintained between calls to start() and removeInitialBinding().
private ChildServiceConnection mInitialBinding;
// Strong binding will make the service priority equal to the priority of the activity. We want
// the OS to be able to kill background renderers as it kills other background apps, so strong
// bindings are maintained only for services that are active at the moment (between
// addStrongBinding() and removeStrongBinding()).
private ChildServiceConnection mStrongBinding;
// Low priority binding maintained in the entire lifetime of the connection, i.e. between calls
// to start() and stop().
private ChildServiceConnection mWaivedBinding;
// Incremented on addStrongBinding(), decremented on removeStrongBinding().
private int mStrongBindingCount;
// Moderate binding will make the service priority equal to the priority of a visible process
// while the app is in the foreground. It will stay bound only while the app is in the
// foreground to protect a background process from the system out-of-memory killer.
private ChildServiceConnection mModerateBinding;
// 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 mChildProcessCommonParameters;
private final boolean mAlwaysInForeground;
private final ChildProcessCreationParams mCreationParams;
// Caches whether non-sandboxed and sandboxed services require an extra
// binding flag provided via ChildProcessCreationParams.
// TODO(mnaganov): Get rid of it after the release of the next Android SDK.
private static Boolean sNeedsExtrabindFlags[] = new Boolean[2];
private static final String TAG = "ChildProcessConnect";
private static class ConnectionParams {
final String[] mCommandLine;
final FileDescriptorInfo[] mFilesToBeMapped;
final IChildProcessCallback mCallback;
ConnectionParams(String[] commandLine, FileDescriptorInfo[] filesToBeMapped,
IChildProcessCallback callback) {
mCommandLine = commandLine;
mFilesToBeMapped = filesToBeMapped;
mCallback = callback;
}
}
// This is set in start() and is used in onServiceConnected().
private ChildProcessConnection.StartCallback mStartCallback;
// This is set in setupConnection() and is later used in doConnectionSetupLocked(), 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 ChildProcessConnection.ConnectionCallback mConnectionCallback;
private class ChildServiceConnection implements ServiceConnection {
private boolean mBound;
private final int mBindFlags;
private Intent createServiceBindIntent() {
Intent intent = new Intent();
if (mCreationParams != null) {
mCreationParams.addIntentExtras(intent);
}
intent.setComponent(mServiceName);
return intent;
}
public ChildServiceConnection(int bindFlags) {
mBindFlags = bindFlags;
}
boolean bind() {
if (!mBound) {
try {
TraceEvent.begin("ChildProcessConnectionImpl.ChildServiceConnection.bind");
Intent intent = createServiceBindIntent();
if (mChildProcessCommonParameters != null) {
intent.putExtras(mChildProcessCommonParameters);
}
mBound = mContext.bindService(intent, this, mBindFlags);
} finally {
TraceEvent.end("ChildProcessConnectionImpl.ChildServiceConnection.bind");
}
}
return mBound;
}
void unbind() {
if (mBound) {
mContext.unbindService(this);
mBound = false;
}
}
boolean isBound() {
return mBound;
}
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
synchronized (mLock) {
// 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(
"ChildProcessConnectionImpl.ChildServiceConnection.onServiceConnected");
mDidOnServiceConnected = true;
mService = IChildProcessService.Stub.asInterface(service);
StartCallback startCallback = mStartCallback;
mStartCallback = null;
boolean boundToUs = false;
try {
boundToUs = mService.bindToCaller();
} catch (RemoteException ex) {
// Do not trigger the StartCallback here, since the service is already
// dead and the DeathCallback will run from onServiceDisconnected().
Log.e(TAG, "Failed to bind service to connection.", ex);
return;
}
if (startCallback != null) {
if (boundToUs) {
startCallback.onChildStarted();
} else {
startCallback.onChildStartFailed();
}
}
if (!boundToUs) {
return;
}
mServiceConnectComplete = true;
// Run the setup if the connection parameters have already been provided. If
// not, doConnectionSetupLocked() will be called from setupConnection().
if (mConnectionParams != null) {
doConnectionSetupLocked();
}
} finally {
TraceEvent.end(
"ChildProcessConnectionImpl.ChildServiceConnection.onServiceConnected");
}
}
}
// Called on the main thread to notify that the child service did not disconnect gracefully.
@Override
public void onServiceDisconnected(ComponentName className) {
synchronized (mLock) {
// Ensure that the disconnection logic runs only once (instead of once per each
// ChildServiceConnection).
if (mServiceDisconnected) {
return;
}
// Stash the status of the oom bindings, since stop() will release all bindings.
mWasOomProtected = isCurrentlyOomProtected();
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.
mDeathCallback.onChildProcessDied(ChildProcessConnectionImpl.this);
// If we have a pending connection callback, we need to communicate the failure to
// the caller.
if (mConnectionCallback != null) {
mConnectionCallback.onConnected(0);
}
mConnectionCallback = null;
}
}
}
ChildProcessConnectionImpl(Context context, int number, boolean inSandbox,
ChildProcessConnection.DeathCallback deathCallback, String serviceClassName,
Bundle childProcessCommonParameters, boolean alwaysInForeground,
ChildProcessCreationParams creationParams) {
mContext = context;
mServiceNumber = number;
mInSandbox = inSandbox;
mDeathCallback = deathCallback;
String packageName =
creationParams != null ? creationParams.getPackageName() : context.getPackageName();
mServiceName = new ComponentName(packageName, serviceClassName + mServiceNumber);
mChildProcessCommonParameters = childProcessCommonParameters;
mAlwaysInForeground = alwaysInForeground;
mCreationParams = creationParams;
int initialFlags = Context.BIND_AUTO_CREATE;
if (mAlwaysInForeground) initialFlags |= Context.BIND_IMPORTANT;
int extraBindFlags = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && mCreationParams != null
&& mCreationParams.getIsExternalService()
&& isExportedService(inSandbox, mContext, mServiceName)) {
extraBindFlags = Context.BIND_EXTERNAL_SERVICE;
}
mInitialBinding = new ChildServiceConnection(initialFlags | extraBindFlags);
mStrongBinding = new ChildServiceConnection(
Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT | extraBindFlags);
mWaivedBinding = new ChildServiceConnection(
Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY | extraBindFlags);
mModerateBinding = new ChildServiceConnection(Context.BIND_AUTO_CREATE | extraBindFlags);
}
private static boolean isExportedService(boolean inSandbox, Context context,
ComponentName serviceName) {
// Check for the cached value first. It is assumed that all pooled child services
// have identical attributes in the manifest.
final int arrayIndex = inSandbox ? 1 : 0;
if (sNeedsExtrabindFlags[arrayIndex] != null) {
return sNeedsExtrabindFlags[arrayIndex].booleanValue();
}
boolean result = false;
try {
PackageManager packageManager = context.getPackageManager();
ServiceInfo serviceInfo = packageManager.getServiceInfo(serviceName, 0);
result = serviceInfo.exported;
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Could not retrieve info about service %s", serviceName, e);
}
sNeedsExtrabindFlags[arrayIndex] = Boolean.valueOf(result);
return result;
}
@Override
public int getServiceNumber() {
return mServiceNumber;
}
@Override
public boolean isInSandbox() {
return mInSandbox;
}
@Override
public String getPackageName() {
return mCreationParams != null ? mCreationParams.getPackageName()
: mContext.getPackageName();
}
@Override
public ChildProcessCreationParams getCreationParams() {
return mCreationParams;
}
@Override
public IChildProcessService getService() {
synchronized (mLock) {
return mService;
}
}
@Override
public int getPid() {
synchronized (mLock) {
return mPid;
}
}
@Override
public void start(ChildProcessConnection.StartCallback startCallback) {
try {
TraceEvent.begin("ChildProcessConnectionImpl.start");
synchronized (mLock) {
assert !ThreadUtils.runningOnUiThread();
assert mConnectionParams == null :
"setupConnection() called before start() in ChildProcessConnectionImpl.";
mStartCallback = startCallback;
if (!mInitialBinding.bind()) {
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?
mDeathCallback.onChildProcessDied(ChildProcessConnectionImpl.this);
} else {
mWaivedBinding.bind();
}
}
} finally {
TraceEvent.end("ChildProcessConnectionImpl.start");
}
}
@Override
public void setupConnection(String[] commandLine, FileDescriptorInfo[] filesToBeMapped,
IChildProcessCallback processCallback, ConnectionCallback connectionCallback) {
synchronized (mLock) {
assert mConnectionParams == null;
if (mServiceDisconnected) {
Log.w(TAG, "Tried to setup a connection that already disconnected.");
connectionCallback.onConnected(0);
return;
}
try {
TraceEvent.begin("ChildProcessConnectionImpl.setupConnection");
mConnectionCallback = connectionCallback;
mConnectionParams =
new ConnectionParams(commandLine, filesToBeMapped, processCallback);
// Run the setup if the service is already connected. If not,
// doConnectionSetupLocked() will be called from onServiceConnected().
if (mServiceConnectComplete) {
doConnectionSetupLocked();
}
} finally {
TraceEvent.end("ChildProcessConnectionImpl.setupConnection");
}
}
}
@Override
public void stop() {
synchronized (mLock) {
mInitialBinding.unbind();
mStrongBinding.unbind();
mWaivedBinding.unbind();
mModerateBinding.unbind();
mStrongBindingCount = 0;
if (mService != null) {
mService = null;
}
mConnectionParams = 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. Has to be called with mLock.
*/
private void doConnectionSetupLocked() {
try {
TraceEvent.begin("ChildProcessConnectionImpl.doConnectionSetupLocked");
assert mServiceConnectComplete && mService != null;
assert mConnectionParams != null;
Bundle bundle = ChildProcessLauncher.createsServiceBundle(
mConnectionParams.mCommandLine, mConnectionParams.mFilesToBeMapped);
try {
mPid = mService.setupConnection(bundle, mConnectionParams.mCallback);
assert mPid != 0 : "Child service claims to be run by a process of pid=0.";
} catch (android.os.RemoteException re) {
Log.e(TAG, "Failed to setup connection.", re);
}
// We proactively close the FDs rather than wait for GC & finalizer.
try {
for (FileDescriptorInfo fileInfo : mConnectionParams.mFilesToBeMapped) {
fileInfo.mFd.close();
}
} catch (IOException ioe) {
Log.w(TAG, "Failed to close FD.", ioe);
}
mConnectionParams = null;
if (mConnectionCallback != null) {
mConnectionCallback.onConnected(mPid);
}
mConnectionCallback = null;
} finally {
TraceEvent.end("ChildProcessConnectionImpl.doConnectionSetupLocked");
}
}
@Override
public boolean isInitialBindingBound() {
synchronized (mLock) {
return mInitialBinding.isBound();
}
}
@Override
public boolean isStrongBindingBound() {
synchronized (mLock) {
return mStrongBinding.isBound();
}
}
@Override
public void removeInitialBinding() {
synchronized (mLock) {
assert !mAlwaysInForeground;
mInitialBinding.unbind();
}
}
@Override
public boolean isOomProtectedOrWasWhenDied() {
synchronized (mLock) {
if (mServiceDisconnected) {
return mWasOomProtected;
} else {
return isCurrentlyOomProtected();
}
}
}
private boolean isCurrentlyOomProtected() {
synchronized (mLock) {
assert !mServiceDisconnected;
if (mAlwaysInForeground) return ChildProcessLauncher.isApplicationInForeground();
return mInitialBinding.isBound() || mStrongBinding.isBound();
}
}
@Override
public void dropOomBindings() {
synchronized (mLock) {
assert !mAlwaysInForeground;
mInitialBinding.unbind();
mStrongBindingCount = 0;
mStrongBinding.unbind();
mModerateBinding.unbind();
}
}
@Override
public void addStrongBinding() {
synchronized (mLock) {
if (mService == null) {
Log.w(TAG, "The connection is not bound for %d", mPid);
return;
}
if (mStrongBindingCount == 0) {
mStrongBinding.bind();
}
mStrongBindingCount++;
}
}
@Override
public void removeStrongBinding() {
synchronized (mLock) {
if (mService == null) {
Log.w(TAG, "The connection is not bound for %d", mPid);
return;
}
assert mStrongBindingCount > 0;
mStrongBindingCount--;
if (mStrongBindingCount == 0) {
mStrongBinding.unbind();
}
}
}
@Override
public boolean isModerateBindingBound() {
synchronized (mLock) {
return mModerateBinding.isBound();
}
}
@Override
public void addModerateBinding() {
synchronized (mLock) {
if (mService == null) {
Log.w(TAG, "The connection is not bound for %d", mPid);
return;
}
mModerateBinding.bind();
}
}
@Override
public void removeModerateBinding() {
synchronized (mLock) {
if (mService == null) {
Log.w(TAG, "The connection is not bound for %d", mPid);
return;
}
mModerateBinding.unbind();
}
}
@VisibleForTesting
public boolean crashServiceForTesting() throws RemoteException {
try {
mService.crashIntentionallyForTesting();
} catch (DeadObjectException e) {
return true;
}
return false;
}
@VisibleForTesting
public boolean isConnected() {
return mService != null;
}
}