// Copyright (c) 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.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.ParcelFileDescriptor; import android.util.Log; import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; import org.chromium.base.CalledByNative; import org.chromium.base.CpuFeatures; import org.chromium.base.ThreadUtils; import org.chromium.content.app.ChildProcessService; import org.chromium.content.common.CommandLine; import org.chromium.content.common.IChildProcessCallback; import org.chromium.content.common.IChildProcessService; import org.chromium.content.common.TraceEvent; public class ChildProcessConnection implements ServiceConnection { interface DeathCallback { void onChildProcessDied(int pid); } // Names of items placed in the bind intent or connection bundle. public static final String EXTRA_COMMAND_LINE = "com.google.android.apps.chrome.extra.command_line"; // Note the FDs may only be passed in the connection bundle. public static final String EXTRA_FILES_PREFIX = "com.google.android.apps.chrome.extra.extraFile_"; public static final String EXTRA_FILES_ID_SUFFIX = "_id"; public static final String EXTRA_FILES_FD_SUFFIX = "_fd"; // Used to pass the CPU core count to child processes. public static final String EXTRA_CPU_COUNT = "com.google.android.apps.chrome.extra.cpu_count"; // Used to pass the CPU features mask to child processes. public static final String EXTRA_CPU_FEATURES = "com.google.android.apps.chrome.extra.cpu_features"; private final Context mContext; private final int mServiceNumber; private final boolean mInSandbox; private final ChildProcessConnection.DeathCallback mDeathCallback; private final Class<? extends ChildProcessService> mServiceClass; // Synchronization: While most internal flow occurs on the UI thread, the public API // (specifically bind and unbind) may be called from any thread, hence all entry point methods // into the class are synchronized on the ChildProcessConnection instance to protect access // to these members. But see also the TODO where AsyncBoundServiceConnection is created. private final Object mUiThreadLock = new Object(); private IChildProcessService mService = null; private boolean mServiceConnectComplete = false; private int mPID = 0; // Process ID of the corresponding child process. private HighPriorityConnection mHighPriorityConnection = null; private int mHighPriorityConnectionCount = 0; private static final String TAG = "ChildProcessConnection"; private static class ConnectionParams { final String[] mCommandLine; final FileDescriptorInfo[] mFilesToBeMapped; final IChildProcessCallback mCallback; final Runnable mOnConnectionCallback; ConnectionParams( String[] commandLine, FileDescriptorInfo[] filesToBeMapped, IChildProcessCallback callback, Runnable onConnectionCallback) { mCommandLine = commandLine; mFilesToBeMapped = filesToBeMapped; mCallback = callback; mOnConnectionCallback = onConnectionCallback; } } // This is only valid while the connection is being established. private ConnectionParams mConnectionParams; private boolean mIsBound; ChildProcessConnection(Context context, int number, boolean inSandbox, ChildProcessConnection.DeathCallback deathCallback, Class<? extends ChildProcessService> serviceClass) { mContext = context; mServiceNumber = number; mInSandbox = inSandbox; mDeathCallback = deathCallback; mServiceClass = serviceClass; } int getServiceNumber() { return mServiceNumber; } boolean isInSandbox() { return mInSandbox; } IChildProcessService getService() { synchronized(mUiThreadLock) { return mService; } } private Intent createServiceBindIntent() { Intent intent = new Intent(); intent.setClassName(mContext, mServiceClass.getName() + mServiceNumber); intent.setPackage(mContext.getPackageName()); return intent; } /** * Bind to an IChildProcessService. This must be followed by a call to setupConnection() * to setup the connection parameters. (These methods are separated to allow the client * to pass whatever parameters they have available here, and complete the remainder * later while reducing the connection setup latency). * @param commandLine (Optional) Command line for the child process. If omitted, then * the command line parameters must instead be passed to setupConnection(). */ void bind(String[] commandLine) { synchronized(mUiThreadLock) { TraceEvent.begin(); assert !ThreadUtils.runningOnUiThread(); final Intent intent = createServiceBindIntent(); if (commandLine != null) { intent.putExtra(EXTRA_COMMAND_LINE, commandLine); } mIsBound = mContext.bindService(intent, this, Context.BIND_AUTO_CREATE); if (!mIsBound) { onBindFailed(); } TraceEvent.end(); } } /** Setup a connection previous bound via a call to bind(). * * This establishes the parameters that were not already supplied in bind. * @param commandLine (Optional) will be ignored if the command line was already sent in bind() * @param fileToBeMapped a list of file descriptors that should be registered * @param callback Used for status updates regarding this process connection. * @param onConnectionCallback will be run when the connection is setup and ready to use. */ void setupConnection( String[] commandLine, FileDescriptorInfo[] filesToBeMapped, IChildProcessCallback callback, Runnable onConnectionCallback) { synchronized(mUiThreadLock) { TraceEvent.begin(); assert mConnectionParams == null; mConnectionParams = new ConnectionParams(commandLine, filesToBeMapped, callback, onConnectionCallback); if (mServiceConnectComplete) { doConnectionSetup(); } TraceEvent.end(); } } /** * Unbind the IChildProcessService. It is safe to call this multiple times. */ void unbind() { synchronized(mUiThreadLock) { if (mIsBound) { mContext.unbindService(this); mIsBound = false; } if (mService != null) { if (mHighPriorityConnection != null) { unbindHighPriority(true); } mService = null; mPID = 0; } mConnectionParams = null; mServiceConnectComplete = false; } } // Called on the main thread to notify that the service is connected. @Override public void onServiceConnected(ComponentName className, IBinder service) { synchronized(mUiThreadLock) { TraceEvent.begin(); mServiceConnectComplete = true; mService = IChildProcessService.Stub.asInterface(service); if (mConnectionParams != null) { doConnectionSetup(); } TraceEvent.end(); } } // Called on the main thread to notify that the bindService() call failed (returned false). private void onBindFailed() { mServiceConnectComplete = true; if (mConnectionParams != null) { doConnectionSetup(); } } /** * Called when the connection parameters have been set, and a connection has been established * (as signaled by onServiceConnected), or if the connection failed (mService will be false). */ private void doConnectionSetup() { TraceEvent.begin(); assert mServiceConnectComplete && mConnectionParams != null; // Capture the callback before it is potentially nulled in unbind(). Runnable onConnectionCallback = mConnectionParams.mOnConnectionCallback; if (onConnectionCallback == null) { unbind(); } else if (mService != null) { Bundle bundle = new Bundle(); bundle.putStringArray(EXTRA_COMMAND_LINE, mConnectionParams.mCommandLine); FileDescriptorInfo[] fileInfos = mConnectionParams.mFilesToBeMapped; ParcelFileDescriptor[] parcelFiles = new ParcelFileDescriptor[fileInfos.length]; for (int i = 0; i < fileInfos.length; i++) { if (fileInfos[i].mFd == -1) { // If someone provided an invalid FD, they are doing something wrong. Log.e(TAG, "Invalid FD (id=" + fileInfos[i].mId + ") for process connection, " + "aborting connection."); return; } String idName = EXTRA_FILES_PREFIX + i + EXTRA_FILES_ID_SUFFIX; String fdName = EXTRA_FILES_PREFIX + i + EXTRA_FILES_FD_SUFFIX; if (fileInfos[i].mAutoClose) { // Adopt the FD, it will be closed when we close the ParcelFileDescriptor. parcelFiles[i] = ParcelFileDescriptor.adoptFd(fileInfos[i].mFd); } else { try { parcelFiles[i] = ParcelFileDescriptor.fromFd(fileInfos[i].mFd); } catch(IOException e) { Log.e(TAG, "Invalid FD provided for process connection, aborting connection.", e); return; } } bundle.putParcelable(fdName, parcelFiles[i]); bundle.putInt(idName, fileInfos[i].mId); } // Add the CPU properties now. bundle.putInt(EXTRA_CPU_COUNT, CpuFeatures.getCount()); bundle.putLong(EXTRA_CPU_FEATURES, CpuFeatures.getMask()); try { mPID = mService.setupConnection(bundle, mConnectionParams.mCallback); } catch (android.os.RemoteException re) { Log.e(TAG, "Failed to setup connection.", re); } // We proactivley close the FDs rather than wait for GC & finalizer. try { for (ParcelFileDescriptor parcelFile : parcelFiles) { if (parcelFile != null) parcelFile.close(); } } catch (IOException ioe) { Log.w(TAG, "Failed to close FD.", ioe); } } mConnectionParams = null; if (onConnectionCallback != null) { onConnectionCallback.run(); } TraceEvent.end(); } // Called on the main thread to notify that the child service did not disconnect gracefully. @Override public void onServiceDisconnected(ComponentName className) { int pid = mPID; // Stash pid & connection callback since unbind() will clear them. Runnable onConnectionCallback = mConnectionParams != null ? mConnectionParams.mOnConnectionCallback : null; Log.w(TAG, "onServiceDisconnected (crash?): pid=" + pid); unbind(); // We don't want to auto-restart on crash. Let the browser do that. if (pid != 0) { mDeathCallback.onChildProcessDied(pid); } if (onConnectionCallback != null) { onConnectionCallback.run(); } } /** * Bind the service with a new high priority connection. This will make the service * as important as the main process. */ void bindHighPriority() { synchronized(mUiThreadLock) { if (mService == null) { Log.w(TAG, "The connection is not bound for " + mPID); return; } if (mHighPriorityConnection == null) { mHighPriorityConnection = new HighPriorityConnection(); mHighPriorityConnection.bind(); } mHighPriorityConnectionCount++; } } /** * Unbind the service as the high priority connection. */ void unbindHighPriority(boolean force) { synchronized(mUiThreadLock) { if (mService == null) { Log.w(TAG, "The connection is not bound for " + mPID); return; } mHighPriorityConnectionCount--; if (force || (mHighPriorityConnectionCount == 0 && mHighPriorityConnection != null)) { mHighPriorityConnection.unbind(); mHighPriorityConnection = null; } } } private class HighPriorityConnection implements ServiceConnection { private boolean mHBound = false; void bind() { final Intent intent = createServiceBindIntent(); mHBound = mContext.bindService(intent, this, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT); } void unbind() { if (mHBound) { mContext.unbindService(this); mHBound = false; } } @Override public void onServiceConnected(ComponentName className, IBinder service) { } @Override public void onServiceDisconnected(ComponentName className) { } } /** * @return The connection PID, or 0 if not yet connected. */ public int getPid() { synchronized(mUiThreadLock) { return mPID; } } }