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