| // 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 android.os.SystemClock; |
| |
| import org.chromium.base.Log; |
| import org.chromium.base.TraceEvent; |
| import org.chromium.base.VisibleForTesting; |
| import org.chromium.base.metrics.CachedMetrics; |
| |
| import java.util.concurrent.TimeUnit; |
| |
| import javax.annotation.Nullable; |
| |
| /** |
| * Manages a connection between the browser activity and a child service. |
| */ |
| public class ChildProcessConnection { |
| private static final String TAG = "ChildProcessConn"; |
| |
| private static final int BIND_SERVICE_TIMEOUT_IN_MS = 10 * 1000; |
| |
| /** |
| * 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. Note onChildProcessDied will be called after this callback, |
| * once the client unbinds and the service gets cleaned. |
| * TODO(jcivelli): crbug.com/736948 we should improve the behavior in such cases. |
| */ |
| void onChildStartFailed(); |
| |
| /** |
| * 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); |
| } |
| |
| /** 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 class ChildServiceConnectionImpl implements ChildServiceConnection, ServiceConnection { |
| private final int mBindFlags; |
| private boolean mBound; |
| |
| private ChildServiceConnectionImpl(int bindFlags) { |
| mBindFlags = bindFlags; |
| } |
| |
| @Override |
| public boolean bind() { |
| if (!mBound) { |
| try { |
| TraceEvent.begin("ChildProcessConnection.ChildServiceConnectionImpl.bind"); |
| Intent intent = new Intent(); |
| intent.setComponent(mServiceName); |
| if (mServiceBundle != null) { |
| intent.putExtras(mServiceBundle); |
| } |
| mBound = mContext.bindService(intent, 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) { |
| mLauncherHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| ChildProcessConnection.this.onServiceConnectedOnLauncherThread(service); |
| } |
| }); |
| } |
| |
| // Called on the main thread to notify that the child service did not disconnect gracefully. |
| @Override |
| public void onServiceDisconnected(ComponentName className) { |
| mLauncherHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| ChildProcessConnection.this.onServiceDisconnectedOnLauncherThread(); |
| } |
| }); |
| } |
| } |
| |
| // CachedMetrics used from this class, because this class can run before native library is |
| // loaded. |
| private static final CachedMetrics.TimesHistogramSample sOnServiceConnectedTimesMetric = |
| new CachedMetrics.TimesHistogramSample( |
| "Android.ChildProcessLauncher.OnServiceConnectedTime", TimeUnit.MILLISECONDS); |
| private static final CachedMetrics |
| .BooleanHistogramSample sOnServiceConnectedTimesMetricTimedOut = |
| new CachedMetrics.BooleanHistogramSample( |
| "Android.ChildProcessLauncher.OnServiceConnectedTimedOut"); |
| |
| private final Handler mLauncherHandler; |
| private final Context mContext; |
| 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 IBinder mCallback; |
| |
| ConnectionParams(Bundle connectionBundle, IBinder callback) { |
| mConnectionBundle = connectionBundle; |
| mCallback = callback; |
| } |
| } |
| |
| // 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; |
| |
| // Workaround bug on some android versions where bindService does not result in |
| // onServiceConnected for sandboxed services; see crbug.com/736066 for details. |
| // This is a delayed callback that will retry bindService with a delay. |
| private Runnable mOnServiceConnectedWatchDog; |
| |
| 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; |
| |
| // Inital moderate binding. |
| private final ChildServiceConnection mInitialBinding; |
| |
| // 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. |
| 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; |
| |
| // Incremented on addStrongBinding(), decremented on removeStrongBinding(). |
| private int mStrongBindingCount; |
| |
| // Indicates whether the connection only has the waived binding (if the connection is unbound, |
| // it contains the state at time of unbinding). |
| private boolean mWaivedBoundOnly; |
| |
| // Set to true once unbind() was called. |
| private boolean mUnbound; |
| |
| // Timestamp when watchdog was last reset, which is equivalent to when start was called. |
| private long mLastWatchdogResetTimestamp; |
| |
| public ChildProcessConnection(Context context, ComponentName serviceName, boolean bindToCaller, |
| boolean bindAsExternalService, Bundle serviceBundle) { |
| mLauncherHandler = new Handler(); |
| mContext = context; |
| mServiceName = serviceName; |
| mServiceBundle = serviceBundle != null ? serviceBundle : new Bundle(); |
| mServiceBundle.putBoolean(ChildProcessConstants.EXTRA_BIND_TO_CALLER, bindToCaller); |
| mBindToCaller = bindToCaller; |
| |
| int defaultFlags = Context.BIND_AUTO_CREATE |
| | (bindAsExternalService ? Context.BIND_EXTERNAL_SERVICE : 0); |
| mInitialBinding = createServiceConnection(defaultFlags); |
| mModerateBinding = createServiceConnection(defaultFlags); |
| mStrongBinding = createServiceConnection(defaultFlags | Context.BIND_IMPORTANT); |
| mWaivedBinding = createServiceConnection(defaultFlags | Context.BIND_WAIVE_PRIORITY); |
| } |
| |
| public final Context getContext() { |
| assert isRunningOnLauncherThread(); |
| return mContext; |
| } |
| |
| public final String getPackageName() { |
| assert isRunningOnLauncherThread(); |
| return mServiceName.getPackageName(); |
| } |
| |
| 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, boolean retryOnTimeout) { |
| try { |
| TraceEvent.begin("ChildProcessConnection.start"); |
| assert isRunningOnLauncherThread(); |
| assert mConnectionParams |
| == null : "setupConnection() called before start() in ChildProcessConnection."; |
| |
| mServiceCallback = serviceCallback; |
| |
| resetWatchdog(useStrongBinding, serviceCallback, retryOnTimeout); |
| if (!bind(useStrongBinding)) { |
| Log.e(TAG, "Failed to establish the service connection."); |
| cancelWatchDog(); |
| // 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 callback optional client specified callbacks 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 IBinder callback, |
| 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, callback); |
| // 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(); |
| cancelWatchDog(); |
| unbind(); |
| mService = null; |
| mConnectionParams = null; |
| notifyChildProcessDied(); |
| } |
| |
| @VisibleForTesting |
| public void onServiceConnectedOnLauncherThread(IBinder service) { |
| assert isRunningOnLauncherThread(); |
| cancelWatchDog(); |
| // 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"); |
| sOnServiceConnectedTimesMetric.record( |
| SystemClock.elapsedRealtime() - mLastWatchdogResetTimestamp); |
| sOnServiceConnectedTimesMetricTimedOut.record(false); |
| |
| mDidOnServiceConnected = true; |
| mService = IChildProcessService.Stub.asInterface(service); |
| |
| if (mBindToCaller) { |
| try { |
| if (!mService.bindToCaller()) { |
| if (mServiceCallback != null) { |
| mServiceCallback.onChildStartFailed(); |
| } |
| 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; |
| |
| // 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"); |
| } |
| } |
| |
| private 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.mCallback); |
| } 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 = useStrongBinding ? mStrongBinding.bind() : mInitialBinding.bind(); |
| if (!success) return false; |
| |
| updateWaivedBoundOnlyState(); |
| mWaivedBinding.bind(); |
| return true; |
| } |
| |
| @VisibleForTesting |
| protected void unbind() { |
| assert isRunningOnLauncherThread(); |
| mUnbound = true; |
| unbindAll(); |
| // Note that we don't update the waived bound only state here as to preserve the state when |
| // disconnected. |
| } |
| |
| private void unbindAll() { |
| mStrongBinding.unbind(); |
| mWaivedBinding.unbind(); |
| mModerateBinding.unbind(); |
| mInitialBinding.unbind(); |
| } |
| |
| public boolean isInitialBindingBound() { |
| assert isRunningOnLauncherThread(); |
| return mInitialBinding.isBound(); |
| } |
| |
| public void addInitialBinding() { |
| assert isRunningOnLauncherThread(); |
| mInitialBinding.bind(); |
| updateWaivedBoundOnlyState(); |
| } |
| |
| public boolean isStrongBindingBound() { |
| assert isRunningOnLauncherThread(); |
| return mStrongBinding.isBound(); |
| } |
| |
| public void removeInitialBinding() { |
| assert isRunningOnLauncherThread(); |
| mInitialBinding.unbind(); |
| updateWaivedBoundOnlyState(); |
| } |
| |
| public void dropOomBindings() { |
| assert isRunningOnLauncherThread(); |
| mInitialBinding.unbind(); |
| |
| mStrongBindingCount = 0; |
| mStrongBinding.unbind(); |
| updateWaivedBoundOnlyState(); |
| |
| mModerateBinding.unbind(); |
| } |
| |
| public void addStrongBinding() { |
| assert isRunningOnLauncherThread(); |
| if (!isConnected()) { |
| Log.w(TAG, "The connection is not bound for %d", getPid()); |
| return; |
| } |
| if (mStrongBindingCount == 0) { |
| mStrongBinding.bind(); |
| updateWaivedBoundOnlyState(); |
| } |
| 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(); |
| updateWaivedBoundOnlyState(); |
| } |
| } |
| |
| 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; |
| } |
| mModerateBinding.bind(); |
| updateWaivedBoundOnlyState(); |
| } |
| |
| public void removeModerateBinding() { |
| assert isRunningOnLauncherThread(); |
| if (!isConnected()) { |
| Log.w(TAG, "The connection is not bound for %d", getPid()); |
| return; |
| } |
| mModerateBinding.unbind(); |
| updateWaivedBoundOnlyState(); |
| } |
| |
| /** |
| * @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 boolean isWaivedBoundOnlyOrWasWhenDied() { |
| // 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. |
| return mWaivedBoundOnly; |
| } |
| |
| // Should be called every time the mInitialBinding or mStrongBinding are bound/unbound. |
| private void updateWaivedBoundOnlyState() { |
| if (!mUnbound) { |
| mWaivedBoundOnly = !mInitialBinding.isBound() && !mStrongBinding.isBound() |
| && !mModerateBinding.isBound(); |
| } |
| } |
| |
| private void notifyChildProcessDied() { |
| if (mServiceCallback != null) { |
| // Guard against nested calls to this method. |
| ServiceCallback serviceCallback = mServiceCallback; |
| mServiceCallback = null; |
| serviceCallback.onChildProcessDied(this); |
| } |
| } |
| |
| private void resetWatchdog(final boolean useStrongBinding, |
| final ServiceCallback serviceCallback, final boolean retryOnTimeout) { |
| assert isRunningOnLauncherThread(); |
| cancelWatchDog(); |
| assert mOnServiceConnectedWatchDog == null; |
| mOnServiceConnectedWatchDog = new Runnable() { |
| @Override |
| public void run() { |
| assert mOnServiceConnectedWatchDog == this; |
| assert !mDidOnServiceConnected; |
| assert mServiceCallback == null; |
| mOnServiceConnectedWatchDog = null; |
| sOnServiceConnectedTimesMetricTimedOut.record(true); |
| if (!retryOnTimeout) return; |
| unbindAll(); |
| start(useStrongBinding, serviceCallback, retryOnTimeout); |
| } |
| }; |
| mLastWatchdogResetTimestamp = SystemClock.elapsedRealtime(); |
| mLauncherHandler.postDelayed(mOnServiceConnectedWatchDog, BIND_SERVICE_TIMEOUT_IN_MS); |
| } |
| |
| private void cancelWatchDog() { |
| assert isRunningOnLauncherThread(); |
| if (mOnServiceConnectedWatchDog == null) return; |
| mLauncherHandler.removeCallbacks(mOnServiceConnectedWatchDog); |
| mOnServiceConnectedWatchDog = null; |
| } |
| |
| private boolean isRunningOnLauncherThread() { |
| return mLauncherHandler.getLooper() == Looper.myLooper(); |
| } |
| |
| @VisibleForTesting |
| protected ChildServiceConnection createServiceConnection(int bindFlags) { |
| assert isRunningOnLauncherThread(); |
| return new ChildServiceConnectionImpl(bindFlags); |
| } |
| |
| @VisibleForTesting |
| public void crashServiceForTesting() throws RemoteException { |
| mService.crashIntentionallyForTesting(); |
| } |
| |
| @VisibleForTesting |
| public boolean didOnServiceConnectedForTesting() { |
| return mDidOnServiceConnected; |
| } |
| |
| @VisibleForTesting |
| protected Handler getLauncherHandler() { |
| return mLauncherHandler; |
| } |
| } |