Commit 9cf15708 authored by Jay Civelli's avatar Jay Civelli Committed by Commit Bot

Splitting ChildProcessLauncherHelper before move to base.

Extracting the launching code that will go to base from
ChildProcessLauncherHelper (CPLH) to ChildProcessLauncher (CPL).
CPLH now creates a CPL that takes care of creating the connection and
setting it up and delegates back to the CPLH for content specific behavior.

Adding tests to validate the CPL API and moving some existing tests to
use CPL instead of CPLH. The new tests use a new test service
(TestChildProcessService) that echoes back the delegate calls called
on it.  
Also removing the CPLH tests validating strong binding connections as
this is already tested in ChildConnectionAllocatorTest.

Bug: 702316
Change-Id: Ic2bda3f621530616cacc60e22007ca36464a0159
Reviewed-on: https://chromium-review.googlesource.com/557890
Commit-Queue: Jay Civelli <jcivelli@chromium.org>
Reviewed-by: default avatarBo Liu <boliu@chromium.org>
Cr-Commit-Position: refs/heads/master@{#485140}
parent bedfe36d
...@@ -114,6 +114,7 @@ android_library("content_java") { ...@@ -114,6 +114,7 @@ android_library("content_java") {
"java/src/org/chromium/content/browser/BrowserStartupController.java", "java/src/org/chromium/content/browser/BrowserStartupController.java",
"java/src/org/chromium/content/browser/ChildConnectionAllocator.java", "java/src/org/chromium/content/browser/ChildConnectionAllocator.java",
"java/src/org/chromium/content/browser/ChildProcessConnection.java", "java/src/org/chromium/content/browser/ChildProcessConnection.java",
"java/src/org/chromium/content/browser/ChildProcessLauncher.java",
"java/src/org/chromium/content/browser/ChildProcessLauncherHelper.java", "java/src/org/chromium/content/browser/ChildProcessLauncherHelper.java",
"java/src/org/chromium/content/browser/ContentChildProcessConstants.java", "java/src/org/chromium/content/browser/ContentChildProcessConstants.java",
"java/src/org/chromium/content/browser/ContentClassFactory.java", "java/src/org/chromium/content/browser/ContentClassFactory.java",
...@@ -373,6 +374,9 @@ group("jni") { ...@@ -373,6 +374,9 @@ group("jni") {
android_library("content_javatests") { android_library("content_javatests") {
testonly = true testonly = true
srcjar_deps = [ "//content/shell/android:content_javatests_aidl" ]
deps = [ deps = [
":content_java", ":content_java",
"//base:base_java", "//base:base_java",
...@@ -396,6 +400,7 @@ android_library("content_javatests") { ...@@ -396,6 +400,7 @@ android_library("content_javatests") {
"//net/android:net_java_test_support", "//net/android:net_java_test_support",
"//third_party/android_support_test_runner:rules_java", "//third_party/android_support_test_runner:rules_java",
"//third_party/android_support_test_runner:runner_java", "//third_party/android_support_test_runner:runner_java",
"//third_party/jsr-305:jsr_305_javalib",
"//third_party/junit", "//third_party/junit",
"//ui/android:ui_java", "//ui/android:ui_java",
"//ui/gfx/geometry/mojo:mojo_java", "//ui/gfx/geometry/mojo:mojo_java",
...@@ -405,6 +410,7 @@ android_library("content_javatests") { ...@@ -405,6 +410,7 @@ android_library("content_javatests") {
"javatests/src/org/chromium/content/browser/BrowserStartupControllerTest.java", "javatests/src/org/chromium/content/browser/BrowserStartupControllerTest.java",
"javatests/src/org/chromium/content/browser/ChildProcessLauncherIntegrationTest.java", "javatests/src/org/chromium/content/browser/ChildProcessLauncherIntegrationTest.java",
"javatests/src/org/chromium/content/browser/ChildProcessLauncherTest.java", "javatests/src/org/chromium/content/browser/ChildProcessLauncherTest.java",
"javatests/src/org/chromium/content/browser/ChildProcessLauncherHelperTest.java",
"javatests/src/org/chromium/content/browser/ClipboardTest.java", "javatests/src/org/chromium/content/browser/ClipboardTest.java",
"javatests/src/org/chromium/content/browser/ContentCommandLineTest.java", "javatests/src/org/chromium/content/browser/ContentCommandLineTest.java",
"javatests/src/org/chromium/content/browser/ContentViewCoreSelectionTest.java", "javatests/src/org/chromium/content/browser/ContentViewCoreSelectionTest.java",
......
...@@ -27,14 +27,14 @@ public class ChildConnectionAllocator { ...@@ -27,14 +27,14 @@ public class ChildConnectionAllocator {
private static final String TAG = "ChildConnAllocator"; private static final String TAG = "ChildConnAllocator";
/** Listener that clients can use to get notified when connections get allocated/freed. */ /** Listener that clients can use to get notified when connections get allocated/freed. */
public interface Listener { public abstract static class Listener {
/** Called when a connection has been allocated, before it gets bound. */ /** Called when a connection has been allocated, before it gets bound. */
void onConnectionAllocated( public void onConnectionAllocated(
ChildConnectionAllocator allocator, ChildProcessConnection connection); ChildConnectionAllocator allocator, ChildProcessConnection connection) {}
/** Called when a connection has been freed. */ /** Called when a connection has been freed. */
void onConnectionFreed( public void onConnectionFreed(
ChildConnectionAllocator allocator, ChildProcessConnection connection); ChildConnectionAllocator allocator, ChildProcessConnection connection) {}
} }
/** Factory interface. Used by tests to specialize created connections. */ /** Factory interface. Used by tests to specialize created connections. */
...@@ -103,8 +103,7 @@ public class ChildConnectionAllocator { ...@@ -103,8 +103,7 @@ public class ChildConnectionAllocator {
// Check that the service exists. // Check that the service exists.
try { try {
// PackageManager#getServiceInfo() throws an exception if the service does not // PackageManager#getServiceInfo() throws an exception if the service does not exist.
// exist.
packageManager.getServiceInfo( packageManager.getServiceInfo(
new ComponentName(packageName, serviceClassName + "0"), 0); new ComponentName(packageName, serviceClassName + "0"), 0);
} catch (PackageManager.NameNotFoundException e) { } catch (PackageManager.NameNotFoundException e) {
......
// Copyright 2017 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.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.TraceEvent;
import org.chromium.base.annotations.SuppressFBWarnings;
import org.chromium.base.process_launcher.ChildProcessConstants;
import org.chromium.base.process_launcher.FileDescriptorInfo;
import java.io.IOException;
/**
* This class is used to start a child process by connecting to a ChildProcessService.
*/
public class ChildProcessLauncher {
private static final String TAG = "ChildProcLauncher";
/** Delegate that client should use to customize the process launching. */
public interface Delegate {
/**
* Called before a connection is allocated.
* Note that this is only called if the ChildProcessLauncher is created with
* {@link #createWithConnectionAllocator}.
* @param serviceBundle the bundle passed in the service intent. Clients can add their own
* extras to the bundle.
*/
void onBeforeConnectionAllocated(Bundle serviceBundle);
/**
* Called before setup is called on the connection.
* @param connectionBundle the bundle passed to the {@link ChildProcessService} in the
* setup call. Clients can add their own extras to the bundle.
*/
void onBeforeConnectionSetup(Bundle connectionBundle);
/**
* Called when the connection was successfully established, meaning the setup call on the
* service was successful.
* @param connection the connection over which the setup call was made.
*/
void onConnectionEstablished(ChildProcessConnection connection);
/**
* Called when a connection has been disconnected. Only invoked if onConnectionEstablished
* was called, meaning the connection was already established.
* @param connection the connection that got disconnected.
*/
void onConnectionLost(ChildProcessConnection connection);
}
/**
* Interface used by clients that already have a bound connection ready when instanciating the
* ChildProcessLauncher.
*/
public interface BoundConnectionProvider {
ChildProcessConnection getConnection(
ChildProcessConnection.ServiceCallback serviceCallback);
}
// Represents an invalid process handle; same as base/process/process.h kNullProcessHandle.
private static final int NULL_PROCESS_HANDLE = 0;
// The handle for the thread we were created on and on which all methods should be called.
private final Handler mLauncherHandler = new Handler();
private final Delegate mDelegate;
private final String[] mCommandLine;
private final FileDescriptorInfo[] mFilesToBeMapped;
// The allocator used to create the connection.
private final BoundConnectionProvider mConnectionProvider;
// The allocator used to create the connection.
private final ChildConnectionAllocator mConnectionAllocator;
// The IBinder provided to the created service.
private final IBinder mIBinderCallback;
// The actual service connection. Set once we have connected to the service.
private ChildProcessConnection mConnection;
/**
* Creates a ChildProcessLauncher using the already bound connection provided.
* Note that onBeforeConnectionAllocated and onConnectionBound will not be invoked on the
* delegate since the connection is already available.
*/
public static ChildProcessLauncher createWithBoundConnectionProvider(Delegate delegate,
String[] commandLine, FileDescriptorInfo[] filesToBeMapped,
BoundConnectionProvider connectionProvider, IBinder binderCallback) {
return new ChildProcessLauncher(delegate, commandLine, filesToBeMapped, connectionProvider,
null /* connectionAllocator */, binderCallback);
}
/**
* Creates a ChildProcessLauncher that will create a connection using the specified
* ChildConnectionAllocator.
*/
public static ChildProcessLauncher createWithConnectionAllocator(Delegate delegate,
String[] commandLine, FileDescriptorInfo[] filesToBeMapped,
ChildConnectionAllocator connectionAllocator, IBinder binderCallback) {
return new ChildProcessLauncher(delegate, commandLine, filesToBeMapped,
null /* connection */, connectionAllocator, binderCallback);
}
@SuppressFBWarnings("EI_EXPOSE_REP2")
private ChildProcessLauncher(Delegate delegate, String[] commandLine,
FileDescriptorInfo[] filesToBeMapped, BoundConnectionProvider connectionProvider,
ChildConnectionAllocator connectionAllocator, IBinder binderCallback) {
// Either a bound connection provider or a connection allocator should be provided.
assert (connectionProvider == null) != (connectionAllocator == null);
mCommandLine = commandLine;
mConnectionProvider = connectionProvider;
mConnectionAllocator = connectionAllocator;
mDelegate = delegate;
mFilesToBeMapped = filesToBeMapped;
mIBinderCallback = binderCallback;
}
/**
* Starts the child process and calls setup on it if {@param setupConnection} is true.
* @param setupConnection whether the setup should be performed on the connection once
* established
* @param queueIfNoFreeConnection whether to queue that request if no service connection is
* available. If the launcher was created with a connection provider, this parameter has no
* effect.
* @return true if the connection was started or was queued.
*/
public boolean start(final boolean setupConnection, final boolean queueIfNoFreeConnection) {
assert isRunningOnLauncherThread();
try {
TraceEvent.begin("ChildProcessLauncher.start");
ChildProcessConnection.ServiceCallback serviceCallback =
new ChildProcessConnection.ServiceCallback() {
@Override
public void onChildStarted() {}
@Override
public void onChildStartFailed() {
assert isRunningOnLauncherThread();
Log.e(TAG, "ChildProcessConnection.start failed, trying again");
mLauncherHandler.post(new Runnable() {
@Override
public void run() {
// The child process may already be bound to another client
// (this can happen if multi-process WebView is used in more
// than one process), so try starting the process again.
// This connection that failed to start has not been freed,
// so a new bound connection will be allocated.
mConnection = null;
start(setupConnection, queueIfNoFreeConnection);
}
});
}
@Override
public void onChildProcessDied(ChildProcessConnection connection) {
assert LauncherThread.runningOnLauncherThread();
assert mConnection == connection;
ChildProcessLauncher.this.onChildProcessDied();
}
};
if (mConnectionProvider != null) {
mConnection = mConnectionProvider.getConnection(serviceCallback);
assert mConnection != null;
setupConnection();
} else {
assert mConnectionAllocator != null;
if (!allocateAndSetupConnection(
serviceCallback, setupConnection, queueIfNoFreeConnection)
&& !queueIfNoFreeConnection) {
return false;
}
}
return true;
} finally {
TraceEvent.end("ChildProcessLauncher.start");
}
}
public ChildProcessConnection getConnection() {
return mConnection;
}
public ChildConnectionAllocator getConnectionAllocator() {
return mConnectionAllocator;
}
private boolean allocateAndSetupConnection(
final ChildProcessConnection.ServiceCallback serviceCallback,
final boolean setupConnection, final boolean queueIfNoFreeConnection) {
assert mConnection == null;
Bundle serviceBundle = new Bundle();
mDelegate.onBeforeConnectionAllocated(serviceBundle);
mConnection = mConnectionAllocator.allocate(
ContextUtils.getApplicationContext(), serviceBundle, serviceCallback);
if (mConnection == null) {
if (!queueIfNoFreeConnection) {
Log.d(TAG, "Failed to allocate a child connection (no queuing).");
return false;
}
// No connection is available at this time. Add a listener so when one becomes
// available we can create the service.
mConnectionAllocator.addListener(new ChildConnectionAllocator.Listener() {
@Override
public void onConnectionFreed(
ChildConnectionAllocator allocator, ChildProcessConnection connection) {
assert allocator == mConnectionAllocator;
if (!allocator.isFreeConnectionAvailable()) return;
allocator.removeListener(this);
allocateAndSetupConnection(
serviceCallback, setupConnection, queueIfNoFreeConnection);
}
});
return false;
}
assert mConnection != null;
if (setupConnection) {
setupConnection();
}
return true;
}
private void setupConnection() {
ChildProcessConnection.ConnectionCallback connectionCallback =
new ChildProcessConnection.ConnectionCallback() {
@Override
public void onConnected(ChildProcessConnection connection) {
assert mConnection == connection;
onServiceConnected();
}
};
Bundle connectionBundle = createConnectionBundle();
mDelegate.onBeforeConnectionSetup(connectionBundle);
mConnection.setupConnection(connectionBundle, getIBinderCallback(), connectionCallback);
}
private void onServiceConnected() {
assert isRunningOnLauncherThread();
Log.d(TAG, "on connect callback, pid=%d", mConnection.getPid());
mDelegate.onConnectionEstablished(mConnection);
// Proactively close the FDs rather than waiting for the GC to do it.
try {
for (FileDescriptorInfo fileInfo : mFilesToBeMapped) {
fileInfo.fd.close();
}
} catch (IOException ioe) {
Log.w(TAG, "Failed to close FD.", ioe);
}
}
public int getPid() {
assert isRunningOnLauncherThread();
return mConnection == null ? NULL_PROCESS_HANDLE : mConnection.getPid();
}
public IBinder getIBinderCallback() {
return mIBinderCallback;
}
private boolean isRunningOnLauncherThread() {
return mLauncherHandler.getLooper() == Looper.myLooper();
}
private Bundle createConnectionBundle() {
Bundle bundle = new Bundle();
bundle.putStringArray(ChildProcessConstants.EXTRA_COMMAND_LINE, mCommandLine);
bundle.putParcelableArray(ChildProcessConstants.EXTRA_FILES, mFilesToBeMapped);
return bundle;
}
private void onChildProcessDied() {
assert LauncherThread.runningOnLauncherThread();
if (getPid() != 0) {
mDelegate.onConnectionLost(mConnection);
}
}
public void stop() {
assert isRunningOnLauncherThread();
Log.d(TAG, "stopping child connection: pid=%d", mConnection.getPid());
mConnection.stop();
}
}
...@@ -15,7 +15,6 @@ import org.chromium.base.ContextUtils; ...@@ -15,7 +15,6 @@ import org.chromium.base.ContextUtils;
import org.chromium.base.CpuFeatures; import org.chromium.base.CpuFeatures;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.base.ThreadUtils; import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.VisibleForTesting; import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative; import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace; import org.chromium.base.annotations.JNINamespace;
...@@ -51,12 +50,6 @@ public class ChildProcessLauncherHelper { ...@@ -51,12 +50,6 @@ public class ChildProcessLauncherHelper {
private static final String PRIVILEGED_SERVICES_NAME_KEY = private static final String PRIVILEGED_SERVICES_NAME_KEY =
"org.chromium.content.browser.PRIVILEGED_SERVICES_NAME"; "org.chromium.content.browser.PRIVILEGED_SERVICES_NAME";
// Represents an invalid process handle; same as base/process/process.h kNullProcessHandle.
private static final int NULL_PROCESS_HANDLE = 0;
// Delay between the call to freeConnection and the connection actually beeing freed.
private static final long FREE_CONNECTION_DELAY_MILLIS = 1;
// A warmed-up connection to a sandboxed service. // A warmed-up connection to a sandboxed service.
private static SpareChildConnection sSpareSandboxedConnection; private static SpareChildConnection sSpareSandboxedConnection;
...@@ -64,6 +57,9 @@ public class ChildProcessLauncherHelper { ...@@ -64,6 +57,9 @@ public class ChildProcessLauncherHelper {
private static final Map<String, ChildConnectionAllocator> private static final Map<String, ChildConnectionAllocator>
sSandboxedChildConnectionAllocatorMap = new HashMap<>(); sSandboxedChildConnectionAllocatorMap = new HashMap<>();
// Map from PID to ChildProcessLauncherHelper.
private static final Map<Integer, ChildProcessLauncherHelper> sLauncherByPid = new HashMap<>();
// Allocator used for non-sandboxed services. // Allocator used for non-sandboxed services.
private static ChildConnectionAllocator sPrivilegedChildConnectionAllocator; private static ChildConnectionAllocator sPrivilegedChildConnectionAllocator;
...@@ -72,40 +68,75 @@ public class ChildProcessLauncherHelper { ...@@ -72,40 +68,75 @@ public class ChildProcessLauncherHelper {
private static int sSandboxedServicesCountForTesting = -1; private static int sSandboxedServicesCountForTesting = -1;
private static String sSandboxedServicesNameForTesting; private static String sSandboxedServicesNameForTesting;
// Map from PID to ChildService connection.
private static Map<Integer, ChildProcessLauncherHelper> sLauncherByPid = new HashMap<>();
// Manages OOM bindings used to bind chind services. Lazily initialized by getBindingManager(). // Manages OOM bindings used to bind chind services. Lazily initialized by getBindingManager().
private static BindingManager sBindingManager; private static BindingManager sBindingManager;
// Whether the main application is currently brought to the foreground. // Whether the main application is currently brought to the foreground.
private static boolean sApplicationInForeground = true; private static boolean sApplicationInForeground = true;
// The IBinder provided to the created service. // Whether the connection is managed by the BindingManager.
private final IBinder mIBinderCallback; private final boolean mUseBindingManager;
// Whether the connection should be setup once connected. Tests can set this to false to
// simulate a connected but not yet setup connection.
private final boolean mDoSetupConnection;
private final ChildProcessCreationParams mCreationParams; private final ChildProcessCreationParams mCreationParams;
private final String[] mCommandLine; // Whether the created process should be sandboxed.
private final boolean mSandboxed;
private final FileDescriptorInfo[] mFilesToBeMapped; private final ChildProcessLauncher.Delegate mLauncherDelegate =
new ChildProcessLauncher.Delegate() {
@Override
public void onBeforeConnectionAllocated(Bundle bundle) {
populateServiceBundle(bundle, mCreationParams);
}
// The allocator used to create the connection. @Override
private final ChildConnectionAllocator mConnectionAllocator; public void onBeforeConnectionSetup(Bundle connectionBundle) {
// Populate the bundle passed to the service setup call with content specific
// parameters.
connectionBundle.putInt(
ContentChildProcessConstants.EXTRA_CPU_COUNT, CpuFeatures.getCount());
connectionBundle.putLong(
ContentChildProcessConstants.EXTRA_CPU_FEATURES, CpuFeatures.getMask());
connectionBundle.putBundle(Linker.EXTRA_LINKER_SHARED_RELROS,
Linker.getInstance().getSharedRelros());
}
// Whether the created process should be sandboxed. @Override
private final boolean mSandboxed; public void onConnectionEstablished(ChildProcessConnection connection) {
int pid = connection.getPid();
assert pid > 0;
sLauncherByPid.put(pid, ChildProcessLauncherHelper.this);
if (mUseBindingManager) {
getBindingManager().addNewConnection(pid, connection);
}
// If the connection fails and pid == 0, the Java-side cleanup was already
// handled by DeathCallback. We still have to call back to native for cleanup
// there.
if (mNativeChildProcessLauncherHelper != 0) {
nativeOnChildProcessStarted(
mNativeChildProcessLauncherHelper, connection.getPid());
}
mNativeChildProcessLauncherHelper = 0;
}
@Override
public void onConnectionLost(ChildProcessConnection connection) {
assert connection.getPid() > 0;
sLauncherByPid.remove(connection.getPid());
if (mUseBindingManager) {
getBindingManager().removeConnection(connection.getPid());
}
}
};
private final ChildProcessLauncher mLauncher;
// Note native pointer is only guaranteed live until nativeOnChildProcessStarted. // Note native pointer is only guaranteed live until nativeOnChildProcessStarted.
private long mNativeChildProcessLauncherHelper; private long mNativeChildProcessLauncherHelper;
// The actual service connection. Set once we have connected to the service.
private ChildProcessConnection mConnection;
@CalledByNative @CalledByNative
private static FileDescriptorInfo makeFdInfo( private static FileDescriptorInfo makeFdInfo(
int id, int fd, boolean autoClose, long offset, long size) { int id, int fd, boolean autoClose, long offset, long size) {
...@@ -152,11 +183,11 @@ public class ChildProcessLauncherHelper { ...@@ -152,11 +183,11 @@ public class ChildProcessLauncherHelper {
? new GpuProcessCallback() ? new GpuProcessCallback()
: null; : null;
ChildProcessLauncherHelper process_launcher = ChildProcessLauncherHelper processLauncher = new ChildProcessLauncherHelper(nativePointer,
new ChildProcessLauncherHelper(nativePointer, creationParams, commandLine, creationParams, commandLine, filesToBeMapped, sandboxed, binderCallback);
filesToBeMapped, sandboxed, binderCallback, true /* doSetupConnection */); processLauncher.mLauncher.start(
process_launcher.start(); true /* doSetupConnection */, true /* queueIfNoFreeConnection */);
return process_launcher; return processLauncher;
} }
/** /**
...@@ -169,23 +200,23 @@ public class ChildProcessLauncherHelper { ...@@ -169,23 +200,23 @@ public class ChildProcessLauncherHelper {
LauncherThread.post(new Runnable() { LauncherThread.post(new Runnable() {
@Override @Override
public void run() { public void run() {
if (sSpareSandboxedConnection != null && !sSpareSandboxedConnection.isEmpty()) { warmUpOnLauncherThread(context);
return;
}
ChildProcessCreationParams creationParams = ChildProcessCreationParams.getDefault();
boolean bindToCallerCheck =
creationParams == null ? false : creationParams.getBindToCallerCheck();
Bundle serviceBundle =
ChildProcessLauncherHelper.createServiceBundle(bindToCallerCheck);
ChildConnectionAllocator allocator =
getConnectionAllocator(context, creationParams, true /* sandboxed */);
sSpareSandboxedConnection =
new SpareChildConnection(context, allocator, serviceBundle);
} }
}); });
} }
private static void warmUpOnLauncherThread(Context context) {
if (sSpareSandboxedConnection != null && !sSpareSandboxedConnection.isEmpty()) {
return;
}
ChildProcessCreationParams creationParams = ChildProcessCreationParams.getDefault();
Bundle serviceBundle = populateServiceBundle(new Bundle(), creationParams);
ChildConnectionAllocator allocator =
getConnectionAllocator(context, creationParams, true /* sandboxed */);
sSpareSandboxedConnection = new SpareChildConnection(context, allocator, serviceBundle);
}
public static String getPackageNameFromCreationParams( public static String getPackageNameFromCreationParams(
Context context, ChildProcessCreationParams params, boolean sandboxed) { Context context, ChildProcessCreationParams params, boolean sandboxed) {
return (sandboxed && params != null) ? params.getPackageNameForSandboxedService() return (sandboxed && params != null) ? params.getPackageNameForSandboxedService()
...@@ -314,6 +345,7 @@ public class ChildProcessLauncherHelper { ...@@ -314,6 +345,7 @@ public class ChildProcessLauncherHelper {
sSandboxedServiceFactoryForTesting); sSandboxedServiceFactoryForTesting);
} }
sSandboxedChildConnectionAllocatorMap.put(packageName, connectionAllocator); sSandboxedChildConnectionAllocatorMap.put(packageName, connectionAllocator);
final ChildConnectionAllocator finalConnectionAllocator = connectionAllocator; final ChildConnectionAllocator finalConnectionAllocator = connectionAllocator;
// Tracks connections removal so the allocator can be freed when no longer used. // Tracks connections removal so the allocator can be freed when no longer used.
connectionAllocator.addListener(new ChildConnectionAllocator.Listener() { connectionAllocator.addListener(new ChildConnectionAllocator.Listener() {
...@@ -321,6 +353,7 @@ public class ChildProcessLauncherHelper { ...@@ -321,6 +353,7 @@ public class ChildProcessLauncherHelper {
public void onConnectionAllocated( public void onConnectionAllocated(
ChildConnectionAllocator allocator, ChildProcessConnection connection) { ChildConnectionAllocator allocator, ChildProcessConnection connection) {
assert connection != null; assert connection != null;
assert allocator == finalConnectionAllocator;
if (!allocator.isFreeConnectionAvailable()) { if (!allocator.isFreeConnectionAvailable()) {
// Proactively releases all the moderate bindings once all the sandboxed // Proactively releases all the moderate bindings once all the sandboxed
// services are allocated, which will be very likely to have some of them // services are allocated, which will be very likely to have some of them
...@@ -349,195 +382,61 @@ public class ChildProcessLauncherHelper { ...@@ -349,195 +382,61 @@ public class ChildProcessLauncherHelper {
private ChildProcessLauncherHelper(long nativePointer, private ChildProcessLauncherHelper(long nativePointer,
ChildProcessCreationParams creationParams, String[] commandLine, ChildProcessCreationParams creationParams, String[] commandLine,
FileDescriptorInfo[] filesToBeMapped, boolean sandboxed, IBinder binderCallback, FileDescriptorInfo[] filesToBeMapped, boolean sandboxed, IBinder binderCallback) {
boolean doSetupConnection) {
assert LauncherThread.runningOnLauncherThread(); assert LauncherThread.runningOnLauncherThread();
mCreationParams = creationParams;
mCommandLine = commandLine;
mFilesToBeMapped = filesToBeMapped;
mNativeChildProcessLauncherHelper = nativePointer; mNativeChildProcessLauncherHelper = nativePointer;
mIBinderCallback = binderCallback; mCreationParams = creationParams;
mDoSetupConnection = doSetupConnection; mUseBindingManager = sandboxed;
mSandboxed = sandboxed; mSandboxed = sandboxed;
mConnectionAllocator = getConnectionAllocator( final ChildConnectionAllocator connectionAllocator = getConnectionAllocator(
ContextUtils.getApplicationContext(), mCreationParams, sandboxed); ContextUtils.getApplicationContext(), mCreationParams, sandboxed);
if (sSpareSandboxedConnection != null && !sSpareSandboxedConnection.isEmpty()
initLinker(); && sSpareSandboxedConnection.isForAllocator(connectionAllocator)) {
} // Use the warmed-up connection.
ChildProcessLauncher.BoundConnectionProvider connectionProvider =
private void start() { new ChildProcessLauncher.BoundConnectionProvider() {
assert LauncherThread.runningOnLauncherThread();
assert mConnection == null;
try {
TraceEvent.begin("ChildProcessLauncher.start");
ChildProcessConnection.ServiceCallback serviceCallback =
new ChildProcessConnection.ServiceCallback() {
@Override
public void onChildStarted() {}
@Override @Override
public void onChildStartFailed() { public ChildProcessConnection getConnection(
assert LauncherThread.runningOnLauncherThread(); ChildProcessConnection.ServiceCallback serviceCallback) {
Log.e(TAG, "ChildProcessConnection.start failed, trying again"); return sSpareSandboxedConnection.getConnection(
LauncherThread.post(new Runnable() { connectionAllocator, serviceCallback);
@Override
public void run() {
// The child process may already be bound to another client
// (this can happen if multi-process WebView is used in more
// than one process), so try starting the process again.
// This connection that failed to start has not been freed,
// so a new bound connection will be allocated.
mConnection = null;
start();
}
});
}
@Override
public void onChildProcessDied(ChildProcessConnection connection) {
assert LauncherThread.runningOnLauncherThread();
assert mConnection == connection;
ChildProcessLauncherHelper.this.onChildProcessDied();
} }
}; };
mLauncher = ChildProcessLauncher.createWithBoundConnectionProvider(mLauncherDelegate,
// Try to use the spare connection if there's one. commandLine, filesToBeMapped, connectionProvider, binderCallback);
if (sSpareSandboxedConnection != null) { } else {
mConnection = sSpareSandboxedConnection.getConnection( mLauncher = ChildProcessLauncher.createWithConnectionAllocator(mLauncherDelegate,
mConnectionAllocator, serviceCallback); commandLine, filesToBeMapped, connectionAllocator, binderCallback);
if (mConnection != null) {
Log.d(TAG, "Using warmed-up connection for service %s.",
mConnection.getServiceName());
// Clear the spare connection now that it's been used.
sSpareSandboxedConnection = null;
}
}
allocateAndSetupConnection(serviceCallback);
} finally {
TraceEvent.end("ChildProcessLauncher.start");
}
}
// Allocates a new connection and calls setUp on it.
// If there are no available connections, it will retry when one becomes available.
private boolean allocateAndSetupConnection(
final ChildProcessConnection.ServiceCallback serviceCallback) {
if (mConnection == null) {
final Context context = ContextUtils.getApplicationContext();
boolean bindToCallerCheck =
mCreationParams == null ? false : mCreationParams.getBindToCallerCheck();
Bundle serviceBundle = createServiceBundle(bindToCallerCheck);
onBeforeConnectionAllocated(serviceBundle);
boolean useBindingManager = mSandboxed;
mConnection = mConnectionAllocator.allocate(context, serviceBundle, serviceCallback);
if (mConnection == null) {
// No connection is available at this time. Add a listener so when one becomes
// available we can create the service.
mConnectionAllocator.addListener(new ChildConnectionAllocator.Listener() {
@Override
public void onConnectionAllocated(ChildConnectionAllocator allocator,
ChildProcessConnection connection) {}
@Override
public void onConnectionFreed(
ChildConnectionAllocator allocator, ChildProcessConnection connection) {
assert allocator == mConnectionAllocator;
if (!allocator.isFreeConnectionAvailable()) return;
allocator.removeListener(this);
boolean success = allocateAndSetupConnection(serviceCallback);
assert success;
}
});
return false;
}
}
assert mConnection != null;
Bundle connectionBundle = createConnectionBundle();
onConnectionBound(mConnection, mConnectionAllocator, connectionBundle);
if (mDoSetupConnection) {
setupConnection(connectionBundle);
}
return true;
}
private void setupConnection(Bundle connectionBundle) {
ChildProcessConnection.ConnectionCallback connectionCallback =
new ChildProcessConnection.ConnectionCallback() {
@Override
public void onConnected(ChildProcessConnection connection) {
assert mConnection == connection;
onChildProcessStarted();
}
};
mConnection.setupConnection(connectionBundle, getIBinderCallback(), connectionCallback);
}
private void onChildProcessStarted() {
assert LauncherThread.runningOnLauncherThread();
int pid = mConnection.getPid();
Log.d(TAG, "on connect callback, pid=%d", pid);
onConnectionEstablished(mConnection);
sLauncherByPid.put(pid, this);
// Proactively close the FDs rather than waiting for the GC to do it.
try {
for (FileDescriptorInfo fileInfo : mFilesToBeMapped) {
fileInfo.fd.close();
}
} catch (IOException ioe) {
Log.w(TAG, "Failed to close FD.", ioe);
}
// If the connection fails and pid == 0, the Java-side cleanup was already handled by
// ServiceCallback.onChildStopped. We still have to call back to native for cleanup there.
if (mNativeChildProcessLauncherHelper != 0) {
nativeOnChildProcessStarted(mNativeChildProcessLauncherHelper, getPid());
}
mNativeChildProcessLauncherHelper = 0;
}
private void onChildProcessDied() {
assert LauncherThread.runningOnLauncherThread();
if (getPid() != 0) {
onConnectionLost(mConnection, getPid());
sLauncherByPid.remove(getPid());
} }
} }
public int getPid() { public int getPid() {
assert LauncherThread.runningOnLauncherThread(); assert LauncherThread.runningOnLauncherThread();
return mConnection == null ? NULL_PROCESS_HANDLE : mConnection.getPid(); return mLauncher.getPid();
} }
// Called on client (UI or IO) thread. // Called on client (UI or IO) thread.
@CalledByNative @CalledByNative
private boolean isOomProtected() { private boolean isOomProtected() {
// mConnection is set on a different thread but does not change once it's been set. So it is ChildProcessConnection connection = mLauncher.getConnection();
// safe to test whether it's null from a different thread. // Here we are accessing the connection from a thread other than the launcher thread, but it
if (mConnection == null) { // does not change once it's been set. So it is safe to test whether it's null here and
// access it afterwards.
if (connection == null) {
return false; return false;
} }
// We consider the process to be child protected if it has a strong or moderate binding and // We consider the process to be child protected if it has a strong or moderate binding and
// the app is in the foreground. // the app is in the foreground.
return sApplicationInForeground && !mConnection.isWaivedBoundOnlyOrWasWhenDied(); return sApplicationInForeground && !connection.isWaivedBoundOnlyOrWasWhenDied();
} }
@CalledByNative @CalledByNative
private void setInForeground(int pid, boolean foreground, boolean boostForPendingViews) { private void setInForeground(int pid, boolean foreground, boolean boostForPendingViews) {
assert LauncherThread.runningOnLauncherThread(); assert LauncherThread.runningOnLauncherThread();
assert mConnection != null; assert mLauncher.getPid() == pid;
assert getPid() == pid;
getBindingManager().setPriority(pid, foreground, boostForPendingViews); getBindingManager().setPriority(pid, foreground, boostForPendingViews);
} }
...@@ -545,10 +444,11 @@ public class ChildProcessLauncherHelper { ...@@ -545,10 +444,11 @@ public class ChildProcessLauncherHelper {
static void stop(int pid) { static void stop(int pid) {
assert LauncherThread.runningOnLauncherThread(); assert LauncherThread.runningOnLauncherThread();
Log.d(TAG, "stopping child connection: pid=%d", pid); Log.d(TAG, "stopping child connection: pid=%d", pid);
ChildProcessLauncherHelper launcher = sLauncherByPid.get(pid); ChildProcessLauncherHelper launcher = getByPid(pid);
// Launcher can be null for single process. // launcher can be null for single process.
if (launcher != null) { if (launcher != null) {
launcher.mConnection.stop(); // Can happen for single process.
launcher.mLauncher.stop();
} }
} }
...@@ -579,8 +479,7 @@ public class ChildProcessLauncherHelper { ...@@ -579,8 +479,7 @@ public class ChildProcessLauncherHelper {
private static boolean sLinkerInitialized; private static boolean sLinkerInitialized;
private static long sLinkerLoadAddress; private static long sLinkerLoadAddress;
@VisibleForTesting private static void initLinker() {
static void initLinker() {
assert LauncherThread.runningOnLauncherThread(); assert LauncherThread.runningOnLauncherThread();
if (sLinkerInitialized) return; if (sLinkerInitialized) return;
if (Linker.isUsed()) { if (Linker.isUsed()) {
...@@ -594,8 +493,9 @@ public class ChildProcessLauncherHelper { ...@@ -594,8 +493,9 @@ public class ChildProcessLauncherHelper {
private static ChromiumLinkerParams getLinkerParamsForNewConnection() { private static ChromiumLinkerParams getLinkerParamsForNewConnection() {
assert LauncherThread.runningOnLauncherThread(); assert LauncherThread.runningOnLauncherThread();
assert sLinkerInitialized;
initLinker();
assert sLinkerInitialized;
if (sLinkerLoadAddress == 0) return null; if (sLinkerLoadAddress == 0) return null;
// Always wait for the shared RELROs in service processes. // Always wait for the shared RELROs in service processes.
...@@ -610,70 +510,18 @@ public class ChildProcessLauncherHelper { ...@@ -610,70 +510,18 @@ public class ChildProcessLauncherHelper {
} }
} }
/** private static Bundle populateServiceBundle(
* Creates the common bundle to be passed to child processes through the service binding intent. Bundle bundle, ChildProcessCreationParams creationParams) {
* If the service gets recreated by the framework the intent will be reused, so these parameters boolean bindToCallerCheck =
* should be common to all processes of that type. creationParams == null ? false : creationParams.getBindToCallerCheck();
*
* @param commandLine Command line params to be passed to the service.
* @param linkerParams Linker params to start the service.
*/
private static Bundle createServiceBundle(boolean bindToCallerCheck) {
initLinker();
Bundle bundle = new Bundle();
bundle.putBoolean(ChildProcessConstants.EXTRA_BIND_TO_CALLER, bindToCallerCheck); bundle.putBoolean(ChildProcessConstants.EXTRA_BIND_TO_CALLER, bindToCallerCheck);
bundle.putParcelable(ContentChildProcessConstants.EXTRA_LINKER_PARAMS, bundle.putParcelable(ContentChildProcessConstants.EXTRA_LINKER_PARAMS,
getLinkerParamsForNewConnection()); getLinkerParamsForNewConnection());
return bundle; return bundle;
} }
public Bundle createConnectionBundle() { public static ChildProcessLauncherHelper getByPid(int pid) {
Bundle bundle = new Bundle(); return sLauncherByPid.get(pid);
bundle.putStringArray(ChildProcessConstants.EXTRA_COMMAND_LINE, mCommandLine);
bundle.putParcelableArray(ChildProcessConstants.EXTRA_FILES, mFilesToBeMapped);
return bundle;
}
// Below are methods that will eventually be moved to a content delegate class.
private void onBeforeConnectionAllocated(Bundle commonParameters) {
// TODO(jcivelli): move createServiceBundle in there.
}
// Called once a connection has been allocated.
private void onConnectionBound(ChildProcessConnection connection,
ChildConnectionAllocator connectionAllocator, Bundle connectionBundle) {
if (mSandboxed && !connectionAllocator.isFreeConnectionAvailable()) {
// Proactively releases all the moderate bindings once all the sandboxed services are
// allocated, which will be very likely to have some of them killed by OOM killer.
getBindingManager().releaseAllModerateBindings();
}
// Popuplate the bundle passed to the service setup call with content specific parameters.
connectionBundle.putInt(
ContentChildProcessConstants.EXTRA_CPU_COUNT, CpuFeatures.getCount());
connectionBundle.putLong(
ContentChildProcessConstants.EXTRA_CPU_FEATURES, CpuFeatures.getMask());
connectionBundle.putBundle(
Linker.EXTRA_LINKER_SHARED_RELROS, Linker.getInstance().getSharedRelros());
}
private void onConnectionEstablished(ChildProcessConnection connection) {
if (mSandboxed) {
getBindingManager().addNewConnection(connection.getPid(), connection);
}
}
// Called when a connection has been disconnected. Only invoked if onConnectionEstablished was
// called, meaning the connection was already established.
private void onConnectionLost(ChildProcessConnection connection, int pid) {
if (mSandboxed) {
getBindingManager().removeConnection(pid);
}
}
private IBinder getIBinderCallback() {
return mIBinderCallback;
} }
// Testing only related methods. // Testing only related methods.
...@@ -682,18 +530,12 @@ public class ChildProcessLauncherHelper { ...@@ -682,18 +530,12 @@ public class ChildProcessLauncherHelper {
ChildProcessCreationParams creationParams, String[] commandLine, ChildProcessCreationParams creationParams, String[] commandLine,
FileDescriptorInfo[] filesToBeMapped, boolean sandboxed, IBinder binderCallback, FileDescriptorInfo[] filesToBeMapped, boolean sandboxed, IBinder binderCallback,
boolean doSetupConnection) { boolean doSetupConnection) {
ChildProcessLauncherHelper launcherHelper = ChildProcessLauncherHelper launcherHelper = new ChildProcessLauncherHelper(
new ChildProcessLauncherHelper(0L, creationParams, commandLine, filesToBeMapped, 0L, creationParams, commandLine, filesToBeMapped, sandboxed, binderCallback);
sandboxed, binderCallback, doSetupConnection); launcherHelper.mLauncher.start(doSetupConnection, true /* queueIfNoFreeConnection */);
launcherHelper.start();
return launcherHelper; return launcherHelper;
} }
@VisibleForTesting
public static ChildProcessLauncherHelper getLauncherForPid(int pid) {
return sLauncherByPid.get(pid);
}
/** @return the count of services set-up and working. */ /** @return the count of services set-up and working. */
@VisibleForTesting @VisibleForTesting
static int getConnectedServicesCountForTesting() { static int getConnectedServicesCountForTesting() {
...@@ -719,19 +561,14 @@ public class ChildProcessLauncherHelper { ...@@ -719,19 +561,14 @@ public class ChildProcessLauncherHelper {
return sSandboxedChildConnectionAllocatorMap.containsKey(packageName); return sSandboxedChildConnectionAllocatorMap.containsKey(packageName);
} }
@VisibleForTesting
ChildProcessConnection getConnection() {
return mConnection;
}
@VisibleForTesting @VisibleForTesting
public ChildProcessConnection getChildProcessConnection() { public ChildProcessConnection getChildProcessConnection() {
return mConnection; return mLauncher.getConnection();
} }
@VisibleForTesting @VisibleForTesting
public ChildConnectionAllocator getChildConnectionAllocatorForTesting() { public ChildConnectionAllocator getChildConnectionAllocatorForTesting() {
return mConnectionAllocator; return mLauncher.getConnectionAllocator();
} }
@VisibleForTesting @VisibleForTesting
...@@ -743,9 +580,8 @@ public class ChildProcessLauncherHelper { ...@@ -743,9 +580,8 @@ public class ChildProcessLauncherHelper {
public static boolean crashProcessForTesting(int pid) { public static boolean crashProcessForTesting(int pid) {
if (sLauncherByPid.get(pid) == null) return false; if (sLauncherByPid.get(pid) == null) return false;
ChildProcessConnection connection = sLauncherByPid.get(pid).mConnection; ChildProcessConnection connection = sLauncherByPid.get(pid).mLauncher.getConnection();
if (connection == null) return false; if (connection == null) return false;
try { try {
connection.crashServiceForTesting(); connection.crashServiceForTesting();
return true; return true;
......
...@@ -84,8 +84,7 @@ public class SpareChildConnection { ...@@ -84,8 +84,7 @@ public class SpareChildConnection {
public ChildProcessConnection getConnection(ChildConnectionAllocator allocator, public ChildProcessConnection getConnection(ChildConnectionAllocator allocator,
@NonNull final ChildProcessConnection.ServiceCallback serviceCallback) { @NonNull final ChildProcessConnection.ServiceCallback serviceCallback) {
assert LauncherThread.runningOnLauncherThread(); assert LauncherThread.runningOnLauncherThread();
if (mConnection == null || allocator != mConnectionAllocator if (isEmpty() || !isForAllocator(allocator) || mConnectionServiceCallback != null) {
|| mConnectionServiceCallback != null) {
return null; return null;
} }
...@@ -117,6 +116,11 @@ public class SpareChildConnection { ...@@ -117,6 +116,11 @@ public class SpareChildConnection {
return mConnection == null || mConnectionServiceCallback != null; return mConnection == null || mConnectionServiceCallback != null;
} }
/** Returns true if this spare connection uses {@param allocator}. */
public boolean isForAllocator(ChildConnectionAllocator allocator) {
return mConnectionAllocator == allocator;
}
private void clearConnection() { private void clearConnection() {
assert LauncherThread.runningOnLauncherThread(); assert LauncherThread.runningOnLauncherThread();
mConnection = null; mConnection = null;
......
// Copyright 2014 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.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.BaseSwitches;
import org.chromium.base.ThreadUtils;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.base.process_launcher.ChildProcessCreationParams;
import org.chromium.base.process_launcher.FileDescriptorInfo;
import org.chromium.base.test.util.Feature;
import org.chromium.content.browser.test.ChildProcessAllocatorSettings;
import org.chromium.content.browser.test.ContentJUnit4ClassRunner;
import org.chromium.content.browser.test.util.Criteria;
import org.chromium.content.browser.test.util.CriteriaHelper;
import org.chromium.content_shell_apk.ChildProcessLauncherTestHelperService;
import org.chromium.content_shell_apk.ChildProcessLauncherTestUtils;
import java.util.concurrent.Callable;
/**
* Instrumentation tests for ChildProcessLauncher.
*/
@RunWith(ContentJUnit4ClassRunner.class)
public class ChildProcessLauncherHelperTest {
// Pseudo command line arguments to instruct the child process to wait until being killed.
// Allowing the process to continue would lead to a crash when attempting to initialize IPC
// channels that are not being set up in this test.
private static final String[] sProcessWaitArguments = {
"_", "--" + BaseSwitches.RENDERER_WAIT_FOR_JAVA_DEBUGGER};
private static final String EXTERNAL_APK_PACKAGE_NAME = "org.chromium.external.apk";
private static final String DEFAULT_SANDBOXED_PROCESS_SERVICE =
"org.chromium.content.app.SandboxedProcessService";
private static final int DONT_BLOCK = 0;
private static final int BLOCK_UNTIL_CONNECTED = 1;
private static final int BLOCK_UNTIL_SETUP = 2;
@Before
public void setUp() throws Exception {
LibraryLoader.get(LibraryProcessType.PROCESS_CHILD).ensureInitialized();
}
/**
* Tests that external APKs and regular use different ChildConnectionAllocators.
*/
@Test
@MediumTest
@Feature({"ProcessManagement"})
@ChildProcessAllocatorSettings(
sandboxedServiceCount = 4, sandboxedServiceName = DEFAULT_SANDBOXED_PROCESS_SERVICE)
public void testAllocatorForPackage() {
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
ChildConnectionAllocator connectionAllocator = getChildConnectionAllocator(
appContext, appContext.getPackageName(), true /* sandboxed */);
ChildConnectionAllocator externalConnectionAllocator = getChildConnectionAllocator(
appContext, EXTERNAL_APK_PACKAGE_NAME, true /* sandboxed */);
Assert.assertNotEquals(connectionAllocator, externalConnectionAllocator);
}
/**
* Tests binding to the same sandboxed service process from multiple processes in the
* same package. This uses the ChildProcessLauncherTestHelperService declared in
* ContentShell.apk as a separate android:process to bind the first (slot 0) service. The
* instrumentation test then tries to bind the same slot, which fails, so the
* ChildProcessLauncher retries on a new connection.
*/
@Test
@MediumTest
@Feature({"ProcessManagement"})
public void testBindServiceFromMultipleProcesses() throws RemoteException {
final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
// Start the Helper service.
class HelperConnection implements ServiceConnection {
Messenger mMessenger = null;
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mMessenger = new Messenger(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {}
}
final HelperConnection serviceConnection = new HelperConnection();
Intent intent = new Intent();
intent.setComponent(new ComponentName(context.getPackageName(),
context.getPackageName() + ".ChildProcessLauncherTestHelperService"));
Assert.assertTrue(context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE));
// Wait for the Helper service to connect.
CriteriaHelper.pollInstrumentationThread(
new Criteria("Failed to get helper service Messenger") {
@Override
public boolean isSatisfied() {
return serviceConnection.mMessenger != null;
}
});
Assert.assertNotNull(serviceConnection.mMessenger);
class ReplyHandler implements Handler.Callback {
Message mMessage;
@Override
public boolean handleMessage(Message msg) {
// Copy the message so its contents outlive this Binder transaction.
mMessage = Message.obtain();
mMessage.copyFrom(msg);
return true;
}
}
final ReplyHandler replyHandler = new ReplyHandler();
// Send a message to the Helper and wait for the reply. This will cause the slot 0
// sandboxed service connection to be bound by a different PID (i.e., not this process).
Message msg = Message.obtain(null, ChildProcessLauncherTestHelperService.MSG_BIND_SERVICE);
msg.replyTo = new Messenger(new Handler(Looper.getMainLooper(), replyHandler));
serviceConnection.mMessenger.send(msg);
CriteriaHelper.pollInstrumentationThread(
new Criteria("Failed waiting for helper service reply") {
@Override
public boolean isSatisfied() {
return replyHandler.mMessage != null;
}
});
// Verify that the Helper was able to launch the sandboxed service.
Assert.assertNotNull(replyHandler.mMessage);
Assert.assertEquals(ChildProcessLauncherTestHelperService.MSG_BIND_SERVICE_REPLY,
replyHandler.mMessage.what);
Assert.assertEquals(
"Connection slot from helper service is not 0", 0, replyHandler.mMessage.arg2);
final int helperConnectionPid = replyHandler.mMessage.arg1;
Assert.assertTrue(helperConnectionPid > 0);
// Launch a service from this process. Since slot 0 is already bound by the Helper, it
// will fail to start and the ChildProcessLauncher will retry and use the slot 1.
ChildProcessCreationParams creationParams = new ChildProcessCreationParams(
context.getPackageName(), false /* isExternalService */,
LibraryProcessType.PROCESS_CHILD, true /* bindToCallerCheck */);
ChildProcessLauncherHelper launcher = startSandboxedChildProcessWithCreationParams(
creationParams, BLOCK_UNTIL_SETUP, true /* doSetupConnection */);
final ChildProcessConnection retryConnection =
ChildProcessLauncherTestUtils.getConnection(launcher);
Assert.assertEquals(
1, ChildProcessLauncherTestUtils.getConnectionServiceNumber(retryConnection));
ChildConnectionAllocator connectionAllocator =
launcher.getChildConnectionAllocatorForTesting();
// Check that only two connections are created.
for (int i = 0; i < connectionAllocator.getNumberOfServices(); ++i) {
ChildProcessConnection sandboxedConn =
connectionAllocator.getChildProcessConnectionAtSlotForTesting(i);
if (i <= 1) {
Assert.assertNotNull(sandboxedConn);
Assert.assertNotNull(
ChildProcessLauncherTestUtils.getConnectionService(sandboxedConn));
} else {
Assert.assertNull(sandboxedConn);
}
}
Assert.assertEquals(
connectionAllocator.getChildProcessConnectionAtSlotForTesting(1), retryConnection);
ChildProcessConnection failedConnection =
connectionAllocator.getChildProcessConnectionAtSlotForTesting(0);
Assert.assertEquals(0, ChildProcessLauncherTestUtils.getConnectionPid(failedConnection));
Assert.assertFalse(ChildProcessLauncherTestUtils.getConnectionService(failedConnection)
.bindToCaller());
CriteriaHelper.pollInstrumentationThread(
new Criteria("Failed waiting retry connection to get pid") {
@Override
public boolean isSatisfied() {
return ChildProcessLauncherTestUtils.getConnectionPid(retryConnection) > 0;
}
});
Assert.assertTrue(ChildProcessLauncherTestUtils.getConnectionPid(retryConnection)
!= helperConnectionPid);
Assert.assertTrue(
ChildProcessLauncherTestUtils.getConnectionService(retryConnection).bindToCaller());
}
private static void warmUpOnUiThreadBlocking(final Context context) {
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
ChildProcessLauncherHelper.warmUp(context);
}
});
ChildProcessConnection connection = getWarmUpConnection();
Assert.assertNotNull(connection);
blockUntilConnected(connection);
}
private void testWarmUpWithCreationParams(ChildProcessCreationParams creationParams) {
if (creationParams != null) {
ChildProcessCreationParams.registerDefault(creationParams);
}
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
warmUpOnUiThreadBlocking(context);
Assert.assertEquals(1, getConnectedSandboxedServicesCount());
ChildProcessLauncherHelper launcherHelper = startSandboxedChildProcessWithCreationParams(
creationParams, BLOCK_UNTIL_SETUP, true /* doSetupConnection */);
// The warm-up connection was used, so no new process should have been created.
Assert.assertEquals(1, getConnectedSandboxedServicesCount());
int pid = getPid(launcherHelper);
Assert.assertNotEquals(0, pid);
stopProcess(launcherHelper);
waitForConnectedSandboxedServicesCount(0);
}
@Test
@MediumTest
@Feature({"ProcessManagement"})
public void testWarmUp() {
// Use the default creation parameters.
testWarmUpWithCreationParams(null /* creationParams */);
}
@Test
@MediumTest
@Feature({"ProcessManagement"})
public void testWarmUpWithBindToCaller() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
ChildProcessCreationParams creationParams = new ChildProcessCreationParams(
context.getPackageName(), false /* isExternalService */,
LibraryProcessType.PROCESS_CHILD, true /* bindToCallerCheck */);
testWarmUpWithCreationParams(creationParams);
}
// Tests that the warm-up connection is freed from its allocator if it crashes.
@Test
@MediumTest
@Feature({"ProcessManagement"})
public void testWarmUpProcessCrashBeforeUse() throws RemoteException {
Assert.assertEquals(0, getConnectedSandboxedServicesCount());
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
warmUpOnUiThreadBlocking(context);
Assert.assertEquals(1, getConnectedSandboxedServicesCount());
// Crash the warm-up connection before it gets used.
ChildProcessConnection connection = getWarmUpConnection();
Assert.assertNotNull(connection);
connection.crashServiceForTesting();
// It should get cleaned-up.
waitForConnectedSandboxedServicesCount(0);
// And subsequent process launches should work.
ChildProcessLauncherHelper launcher = startSandboxedChildProcess(
null /* packageName */, BLOCK_UNTIL_SETUP, true /* doSetupConnection */);
Assert.assertEquals(1, getConnectedSandboxedServicesCount());
Assert.assertNotNull(ChildProcessLauncherTestUtils.getConnection(launcher));
}
// Tests that the warm-up connection is freed from its allocator if it crashes after being used.
@Test
@MediumTest
@Feature({"ProcessManagement"})
public void testWarmUpProcessCrashAfterUse() throws RemoteException {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
warmUpOnUiThreadBlocking(context);
Assert.assertEquals(1, getConnectedSandboxedServicesCount());
ChildProcessLauncherHelper launcherHelper = startSandboxedChildProcessWithCreationParams(
null /* creationParams */, BLOCK_UNTIL_SETUP, true /* doSetupConnection */);
// The warm-up connection was used, so no new process should have been created.
Assert.assertEquals(1, getConnectedSandboxedServicesCount());
int pid = getPid(launcherHelper);
Assert.assertNotEquals(0, pid);
ChildProcessConnection connection = retrieveConnection(launcherHelper);
connection.crashServiceForTesting();
waitForConnectedSandboxedServicesCount(0);
}
@Test
@MediumTest
@Feature({"ProcessManagement"})
public void testSandboxedAllocatorFreed() {
final String packageName =
InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName();
ChildProcessLauncherHelper launcher = startSandboxedChildProcess(
null /* packageName */, BLOCK_UNTIL_SETUP, true /* doSetupConnection */);
Assert.assertTrue(hasSandboxedConnectionAllocatorForPackage(packageName));
stopProcess(launcher);
// Poll until allocator is removed. Need to poll here because actually freeing a connection
// from allocator is a posted task, rather than a direct call from stop.
CriteriaHelper.pollInstrumentationThread(
new Criteria("The connection allocator was not removed.") {
@Override
public boolean isSatisfied() {
return !hasSandboxedConnectionAllocatorForPackage(packageName);
}
});
}
@Test
@MediumTest
@Feature({"ProcessManagement"})
@ChildProcessAllocatorSettings(sandboxedServiceCount = 4)
public void testCustomCreationParamDoesNotReuseWarmupConnection() {
// Since warmUp only uses default params.
final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
ChildProcessCreationParams defaultCreationParams =
getDefaultChildProcessCreationParams(context.getPackageName());
ChildProcessCreationParams.registerDefault(defaultCreationParams);
ChildProcessCreationParams otherCreationParams = getDefaultChildProcessCreationParams(
InstrumentationRegistry.getInstrumentation().getContext().getPackageName());
warmUpOnUiThreadBlocking(context);
Assert.assertEquals(1, getConnectedSandboxedServicesCount());
// First create a connnection with different creation params than the default, it should not
// use the warmup connection (note that it won't bind since we are using the wrong package,
// but we need to use a different package to differentiate them, and we can only have 1
// valid package per app).
startSandboxedChildProcessWithCreationParams(
otherCreationParams, DONT_BLOCK, false /* doSetupConnection */);
Assert.assertNotNull(getWarmUpConnection());
// Then start a process with the default creation params, the warmup-connection should be
// used.
startSandboxedChildProcessWithCreationParams(
defaultCreationParams, BLOCK_UNTIL_SETUP, true /* doSetupConnection */);
Assert.assertNull(getWarmUpConnection());
}
@Test
@MediumTest
@Feature({"ProcessManagement"})
public void testLauncherCleanup() throws RemoteException {
ChildProcessLauncherHelper launcher = startSandboxedChildProcess(
null /* packageName */, BLOCK_UNTIL_SETUP, true /* doSetupConnection */);
int pid = getPid(launcher);
Assert.assertNotEquals(0, pid);
// Stop the process explicitly, the launcher should get cleared.
stopProcess(launcher);
waitForConnectedSandboxedServicesCount(0);
launcher = startSandboxedChildProcess(
null /* packageName */, BLOCK_UNTIL_SETUP, true /* doSetupConnection */);
pid = getPid(launcher);
Assert.assertNotEquals(0, pid);
// This time crash the connection, the launcher should also get cleared.
ChildProcessConnection connection = retrieveConnection(launcher);
connection.crashServiceForTesting();
waitForConnectedSandboxedServicesCount(0);
}
private static ChildProcessLauncherHelper startSandboxedChildProcess(
final String packageName, int blockingPolicy, final boolean doSetupConnection) {
ChildProcessCreationParams creationParams =
packageName == null ? null : getDefaultChildProcessCreationParams(packageName);
return startSandboxedChildProcessWithCreationParams(
creationParams, blockingPolicy, doSetupConnection);
}
private static ChildProcessLauncherHelper startSandboxedChildProcessWithCreationParams(
final ChildProcessCreationParams creationParams, int blockingPolicy,
final boolean doSetupConnection) {
assert doSetupConnection || blockingPolicy != BLOCK_UNTIL_SETUP;
ChildProcessLauncherHelper launcher =
ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
new Callable<ChildProcessLauncherHelper>() {
@Override
public ChildProcessLauncherHelper call() {
return ChildProcessLauncherHelper.createAndStartForTesting(
creationParams, sProcessWaitArguments,
new FileDescriptorInfo[0], true /* sandboxed */,
null /* binderCallback */, doSetupConnection);
}
});
if (blockingPolicy != DONT_BLOCK) {
assert blockingPolicy == BLOCK_UNTIL_CONNECTED || blockingPolicy == BLOCK_UNTIL_SETUP;
blockUntilConnected(launcher);
if (blockingPolicy == BLOCK_UNTIL_SETUP) {
blockUntilSetup(launcher);
}
}
return launcher;
}
private static void blockUntilConnected(final ChildProcessLauncherHelper launcher) {
CriteriaHelper.pollInstrumentationThread(
new Criteria("The connection wasn't established.") {
@Override
public boolean isSatisfied() {
return launcher.getChildProcessConnection() != null
&& launcher.getChildProcessConnection().isConnected();
}
});
}
private static void blockUntilConnected(final ChildProcessConnection connection) {
CriteriaHelper.pollInstrumentationThread(
new Criteria("The connection wasn't established.") {
@Override
public boolean isSatisfied() {
return connection.isConnected();
}
});
}
private static void blockUntilSetup(final ChildProcessLauncherHelper launcher) {
CriteriaHelper.pollInstrumentationThread(
new Criteria("The connection wasn't established.") {
@Override
public boolean isSatisfied() {
return getPid(launcher) != 0;
}
});
}
private static ChildConnectionAllocator getChildConnectionAllocator(
final Context context, final String packageName, final boolean sandboxed) {
return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
new Callable<ChildConnectionAllocator>() {
@Override
public ChildConnectionAllocator call() {
return ChildProcessLauncherHelper.getConnectionAllocator(context,
getDefaultChildProcessCreationParams(packageName), sandboxed);
}
});
}
// Returns the number of sandboxed connection currently connected,
private static int getConnectedSandboxedServicesCount() {
return getConnectedSandboxedServicesCountForPackage(null /* packageName */);
}
// Returns the number of sandboxed connection matching the specificed package name that are
// connected,
private static int getConnectedSandboxedServicesCountForPackage(final String packageName) {
return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(new Callable<Integer>() {
@Override
public Integer call() {
return ChildProcessLauncherHelper.getConnectedSandboxedServicesCountForTesting(
packageName);
}
});
}
// Blocks until the number of sandboxed connections reaches targetCount.
private static void waitForConnectedSandboxedServicesCount(int targetCount) {
CriteriaHelper.pollInstrumentationThread(
Criteria.equals(targetCount, new Callable<Integer>() {
@Override
public Integer call() {
return getConnectedSandboxedServicesCountForPackage(null /* packageName */);
}
}));
}
private static ChildProcessCreationParams getDefaultChildProcessCreationParams(
String packageName) {
return packageName == null
? null
: new ChildProcessCreationParams(packageName, false /* isExternalService */,
LibraryProcessType.PROCESS_CHILD, false /* bindToCallerCheck */);
}
private static boolean hasSandboxedConnectionAllocatorForPackage(final String packageName) {
return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(new Callable<Boolean>() {
@Override
public Boolean call() {
return ChildProcessLauncherHelper.hasSandboxedConnectionAllocatorForPackage(
packageName);
}
});
}
private static ChildProcessConnection retrieveConnection(
final ChildProcessLauncherHelper launcherHelper) {
CriteriaHelper.pollInstrumentationThread(
new Criteria("Failed waiting for child process to connect") {
@Override
public boolean isSatisfied() {
return ChildProcessLauncherTestUtils.getConnection(launcherHelper) != null;
}
});
return ChildProcessLauncherTestUtils.getConnection(launcherHelper);
}
private static void stopProcess(ChildProcessLauncherHelper launcherHelper) {
final ChildProcessConnection connection = retrieveConnection(launcherHelper);
ChildProcessLauncherTestUtils.runOnLauncherThreadBlocking(new Runnable() {
@Override
public void run() {
ChildProcessLauncherHelper.stop(connection.getPid());
}
});
}
private static int getPid(final ChildProcessLauncherHelper launcherHelper) {
return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(new Callable<Integer>() {
@Override
public Integer call() {
return launcherHelper.getPid();
}
});
}
private static ChildProcessConnection getWarmUpConnection() {
return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
new Callable<ChildProcessConnection>() {
@Override
public ChildProcessConnection call() {
return ChildProcessLauncherHelper.getWarmUpConnectionForTesting();
}
});
}
}
// Copyright 2014 The Chromium Authors. All rights reserved. // Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
...@@ -6,15 +6,11 @@ package org.chromium.content.browser; ...@@ -6,15 +6,11 @@ package org.chromium.content.browser;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.os.Bundle;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException; import android.os.RemoteException;
import android.support.test.InstrumentationRegistry; import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.filters.MediumTest; import android.support.test.filters.MediumTest;
import org.junit.Assert; import org.junit.Assert;
...@@ -22,690 +18,584 @@ import org.junit.Before; ...@@ -22,690 +18,584 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.chromium.base.BaseSwitches;
import org.chromium.base.ThreadUtils;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.base.process_launcher.ChildProcessCreationParams;
import org.chromium.base.process_launcher.FileDescriptorInfo; import org.chromium.base.process_launcher.FileDescriptorInfo;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.Feature; import org.chromium.base.test.util.Feature;
import org.chromium.content.browser.test.ChildProcessAllocatorSettings;
import org.chromium.content.browser.test.ContentJUnit4ClassRunner; import org.chromium.content.browser.test.ContentJUnit4ClassRunner;
import org.chromium.content.browser.test.util.Criteria; import org.chromium.content.browser.test.util.Criteria;
import org.chromium.content.browser.test.util.CriteriaHelper; import org.chromium.content.browser.test.util.CriteriaHelper;
import org.chromium.content_shell_apk.ChildProcessLauncherTestHelperService;
import org.chromium.content_shell_apk.ChildProcessLauncherTestUtils; import org.chromium.content_shell_apk.ChildProcessLauncherTestUtils;
import org.chromium.content_shell_apk.IChildProcessTest;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
/** /**
* Instrumentation tests for ChildProcessLauncher. * Instrumentation tests for ChildProcessLauncher.
*/ */
@RunWith(ContentJUnit4ClassRunner.class) @RunWith(ContentJUnit4ClassRunner.class)
public class ChildProcessLauncherTest { public class ChildProcessLauncherTest {
// Pseudo command line arguments to instruct the child process to wait until being killed. private static final long CONDITION_WAIT_TIMEOUT_MS = 5000;
// Allowing the process to continue would lead to a crash when attempting to initialize IPC
// channels that are not being set up in this test.
private static final String[] sProcessWaitArguments = {
"_", "--" + BaseSwitches.RENDERER_WAIT_FOR_JAVA_DEBUGGER };
private static final String EXTERNAL_APK_PACKAGE_NAME = "org.chromium.external.apk";
private static final String DEFAULT_SANDBOXED_PROCESS_SERVICE =
"org.chromium.content.app.SandboxedProcessService";
private static final int DONT_BLOCK = 0;
private static final int BLOCK_UNTIL_CONNECTED = 1;
private static final int BLOCK_UNTIL_SETUP = 2;
@Before private static final String SERVICE_PACKAGE_NAME = "org.chromium.content_shell_apk";
public void setUp() throws Exception { private static final String SERVICE_NAME_META_DATA_KEY =
LibraryLoader.get(LibraryProcessType.PROCESS_CHILD).ensureInitialized(); "org.chromium.content.browser.TEST_SERVICES_NAME";
ChildProcessLauncherTestUtils.runOnLauncherThreadBlocking(new Runnable() { private static final String SERVICE_COUNT_META_DATA_KEY =
@Override "org.chromium.content.browser.NUM_TEST_SERVICES";
public void run() { private static final String SERVICE0_FULL_NAME =
ChildProcessLauncherHelper.initLinker(); "org.chromium.content_shell_apk.TestChildProcessService0";
}
});
}
/** private static final String EXTRA_SERVICE_PARAM = "org.chromium.content.browser.SERVICE_EXTRA";
* Tests cleanup for a connection that fails to connect in the first place. private static final String EXTRA_SERVICE_PARAM_VALUE = "SERVICE_EXTRA";
*/
@Test
@MediumTest
@Feature({"ProcessManagement"})
@ChildProcessAllocatorSettings(sandboxedServiceCount = 4)
public void testServiceFailedToBind() {
Assert.assertEquals(0, getConnectedSandboxedServicesCount());
// Try to allocate a connection to service class in incorrect package. We can do that by private static final String EXTRA_CONNECTION_PARAM =
// using the instrumentation context (getContext()) instead of the app context "org.chromium.content.browser.CONNECTION_EXTRA";
// (getTargetContext()). private static final String EXTRA_CONNECTION_PARAM_VALUE = "CONNECTION_EXTRA";
Context context = InstrumentationRegistry.getInstrumentation().getContext();
startSandboxedChildProcess(
context.getPackageName(), DONT_BLOCK, true /* doSetupConnection */);
// Verify that the connection is not considered as allocated. private static final int CONNECTION_BLOCK_UNTIL_CONNECTED = 1;
waitForConnectedSandboxedServicesCount(0); private static final int CONNECTION_BLOCK_UNTIL_SETUP = 2;
}
/** /**
* Tests cleanup for a connection that terminates before setup. * A factory used to create a ChildProcessLauncher with a bound connection or with a connection
* connection allocator so that the test code can be reused for both scenarios.
*/ */
@Test private abstract static class ChildProcessLauncherFactory {
@MediumTest private final boolean mConnectionProvided;
@Feature({"ProcessManagement"})
public void testServiceCrashedBeforeSetup() throws RemoteException {
Assert.assertEquals(0, getConnectedSandboxedServicesCount());
// Start and connect to a new service. public ChildProcessLauncherFactory(boolean connectionProvided) {
ChildProcessLauncherHelper launcher = startSandboxedChildProcess( mConnectionProvided = connectionProvided;
null /* packageName */, BLOCK_UNTIL_CONNECTED, false /* doSetupConnection */); }
// Verify that the service is bound but not yet set up.
Assert.assertEquals(1, getConnectedSandboxedServicesCount());
ChildProcessConnection connection = retrieveConnection(launcher);
Assert.assertNotNull(connection);
Assert.assertTrue(connection.isConnected());
Assert.assertEquals(0, ChildProcessLauncherTestUtils.getConnectionPid(connection));
// Crash the service. public abstract ChildProcessLauncher createChildProcessLauncher(
connection.crashServiceForTesting(); ChildProcessLauncher.Delegate delegate, String[] commandLine,
FileDescriptorInfo[] filesToBeMapped, IBinder binderCallback);
// Verify that the connection gets cleaned-up. public boolean isConnectionProvided() {
waitForConnectedSandboxedServicesCount(0); return mConnectionProvided;
}
} }
/** private static final ChildProcessLauncher.Delegate EMPTY_LAUNCHER_DELEGATE =
* Tests cleanup for a connection that terminates after setup. new ChildProcessLauncher.Delegate() {
*/ @Override
@Test public void onBeforeConnectionAllocated(Bundle serviceBundle) {}
@MediumTest
@Feature({"ProcessManagement"})
public void testServiceCrashedAfterSetup() throws RemoteException {
Assert.assertEquals(0, getConnectedSandboxedServicesCount());
// Start and connect to a new service. @Override
ChildProcessLauncherHelper launcher = startSandboxedChildProcess( public void onBeforeConnectionSetup(Bundle connectionBundle) {}
null /* packageName */, BLOCK_UNTIL_SETUP, true /* doSetupConnection */);
Assert.assertEquals(1, getConnectedSandboxedServicesCount());
int pid = getPid(launcher); @Override
Assert.assertNotEquals(0, pid); public void onConnectionEstablished(ChildProcessConnection connection) {}
// Crash the service. @Override
ChildProcessConnection connection = retrieveConnection(launcher); public void onConnectionLost(ChildProcessConnection connection) {}
connection.crashServiceForTesting(); };
// Verify that the connection gets cleaned-up. private ChildConnectionAllocator mConnectionAllocator;
waitForConnectedSandboxedServicesCount(0);
// Verify that the connection pid remains set after termination. @Before
Assert.assertTrue(ChildProcessLauncherTestUtils.getConnectionPid(connection) != 0); public void setUp() throws Exception {
// And that the launcher is cleared. mConnectionAllocator = ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
Assert.assertNull(ChildProcessLauncherHelper.getLauncherForPid(pid)); new Callable<ChildConnectionAllocator>() {
@Override
public ChildConnectionAllocator call() {
Context context =
InstrumentationRegistry.getInstrumentation().getTargetContext();
return ChildConnectionAllocator.create(context, null /* creationParams */,
SERVICE_PACKAGE_NAME, SERVICE_NAME_META_DATA_KEY,
SERVICE_COUNT_META_DATA_KEY, false /* bindAsExternalService */,
false /* useStrongBinding */);
}
});
} }
/** private static class IChildProcessBinder extends IChildProcessTest.Stub {
* Tests that connection requests get queued when no slot is availabe and created once a slot private final CallbackHelper mOnConnectionSetupHelper = new CallbackHelper();
* frees up . private final CallbackHelper mOnLoadNativeHelper = new CallbackHelper();
*/ private final CallbackHelper mOnBeforeMainHelper = new CallbackHelper();
@Test private final CallbackHelper mOnRunMainHelper = new CallbackHelper();
@MediumTest private final CallbackHelper mOnDestroyHelper = new CallbackHelper();
@Feature({"ProcessManagement"})
@ChildProcessAllocatorSettings(sandboxedServiceCount = 1) // Can be accessed after mOnConnectionSetupCalled is signaled.
public void testPendingSpawnQueue() throws RemoteException { private boolean mServiceCreated;
Assert.assertEquals(0, getConnectedSandboxedServicesCount()); private Bundle mServiceBundle;
private Bundle mConnectionBundle;
// Can be accessed after mOnLoadNativeCalled is signaled.
private boolean mNativeLibraryLoaded;
// Can be accessed after mOnBeforeMainCalled is signaled.
private String[] mCommandLine;
@Override
public void onConnectionSetup(
boolean serviceCreatedCalled, Bundle serviceBundle, Bundle connectionBundle) {
mServiceCreated = serviceCreatedCalled;
mServiceBundle = serviceBundle;
mConnectionBundle = connectionBundle;
Assert.assertEquals(0, mOnConnectionSetupHelper.getCallCount());
mOnConnectionSetupHelper.notifyCalled();
}
@Override
public void onLoadNativeLibrary(boolean loadedSuccessfully) {
mNativeLibraryLoaded = loadedSuccessfully;
Assert.assertEquals(0, mOnLoadNativeHelper.getCallCount());
mOnLoadNativeHelper.notifyCalled();
}
ChildProcessLauncherHelper launcher1 = startSandboxedChildProcess( @Override
null /* packageName */, BLOCK_UNTIL_SETUP, true /* doSetupConnection */); public void onBeforeMain(String[] commandLine) {
Assert.assertEquals(1, getConnectedSandboxedServicesCount()); mCommandLine = commandLine;
Assert.assertNotNull(ChildProcessLauncherTestUtils.getConnection(launcher1)); Assert.assertEquals(0, mOnBeforeMainHelper.getCallCount());
mOnBeforeMainHelper.notifyCalled();
}
ChildProcessLauncherHelper launcher2 = startSandboxedChildProcess( @Override
null /* packageName */, DONT_BLOCK, true /* doSetupConnection */); public void onRunMain() {
Assert.assertEquals(1, getConnectedSandboxedServicesCount()); Assert.assertEquals(0, mOnRunMainHelper.getCallCount());
Assert.assertNull(ChildProcessLauncherTestUtils.getConnection(launcher2)); mOnRunMainHelper.notifyCalled();
}
final ChildProcessConnection connection1 = @Override
ChildProcessLauncherTestUtils.getConnection(launcher1); public void onDestroy() {
connection1.crashServiceForTesting(); Assert.assertEquals(0, mOnDestroyHelper.getCallCount());
mOnDestroyHelper.notifyCalled();
}
// The previous service crashing should have freed a connection that should be used for the public void waitForOnConnectionSetupCalled() throws InterruptedException, TimeoutException {
// pending process. mOnConnectionSetupHelper.waitForCallback(0 /* currentCallCount */);
blockUntilConnected(launcher2); }
}
/** public void waitForOnNativeLibraryCalled() throws InterruptedException, TimeoutException {
* Tests that external APKs and regular use different ChildConnectionAllocators. mOnLoadNativeHelper.waitForCallback(0 /* currentCallCount */);
*/ }
@Test
@MediumTest public void waitOnBeforeMainCalled() throws InterruptedException, TimeoutException {
@Feature({"ProcessManagement"}) mOnBeforeMainHelper.waitForCallback(0 /* currentCallCount */);
@ChildProcessAllocatorSettings( }
sandboxedServiceCount = 4, sandboxedServiceName = DEFAULT_SANDBOXED_PROCESS_SERVICE)
public void testAllocatorForPackage() { public void waitOnRunMainCalled() throws InterruptedException, TimeoutException {
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); mOnRunMainHelper.waitForCallback(0 /* currentCallCount */);
}
ChildConnectionAllocator connectionAllocator = getChildConnectionAllocator(
appContext, appContext.getPackageName(), true /* sandboxed */); public void waitOnDestroyCalled() throws InterruptedException, TimeoutException {
ChildConnectionAllocator externalConnectionAllocator = getChildConnectionAllocator( mOnDestroyHelper.waitForCallback(0 /* currentCallCount */);
appContext, EXTERNAL_APK_PACKAGE_NAME, true /* sandboxed */); }
Assert.assertNotEquals(connectionAllocator, externalConnectionAllocator); };
}
/** /**
* Tests binding to the same sandboxed service process from multiple processes in the * Creates a child process with the ChildProcessLauncher created with {@param launcherFactory}
* same package. This uses the ChildProcessLauncherTestHelperService declared in * and tests that the all callbacks on the client and in the service are called appropriately.
* ContentShell.apk as a separate android:process to bind the first (slot 0) service. The * The service echos back the delegate calls through the IBinder callback so that the test can
* instrumentation test then tries to bind the same slot, which fails, so the * validate them.
* ChildProcessLauncher retries on a new connection.
*/ */
@Test private void testProcessLauncher(final ChildProcessLauncherFactory launcherFactory)
@MediumTest throws InterruptedException, TimeoutException {
@Feature({"ProcessManagement"}) // ConditionVariables used to check the ChildProcessLauncher.Delegate methods get called.
public void testBindServiceFromMultipleProcesses() throws RemoteException { final CallbackHelper onBeforeConnectionAllocatedHelper = new CallbackHelper();
final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); final CallbackHelper onBeforeConnectionSetupHelper = new CallbackHelper();
final CallbackHelper onConnectionEstablishedHelper = new CallbackHelper();
final CallbackHelper onConnectionLostHelper = new CallbackHelper();
final ChildProcessLauncher.Delegate delegate = new ChildProcessLauncher.Delegate() {
@Override
public void onBeforeConnectionAllocated(Bundle serviceBundle) {
// Should only be called when the ChildProcessLauncher creates the connection.
Assert.assertFalse(launcherFactory.isConnectionProvided());
Assert.assertEquals(0, onBeforeConnectionAllocatedHelper.getCallCount());
serviceBundle.putString(EXTRA_SERVICE_PARAM, EXTRA_SERVICE_PARAM_VALUE);
onBeforeConnectionAllocatedHelper.notifyCalled();
}
// Start the Helper service. @Override
class HelperConnection implements ServiceConnection { public void onBeforeConnectionSetup(Bundle connectionBundle) {
Messenger mMessenger = null; connectionBundle.putString(EXTRA_CONNECTION_PARAM, EXTRA_CONNECTION_PARAM_VALUE);
Assert.assertEquals(0, onBeforeConnectionSetupHelper.getCallCount());
onBeforeConnectionSetupHelper.notifyCalled();
}
@Override @Override
public void onServiceConnected(ComponentName name, IBinder service) { public void onConnectionEstablished(ChildProcessConnection connection) {
mMessenger = new Messenger(service); Assert.assertEquals(0, onConnectionEstablishedHelper.getCallCount());
onConnectionEstablishedHelper.notifyCalled();
} }
@Override @Override
public void onServiceDisconnected(ComponentName name) {} public void onConnectionLost(ChildProcessConnection connection) {
} Assert.assertEquals(0, onConnectionLostHelper.getCallCount());
final HelperConnection serviceConnection = new HelperConnection(); onConnectionLostHelper.notifyCalled();
}
};
Intent intent = new Intent(); final String[] commandLine = new String[] {"--test-param1", "--test-param2"};
intent.setComponent(new ComponentName(context.getPackageName(), final FileDescriptorInfo[] filesToBeMapped = new FileDescriptorInfo[0];
context.getPackageName() + ".ChildProcessLauncherTestHelperService"));
Assert.assertTrue(context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE));
// Wait for the Helper service to connect. final IChildProcessBinder childProcessBinder = new IChildProcessBinder();
CriteriaHelper.pollInstrumentationThread(
new Criteria("Failed to get helper service Messenger") {
@Override
public boolean isSatisfied() {
return serviceConnection.mMessenger != null;
}
});
Assert.assertNotNull(serviceConnection.mMessenger); final ChildProcessLauncher processLauncher =
ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
new Callable<ChildProcessLauncher>() {
@Override
public ChildProcessLauncher call() {
ChildProcessLauncher processLauncher =
launcherFactory.createChildProcessLauncher(delegate,
commandLine, filesToBeMapped, childProcessBinder);
processLauncher.start(true /* setupConnection */,
false /*queueIfNoFreeConnection */);
return processLauncher;
}
});
class ReplyHandler implements Handler.Callback { Assert.assertNotNull(processLauncher);
Message mMessage;
@Override if (!launcherFactory.isConnectionProvided()) {
public boolean handleMessage(Message msg) { onBeforeConnectionAllocatedHelper.waitForCallback(0 /* currentCallback */);
// Copy the message so its contents outlive this Binder transaction.
mMessage = Message.obtain();
mMessage.copyFrom(msg);
return true;
}
} }
final ReplyHandler replyHandler = new ReplyHandler();
// Send a message to the Helper and wait for the reply. This will cause the slot 0
// sandboxed service connection to be bound by a different PID (i.e., not this process).
Message msg = Message.obtain(null, ChildProcessLauncherTestHelperService.MSG_BIND_SERVICE);
msg.replyTo = new Messenger(new Handler(Looper.getMainLooper(), replyHandler));
serviceConnection.mMessenger.send(msg);
CriteriaHelper.pollInstrumentationThread( onBeforeConnectionSetupHelper.waitForCallback(0 /* currentCallback */);
new Criteria("Failed waiting for helper service reply") {
@Override
public boolean isSatisfied() {
return replyHandler.mMessage != null;
}
});
// Verify that the Helper was able to launch the sandboxed service. // Wait for the service to notify its onConnectionSetup was called.
Assert.assertNotNull(replyHandler.mMessage); childProcessBinder.waitForOnConnectionSetupCalled();
Assert.assertEquals(ChildProcessLauncherTestHelperService.MSG_BIND_SERVICE_REPLY, Assert.assertTrue(childProcessBinder.mServiceCreated);
replyHandler.mMessage.what); Assert.assertNotNull(childProcessBinder.mServiceBundle);
Assert.assertEquals( Assert.assertNotNull(childProcessBinder.mConnectionBundle);
"Connection slot from helper service is not 0", 0, replyHandler.mMessage.arg2); if (!launcherFactory.isConnectionProvided()) {
Assert.assertEquals(EXTRA_SERVICE_PARAM_VALUE,
final int helperConnectionPid = replyHandler.mMessage.arg1; childProcessBinder.mServiceBundle.getString(EXTRA_SERVICE_PARAM));
Assert.assertTrue(helperConnectionPid > 0);
// Launch a service from this process. Since slot 0 is already bound by the Helper, it
// will fail to start and the ChildProcessLauncher will retry and use the slot 1.
ChildProcessCreationParams creationParams = new ChildProcessCreationParams(
context.getPackageName(), false /* isExternalService */,
LibraryProcessType.PROCESS_CHILD, true /* bindToCallerCheck */);
ChildProcessLauncherHelper launcher = startSandboxedChildProcessWithCreationParams(
creationParams, BLOCK_UNTIL_SETUP, true /* doSetupConnection */);
final ChildProcessConnection retryConnection =
ChildProcessLauncherTestUtils.getConnection(launcher);
Assert.assertEquals(
1, ChildProcessLauncherTestUtils.getConnectionServiceNumber(retryConnection));
ChildConnectionAllocator connectionAllocator =
launcher.getChildConnectionAllocatorForTesting();
// Check that only two connections are created.
for (int i = 0; i < connectionAllocator.getNumberOfServices(); ++i) {
ChildProcessConnection sandboxedConn =
connectionAllocator.getChildProcessConnectionAtSlotForTesting(i);
if (i <= 1) {
Assert.assertNotNull(sandboxedConn);
Assert.assertNotNull(
ChildProcessLauncherTestUtils.getConnectionService(sandboxedConn));
} else {
Assert.assertNull(sandboxedConn);
}
} }
Assert.assertEquals(EXTRA_CONNECTION_PARAM_VALUE,
childProcessBinder.mConnectionBundle.getString(EXTRA_CONNECTION_PARAM));
Assert.assertEquals( // Wait for the client onConnectionEstablished call.
connectionAllocator.getChildProcessConnectionAtSlotForTesting(1), retryConnection); onConnectionEstablishedHelper.waitForCallback(0 /* currentCallback */);
ChildProcessConnection failedConnection = // Wait for the service to notify its library got loaded.
connectionAllocator.getChildProcessConnectionAtSlotForTesting(0); childProcessBinder.waitForOnNativeLibraryCalled();
Assert.assertEquals(0, ChildProcessLauncherTestUtils.getConnectionPid(failedConnection)); Assert.assertTrue(childProcessBinder.mNativeLibraryLoaded);
Assert.assertFalse(ChildProcessLauncherTestUtils.getConnectionService(failedConnection)
.bindToCaller());
CriteriaHelper.pollInstrumentationThread( // Wait for the service to notify its onBeforeMain was called.
new Criteria("Failed waiting retry connection to get pid") { childProcessBinder.waitOnBeforeMainCalled();
@Override Assert.assertArrayEquals(commandLine, childProcessBinder.mCommandLine);
public boolean isSatisfied() {
return ChildProcessLauncherTestUtils.getConnectionPid(retryConnection) > 0;
}
});
Assert.assertTrue(ChildProcessLauncherTestUtils.getConnectionPid(retryConnection)
!= helperConnectionPid);
Assert.assertTrue(
ChildProcessLauncherTestUtils.getConnectionService(retryConnection).bindToCaller());
}
private static void warmUpOnUiThreadBlocking(final Context context) { // Wait for the service to notify its onRunMain was called.
ThreadUtils.runOnUiThreadBlocking(new Runnable() { childProcessBinder.waitOnRunMainCalled();
// Stop the launcher.
ChildProcessLauncherTestUtils.runOnLauncherThreadBlocking(new Runnable() {
@Override @Override
public void run() { public void run() {
ChildProcessLauncherHelper.warmUp(context); processLauncher.stop();
} }
}); });
ChildProcessConnection connection = getWarmUpConnection(); // Wait for service to notify its onDestroy was called.
Assert.assertNotNull(connection); childProcessBinder.waitOnDestroyCalled();
blockUntilConnected(connection); // The client should also get a notification that the connection was lost.
onConnectionLostHelper.waitForCallback(0 /* currentCallback */);
} }
private void testWarmUpWithCreationParams(ChildProcessCreationParams creationParams) { @Test
if (creationParams != null) { @LargeTest
ChildProcessCreationParams.registerDefault(creationParams); @Feature({"ProcessManagement"})
} public void testLaunchServiceCreatedWithConnectionAllocator() throws Exception {
final ChildProcessLauncherFactory childProcessLauncherFactory =
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); new ChildProcessLauncherFactory(false /* providesConnection */) {
warmUpOnUiThreadBlocking(context); @Override
public ChildProcessLauncher createChildProcessLauncher(
Assert.assertEquals(1, getConnectedSandboxedServicesCount()); ChildProcessLauncher.Delegate delegate, String[] commandLine,
FileDescriptorInfo[] filesToBeMapped, IBinder binderCallback) {
return ChildProcessLauncher.createWithConnectionAllocator(delegate,
commandLine, filesToBeMapped, mConnectionAllocator, binderCallback);
}
};
ChildProcessLauncherHelper launcherHelper = startSandboxedChildProcessWithCreationParams( testProcessLauncher(childProcessLauncherFactory);
creationParams, BLOCK_UNTIL_SETUP, true /* doSetupConnection */); }
// The warm-up connection was used, so no new process should have been created. @Test
Assert.assertEquals(1, getConnectedSandboxedServicesCount()); @LargeTest
@Feature({"ProcessManagement"})
public void testLaunchServiceCreatedWithBoundConnection() throws Exception {
// Wraps the serviceCallback provided by the ChildProcessLauncher so that the
// ChildProcessConnection can forward to them appropriately.
final AtomicReference<ChildProcessConnection.ServiceCallback> serviceCallbackWrapper =
new AtomicReference<>();
final ChildProcessConnection boundConnection =
ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(new Callable<
ChildProcessConnection>() {
@Override
public ChildProcessConnection call() {
Context context =
InstrumentationRegistry.getInstrumentation().getTargetContext();
ComponentName serviceName =
new ComponentName(SERVICE_PACKAGE_NAME, SERVICE0_FULL_NAME);
ChildProcessConnection connection = new ChildProcessConnection(context,
serviceName, false /* bindAsExternalService */,
new Bundle() /* serviceBundle */, null /* creationParams */);
connection.start(false /* useStrongBinding */,
new ChildProcessConnection.ServiceCallback() {
@Override
public void onChildStarted() {
if (serviceCallbackWrapper.get() != null) {
serviceCallbackWrapper.get().onChildStarted();
}
}
@Override
public void onChildStartFailed() {
if (serviceCallbackWrapper.get() != null) {
serviceCallbackWrapper.get().onChildStartFailed();
}
}
@Override
public void onChildProcessDied(
ChildProcessConnection connection) {
if (serviceCallbackWrapper.get() != null) {
serviceCallbackWrapper.get().onChildProcessDied(
connection);
}
}
});
return connection;
}
});
int pid = getPid(launcherHelper); Assert.assertNotNull(boundConnection);
Assert.assertNotEquals(0, pid);
stopProcess(launcherHelper); CriteriaHelper.pollInstrumentationThread(new Criteria("Connection failed to connect") {
@Override
public boolean isSatisfied() {
return boundConnection.isConnected();
}
});
waitForConnectedSandboxedServicesCount(0); final ChildProcessLauncher.BoundConnectionProvider connectionProvider =
Assert.assertNull(ChildProcessLauncherHelper.getLauncherForPid(pid)); new ChildProcessLauncher.BoundConnectionProvider() {
} @Override
public ChildProcessConnection getConnection(
ChildProcessConnection.ServiceCallback serviceCallback) {
serviceCallbackWrapper.set(serviceCallback);
return boundConnection;
}
};
@Test final ChildProcessLauncherFactory childProcessLauncherFactory =
@MediumTest new ChildProcessLauncherFactory(true /* providesConnection */) {
@Feature({"ProcessManagement"}) @Override
public void testWarmUp() { public ChildProcessLauncher createChildProcessLauncher(
// Use the default creation parameters. ChildProcessLauncher.Delegate delegate, String[] commandLine,
testWarmUpWithCreationParams(null /* creationParams */); FileDescriptorInfo[] filesToBeMapped, IBinder binderCallback) {
} return ChildProcessLauncher.createWithBoundConnectionProvider(delegate,
commandLine, filesToBeMapped, connectionProvider, binderCallback);
}
};
@Test testProcessLauncher(childProcessLauncherFactory);
@MediumTest
@Feature({"ProcessManagement"})
public void testWarmUpWithBindToCaller() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
ChildProcessCreationParams creationParams = new ChildProcessCreationParams(
context.getPackageName(), false /* isExternalService */,
LibraryProcessType.PROCESS_CHILD, true /* bindToCallerCheck */);
testWarmUpWithCreationParams(creationParams);
} }
// Tests that the warm-up connection is freed from its allocator if it crashes. /**
* Tests cleanup for a connection that fails to connect in the first place.
*/
@Test @Test
@MediumTest @MediumTest
@Feature({"ProcessManagement"}) @Feature({"ProcessManagement"})
public void testWarmUpProcessCrashBeforeUse() throws RemoteException { public void testServiceFailedToBind() {
Assert.assertEquals(0, getConnectedSandboxedServicesCount()); final ChildConnectionAllocator badConnectionAllocator =
ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); new Callable<ChildConnectionAllocator>() {
warmUpOnUiThreadBlocking(context); @Override
public ChildConnectionAllocator call() {
Assert.assertEquals(1, getConnectedSandboxedServicesCount()); return ChildConnectionAllocator.createForTest(
null /* creationParams */, "org.chromium.wrong_package",
"WrongService", 2 /* serviceCount */,
false /* bindAsExternalService */,
false /* useStrongBinding */);
}
});
Assert.assertFalse(badConnectionAllocator.anyConnectionAllocated());
// Crash the warm-up connection before it gets used. // Try to allocate a connection to service class in incorrect package. We can do that by
ChildProcessConnection connection = getWarmUpConnection(); // using the instrumentation context (getContext()) instead of the app context
Assert.assertNotNull(connection); // (getTargetContext()).
connection.crashServiceForTesting(); ChildProcessLauncher processLauncher = createChildProcessLauncher(badConnectionAllocator,
true /* setupConnection */, false /* queueIfNoFreeConnection */);
// It should get cleaned-up. Assert.assertNotNull(processLauncher);
waitForConnectedSandboxedServicesCount(0);
// And subsequent process launches should work. // Verify that the connection is not considered as allocated (or only briefly, as the
ChildProcessLauncherHelper launcher = startSandboxedChildProcess( // freeing is delayed).
null /* packageName */, BLOCK_UNTIL_SETUP, true /* doSetupConnection */); waitForConnectionAllocatorState(badConnectionAllocator, true /* isEmpty */);
Assert.assertEquals(1, getConnectedSandboxedServicesCount());
Assert.assertNotNull(ChildProcessLauncherTestUtils.getConnection(launcher));
} }
// Tests that the warm-up connection is freed from its allocator if it crashes after being used. /**
* Tests cleanup for a connection that terminates before setup.
*/
@Test @Test
@MediumTest @MediumTest
@Feature({"ProcessManagement"}) @Feature({"ProcessManagement"})
public void testWarmUpProcessCrashAfterUse() throws RemoteException { public void testServiceCrashedBeforeSetup() throws RemoteException {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); Assert.assertFalse(mConnectionAllocator.anyConnectionAllocated());
warmUpOnUiThreadBlocking(context);
Assert.assertEquals(1, getConnectedSandboxedServicesCount());
ChildProcessLauncherHelper launcherHelper = startSandboxedChildProcessWithCreationParams(
null /* creationParams */, BLOCK_UNTIL_SETUP, true /* doSetupConnection */);
// The warm-up connection was used, so no new process should have been created. // Start and connect to a new service.
Assert.assertEquals(1, getConnectedSandboxedServicesCount()); ChildProcessLauncher processLauncher = createChildProcessLauncher(mConnectionAllocator,
false /* setupConnection */, false /* queueIfNoFreeConnection */);
int pid = getPid(launcherHelper); // Verify that the service is bound but not yet set up.
Assert.assertNotEquals(0, pid); Assert.assertTrue(mConnectionAllocator.anyConnectionAllocated());
ChildProcessConnection connection = processLauncher.getConnection();
Assert.assertNotNull(connection);
waitForConnectionState(connection, CONNECTION_BLOCK_UNTIL_CONNECTED);
Assert.assertEquals(0, getConnectionPid(connection));
ChildProcessConnection connection = retrieveConnection(launcherHelper); // Crash the service.
connection.crashServiceForTesting(); connection.crashServiceForTesting();
waitForConnectedSandboxedServicesCount(0); // Verify that the connection gets cleaned-up.
Assert.assertNull(ChildProcessLauncherHelper.getLauncherForPid(pid)); waitForConnectionAllocatorState(mConnectionAllocator, true /* isEmpty */);
} }
@Test @Test
@MediumTest @MediumTest
@Feature({"ProcessManagement"}) @Feature({"ProcessManagement"})
public void testSandboxedAllocatorFreed() { public void testServiceCrashedAfterSetup() throws RemoteException {
final String packageName = Assert.assertFalse(mConnectionAllocator.anyConnectionAllocated());
InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName();
ChildProcessLauncherHelper launcher = startSandboxedChildProcess( // Start and connect to a new service.
null /* packageName */, BLOCK_UNTIL_SETUP, true /* doSetupConnection */); ChildProcessLauncher processLauncher = createChildProcessLauncher(mConnectionAllocator,
true /* setupConnection */, false /* queueIfNoFreeConnection */);
Assert.assertTrue(hasSandboxedConnectionAllocatorForPackage(packageName)); Assert.assertTrue(mConnectionAllocator.anyConnectionAllocated());
ChildProcessConnection connection = processLauncher.getConnection();
Assert.assertNotNull(connection);
waitForConnectionState(connection, CONNECTION_BLOCK_UNTIL_SETUP);
// We are passed set-up, the connection should have received its PID.
Assert.assertNotEquals(0, getConnectionPid(connection));
stopProcess(launcher); // Crash the service.
connection.crashServiceForTesting();
// Poll until allocator is removed. Need to poll here because actually freeing a connection // Verify that the connection gets cleaned-up.
// from allocator is a posted task, rather than a direct call from stop. waitForConnectionAllocatorState(mConnectionAllocator, true /* isEmpty */);
CriteriaHelper.pollInstrumentationThread(
new Criteria("The connection allocator was not removed.") {
@Override
public boolean isSatisfied() {
return !hasSandboxedConnectionAllocatorForPackage(packageName);
}
});
}
@Test // Verify that the connection pid remains set after termination.
@MediumTest Assert.assertNotEquals(0, getConnectionPid(connection));
@Feature({"ProcessManagement"})
@ChildProcessAllocatorSettings(sandboxedServiceCount = 4)
public void testCustomCreationParamDoesNotReuseWarmupConnection() {
// Since warmUp only uses default params.
final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
ChildProcessCreationParams defaultCreationParams =
getDefaultChildProcessCreationParams(context.getPackageName());
ChildProcessCreationParams.registerDefault(defaultCreationParams);
ChildProcessCreationParams otherCreationParams = getDefaultChildProcessCreationParams(
InstrumentationRegistry.getInstrumentation().getContext().getPackageName());
warmUpOnUiThreadBlocking(context);
Assert.assertEquals(1, getConnectedSandboxedServicesCount());
// First create a connnection with different creation params than the default, it should not
// use the warmup connection (note that it won't bind since we are using the wrong package,
// but we need to use a different package to differentiate them, and we can only have 1
// valid package per app).
startSandboxedChildProcessWithCreationParams(
otherCreationParams, DONT_BLOCK, false /* doSetupConnection */);
Assert.assertNotNull(getWarmUpConnection());
// Then start a process with the default creation params, the warmup-connection should be
// used.
startSandboxedChildProcessWithCreationParams(
defaultCreationParams, BLOCK_UNTIL_SETUP, true /* doSetupConnection */);
Assert.assertNull(getWarmUpConnection());
} }
@Test @Test
@MediumTest @MediumTest
@Feature({"ProcessManagement"}) @Feature({"ProcessManagement"})
public void testUseStrongBindingConnection() { public void testPendingSpawnQueue() throws RemoteException {
// Since warmUp only uses default params. Assert.assertFalse(mConnectionAllocator.anyConnectionAllocated());
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
ChildProcessCreationParams creationParams = new ChildProcessCreationParams( // Launch 4 processes. Since we have only 2 services, the 3rd and 4th should get queued.
context.getPackageName(), false /* isExternalService */, ChildProcessLauncher[] launchers = new ChildProcessLauncher[4];
LibraryProcessType.PROCESS_CHILD, true /* bindToCallerCheck */); ChildProcessConnection[] connections = new ChildProcessConnection[4];
for (int i = 0; i < 4; i++) {
for (final boolean sandboxed : new boolean[] {true, false}) { launchers[i] = createChildProcessLauncher(mConnectionAllocator,
ChildProcessLauncherHelper launcher = ChildProcessLauncherTestUtils.startForTesting( true /* setupConnection */, true /* queueIfNoFreeConnection */);
sandboxed, sProcessWaitArguments, new FileDescriptorInfo[0], creationParams, Assert.assertNotNull(launchers[i]);
true /* doSetupConnection */); connections[i] = launchers[i].getConnection();
final ChildProcessConnection connection =
ChildProcessLauncherTestUtils.getConnection(launcher);
Assert.assertNotNull(connection);
ChildProcessLauncherTestUtils.runOnLauncherThreadBlocking(new Runnable() {
@Override
public void run() {
// Only non sandboxed connections should use strong bindings.
Assert.assertNotEquals(sandboxed, connection.isStrongBindingBound());
}
});
} }
} Assert.assertNotNull(connections[0]);
Assert.assertNotNull(connections[1]);
Assert.assertNull(connections[2]);
Assert.assertNull(connections[3]);
@Test // Test creating a launcher with queueIfNoFreeConnection false with no connection available.
@MediumTest Assert.assertNull(createChildProcessLauncher(mConnectionAllocator,
@Feature({"ProcessManagement"}) true /* setupConnection */, false /* queueIfNoFreeConnection */));
public void testLauncherCleanup() throws RemoteException {
ChildProcessLauncherHelper launcher = startSandboxedChildProcess(
null /* packageName */, BLOCK_UNTIL_SETUP, true /* doSetupConnection */);
int pid = getPid(launcher);
Assert.assertNotEquals(0, pid);
Assert.assertNotNull(ChildProcessLauncherHelper.getLauncherForPid(pid));
// Stop the process explicitly, the launcher should get cleared.
stopProcess(launcher);
waitForConnectedSandboxedServicesCount(0);
Assert.assertNull(ChildProcessLauncherHelper.getLauncherForPid(pid));
launcher = startSandboxedChildProcess(
null /* packageName */, BLOCK_UNTIL_SETUP, true /* doSetupConnection */);
pid = getPid(launcher);
Assert.assertNotEquals(0, pid);
Assert.assertNotNull(ChildProcessLauncherHelper.getLauncherForPid(pid));
// This time crash the connection, the launcher should also get cleared.
ChildProcessConnection connection = retrieveConnection(launcher);
connection.crashServiceForTesting();
waitForConnectedSandboxedServicesCount(0);
Assert.assertNull(ChildProcessLauncherHelper.getLauncherForPid(pid));
}
private static ChildProcessLauncherHelper startSandboxedChildProcess( // Stop one connection, that should free-up a connection and the first queued launcher
final String packageName, int blockingPolicy, final boolean doSetupConnection) { // should use it.
ChildProcessCreationParams creationParams = stopLauncher(launchers[0]);
packageName == null ? null : getDefaultChildProcessCreationParams(packageName); waitUntilLauncherSetup(launchers[2]);
return startSandboxedChildProcessWithCreationParams(
creationParams, blockingPolicy, doSetupConnection);
}
private static ChildProcessLauncherHelper startSandboxedChildProcessWithCreationParams( // Last launcher is still queued.
final ChildProcessCreationParams creationParams, int blockingPolicy, Assert.assertNull(launchers[3].getConnection());
final boolean doSetupConnection) {
assert doSetupConnection || blockingPolicy != BLOCK_UNTIL_SETUP; // Crash another launcher. It should free-up another connection that the queued-up launcher
ChildProcessLauncherHelper launcher = // should use.
ChildProcessLauncherTestUtils.runOnLauncherAndGetResult( connections[1].crashServiceForTesting();
new Callable<ChildProcessLauncherHelper>() { waitUntilLauncherSetup(launchers[3]);
@Override
public ChildProcessLauncherHelper call() {
return ChildProcessLauncherHelper.createAndStartForTesting(
creationParams, sProcessWaitArguments,
new FileDescriptorInfo[0], true /* sandboxed */,
null /* binderCallback */, doSetupConnection);
}
});
if (blockingPolicy != DONT_BLOCK) {
assert blockingPolicy == BLOCK_UNTIL_CONNECTED || blockingPolicy == BLOCK_UNTIL_SETUP;
blockUntilConnected(launcher);
if (blockingPolicy == BLOCK_UNTIL_SETUP) {
blockUntilSetup(launcher);
}
}
return launcher;
} }
private static void blockUntilConnected(final ChildProcessLauncherHelper launcher) { private static ChildProcessLauncher createChildProcessLauncher(
CriteriaHelper.pollInstrumentationThread( final ChildConnectionAllocator connectionAllocator, final boolean setupConnection,
new Criteria("The connection wasn't established.") { final boolean queueIfNoFreeConnection) {
return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
new Callable<ChildProcessLauncher>() {
@Override @Override
public boolean isSatisfied() { public ChildProcessLauncher call() {
return launcher.getConnection() != null ChildProcessLauncher processLauncher =
&& launcher.getConnection().isConnected(); ChildProcessLauncher.createWithConnectionAllocator(
EMPTY_LAUNCHER_DELEGATE, new String[0],
new FileDescriptorInfo[0], connectionAllocator,
null /* binderCallback */);
if (!processLauncher.start(setupConnection, queueIfNoFreeConnection)) {
return null;
}
return processLauncher;
} }
}); });
} }
private static void blockUntilConnected(final ChildProcessConnection connection) { private static void waitForConnectionAllocatorState(
final ChildConnectionAllocator connectionAllocator, final boolean emptyState) {
CriteriaHelper.pollInstrumentationThread( CriteriaHelper.pollInstrumentationThread(
new Criteria("The connection wasn't established.") { new Criteria("Failed to wait for connection allocator.") {
@Override @Override
public boolean isSatisfied() { public boolean isSatisfied() {
return connection.isConnected(); return emptyState ? !connectionAllocator.anyConnectionAllocated()
: connectionAllocator.anyConnectionAllocated();
} }
}); });
} }
private static void blockUntilSetup(final ChildProcessLauncherHelper launcher) { private static void waitForConnectionState(
final ChildProcessConnection connection, final int connectionState) {
assert connectionState == CONNECTION_BLOCK_UNTIL_CONNECTED
|| connectionState == CONNECTION_BLOCK_UNTIL_SETUP;
CriteriaHelper.pollInstrumentationThread( CriteriaHelper.pollInstrumentationThread(
new Criteria("The connection wasn't established.") { new Criteria("Failed wait for connection to connect.") {
@Override @Override
public boolean isSatisfied() { public boolean isSatisfied() {
return getPid(launcher) != 0; if (connectionState == CONNECTION_BLOCK_UNTIL_CONNECTED) {
} return connection.isConnected();
}); }
} assert connectionState == CONNECTION_BLOCK_UNTIL_SETUP;
return getConnectionPid(connection) != 0;
private static ChildConnectionAllocator getChildConnectionAllocator(
final Context context, final String packageName, final boolean sandboxed) {
return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
new Callable<ChildConnectionAllocator>() {
@Override
public ChildConnectionAllocator call() {
return ChildProcessLauncherHelper.getConnectionAllocator(context,
getDefaultChildProcessCreationParams(packageName), sandboxed);
} }
}); });
} }
// Returns the number of sandboxed connection currently connected, private static void waitUntilLauncherSetup(final ChildProcessLauncher launcher) {
private static int getConnectedSandboxedServicesCount() {
return getConnectedSandboxedServicesCountForPackage(null /* packageName */);
}
// Returns the number of sandboxed connection matching the specificed package name that are
// connected,
private static int getConnectedSandboxedServicesCountForPackage(final String packageName) {
return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
new Callable<Integer>() {
@Override
public Integer call() {
return ChildProcessLauncherHelper
.getConnectedSandboxedServicesCountForTesting(packageName);
}
});
}
// Blocks until the number of sandboxed connections reaches targetCount.
private static void waitForConnectedSandboxedServicesCount(int targetCount) {
CriteriaHelper.pollInstrumentationThread(
Criteria.equals(targetCount, new Callable<Integer>() {
@Override
public Integer call() {
return getConnectedSandboxedServicesCountForPackage(null /* packageName */);
}
}));
}
private static ChildProcessCreationParams getDefaultChildProcessCreationParams(
String packageName) {
return packageName == null
? null
: new ChildProcessCreationParams(packageName, false /* isExternalService */,
LibraryProcessType.PROCESS_CHILD, false /* bindToCallerCheck */);
}
private static boolean hasSandboxedConnectionAllocatorForPackage(final String packageName) {
return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(new Callable<Boolean>() {
@Override
public Boolean call() {
return ChildProcessLauncherHelper.hasSandboxedConnectionAllocatorForPackage(
packageName);
}
});
}
private static ChildProcessConnection retrieveConnection(
final ChildProcessLauncherHelper launcherHelper) {
CriteriaHelper.pollInstrumentationThread( CriteriaHelper.pollInstrumentationThread(
new Criteria("Failed waiting for child process to connect") { new Criteria("Failed wait for launcher to connect.") {
@Override @Override
public boolean isSatisfied() { public boolean isSatisfied() {
return ChildProcessLauncherTestUtils.getConnection(launcherHelper) != null; return launcher.getConnection() != null;
} }
}); });
waitForConnectionState(launcher.getConnection(), CONNECTION_BLOCK_UNTIL_SETUP);
return ChildProcessLauncherTestUtils.getConnection(launcherHelper);
} }
private static void stopProcess(ChildProcessLauncherHelper launcherHelper) { private static int getConnectionPid(final ChildProcessConnection connection) {
final ChildProcessConnection connection = retrieveConnection(launcherHelper); return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(new Callable<Integer>() {
ChildProcessLauncherTestUtils.runOnLauncherThreadBlocking(new Runnable() {
@Override @Override
public void run() { public Integer call() {
ChildProcessLauncherHelper.stop(connection.getPid()); return connection.getPid();
} }
}); });
} }
private static int getPid(final ChildProcessLauncherHelper launcherHelper) { private static void stopLauncher(final ChildProcessLauncher launcher) {
return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(new Callable<Integer>() { ChildProcessLauncherTestUtils.runOnLauncherThreadBlocking(new Runnable() {
@Override @Override
public Integer call() { public void run() {
return launcherHelper.getPid(); launcher.stop();
} }
}); });
} }
private static ChildProcessConnection getWarmUpConnection() {
return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
new Callable<ChildProcessConnection>() {
@Override
public ChildProcessConnection call() {
return ChildProcessLauncherHelper.getWarmUpConnectionForTesting();
}
});
}
} }
...@@ -99,6 +99,9 @@ android_resources("content_shell_apk_resources") { ...@@ -99,6 +99,9 @@ android_resources("content_shell_apk_resources") {
android_library("content_shell_apk_java") { android_library("content_shell_apk_java") {
testonly = true testonly = true
srcjar_deps = [ ":content_javatests_aidl" ]
deps = [ deps = [
":content_shell_apk_resources", ":content_shell_apk_resources",
":content_shell_java", ":content_shell_java",
...@@ -107,6 +110,7 @@ android_library("content_shell_apk_java") { ...@@ -107,6 +110,7 @@ android_library("content_shell_apk_java") {
"//content/public/android:content_java", "//content/public/android:content_java",
"//media/capture/video/android:capture_java", "//media/capture/video/android:capture_java",
"//net/android:net_java", "//net/android:net_java",
"//third_party/jsr-305:jsr_305_javalib",
"//ui/android:ui_java", "//ui/android:ui_java",
] ]
...@@ -118,6 +122,16 @@ android_library("content_shell_apk_java") { ...@@ -118,6 +122,16 @@ android_library("content_shell_apk_java") {
"shell_apk/src/org/chromium/content_shell_apk/ChildProcessLauncherTestUtils.java", "shell_apk/src/org/chromium/content_shell_apk/ChildProcessLauncherTestUtils.java",
"shell_apk/src/org/chromium/content_shell_apk/ContentShellActivity.java", "shell_apk/src/org/chromium/content_shell_apk/ContentShellActivity.java",
"shell_apk/src/org/chromium/content_shell_apk/ContentShellApplication.java", "shell_apk/src/org/chromium/content_shell_apk/ContentShellApplication.java",
"shell_apk/src/org/chromium/content_shell_apk/TestChildProcessService.java",
"shell_apk/src/org/chromium/content_shell_apk/TestChildProcessService0.java",
"shell_apk/src/org/chromium/content_shell_apk/TestChildProcessService1.java",
]
}
android_aidl("content_javatests_aidl") {
import_include = [ "shell_apk/src" ]
sources = [
"shell_apk/src/org/chromium/content_shell_apk/IChildProcessTest.aidl",
] ]
} }
......
...@@ -67,5 +67,20 @@ ...@@ -67,5 +67,20 @@
<service android:name="org.chromium.content_shell_apk.ChildProcessLauncherTestHelperService" <service android:name="org.chromium.content_shell_apk.ChildProcessLauncherTestHelperService"
android:process=":ChildProcessLauncherHelper" /> android:process=":ChildProcessLauncherHelper" />
<!-- The following entries are for ChildProcessLauncherTest. They should eventually be moved
to base. -->
{% set num_test_services = 2 %}
<meta-data android:name="org.chromium.content.browser.NUM_TEST_SERVICES"
android:value="{{ num_test_services }}"/>
<meta-data android:name="org.chromium.content.browser.TEST_SERVICES_NAME"
android:value="org.chromium.content_shell_apk.TestChildProcessService"/>
{% for i in range(num_test_services) %}
<service android:name="org.chromium.content_shell_apk.TestChildProcessService{{ i }}"
android:process=":test_child_service_process{{ i }}"
android:isolatedProcess="true"
android:exported="false" />
{% endfor %}
</application> </application>
</manifest> </manifest>
// Copyright 2017 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_shell_apk;
import android.os.Bundle;
/**
* Interface provided to the TestChildProcessService. Used to echo back the calls made on the
* ChildProcessServiceDelegate to the test process.
*/
interface IChildProcessTest {
// Called by the service when onConnectionSetup is received. Echos back the parameters received
// so far.
oneway void onConnectionSetup(boolean serviceCreatedCalled, in Bundle serviceBundle, in Bundle connectionBundle);
oneway void onLoadNativeLibrary(boolean loadedSuccessfully);
oneway void onBeforeMain(in String[] commandLine);
oneway void onRunMain();
oneway void onDestroy();
}
per-file *.aidl=set noparent
per-file *.aidl=file://ipc/SECURITY_OWNERS
// Copyright 2017 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_shell_apk;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.util.SparseArray;
import org.chromium.base.CommandLine;
import org.chromium.base.Log;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.base.process_launcher.ChildProcessService;
import org.chromium.base.process_launcher.ChildProcessServiceDelegate;
import javax.annotation.concurrent.GuardedBy;
/**
* Child service started by ChildProcessLauncherTest.
*/
public class TestChildProcessService extends ChildProcessService {
private static final String TAG = "TestProcessService";
private static final long MAIN_BLOCKING_DURATION_MS = 5000;
private static class TestChildProcessServiceDelegate implements ChildProcessServiceDelegate {
private final Object mConnectionSetupLock = new Object();
@GuardedBy("mConnectionSetupLock")
private boolean mConnectionSetup;
private boolean mServiceCreated;
private Bundle mServiceBundle;
private String[] mCommandLine;
private IChildProcessTest mIChildProcessTest;
@Override
public void onServiceCreated() {
mServiceCreated = true;
}
@Override
public void onServiceBound(Intent intent) {
mServiceBundle = intent.getExtras();
}
@Override
public void onConnectionSetup(Bundle connectionBundle, IBinder callback) {
if (callback != null) {
mIChildProcessTest = IChildProcessTest.Stub.asInterface(callback);
}
if (mIChildProcessTest != null) {
try {
mIChildProcessTest.onConnectionSetup(
mServiceCreated, mServiceBundle, connectionBundle);
} catch (RemoteException re) {
Log.e(TAG, "Failed to call IChildProcessTest.onConnectionSetup.", re);
}
}
synchronized (mConnectionSetupLock) {
mConnectionSetup = true;
mConnectionSetupLock.notifyAll();
}
}
@Override
public void onDestroy() {
if (mIChildProcessTest == null) return;
try {
mIChildProcessTest.onDestroy();
} catch (RemoteException re) {
Log.e(TAG, "Failed to call IChildProcessTest.onDestroy.", re);
}
}
@Override
public boolean loadNativeLibrary(Context hostContext) {
// Store the command line before loading the library to avoid an assert in CommandLine.
mCommandLine = CommandLine.getJavaSwitchesOrNull();
LibraryLoader libraryLoader = null;
boolean isLoaded = false;
try {
libraryLoader = LibraryLoader.get(LibraryProcessType.PROCESS_CHILD);
libraryLoader.loadNow();
libraryLoader.ensureInitialized();
isLoaded = true;
} catch (ProcessInitException e) {
Log.e(TAG, "Failed to load native library.", e);
}
// Loading the library happen on the main thread and onConnectionSetup is called from
// the client. Wait for onConnectionSetup so mIChildProcessTest is set.
synchronized (mConnectionSetupLock) {
while (!mConnectionSetup) {
try {
mConnectionSetupLock.wait();
} catch (InterruptedException e) {
// Ignore.
}
}
}
if (mIChildProcessTest != null) {
try {
mIChildProcessTest.onLoadNativeLibrary(isLoaded);
} catch (RemoteException re) {
Log.e(TAG, "Failed to call IChildProcessTest.onLoadNativeLibrary.", re);
}
}
return true;
}
@Override
public SparseArray<String> getFileDescriptorsIdsToKeys() {
return null;
}
@Override
public void onBeforeMain() {
if (mIChildProcessTest == null) return;
try {
mIChildProcessTest.onBeforeMain(mCommandLine);
} catch (RemoteException re) {
Log.e(TAG, "Failed to call IChildProcessTest.onBeforeMain.", re);
}
}
@Override
public void runMain() {
if (mIChildProcessTest != null) {
try {
mIChildProcessTest.onRunMain();
} catch (RemoteException re) {
Log.e(TAG, "Failed to call IChildProcessTest.onRunMain.", re);
}
}
// Run a message loop to keep the service from exiting.
Looper.prepare();
Looper.loop();
}
};
public TestChildProcessService() {
super(new TestChildProcessServiceDelegate());
}
}
// Copyright 2017 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_shell_apk;
/** One of the TestChildProcessService defined in the AndroidManifest.xml. */
public class TestChildProcessService0 extends TestChildProcessService {}
// Copyright 2017 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_shell_apk;
/** One of the TestChildProcessService defined in the AndroidManifest.xml. */
public class TestChildProcessService1 extends TestChildProcessService {}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment