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