| 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 | } |