Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1637)

Unified Diff: base/test/android/java/src/org/chromium/base/MultiprocessTestClientLauncher.java

Issue 2549363004: Multiprocess test client: Android child process launcher rework. (Closed)
Patch Set: content_unittests + sync Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: base/test/android/java/src/org/chromium/base/MultiprocessTestClientLauncher.java
diff --git a/base/test/android/java/src/org/chromium/base/MultiprocessTestClientLauncher.java b/base/test/android/java/src/org/chromium/base/MultiprocessTestClientLauncher.java
new file mode 100644
index 0000000000000000000000000000000000000000..6c1f3981f9afdc0586d5067047cce5374bef9410
--- /dev/null
+++ b/base/test/android/java/src/org/chromium/base/MultiprocessTestClientLauncher.java
@@ -0,0 +1,293 @@
+// 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 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);
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698