ChildProcessConnection.java revision c2e0dbddbe15c98d52c4786dac06cb8952a8ae6d
1// Copyright (c) 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.ThreadUtils;
25import org.chromium.content.app.ChildProcessService;
26import org.chromium.content.common.CommandLine;
27import org.chromium.content.common.IChildProcessCallback;
28import org.chromium.content.common.IChildProcessService;
29import org.chromium.content.common.TraceEvent;
30
31public class ChildProcessConnection implements ServiceConnection {
32    interface DeathCallback {
33        void onChildProcessDied(int pid);
34    }
35
36    // Names of items placed in the bind intent or connection bundle.
37    public static final String EXTRA_COMMAND_LINE =
38            "com.google.android.apps.chrome.extra.command_line";
39    // Note the FDs may only be passed in the connection bundle.
40    public static final String EXTRA_FILES_PREFIX =
41            "com.google.android.apps.chrome.extra.extraFile_";
42    public static final String EXTRA_FILES_ID_SUFFIX = "_id";
43    public static final String EXTRA_FILES_FD_SUFFIX = "_fd";
44
45    // Used to pass the CPU core count to child processes.
46    public static final String EXTRA_CPU_COUNT =
47            "com.google.android.apps.chrome.extra.cpu_count";
48    // Used to pass the CPU features mask to child processes.
49    public static final String EXTRA_CPU_FEATURES =
50            "com.google.android.apps.chrome.extra.cpu_features";
51
52    private final Context mContext;
53    private final int mServiceNumber;
54    private final boolean mInSandbox;
55    private final ChildProcessConnection.DeathCallback mDeathCallback;
56    private final Class<? extends ChildProcessService> mServiceClass;
57
58    // Synchronization: While most internal flow occurs on the UI thread, the public API
59    // (specifically bind and unbind) may be called from any thread, hence all entry point methods
60    // into the class are synchronized on the ChildProcessConnection instance to protect access
61    // to these members. But see also the TODO where AsyncBoundServiceConnection is created.
62    private final Object mUiThreadLock = new Object();
63    private IChildProcessService mService = null;
64    private boolean mServiceConnectComplete = false;
65    private int mPID = 0;  // Process ID of the corresponding child process.
66    private HighPriorityConnection mHighPriorityConnection = null;
67    private int mHighPriorityConnectionCount = 0;
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 Runnable mOnConnectionCallback;
76
77        ConnectionParams(
78                String[] commandLine,
79                FileDescriptorInfo[] filesToBeMapped,
80                IChildProcessCallback callback,
81                Runnable onConnectionCallback) {
82            mCommandLine = commandLine;
83            mFilesToBeMapped = filesToBeMapped;
84            mCallback = callback;
85            mOnConnectionCallback = onConnectionCallback;
86        }
87    }
88
89    // This is only valid while the connection is being established.
90    private ConnectionParams mConnectionParams;
91    private boolean mIsBound;
92
93    ChildProcessConnection(Context context, int number, boolean inSandbox,
94            ChildProcessConnection.DeathCallback deathCallback,
95            Class<? extends ChildProcessService> serviceClass) {
96        mContext = context;
97        mServiceNumber = number;
98        mInSandbox = inSandbox;
99        mDeathCallback = deathCallback;
100        mServiceClass = serviceClass;
101    }
102
103    int getServiceNumber() {
104        return mServiceNumber;
105    }
106
107    boolean isInSandbox() {
108        return mInSandbox;
109    }
110
111    IChildProcessService getService() {
112        synchronized(mUiThreadLock) {
113            return mService;
114        }
115    }
116
117    private Intent createServiceBindIntent() {
118        Intent intent = new Intent();
119        intent.setClassName(mContext, mServiceClass.getName() + mServiceNumber);
120        intent.setPackage(mContext.getPackageName());
121        return intent;
122    }
123
124    /**
125     * Bind to an IChildProcessService. This must be followed by a call to setupConnection()
126     * to setup the connection parameters. (These methods are separated to allow the client
127     * to pass whatever parameters they have available here, and complete the remainder
128     * later while reducing the connection setup latency).
129     * @param commandLine (Optional) Command line for the child process. If omitted, then
130     *                    the command line parameters must instead be passed to setupConnection().
131     */
132    void bind(String[] commandLine) {
133        synchronized(mUiThreadLock) {
134            TraceEvent.begin();
135            assert !ThreadUtils.runningOnUiThread();
136
137            final Intent intent = createServiceBindIntent();
138
139            if (commandLine != null) {
140                intent.putExtra(EXTRA_COMMAND_LINE, commandLine);
141            }
142
143            mIsBound = mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
144            if (!mIsBound) {
145                onBindFailed();
146            }
147            TraceEvent.end();
148        }
149    }
150
151    /** Setup a connection previous bound via a call to bind().
152     *
153     * This establishes the parameters that were not already supplied in bind.
154     * @param commandLine (Optional) will be ignored if the command line was already sent in bind()
155     * @param fileToBeMapped a list of file descriptors that should be registered
156     * @param callback Used for status updates regarding this process connection.
157     * @param onConnectionCallback will be run when the connection is setup and ready to use.
158     */
159    void setupConnection(
160            String[] commandLine,
161            FileDescriptorInfo[] filesToBeMapped,
162            IChildProcessCallback callback,
163            Runnable onConnectionCallback) {
164        synchronized(mUiThreadLock) {
165            TraceEvent.begin();
166            assert mConnectionParams == null;
167            mConnectionParams = new ConnectionParams(commandLine, filesToBeMapped, callback,
168                    onConnectionCallback);
169            if (mServiceConnectComplete) {
170                doConnectionSetup();
171            }
172            TraceEvent.end();
173        }
174    }
175
176    /**
177     * Unbind the IChildProcessService. It is safe to call this multiple times.
178     */
179    void unbind() {
180        synchronized(mUiThreadLock) {
181            if (mIsBound) {
182                mContext.unbindService(this);
183                mIsBound = false;
184            }
185            if (mService != null) {
186                if (mHighPriorityConnection != null) {
187                    unbindHighPriority(true);
188                }
189                mService = null;
190                mPID = 0;
191            }
192            mConnectionParams = null;
193            mServiceConnectComplete = false;
194        }
195    }
196
197    // Called on the main thread to notify that the service is connected.
198    @Override
199    public void onServiceConnected(ComponentName className, IBinder service) {
200        synchronized(mUiThreadLock) {
201            TraceEvent.begin();
202            mServiceConnectComplete = true;
203            mService = IChildProcessService.Stub.asInterface(service);
204            if (mConnectionParams != null) {
205                doConnectionSetup();
206            }
207            TraceEvent.end();
208        }
209    }
210
211    // Called on the main thread to notify that the bindService() call failed (returned false).
212    private void onBindFailed() {
213        mServiceConnectComplete = true;
214        if (mConnectionParams != null) {
215            doConnectionSetup();
216        }
217    }
218
219    /**
220     * Called when the connection parameters have been set, and a connection has been established
221     * (as signaled by onServiceConnected), or if the connection failed (mService will be false).
222     */
223    private void doConnectionSetup() {
224        TraceEvent.begin();
225        assert mServiceConnectComplete && mConnectionParams != null;
226        // Capture the callback before it is potentially nulled in unbind().
227        Runnable onConnectionCallback = mConnectionParams.mOnConnectionCallback;
228        if (onConnectionCallback == null) {
229            unbind();
230        } else if (mService != null) {
231            Bundle bundle = new Bundle();
232            bundle.putStringArray(EXTRA_COMMAND_LINE, mConnectionParams.mCommandLine);
233
234            FileDescriptorInfo[] fileInfos = mConnectionParams.mFilesToBeMapped;
235            ParcelFileDescriptor[] parcelFiles = new ParcelFileDescriptor[fileInfos.length];
236            for (int i = 0; i < fileInfos.length; i++) {
237                if (fileInfos[i].mFd == -1) {
238                    // If someone provided an invalid FD, they are doing something wrong.
239                    Log.e(TAG, "Invalid FD (id=" + fileInfos[i].mId + ") for process connection, "
240                          + "aborting connection.");
241                    return;
242                }
243                String idName = EXTRA_FILES_PREFIX + i + EXTRA_FILES_ID_SUFFIX;
244                String fdName = EXTRA_FILES_PREFIX + i + EXTRA_FILES_FD_SUFFIX;
245                if (fileInfos[i].mAutoClose) {
246                    // Adopt the FD, it will be closed when we close the ParcelFileDescriptor.
247                    parcelFiles[i] = ParcelFileDescriptor.adoptFd(fileInfos[i].mFd);
248                } else {
249                    try {
250                        parcelFiles[i] = ParcelFileDescriptor.fromFd(fileInfos[i].mFd);
251                    } catch(IOException e) {
252                        Log.e(TAG,
253                              "Invalid FD provided for process connection, aborting connection.",
254                              e);
255                        return;
256                    }
257
258                }
259                bundle.putParcelable(fdName, parcelFiles[i]);
260                bundle.putInt(idName, fileInfos[i].mId);
261            }
262            // Add the CPU properties now.
263            bundle.putInt(EXTRA_CPU_COUNT, CpuFeatures.getCount());
264            bundle.putLong(EXTRA_CPU_FEATURES, CpuFeatures.getMask());
265
266            try {
267                mPID = mService.setupConnection(bundle, mConnectionParams.mCallback);
268            } catch (android.os.RemoteException re) {
269                Log.e(TAG, "Failed to setup connection.", re);
270            }
271            // We proactivley close the FDs rather than wait for GC & finalizer.
272            try {
273                for (ParcelFileDescriptor parcelFile : parcelFiles) {
274                    if (parcelFile != null) parcelFile.close();
275                }
276            } catch (IOException ioe) {
277                Log.w(TAG, "Failed to close FD.", ioe);
278            }
279        }
280        mConnectionParams = null;
281        if (onConnectionCallback != null) {
282            onConnectionCallback.run();
283        }
284        TraceEvent.end();
285    }
286
287    // Called on the main thread to notify that the child service did not disconnect gracefully.
288    @Override
289    public void onServiceDisconnected(ComponentName className) {
290        int pid = mPID;  // Stash pid & connection callback since unbind() will clear them.
291        Runnable onConnectionCallback =
292            mConnectionParams != null ? mConnectionParams.mOnConnectionCallback : null;
293        Log.w(TAG, "onServiceDisconnected (crash?): pid=" + pid);
294        unbind();  // We don't want to auto-restart on crash. Let the browser do that.
295        if (pid != 0) {
296            mDeathCallback.onChildProcessDied(pid);
297        }
298        if (onConnectionCallback != null) {
299            onConnectionCallback.run();
300        }
301    }
302
303    /**
304     * Bind the service with a new high priority connection. This will make the service
305     * as important as the main process.
306     */
307    void bindHighPriority() {
308        synchronized(mUiThreadLock) {
309            if (mService == null) {
310                Log.w(TAG, "The connection is not bound for " + mPID);
311                return;
312            }
313            if (mHighPriorityConnection == null) {
314                mHighPriorityConnection = new HighPriorityConnection();
315                mHighPriorityConnection.bind();
316            }
317            mHighPriorityConnectionCount++;
318        }
319    }
320
321    /**
322     * Unbind the service as the high priority connection.
323     */
324    void unbindHighPriority(boolean force) {
325        synchronized(mUiThreadLock) {
326            if (mService == null) {
327                Log.w(TAG, "The connection is not bound for " + mPID);
328                return;
329            }
330            mHighPriorityConnectionCount--;
331            if (force || (mHighPriorityConnectionCount == 0 && mHighPriorityConnection != null)) {
332                mHighPriorityConnection.unbind();
333                mHighPriorityConnection = null;
334            }
335        }
336    }
337
338    private class HighPriorityConnection implements ServiceConnection {
339
340        private boolean mHBound = false;
341
342        void bind() {
343            final Intent intent = createServiceBindIntent();
344
345            mHBound = mContext.bindService(intent, this,
346                    Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT);
347        }
348
349        void unbind() {
350            if (mHBound) {
351                mContext.unbindService(this);
352                mHBound = false;
353            }
354        }
355
356        @Override
357        public void onServiceConnected(ComponentName className, IBinder service) {
358        }
359
360        @Override
361        public void onServiceDisconnected(ComponentName className) {
362        }
363    }
364
365    /**
366     * @return The connection PID, or 0 if not yet connected.
367     */
368    public int getPid() {
369        synchronized(mUiThreadLock) {
370            return mPID;
371        }
372    }
373}
374