blob: 71eadb2ce00779b878f1f05174877c8244d18521 [file] [log] [blame]
// Copyright 2016 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.base;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.process_launcher.FileDescriptorInfo;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import javax.annotation.concurrent.GuardedBy;
/**
* Helper class for launching test client processes for multiprocess unit tests.
*/
@JNINamespace("base::android")
public final class MultiprocessTestClientLauncher {
private static final String TAG = "cr_MProcTCLauncher";
private static ConnectionAllocator sConnectionAllocator = new ConnectionAllocator();
// Not supposed to be instantiated.
private MultiprocessTestClientLauncher() {}
private static class ConnectionAllocator {
// Services are identified by a slot number, which is used in the service name to
// differentiate them (MultiprocessTestClientService0, MultiprocessTestClientService1, ...).
// They are stored in a FIFO queue in order to minimize the risk of the framework reusing a
// service without restarting its associated process (which can cause all kind of problems
// with static native variables already being initialized).
private static final int MAX_SUBPROCESS_COUNT = 5;
private final Object mLock = new Object();
@GuardedBy("mLock")
private final Queue<Integer> mFreeServiceSlot = new LinkedList<>();
@GuardedBy("mLock")
private final List<ClientServiceConnection> mConnections = new ArrayList<>();
public ConnectionAllocator() {
synchronized (mLock) {
for (int i = 0; i < MAX_SUBPROCESS_COUNT; i++) {
mFreeServiceSlot.add(i);
}
}
}
public ClientServiceConnection allocateConnection(
String[] commandLine, FileDescriptorInfo[] filesToMap) {
synchronized (mLock) {
while (mFreeServiceSlot.isEmpty()) {
try {
mLock.wait();
} catch (InterruptedException ie) {
Log.e(TAG, "Interrupted while waiting for a free connection.");
}
}
int slot = mFreeServiceSlot.remove();
ClientServiceConnection connection =
new ClientServiceConnection(slot, commandLine, filesToMap);
mConnections.add(connection);
return connection;
}
}
public void freeConnection(ClientServiceConnection connection) {
synchronized (mLock) {
mFreeServiceSlot.add(connection.getSlot());
mConnections.remove(connection);
}
}
public ClientServiceConnection getConnectionByPid(int pid) {
synchronized (mLock) {
// List of connections is short, iterating is OK.
for (ClientServiceConnection connection : mConnections) {
if (connection.getPid() == pid) {
return connection;
}
}
}
return null;
}
}
private static class ClientServiceConnection implements ServiceConnection {
private final String[] mCommandLine;
private final FileDescriptorInfo[] mFilesToMap;
private final Object mConnectedLock = new Object();
private final int mSlot;
private ITestClient mService = null;
@GuardedBy("mConnectedLock")
private boolean mConnected;
private int mPid;
ClientServiceConnection(int slot, String[] commandLine, FileDescriptorInfo[] filesToMap) {
mSlot = slot;
mCommandLine = commandLine;
mFilesToMap = filesToMap;
}
public void waitForConnection() {
synchronized (mConnectedLock) {
while (!mConnected) {
try {
mConnectedLock.wait();
} catch (InterruptedException ie) {
Log.e(TAG, "Interrupted while waiting for connection.");
}
}
}
}
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
try {
mService = ITestClient.Stub.asInterface(service);
mPid = mService.launch(mCommandLine, mFilesToMap);
synchronized (mConnectedLock) {
mConnected = true;
mConnectedLock.notifyAll();
}
} catch (RemoteException e) {
Log.e(TAG, "Connect failed");
}
}
@Override
public void onServiceDisconnected(ComponentName className) {
if (mPid == 0) {
Log.e(TAG, "Early ClientServiceConnection disconnection.");
return;
}
sConnectionAllocator.freeConnection(this);
}
public ITestClient getService() {
return mService;
}
public String getServiceClassName() {
// In order to use different processes, we have to declare multiple services in the
// AndroidManifest.xml file, each service associated with its own process. The various
// services are functionnaly identical but need to each have their own class.
// We differentiate them by their class name having a trailing number.
return MultiprocessTestClientService.class.getName() + mSlot;
}
public boolean isConnected() {
synchronized (mConnectedLock) {
return mConnected;
}
}
public int getSlot() {
return mSlot;
}
public int getPid() {
return mPid;
}
}
/**
* Spawns and connects to a child process.
* May not be called from the main thread.
*
* @param context context used to obtain the application context.
* @param commandLine the child process command line argv.
* @return the PID of the started process or 0 if the process could not be started.
*/
@CalledByNative
private static int launchClient(final Context context, final String[] commandLine,
final FileDescriptorInfo[] filesToMap) {
if (ThreadUtils.runningOnUiThread()) {
// This can't be called on the main thread as the native side will block until
// onServiceConnected above is called, which cannot happen if the main thread is
// blocked.
throw new RuntimeException("launchClient cannot be called on the main thread");
}
ClientServiceConnection connection =
sConnectionAllocator.allocateConnection(commandLine, filesToMap);
Intent intent = new Intent();
String className = connection.getServiceClassName();
intent.setComponent(new ComponentName(context.getPackageName(), className));
if (!context.bindService(
intent, connection, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT)) {
Log.e(TAG, "Failed to bind service: " + context.getPackageName() + "." + className);
sConnectionAllocator.freeConnection(connection);
return 0;
}
connection.waitForConnection();
return connection.getPid();
}
/**
* Blocks until the main method invoked by a previous call to launchClient terminates or until
* the specified time-out expires.
* Returns immediately if main has already returned.
* @param context context used to obtain the application context.
* @param pid the process ID that was returned by the call to launchClient
* @param timeoutMs the timeout in milliseconds after which the method returns even if main has
* not returned.
* @return the return code returned by the main method or whether it timed-out.
*/
@CalledByNative
private static MainReturnCodeResult waitForMainToReturn(
Context context, int pid, int timeoutMs) {
ClientServiceConnection connection = sConnectionAllocator.getConnectionByPid(pid);
if (connection == null) {
Log.e(TAG, "waitForMainToReturn called on unknown connection for pid " + pid);
return null;
}
try {
return connection.getService().waitForMainToReturn(timeoutMs);
} catch (RemoteException e) {
Log.e(TAG, "Remote call to waitForMainToReturn failed.");
return null;
} finally {
freeConnection(context, connection);
}
}
@CalledByNative
private static boolean terminate(Context context, int pid, int exitCode, boolean wait) {
ClientServiceConnection connection = sConnectionAllocator.getConnectionByPid(pid);
if (connection == null) {
Log.e(TAG, "terminate called on unknown connection for pid " + pid);
return false;
}
try {
if (wait) {
connection.getService().forceStopSynchronous(exitCode);
} else {
connection.getService().forceStop(exitCode);
}
} catch (RemoteException e) {
// We expect this failure, since the forceStop's service implementation calls
// System.exit().
} finally {
freeConnection(context, connection);
}
return true;
}
private static void freeConnection(Context context, ClientServiceConnection connection) {
context.unbindService(connection);
sConnectionAllocator.freeConnection(connection);
}
/** Does not take ownership of of fds. */
@CalledByNative
private static FileDescriptorInfo[] makeFdInfoArray(int[] keys, int[] fds) {
FileDescriptorInfo[] fdInfos = new FileDescriptorInfo[keys.length];
for (int i = 0; i < keys.length; i++) {
FileDescriptorInfo fdInfo = makeFdInfo(keys[i], fds[i]);
if (fdInfo == null) {
Log.e(TAG, "Failed to make file descriptor (" + keys[i] + ", " + fds[i] + ").");
return null;
}
fdInfos[i] = fdInfo;
}
return fdInfos;
}
private static FileDescriptorInfo makeFdInfo(int id, int fd) {
ParcelFileDescriptor parcelableFd = null;
try {
parcelableFd = ParcelFileDescriptor.fromFd(fd);
} catch (IOException e) {
Log.e(TAG, "Invalid FD provided for process connection, aborting connection.", e);
return null;
}
return new FileDescriptorInfo(id, parcelableFd, 0 /* offset */, 0 /* size */);
}
}