ChildProcessConnection.java revision d0247b1b59f9c528cb6df88b4f2b9afaf80d181e
1// Copyright 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.content.browser;
6
7import android.content.ComponentName;
8import android.content.Context;
9import android.content.Intent;
10import android.content.ServiceConnection;
11import android.os.AsyncTask;
12import android.os.Bundle;
13import android.os.Handler;
14import android.os.IBinder;
15import android.os.Looper;
16import android.os.ParcelFileDescriptor;
17import android.util.Log;
18
19import java.io.IOException;
20import java.util.concurrent.atomic.AtomicBoolean;
21
22import org.chromium.base.CalledByNative;
23import org.chromium.base.CpuFeatures;
24import org.chromium.base.SysUtils;
25import org.chromium.base.ThreadUtils;
26import org.chromium.content.app.ChildProcessService;
27import org.chromium.content.common.CommandLine;
28import org.chromium.content.common.IChildProcessCallback;
29import org.chromium.content.common.IChildProcessService;
30import org.chromium.content.common.TraceEvent;
31
32/**
33 * Manages a connection between the browser activity and a child service. The class is responsible
34 * for estabilishing the connection (start()), closing it (stop()) and increasing the priority of
35 * the service when it is in active use (between calls to attachAsActive() and detachAsActive()).
36 */
37public class ChildProcessConnection {
38    /**
39     * Used to notify the consumer about disconnection of the service. This callback is provided
40     * earlier than ConnectionCallbacks below, as a child process might die before the connection is
41     * fully set up.
42     */
43    interface DeathCallback {
44        void onChildProcessDied(int pid);
45    }
46
47    /**
48     * Used to notify the consumer about the connection being established.
49     */
50    interface ConnectionCallback {
51        /**
52         * Called when the connection to the service is established.
53         * @param pid Pid of the child process.
54         */
55        void onConnected(int pid);
56    }
57
58    // Names of items placed in the bind intent or connection bundle.
59    public static final String EXTRA_COMMAND_LINE =
60            "com.google.android.apps.chrome.extra.command_line";
61    // Note the FDs may only be passed in the connection bundle.
62    public static final String EXTRA_FILES_PREFIX =
63            "com.google.android.apps.chrome.extra.extraFile_";
64    public static final String EXTRA_FILES_ID_SUFFIX = "_id";
65    public static final String EXTRA_FILES_FD_SUFFIX = "_fd";
66
67    // Used to pass the CPU core count to child processes.
68    public static final String EXTRA_CPU_COUNT =
69            "com.google.android.apps.chrome.extra.cpu_count";
70    // Used to pass the CPU features mask to child processes.
71    public static final String EXTRA_CPU_FEATURES =
72            "com.google.android.apps.chrome.extra.cpu_features";
73
74    private final Context mContext;
75    private final int mServiceNumber;
76    private final boolean mInSandbox;
77    private final ChildProcessConnection.DeathCallback mDeathCallback;
78    private final Class<? extends ChildProcessService> mServiceClass;
79
80    // Synchronization: While most internal flow occurs on the UI thread, the public API
81    // (specifically start and stop) may be called from any thread, hence all entry point methods
82    // into the class are synchronized on the lock to protect access to these members. But see also
83    // the TODO where AsyncBoundServiceConnection is created.
84    private final Object mLock = new Object();
85    private IChildProcessService mService = null;
86    // Set to true when the service connect is finished, even if it fails.
87    private boolean mServiceConnectComplete = false;
88    // Set to true when the service disconnects, as opposed to being properly closed. This happens
89    // when the process crashes or gets killed by the system out-of-memory killer.
90    private boolean mServiceDisconnected = false;
91    private int mPID = 0;  // Process ID of the corresponding child process.
92    // Initial binding protects the newly spawned process from being killed before it is put to use,
93    // it is maintained between calls to start() and removeInitialBinding().
94    private ChildServiceConnection mInitialBinding = null;
95    // Strong binding will make the service priority equal to the priority of the activity. We want
96    // the OS to be able to kill background renderers as it kills other background apps, so strong
97    // bindings are maintained only for services that are active at the moment (between
98    // attachAsActive() and detachAsActive()).
99    private ChildServiceConnection mStrongBinding = null;
100    // Low priority binding maintained in the entire lifetime of the connection, i.e. between calls
101    // to start() and stop().
102    private ChildServiceConnection mWaivedBinding = null;
103    // Incremented on attachAsActive(), decremented on detachAsActive().
104    private int mAttachAsActiveCount = 0;
105
106    private static final String TAG = "ChildProcessConnection";
107
108    private static class ConnectionParams {
109        final String[] mCommandLine;
110        final FileDescriptorInfo[] mFilesToBeMapped;
111        final IChildProcessCallback mCallback;
112
113        ConnectionParams(String[] commandLine, FileDescriptorInfo[] filesToBeMapped,
114                IChildProcessCallback callback) {
115            mCommandLine = commandLine;
116            mFilesToBeMapped = filesToBeMapped;
117            mCallback = callback;
118        }
119    }
120
121    // This is set by the consumer of the class in setupConnection() and is later used in
122    // doSetupConnection(), after which the variable is cleared. Therefore this is only valid while
123    // the connection is being set up.
124    private ConnectionParams mConnectionParams;
125
126    // Callbacks used to notify the consumer about connection events. This is also provided in
127    // setupConnection(), but remains valid after setup.
128    private ChildProcessConnection.ConnectionCallback mConnectionCallback;
129
130    private class ChildServiceConnection implements ServiceConnection {
131        private boolean mBound = false;
132
133        private final int mBindFlags;
134
135        public ChildServiceConnection(int bindFlags) {
136            mBindFlags = bindFlags;
137        }
138
139        boolean bind(String[] commandLine) {
140            if (!mBound) {
141                final Intent intent = createServiceBindIntent();
142                if (commandLine != null) {
143                    intent.putExtra(EXTRA_COMMAND_LINE, commandLine);
144                }
145                mBound = mContext.bindService(intent, this, mBindFlags);
146            }
147            return mBound;
148        }
149
150        void unbind() {
151            if (mBound) {
152                mContext.unbindService(this);
153                mBound = false;
154            }
155        }
156
157        boolean isBound() {
158            return mBound;
159        }
160
161        @Override
162        public void onServiceConnected(ComponentName className, IBinder service) {
163            synchronized(mLock) {
164                // A flag from the parent class ensures we run the post-connection logic only once
165                // (instead of once per each ChildServiceConnection).
166                if (mServiceConnectComplete) {
167                    return;
168                }
169                TraceEvent.begin();
170                mServiceConnectComplete = true;
171                mService = IChildProcessService.Stub.asInterface(service);
172                // Make sure that the connection parameters have already been provided. If not,
173                // doConnectionSetup() will be called from setupConnection().
174                if (mConnectionParams != null) {
175                    doConnectionSetup();
176                }
177                TraceEvent.end();
178            }
179        }
180
181
182        // Called on the main thread to notify that the child service did not disconnect gracefully.
183        @Override
184        public void onServiceDisconnected(ComponentName className) {
185            // Ensure that the disconnection logic runs only once (instead of once per each
186            // ChildServiceConnection).
187            if (mServiceDisconnected) {
188                return;
189            }
190            mServiceDisconnected = true;
191            int pid = mPID;  // Stash the pid for DeathCallback since stop() will clear it.
192            boolean disconnectedWhileBeingSetUp = mConnectionParams != null;
193            Log.w(TAG, "onServiceDisconnected (crash or killed by oom): pid=" + pid);
194            stop();  // We don't want to auto-restart on crash. Let the browser do that.
195            if (pid != 0) {
196                mDeathCallback.onChildProcessDied(pid);
197            }
198            // TODO(ppi): does anyone know why we need to do that?
199            if (disconnectedWhileBeingSetUp && mConnectionCallback != null) {
200                mConnectionCallback.onConnected(0);
201            }
202        }
203    }
204
205    ChildProcessConnection(Context context, int number, boolean inSandbox,
206            ChildProcessConnection.DeathCallback deathCallback,
207            Class<? extends ChildProcessService> serviceClass) {
208        mContext = context;
209        mServiceNumber = number;
210        mInSandbox = inSandbox;
211        mDeathCallback = deathCallback;
212        mServiceClass = serviceClass;
213        mInitialBinding = new ChildServiceConnection(Context.BIND_AUTO_CREATE);
214        mStrongBinding = new ChildServiceConnection(
215                Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT);
216        mWaivedBinding = new ChildServiceConnection(
217                Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY);
218    }
219
220    int getServiceNumber() {
221        return mServiceNumber;
222    }
223
224    boolean isInSandbox() {
225        return mInSandbox;
226    }
227
228    IChildProcessService getService() {
229        synchronized(mLock) {
230            return mService;
231        }
232    }
233
234    private Intent createServiceBindIntent() {
235        Intent intent = new Intent();
236        intent.setClassName(mContext, mServiceClass.getName() + mServiceNumber);
237        intent.setPackage(mContext.getPackageName());
238        return intent;
239    }
240
241    /**
242     * Starts a connection to an IChildProcessService. This must be followed by a call to
243     * setupConnection() to setup the connection parameters. start() and setupConnection() are
244     * separate to allow the client to pass whatever parameters they have available here, and
245     * complete the remainder later while reducing the connection setup latency.
246     * @param commandLine (Optional) Command line for the child process. If omitted, then
247     *                    the command line parameters must instead be passed to setupConnection().
248     */
249    void start(String[] commandLine) {
250        synchronized(mLock) {
251            TraceEvent.begin();
252            assert !ThreadUtils.runningOnUiThread();
253
254            if (!mInitialBinding.bind(commandLine)) {
255                onBindFailed();
256            } else {
257                mWaivedBinding.bind(null);
258            }
259            TraceEvent.end();
260        }
261    }
262
263    /**
264     * Setups the connection after it was started with start(). This method should be called by the
265     * consumer of the class to set up additional connection parameters.
266     * @param commandLine (Optional) will be ignored if the command line was already sent in bind()
267     * @param fileToBeMapped a list of file descriptors that should be registered
268     * @param callback Used for status updates regarding this process connection.
269     * @param connectionCallbacks will notify the consumer about the connection being established
270     * and the status of the out-of-memory bindings being bound for the connection.
271     */
272    void setupConnection(
273            String[] commandLine,
274            FileDescriptorInfo[] filesToBeMapped,
275            IChildProcessCallback processCallback,
276            ConnectionCallback connectionCallbacks) {
277        synchronized(mLock) {
278            TraceEvent.begin();
279            assert mConnectionParams == null;
280            mConnectionCallback = connectionCallbacks;
281            mConnectionParams = new ConnectionParams(commandLine, filesToBeMapped, processCallback);
282            // Make sure that the service is already connected. If not, doConnectionSetup() will be
283            // called from onServiceConnected().
284            if (mServiceConnectComplete) {
285                doConnectionSetup();
286            }
287            TraceEvent.end();
288        }
289    }
290
291    /**
292     * Terminates the connection to IChildProcessService, closing all bindings. It is safe to call
293     * this multiple times.
294     */
295    void stop() {
296        synchronized(mLock) {
297            mInitialBinding.unbind();
298            mStrongBinding.unbind();
299            mWaivedBinding.unbind();
300            mAttachAsActiveCount = 0;
301            if (mService != null) {
302                mService = null;
303                mPID = 0;
304            }
305            mConnectionParams = null;
306            mServiceConnectComplete = false;
307        }
308    }
309
310    // Called on the main thread to notify that the bindService() call failed (returned false).
311    private void onBindFailed() {
312        mServiceConnectComplete = true;
313        if (mConnectionParams != null) {
314            doConnectionSetup();
315        }
316    }
317
318    /**
319     * Called after the connection parameters have been set (in setupConnection()) *and* a
320     * connection has been established (as signaled by onServiceConnected()) or failed (as signaled
321     * by onBindFailed(), in this case mService will be null). These two events can happen in any
322     * order.
323     */
324    private void doConnectionSetup() {
325        TraceEvent.begin();
326        assert mServiceConnectComplete && mConnectionParams != null;
327
328        if (mService != null) {
329            Bundle bundle = new Bundle();
330            bundle.putStringArray(EXTRA_COMMAND_LINE, mConnectionParams.mCommandLine);
331
332            FileDescriptorInfo[] fileInfos = mConnectionParams.mFilesToBeMapped;
333            ParcelFileDescriptor[] parcelFiles = new ParcelFileDescriptor[fileInfos.length];
334            for (int i = 0; i < fileInfos.length; i++) {
335                if (fileInfos[i].mFd == -1) {
336                    // If someone provided an invalid FD, they are doing something wrong.
337                    Log.e(TAG, "Invalid FD (id=" + fileInfos[i].mId + ") for process connection, "
338                          + "aborting connection.");
339                    return;
340                }
341                String idName = EXTRA_FILES_PREFIX + i + EXTRA_FILES_ID_SUFFIX;
342                String fdName = EXTRA_FILES_PREFIX + i + EXTRA_FILES_FD_SUFFIX;
343                if (fileInfos[i].mAutoClose) {
344                    // Adopt the FD, it will be closed when we close the ParcelFileDescriptor.
345                    parcelFiles[i] = ParcelFileDescriptor.adoptFd(fileInfos[i].mFd);
346                } else {
347                    try {
348                        parcelFiles[i] = ParcelFileDescriptor.fromFd(fileInfos[i].mFd);
349                    } catch(IOException e) {
350                        Log.e(TAG,
351                              "Invalid FD provided for process connection, aborting connection.",
352                              e);
353                        return;
354                    }
355
356                }
357                bundle.putParcelable(fdName, parcelFiles[i]);
358                bundle.putInt(idName, fileInfos[i].mId);
359            }
360            // Add the CPU properties now.
361            bundle.putInt(EXTRA_CPU_COUNT, CpuFeatures.getCount());
362            bundle.putLong(EXTRA_CPU_FEATURES, CpuFeatures.getMask());
363
364            try {
365                mPID = mService.setupConnection(bundle, mConnectionParams.mCallback);
366            } catch (android.os.RemoteException re) {
367                Log.e(TAG, "Failed to setup connection.", re);
368            }
369            // We proactively close the FDs rather than wait for GC & finalizer.
370            try {
371                for (ParcelFileDescriptor parcelFile : parcelFiles) {
372                    if (parcelFile != null) parcelFile.close();
373                }
374            } catch (IOException ioe) {
375                Log.w(TAG, "Failed to close FD.", ioe);
376            }
377        }
378        mConnectionParams = null;
379
380        if (mConnectionCallback != null) {
381            mConnectionCallback.onConnected(getPid());
382        }
383        TraceEvent.end();
384    }
385
386    /** @return true iff the initial oom binding is currently bound. */
387    boolean isInitialBindingBound() {
388        synchronized(mLock) {
389            return mInitialBinding.isBound();
390        }
391    }
392
393    /**
394     * Called to remove the strong binding estabilished when the connection was started. It is safe
395     * to call this multiple times.
396     */
397    void removeInitialBinding() {
398        synchronized(mLock) {
399            mInitialBinding.unbind();
400        }
401    }
402
403    /**
404     * Called when the service becomes active, ie important to the caller. This is handled by
405     * setting up a binding that will make the service as important as the main process. We allow
406     * callers to indicate the same connection as active multiple times. Instead of maintaining
407     * multiple bindings, we count the requests and unbind when the count drops to zero.
408     */
409    void attachAsActive() {
410        synchronized(mLock) {
411            if (mService == null) {
412                Log.w(TAG, "The connection is not bound for " + mPID);
413                return;
414            }
415            if (mAttachAsActiveCount == 0) {
416                mStrongBinding.bind(null);
417            }
418            mAttachAsActiveCount++;
419        }
420    }
421
422    /**
423     * Called when the service is no longer considered active.
424     */
425    void detachAsActive() {
426        synchronized(mLock) {
427            if (mService == null) {
428                Log.w(TAG, "The connection is not bound for " + mPID);
429                return;
430            }
431            assert mAttachAsActiveCount > 0;
432            mAttachAsActiveCount--;
433            if (mAttachAsActiveCount == 0) {
434                mStrongBinding.unbind();
435            }
436        }
437    }
438
439    /**
440     * @return The connection PID, or 0 if not yet connected.
441     */
442    int getPid() {
443        synchronized(mLock) {
444            return mPID;
445        }
446    }
447}
448