Chromium Code Reviews| 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..cd4ab7aee739ec5304b17af4303881284b405e1a |
| --- /dev/null |
| +++ b/base/test/android/java/src/org/chromium/base/MultiprocessTestClientLauncher.java |
| @@ -0,0 +1,287 @@ |
| +// 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; |
| + |
| +/** |
| + * Helper class for launching test client processes for multiprocess unit tests. |
| + */ |
| +@JNINamespace("base::android") |
| +public class MultiprocessTestClientLauncher { |
| + private static final String TAG = "MultiprocTCLauncher"; |
| + |
| + private static ConnectionAllocator sConnectionAllocator = new ConnectionAllocator(); |
| + |
| + // Not supposed to be instanciated. |
|
Ken Rockot(use gerrit already)
2016/12/12 22:23:08
nit: instantiated
Jay Civelli
2016/12/13 18:23:25
Done.
|
| + 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(); |
| + |
| + // You must hold mLock to access any member variable below: |
| + private final Queue<Integer> mFreeServiceSlot = new LinkedList<>(); |
| + 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."); |
| + return null; |
| + } |
| + } |
| + |
| + 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 ITestClient mService = null; |
| + private final String[] mCommandLine; |
| + private final FileDescriptorInfo[] mFilesToMap; |
| + private boolean mConnected; |
| + private final Object mConnectedLock = new Object(); |
| + private int mPid; |
| + private final int mSlot; |
| + |
| + 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() { |
| + return MultiprocessTestClientService.class.getName() + mSlot; |
| + } |
| + |
| + public boolean isConnected() { |
| + 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); |
| + if (connection == null) { |
| + Log.e(TAG, "Failed to allocate connection, ran out of services to allocate."); |
| + return 0; |
| + } |
| + |
| + 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.exits. |
| + } 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[] ids, int[] fds) { |
| + FileDescriptorInfo[] fdInfos = new FileDescriptorInfo[ids.length]; |
| + for (int i = 0; i < ids.length; i++) { |
| + FileDescriptorInfo fdInfo = makeFdInfo(ids[i], fds[i]); |
| + if (fdInfo == null) { |
| + Log.e(TAG, "Failed to make file descriptor (" + ids[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); |
| + } |
| +} |