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.common.CommandLine; 28import org.chromium.content.common.IChildProcessCallback; 29import org.chromium.content.common.IChildProcessService; 30import 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 */ 37public 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 ChildProcessConnection instance to protect access to 99 // these members. But see also the TODO where AsyncBoundServiceConnection is created. 100 private final Object mUiThreadLock = 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(mUiThreadLock) { 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(mUiThreadLock) { 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(mUiThreadLock) { 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(mUiThreadLock) { 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(mUiThreadLock) { 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(mUiThreadLock) { 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(mUiThreadLock) { 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(mUiThreadLock) { 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(mUiThreadLock) { 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(mUiThreadLock) { 492 return mPID; 493 } 494 } 495} 496