blob: 5ff7ab20e59b36e6d88d9e93ba909e87687264f9 [file] [log] [blame]
// 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.content.ComponentName;
import android.content.Context;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
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.process_launcher.FileDescriptorInfo;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.Feature;
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.ChildProcessLauncherTestUtils;
import org.chromium.content_shell_apk.IChildProcessTest;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
/**
* Instrumentation tests for ChildProcessLauncher.
*/
@RunWith(ContentJUnit4ClassRunner.class)
public class ChildProcessLauncherTest {
private static final long CONDITION_WAIT_TIMEOUT_MS = 5000;
private static final String SERVICE_PACKAGE_NAME = "org.chromium.content_shell_apk";
private static final String SERVICE_NAME_META_DATA_KEY =
"org.chromium.content.browser.TEST_SERVICES_NAME";
private static final String SERVICE_COUNT_META_DATA_KEY =
"org.chromium.content.browser.NUM_TEST_SERVICES";
private static final String SERVICE0_FULL_NAME =
"org.chromium.content_shell_apk.TestChildProcessService0";
private static final String EXTRA_SERVICE_PARAM = "org.chromium.content.browser.SERVICE_EXTRA";
private static final String EXTRA_SERVICE_PARAM_VALUE = "SERVICE_EXTRA";
private static final String EXTRA_CONNECTION_PARAM =
"org.chromium.content.browser.CONNECTION_EXTRA";
private static final String EXTRA_CONNECTION_PARAM_VALUE = "CONNECTION_EXTRA";
private static final int CONNECTION_BLOCK_UNTIL_CONNECTED = 1;
private static final int CONNECTION_BLOCK_UNTIL_SETUP = 2;
/**
* 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.
*/
private abstract static class ChildProcessLauncherFactory {
private final boolean mConnectionProvided;
public ChildProcessLauncherFactory(boolean connectionProvided) {
mConnectionProvided = connectionProvided;
}
public abstract ChildProcessLauncher createChildProcessLauncher(
ChildProcessLauncher.Delegate delegate, String[] commandLine,
FileDescriptorInfo[] filesToBeMapped, IBinder binderCallback);
public boolean isConnectionProvided() {
return mConnectionProvided;
}
}
private static final ChildProcessLauncher.Delegate EMPTY_LAUNCHER_DELEGATE =
new ChildProcessLauncher.Delegate() {
@Override
public void onBeforeConnectionAllocated(Bundle serviceBundle) {}
@Override
public void onBeforeConnectionSetup(Bundle connectionBundle) {}
@Override
public void onConnectionEstablished(ChildProcessConnection connection) {}
@Override
public void onConnectionLost(ChildProcessConnection connection) {}
};
private ChildConnectionAllocator mConnectionAllocator;
@Before
public void setUp() throws Exception {
mConnectionAllocator = ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
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 {
private final CallbackHelper mOnConnectionSetupHelper = new CallbackHelper();
private final CallbackHelper mOnLoadNativeHelper = new CallbackHelper();
private final CallbackHelper mOnBeforeMainHelper = new CallbackHelper();
private final CallbackHelper mOnRunMainHelper = new CallbackHelper();
private final CallbackHelper mOnDestroyHelper = new CallbackHelper();
// Can be accessed after mOnConnectionSetupCalled is signaled.
private boolean mServiceCreated;
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();
}
@Override
public void onBeforeMain(String[] commandLine) {
mCommandLine = commandLine;
Assert.assertEquals(0, mOnBeforeMainHelper.getCallCount());
mOnBeforeMainHelper.notifyCalled();
}
@Override
public void onRunMain() {
Assert.assertEquals(0, mOnRunMainHelper.getCallCount());
mOnRunMainHelper.notifyCalled();
}
@Override
public void onDestroy() {
Assert.assertEquals(0, mOnDestroyHelper.getCallCount());
mOnDestroyHelper.notifyCalled();
}
public void waitForOnConnectionSetupCalled() throws InterruptedException, TimeoutException {
mOnConnectionSetupHelper.waitForCallback(0 /* currentCallCount */);
}
public void waitForOnNativeLibraryCalled() throws InterruptedException, TimeoutException {
mOnLoadNativeHelper.waitForCallback(0 /* currentCallCount */);
}
public void waitOnBeforeMainCalled() throws InterruptedException, TimeoutException {
mOnBeforeMainHelper.waitForCallback(0 /* currentCallCount */);
}
public void waitOnRunMainCalled() throws InterruptedException, TimeoutException {
mOnRunMainHelper.waitForCallback(0 /* currentCallCount */);
}
public void waitOnDestroyCalled() throws InterruptedException, TimeoutException {
mOnDestroyHelper.waitForCallback(0 /* currentCallCount */);
}
};
/**
* Creates a child process with the ChildProcessLauncher created with {@param launcherFactory}
* and tests that the all callbacks on the client and in the service are called appropriately.
* The service echos back the delegate calls through the IBinder callback so that the test can
* validate them.
*/
private void testProcessLauncher(final ChildProcessLauncherFactory launcherFactory)
throws InterruptedException, TimeoutException {
// ConditionVariables used to check the ChildProcessLauncher.Delegate methods get called.
final CallbackHelper onBeforeConnectionAllocatedHelper = new CallbackHelper();
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();
}
@Override
public void onBeforeConnectionSetup(Bundle connectionBundle) {
connectionBundle.putString(EXTRA_CONNECTION_PARAM, EXTRA_CONNECTION_PARAM_VALUE);
Assert.assertEquals(0, onBeforeConnectionSetupHelper.getCallCount());
onBeforeConnectionSetupHelper.notifyCalled();
}
@Override
public void onConnectionEstablished(ChildProcessConnection connection) {
Assert.assertEquals(0, onConnectionEstablishedHelper.getCallCount());
onConnectionEstablishedHelper.notifyCalled();
}
@Override
public void onConnectionLost(ChildProcessConnection connection) {
Assert.assertEquals(0, onConnectionLostHelper.getCallCount());
onConnectionLostHelper.notifyCalled();
}
};
final String[] commandLine = new String[] {"--test-param1", "--test-param2"};
final FileDescriptorInfo[] filesToBeMapped = new FileDescriptorInfo[0];
final IChildProcessBinder childProcessBinder = new IChildProcessBinder();
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;
}
});
Assert.assertNotNull(processLauncher);
if (!launcherFactory.isConnectionProvided()) {
onBeforeConnectionAllocatedHelper.waitForCallback(0 /* currentCallback */);
}
onBeforeConnectionSetupHelper.waitForCallback(0 /* currentCallback */);
// Wait for the service to notify its onConnectionSetup was called.
childProcessBinder.waitForOnConnectionSetupCalled();
Assert.assertTrue(childProcessBinder.mServiceCreated);
Assert.assertNotNull(childProcessBinder.mServiceBundle);
Assert.assertNotNull(childProcessBinder.mConnectionBundle);
if (!launcherFactory.isConnectionProvided()) {
Assert.assertEquals(EXTRA_SERVICE_PARAM_VALUE,
childProcessBinder.mServiceBundle.getString(EXTRA_SERVICE_PARAM));
}
Assert.assertEquals(EXTRA_CONNECTION_PARAM_VALUE,
childProcessBinder.mConnectionBundle.getString(EXTRA_CONNECTION_PARAM));
// Wait for the client onConnectionEstablished call.
onConnectionEstablishedHelper.waitForCallback(0 /* currentCallback */);
// Wait for the service to notify its library got loaded.
childProcessBinder.waitForOnNativeLibraryCalled();
Assert.assertTrue(childProcessBinder.mNativeLibraryLoaded);
// Wait for the service to notify its onBeforeMain was called.
childProcessBinder.waitOnBeforeMainCalled();
Assert.assertArrayEquals(commandLine, childProcessBinder.mCommandLine);
// Wait for the service to notify its onRunMain was called.
childProcessBinder.waitOnRunMainCalled();
// Stop the launcher.
ChildProcessLauncherTestUtils.runOnLauncherThreadBlocking(new Runnable() {
@Override
public void run() {
processLauncher.stop();
}
});
// Wait for service to notify its onDestroy was called.
childProcessBinder.waitOnDestroyCalled();
// The client should also get a notification that the connection was lost.
onConnectionLostHelper.waitForCallback(0 /* currentCallback */);
}
@Test
@LargeTest
@Feature({"ProcessManagement"})
public void testLaunchServiceCreatedWithConnectionAllocator() throws Exception {
final ChildProcessLauncherFactory childProcessLauncherFactory =
new ChildProcessLauncherFactory(false /* providesConnection */) {
@Override
public ChildProcessLauncher createChildProcessLauncher(
ChildProcessLauncher.Delegate delegate, String[] commandLine,
FileDescriptorInfo[] filesToBeMapped, IBinder binderCallback) {
return ChildProcessLauncher.createWithConnectionAllocator(delegate,
commandLine, filesToBeMapped, mConnectionAllocator, binderCallback);
}
};
testProcessLauncher(childProcessLauncherFactory);
}
@Test
@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;
}
});
Assert.assertNotNull(boundConnection);
CriteriaHelper.pollInstrumentationThread(new Criteria("Connection failed to connect") {
@Override
public boolean isSatisfied() {
return boundConnection.isConnected();
}
});
final ChildProcessLauncher.BoundConnectionProvider connectionProvider =
new ChildProcessLauncher.BoundConnectionProvider() {
@Override
public ChildProcessConnection getConnection(
ChildProcessConnection.ServiceCallback serviceCallback) {
serviceCallbackWrapper.set(serviceCallback);
return boundConnection;
}
};
final ChildProcessLauncherFactory childProcessLauncherFactory =
new ChildProcessLauncherFactory(true /* providesConnection */) {
@Override
public ChildProcessLauncher createChildProcessLauncher(
ChildProcessLauncher.Delegate delegate, String[] commandLine,
FileDescriptorInfo[] filesToBeMapped, IBinder binderCallback) {
return ChildProcessLauncher.createWithBoundConnectionProvider(delegate,
commandLine, filesToBeMapped, connectionProvider, binderCallback);
}
};
testProcessLauncher(childProcessLauncherFactory);
}
/**
* Tests cleanup for a connection that fails to connect in the first place.
*/
@Test
@MediumTest
@Feature({"ProcessManagement"})
public void testServiceFailedToBind() {
final ChildConnectionAllocator badConnectionAllocator =
ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
new Callable<ChildConnectionAllocator>() {
@Override
public ChildConnectionAllocator call() {
return ChildConnectionAllocator.createForTest(
null /* creationParams */, "org.chromium.wrong_package",
"WrongService", 2 /* serviceCount */,
false /* bindAsExternalService */,
false /* useStrongBinding */);
}
});
Assert.assertFalse(badConnectionAllocator.anyConnectionAllocated());
// Try to allocate a connection to service class in incorrect package. We can do that by
// using the instrumentation context (getContext()) instead of the app context
// (getTargetContext()).
ChildProcessLauncher processLauncher = createChildProcessLauncher(badConnectionAllocator,
true /* setupConnection */, false /* queueIfNoFreeConnection */);
Assert.assertNotNull(processLauncher);
// Verify that the connection is not considered as allocated (or only briefly, as the
// freeing is delayed).
waitForConnectionAllocatorState(badConnectionAllocator, true /* isEmpty */);
}
/**
* Tests cleanup for a connection that terminates before setup.
*/
@Test
@MediumTest
@Feature({"ProcessManagement"})
public void testServiceCrashedBeforeSetup() throws RemoteException {
Assert.assertFalse(mConnectionAllocator.anyConnectionAllocated());
// Start and connect to a new service.
ChildProcessLauncher processLauncher = createChildProcessLauncher(mConnectionAllocator,
false /* setupConnection */, false /* queueIfNoFreeConnection */);
// Verify that the service is bound but not yet set up.
Assert.assertTrue(mConnectionAllocator.anyConnectionAllocated());
ChildProcessConnection connection = processLauncher.getConnection();
Assert.assertNotNull(connection);
waitForConnectionState(connection, CONNECTION_BLOCK_UNTIL_CONNECTED);
Assert.assertEquals(0, getConnectionPid(connection));
// Crash the service.
connection.crashServiceForTesting();
// Verify that the connection gets cleaned-up.
waitForConnectionAllocatorState(mConnectionAllocator, true /* isEmpty */);
}
@Test
@MediumTest
@Feature({"ProcessManagement"})
public void testServiceCrashedAfterSetup() throws RemoteException {
Assert.assertFalse(mConnectionAllocator.anyConnectionAllocated());
// Start and connect to a new service.
ChildProcessLauncher processLauncher = createChildProcessLauncher(mConnectionAllocator,
true /* setupConnection */, false /* queueIfNoFreeConnection */);
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));
// Crash the service.
connection.crashServiceForTesting();
// Verify that the connection gets cleaned-up.
waitForConnectionAllocatorState(mConnectionAllocator, true /* isEmpty */);
// Verify that the connection pid remains set after termination.
Assert.assertNotEquals(0, getConnectionPid(connection));
}
@Test
@MediumTest
@Feature({"ProcessManagement"})
public void testPendingSpawnQueue() throws RemoteException {
Assert.assertFalse(mConnectionAllocator.anyConnectionAllocated());
// Launch 4 processes. Since we have only 2 services, the 3rd and 4th should get queued.
ChildProcessLauncher[] launchers = new ChildProcessLauncher[4];
ChildProcessConnection[] connections = new ChildProcessConnection[4];
for (int i = 0; i < 4; i++) {
launchers[i] = createChildProcessLauncher(mConnectionAllocator,
true /* setupConnection */, true /* queueIfNoFreeConnection */);
Assert.assertNotNull(launchers[i]);
connections[i] = launchers[i].getConnection();
}
Assert.assertNotNull(connections[0]);
Assert.assertNotNull(connections[1]);
Assert.assertNull(connections[2]);
Assert.assertNull(connections[3]);
// Test creating a launcher with queueIfNoFreeConnection false with no connection available.
Assert.assertNull(createChildProcessLauncher(mConnectionAllocator,
true /* setupConnection */, false /* queueIfNoFreeConnection */));
// Stop one connection, that should free-up a connection and the first queued launcher
// should use it.
stopLauncher(launchers[0]);
waitUntilLauncherSetup(launchers[2]);
// Last launcher is still queued.
Assert.assertNull(launchers[3].getConnection());
// Crash another launcher. It should free-up another connection that the queued-up launcher
// should use.
connections[1].crashServiceForTesting();
waitUntilLauncherSetup(launchers[3]);
}
private static ChildProcessLauncher createChildProcessLauncher(
final ChildConnectionAllocator connectionAllocator, final boolean setupConnection,
final boolean queueIfNoFreeConnection) {
return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
new Callable<ChildProcessLauncher>() {
@Override
public ChildProcessLauncher call() {
ChildProcessLauncher processLauncher =
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 waitForConnectionAllocatorState(
final ChildConnectionAllocator connectionAllocator, final boolean emptyState) {
CriteriaHelper.pollInstrumentationThread(
new Criteria("Failed to wait for connection allocator.") {
@Override
public boolean isSatisfied() {
return emptyState ? !connectionAllocator.anyConnectionAllocated()
: connectionAllocator.anyConnectionAllocated();
}
});
}
private static void waitForConnectionState(
final ChildProcessConnection connection, final int connectionState) {
assert connectionState == CONNECTION_BLOCK_UNTIL_CONNECTED
|| connectionState == CONNECTION_BLOCK_UNTIL_SETUP;
CriteriaHelper.pollInstrumentationThread(
new Criteria("Failed wait for connection to connect.") {
@Override
public boolean isSatisfied() {
if (connectionState == CONNECTION_BLOCK_UNTIL_CONNECTED) {
return connection.isConnected();
}
assert connectionState == CONNECTION_BLOCK_UNTIL_SETUP;
return getConnectionPid(connection) != 0;
}
});
}
private static void waitUntilLauncherSetup(final ChildProcessLauncher launcher) {
CriteriaHelper.pollInstrumentationThread(
new Criteria("Failed wait for launcher to connect.") {
@Override
public boolean isSatisfied() {
return launcher.getConnection() != null;
}
});
waitForConnectionState(launcher.getConnection(), CONNECTION_BLOCK_UNTIL_SETUP);
}
private static int getConnectionPid(final ChildProcessConnection connection) {
return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(new Callable<Integer>() {
@Override
public Integer call() {
return connection.getPid();
}
});
}
private static void stopLauncher(final ChildProcessLauncher launcher) {
ChildProcessLauncherTestUtils.runOnLauncherThreadBlocking(new Runnable() {
@Override
public void run() {
launcher.stop();
}
});
}
}