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