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