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