Index: content/public/android/java/org/chromium/content/browser/SandboxedProcessConnection.java |
diff --git a/content/public/android/java/org/chromium/content/browser/SandboxedProcessConnection.java b/content/public/android/java/org/chromium/content/browser/SandboxedProcessConnection.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..acc33ad6e957b7db5587e6288a5a36f248abdbf2 |
--- /dev/null |
+++ b/content/public/android/java/org/chromium/content/browser/SandboxedProcessConnection.java |
@@ -0,0 +1,429 @@ |
+// Copyright (c) 2012 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.content.browser; |
+ |
+import android.content.ComponentName; |
+import android.content.Context; |
+import android.content.Intent; |
+import android.content.ServiceConnection; |
+import android.os.AsyncTask; |
+import android.os.Bundle; |
+import android.os.Handler; |
+import android.os.IBinder; |
+import android.os.Looper; |
+import android.os.ParcelFileDescriptor; |
+import android.util.Log; |
+ |
+import java.util.concurrent.atomic.AtomicBoolean; |
+ |
+import org.chromium.base.CalledByNative; |
+import org.chromium.content.app.SandboxedProcessService; |
+import org.chromium.content.browser.CommandLine; |
+import org.chromium.content.common.ISandboxedProcessCallback; |
+import org.chromium.content.common.ISandboxedProcessService; |
+// TODO(michaelbai): Move to org.chromium.content.commnon. |
+import org.chromium.content.browser.TraceEvent; |
+ |
+public class SandboxedProcessConnection { |
+ interface DeathCallback { |
+ void onSandboxedProcessDied(int pid); |
+ } |
+ |
+ // Names of items placed in the bind intent or connection bundle. |
+ public static final String EXTRA_COMMAND_LINE = |
+ "com.google.android.apps.chrome.extra.sandbox_command_line"; |
+ // Note the FD may only be passed in the connection bundle. |
+ public static final String EXTRA_IPC_FD = "com.google.android.apps.chrome.extra.sandbox_ipcFd"; |
+ public static final String EXTRA_CRASH_FD = |
+ "com.google.android.apps.chrome.extra.sandbox_crashFd"; |
+ public static final String EXTRA_CHROME_PAK_FD = |
+ "com.google.android.apps.chrome.extra.sandbox_chromePakFd"; |
+ public static final String EXTRA_LOCALE_PAK_FD = |
+ "com.google.android.apps.chrome.extra.sandbox_localePakFd"; |
+ |
+ private final Context mContext; |
+ private final int mServiceNumber; |
+ private final SandboxedProcessConnection.DeathCallback mDeathCallback; |
+ |
+ // Synchronization: While most internal flow occurs on the UI thread, the public API |
+ // (specifically bind and unbind) may be called from any thread, hence all entry point methods |
+ // into the class are synchronized on the SandboxedProcessConnection instance to protect access |
+ // to these members. But see also the TODO where AsyncBoundServiceConnection is created. |
+ private ISandboxedProcessService mService = null; |
+ private boolean mServiceConnectComplete = false; |
+ private int mPID = 0; // Process ID of the corresponding sandboxed process. |
+ private HighPriorityConnection mHighPriorityConnection = null; |
+ private int mHighPriorityConnectionCount = 0; |
+ |
+ private static final String TAG = "SandboxedProcessConnection"; |
+ |
+ private static class ConnectionParams { |
+ final String[] mCommandLine; |
+ final int mIpcFd; |
+ final int mCrashFd; |
+ final int mChromePakFd; |
+ final int mLocalePakFd; |
+ final ISandboxedProcessCallback mCallback; |
+ final Runnable mOnConnectionCallback; |
+ |
+ ConnectionParams( |
+ String[] commandLine, |
+ int ipcFd, |
+ int crashFd, |
+ int chromePakFd, |
+ int localePakFd, |
+ ISandboxedProcessCallback callback, |
+ Runnable onConnectionCallback) { |
+ mCommandLine = commandLine; |
+ mIpcFd = ipcFd; |
+ mCrashFd = crashFd; |
+ mChromePakFd = chromePakFd; |
+ mLocalePakFd = localePakFd; |
+ mCallback = callback; |
+ mOnConnectionCallback = onConnectionCallback; |
+ } |
+ }; |
+ |
+ // Implement the ServiceConnection as an inner class, so it can stem the service |
+ // callbacks when cancelled or unbound. |
+ class AsyncBoundServiceConnection extends AsyncTask<Intent, Void, Boolean> |
+ implements ServiceConnection { |
+ private boolean mIsDestroyed = false; |
+ private AtomicBoolean mIsBound = new AtomicBoolean(false); |
+ |
+ // AsyncTask |
+ @Override |
+ protected Boolean doInBackground(Intent... intents) { |
+ boolean isBound = mContext.bindService(intents[0], this, Context.BIND_AUTO_CREATE); |
+ mIsBound.set(isBound); |
+ return isBound; |
+ } |
+ |
+ @Override |
+ protected void onPostExecute(Boolean boundOK) { |
+ synchronized (SandboxedProcessConnection.this) { |
+ if (!boundOK && !mIsDestroyed) { |
+ SandboxedProcessConnection.this.onBindFailed(); |
+ } |
+ // else: bind will complete asynchronously with a callback to onServiceConnected(). |
+ } |
+ } |
+ |
+ @Override |
+ protected void onCancelled(Boolean boundOK) { |
+ // According to {@link AsyncTask#onCancelled(Object)}, the Object can be null. |
+ if (boundOK != null && boundOK) { |
+ unBindIfAble(); |
+ } |
+ } |
+ |
+ /** |
+ * Unbinds this connection if it hasn't already been unbound. There's a guard to check that |
+ * we haven't already been unbound because the Browser process cancelling a connection can |
+ * race with something else (Android?) cancelling the connection. |
+ */ |
+ private void unBindIfAble() { |
+ if (mIsBound.getAndSet(false)) { |
+ mContext.unbindService(this); |
+ } |
+ } |
+ |
+ // ServiceConnection |
+ @Override |
+ public void onServiceConnected(ComponentName name, IBinder service) { |
+ synchronized (SandboxedProcessConnection.this) { |
+ if (!mIsDestroyed) { |
+ SandboxedProcessConnection.this.onServiceConnected(name, service); |
+ } |
+ } |
+ } |
+ |
+ @Override |
+ public void onServiceDisconnected(ComponentName name) { |
+ synchronized (SandboxedProcessConnection.this) { |
+ if (!mIsDestroyed) { |
+ SandboxedProcessConnection.this.onServiceDisconnected(name); |
+ } |
+ } |
+ } |
+ |
+ public void destroy() { |
+ assert Thread.holdsLock(SandboxedProcessConnection.this); |
+ if (!cancel(false)) { |
+ unBindIfAble(); |
+ } |
+ mIsDestroyed = true; |
+ } |
+ } |
+ |
+ // This is only valid while the connection is being established. |
+ private ConnectionParams mConnectionParams; |
+ private AsyncBoundServiceConnection mServiceConnection; |
+ |
+ SandboxedProcessConnection(Context context, int number, |
+ SandboxedProcessConnection.DeathCallback deathCallback) { |
+ mContext = context; |
+ mServiceNumber = number; |
+ mDeathCallback = deathCallback; |
+ } |
+ |
+ int getServiceNumber() { |
+ return mServiceNumber; |
+ } |
+ |
+ synchronized ISandboxedProcessService getService() { |
+ return mService; |
+ } |
+ |
+ private Intent createServiceBindIntent() { |
+ Intent intent = new Intent(); |
+ String n = SandboxedProcessService.class.getName(); |
+ intent.setClassName(mContext, n + mServiceNumber); |
+ intent.setPackage(mContext.getPackageName()); |
+ return intent; |
+ } |
+ |
+ /** |
+ * Bind to an ISandboxedProcessService. This must be followed by a call to setupConnection() |
+ * to setup the connection parameters. (These methods are separated to allow the client |
+ * to pass whatever parameters they have available here, and complete the remainder |
+ * later while reducing the connection setup latency). |
+ * |
+ * @param commandLine (Optional) Command line for the sandboxed process. If omitted, then |
+ * the command line parameters must instead be passed to setupConnection(). |
+ */ |
+ synchronized void bind(String[] commandLine) { |
+ TraceEvent.begin(); |
+ final Intent intent = createServiceBindIntent(); |
+ |
+ if (commandLine != null) { |
+ intent.putExtra(EXTRA_COMMAND_LINE, commandLine); |
+ } |
+ // TODO(joth): By the docs, AsyncTasks should only be created on the UI thread, but |
+ // bind() currently 'may' be called on any thread. In practice it's only ever called |
+ // from UI, but it's not guaranteed. See http://b/5694925. |
+ Looper mainLooper = Looper.getMainLooper(); |
+ if (Looper.myLooper() == mainLooper) { |
+ mServiceConnection = new AsyncBoundServiceConnection(); |
+ // On completion this will call back to onServiceConnected(). |
+ mServiceConnection.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, intent); |
+ } else { |
+ // TODO(jcivelli): http://b/5694925 we only have to post to the UI thread because we use |
+ // an AsyncTask and it requires it. Replace that AsyncTask by running our own thread and |
+ // change this so we run directly from the current thread. |
+ new Handler(mainLooper).postAtFrontOfQueue(new Runnable() { |
+ public void run() { |
+ mServiceConnection = new AsyncBoundServiceConnection(); |
+ // On completion this will call back to onServiceConnected(). |
+ mServiceConnection.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, |
+ intent); |
+ } |
+ }); |
+ } |
+ TraceEvent.end(); |
+ } |
+ |
+ /** Setup a connection previous bound via a call to bind(). |
+ * |
+ * This establishes the parameters that were not already supplied in bind. |
+ * @param commandLine (Optional) will be ignored if the command line was already sent in bind() |
+ * @param ipcFd The file descriptor that will be used by the sandbox process for IPC. |
+ * @param crashFd (Optional) file descriptor that will be used for crash dumps. |
+ * @param callback Used for status updates regarding this process connection. |
+ * @param onConnectionCallback will be run when the connection is setup and ready to use. |
+ */ |
+ synchronized void setupConnection( |
+ String[] commandLine, |
+ int ipcFd, |
+ int crashFd, |
+ int chromePakFd, |
+ int localePakFd, |
+ ISandboxedProcessCallback callback, |
+ Runnable onConnectionCallback) { |
+ TraceEvent.begin(); |
+ assert mConnectionParams == null; |
+ mConnectionParams = new ConnectionParams(commandLine, ipcFd, crashFd, chromePakFd, |
+ localePakFd, callback, onConnectionCallback); |
+ if (mServiceConnectComplete) { |
+ doConnectionSetup(); |
+ } |
+ TraceEvent.end(); |
+ } |
+ |
+ /** |
+ * Unbind the ISandboxedProcessService. It is safe to call this multiple times. |
+ */ |
+ synchronized void unbind() { |
+ if (mServiceConnection != null) { |
+ mServiceConnection.destroy(); |
+ mServiceConnection = null; |
+ } |
+ if (mService != null) { |
+ if (mHighPriorityConnection != null) { |
+ unbindHighPriority(true); |
+ } |
+ mService = null; |
+ mPID = 0; |
+ } |
+ mConnectionParams = null; |
+ mServiceConnectComplete = false; |
+ } |
+ |
+ // Called on the main thread to notify that the service is connected. |
+ private void onServiceConnected(ComponentName className, IBinder service) { |
+ assert Thread.holdsLock(this); |
+ TraceEvent.begin(); |
+ mServiceConnectComplete = true; |
+ mService = ISandboxedProcessService.Stub.asInterface(service); |
+ if (mConnectionParams != null) { |
+ doConnectionSetup(); |
+ } |
+ TraceEvent.end(); |
+ } |
+ |
+ // Called on the main thread to notify that the bindService() call failed (returned false). |
+ private void onBindFailed() { |
+ assert Thread.holdsLock(this); |
+ mServiceConnectComplete = true; |
+ if (mConnectionParams != null) { |
+ doConnectionSetup(); |
+ } |
+ } |
+ |
+ /** |
+ * Called when the connection parameters have been set, and a connection has been established |
+ * (as signaled by onServiceConnected), or if the connection failed (mService will be false). |
+ */ |
+ private void doConnectionSetup() { |
+ TraceEvent.begin(); |
+ assert mServiceConnectComplete && mConnectionParams != null; |
+ // Capture the callback before it is potentially nulled in unbind(). |
+ Runnable onConnectionCallback = |
+ mConnectionParams != null ? mConnectionParams.mOnConnectionCallback : null; |
+ if (onConnectionCallback == null) { |
+ unbind(); |
+ } else if (mService != null) { |
+ try { |
+ ParcelFileDescriptor ipcFdParcel = |
+ ParcelFileDescriptor.fromFd(mConnectionParams.mIpcFd); |
+ Bundle bundle = new Bundle(); |
+ bundle.putStringArray(EXTRA_COMMAND_LINE, mConnectionParams.mCommandLine); |
+ bundle.putParcelable(EXTRA_IPC_FD, ipcFdParcel); |
+ |
+ ParcelFileDescriptor chromePakFdParcel = |
+ ParcelFileDescriptor.fromFd(mConnectionParams.mChromePakFd); |
+ bundle.putParcelable(EXTRA_CHROME_PAK_FD, chromePakFdParcel); |
+ |
+ ParcelFileDescriptor localePakFdParcel = |
+ ParcelFileDescriptor.fromFd(mConnectionParams.mLocalePakFd); |
+ bundle.putParcelable(EXTRA_LOCALE_PAK_FD, localePakFdParcel); |
+ |
+ try { |
+ ParcelFileDescriptor crashFdParcel = |
+ ParcelFileDescriptor.fromFd(mConnectionParams.mCrashFd); |
+ bundle.putParcelable(EXTRA_CRASH_FD, crashFdParcel); |
+ // We will let the GC close the crash ParcelFileDescriptor. |
+ } catch (java.io.IOException e) { |
+ Log.w(TAG, "Invalid crash Fd. Native crash reporting will be disabled."); |
+ } |
+ |
+ mPID = mService.setupConnection(bundle, mConnectionParams.mCallback); |
+ ipcFdParcel.close(); // We proactivley close now rather than wait for GC & |
+ // finalizer. |
+ } catch(java.io.IOException e) { |
+ Log.w(TAG, "Invalid ipc FD."); |
+ } catch(android.os.RemoteException e) { |
+ Log.w(TAG, "Exception when trying to call service method: " |
+ + e); |
+ } |
+ } |
+ mConnectionParams = null; |
+ if (onConnectionCallback != null) { |
+ onConnectionCallback.run(); |
+ } |
+ TraceEvent.end(); |
+ } |
+ |
+ // Called on the main thread to notify that the sandboxed service did not disconnect gracefully. |
+ private void onServiceDisconnected(ComponentName className) { |
+ assert Thread.holdsLock(this); |
+ int pid = mPID; // Stash pid & connection callback since unbind() will clear them. |
+ Runnable onConnectionCallback = |
+ mConnectionParams != null ? mConnectionParams.mOnConnectionCallback : null; |
+ Log.w(TAG, "onServiceDisconnected (crash?): pid=" + pid); |
+ unbind(); // We don't want to auto-restart on crash. Let the browser do that. |
+ if (pid != 0) { |
+ mDeathCallback.onSandboxedProcessDied(pid); |
+ } |
+ if (onConnectionCallback != null) { |
+ onConnectionCallback.run(); |
+ } |
+ } |
+ |
+ /** |
+ * Bind the service with a new high priority connection. This will make the service |
+ * as important as the main process. |
+ */ |
+ synchronized void bindHighPriority() { |
+ if (mService == null) { |
+ Log.w(TAG, "The connection is not bound for " + mPID); |
+ return; |
+ } |
+ if (mHighPriorityConnection == null) { |
+ mHighPriorityConnection = new HighPriorityConnection(); |
+ mHighPriorityConnection.bind(); |
+ } |
+ mHighPriorityConnectionCount++; |
+ } |
+ |
+ /** |
+ * Unbind the service as the high priority connection. |
+ */ |
+ synchronized void unbindHighPriority(boolean force) { |
+ if (mService == null) { |
+ Log.w(TAG, "The connection is not bound for " + mPID); |
+ return; |
+ } |
+ mHighPriorityConnectionCount--; |
+ if (force || (mHighPriorityConnectionCount == 0 && mHighPriorityConnection != null)) { |
+ mHighPriorityConnection.unbind(); |
+ mHighPriorityConnection = null; |
+ } |
+ } |
+ |
+ private class HighPriorityConnection implements ServiceConnection { |
+ |
+ private boolean mHBound = false; |
+ |
+ void bind() { |
+ final Intent intent = createServiceBindIntent(); |
+ |
+ mHBound = mContext.bindService(intent, this, |
+ Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT); |
+ } |
+ |
+ void unbind() { |
+ if (mHBound) { |
+ mContext.unbindService(this); |
+ mHBound = false; |
+ } |
+ } |
+ |
+ @Override |
+ public void onServiceConnected(ComponentName className, IBinder service) { |
+ } |
+ |
+ @Override |
+ public void onServiceDisconnected(ComponentName className) { |
+ } |
+ } |
+ |
+ /** |
+ * @return The connection PID, or 0 if not yet connected. |
+ */ |
+ synchronized public int getPid() { |
+ return mPID; |
+ } |
+} |