OLD | NEW |
(Empty) | |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package org.chromium.base; |
| 6 |
| 7 import android.content.ComponentName; |
| 8 import android.content.Context; |
| 9 import android.content.Intent; |
| 10 import android.content.ServiceConnection; |
| 11 import android.os.IBinder; |
| 12 import android.os.ParcelFileDescriptor; |
| 13 import android.os.RemoteException; |
| 14 |
| 15 import org.chromium.base.annotations.CalledByNative; |
| 16 import org.chromium.base.annotations.JNINamespace; |
| 17 |
| 18 import java.io.IOException; |
| 19 import java.util.ArrayList; |
| 20 import java.util.LinkedList; |
| 21 import java.util.List; |
| 22 import java.util.Queue; |
| 23 |
| 24 import javax.annotation.concurrent.GuardedBy; |
| 25 |
| 26 /** |
| 27 * Helper class for launching test client processes for multiprocess unit tests. |
| 28 */ |
| 29 @JNINamespace("base::android") |
| 30 public final class MultiprocessTestClientLauncher { |
| 31 private static final String TAG = "cr_MProcTCLauncher"; |
| 32 |
| 33 private static ConnectionAllocator sConnectionAllocator = new ConnectionAllo
cator(); |
| 34 |
| 35 // Not supposed to be instantiated. |
| 36 private MultiprocessTestClientLauncher() {} |
| 37 |
| 38 private static class ConnectionAllocator { |
| 39 // Services are identified by a slot number, which is used in the servic
e name to |
| 40 // differentiate them (MultiprocessTestClientService0, MultiprocessTestC
lientService1, ...). |
| 41 // They are stored in a FIFO queue in order to minimize the risk of the
framework reusing a |
| 42 // service without restarting its associated process (which can cause al
l kind of problems |
| 43 // with static native variables already being initialized). |
| 44 private static final int MAX_SUBPROCESS_COUNT = 5; |
| 45 |
| 46 private final Object mLock = new Object(); |
| 47 |
| 48 @GuardedBy("mLock") |
| 49 private final Queue<Integer> mFreeServiceSlot = new LinkedList<>(); |
| 50 @GuardedBy("mLock") |
| 51 private final List<ClientServiceConnection> mConnections = new ArrayList
<>(); |
| 52 |
| 53 public ConnectionAllocator() { |
| 54 synchronized (mLock) { |
| 55 for (int i = 0; i < MAX_SUBPROCESS_COUNT; i++) { |
| 56 mFreeServiceSlot.add(i); |
| 57 } |
| 58 } |
| 59 } |
| 60 |
| 61 public ClientServiceConnection allocateConnection( |
| 62 String[] commandLine, FileDescriptorInfo[] filesToMap) { |
| 63 synchronized (mLock) { |
| 64 while (mFreeServiceSlot.isEmpty()) { |
| 65 try { |
| 66 mLock.wait(); |
| 67 } catch (InterruptedException ie) { |
| 68 Log.e(TAG, "Interrupted while waiting for a free connect
ion."); |
| 69 } |
| 70 } |
| 71 |
| 72 int slot = mFreeServiceSlot.remove(); |
| 73 ClientServiceConnection connection = |
| 74 new ClientServiceConnection(slot, commandLine, filesToMa
p); |
| 75 mConnections.add(connection); |
| 76 return connection; |
| 77 } |
| 78 } |
| 79 |
| 80 public void freeConnection(ClientServiceConnection connection) { |
| 81 synchronized (mLock) { |
| 82 mFreeServiceSlot.add(connection.getSlot()); |
| 83 mConnections.remove(connection); |
| 84 } |
| 85 } |
| 86 |
| 87 public ClientServiceConnection getConnectionByPid(int pid) { |
| 88 synchronized (mLock) { |
| 89 // List of connections is short, iterating is OK. |
| 90 for (ClientServiceConnection connection : mConnections) { |
| 91 if (connection.getPid() == pid) { |
| 92 return connection; |
| 93 } |
| 94 } |
| 95 } |
| 96 return null; |
| 97 } |
| 98 } |
| 99 |
| 100 private static class ClientServiceConnection implements ServiceConnection { |
| 101 private final String[] mCommandLine; |
| 102 private final FileDescriptorInfo[] mFilesToMap; |
| 103 private final Object mConnectedLock = new Object(); |
| 104 private final int mSlot; |
| 105 private ITestClient mService = null; |
| 106 @GuardedBy("mConnectedLock") |
| 107 private boolean mConnected; |
| 108 private int mPid; |
| 109 |
| 110 ClientServiceConnection(int slot, String[] commandLine, FileDescriptorIn
fo[] filesToMap) { |
| 111 mSlot = slot; |
| 112 mCommandLine = commandLine; |
| 113 mFilesToMap = filesToMap; |
| 114 } |
| 115 |
| 116 public void waitForConnection() { |
| 117 synchronized (mConnectedLock) { |
| 118 while (!mConnected) { |
| 119 try { |
| 120 mConnectedLock.wait(); |
| 121 } catch (InterruptedException ie) { |
| 122 Log.e(TAG, "Interrupted while waiting for connection."); |
| 123 } |
| 124 } |
| 125 } |
| 126 } |
| 127 |
| 128 @Override |
| 129 public void onServiceConnected(ComponentName className, IBinder service)
{ |
| 130 try { |
| 131 mService = ITestClient.Stub.asInterface(service); |
| 132 mPid = mService.launch(mCommandLine, mFilesToMap); |
| 133 synchronized (mConnectedLock) { |
| 134 mConnected = true; |
| 135 mConnectedLock.notifyAll(); |
| 136 } |
| 137 } catch (RemoteException e) { |
| 138 Log.e(TAG, "Connect failed"); |
| 139 } |
| 140 } |
| 141 |
| 142 @Override |
| 143 public void onServiceDisconnected(ComponentName className) { |
| 144 if (mPid == 0) { |
| 145 Log.e(TAG, "Early ClientServiceConnection disconnection."); |
| 146 return; |
| 147 } |
| 148 sConnectionAllocator.freeConnection(this); |
| 149 } |
| 150 |
| 151 public ITestClient getService() { |
| 152 return mService; |
| 153 } |
| 154 |
| 155 public String getServiceClassName() { |
| 156 // In order to use different processes, we have to declare multiple
services in the |
| 157 // AndroidManifest.xml file, each service associated with its own pr
ocess. The various |
| 158 // services are functionnaly identical but need to each have their o
wn class. |
| 159 // We differentiate them by their class name having a trailing numbe
r. |
| 160 return MultiprocessTestClientService.class.getName() + mSlot; |
| 161 } |
| 162 |
| 163 public boolean isConnected() { |
| 164 synchronized (mConnectedLock) { |
| 165 return mConnected; |
| 166 } |
| 167 } |
| 168 |
| 169 public int getSlot() { |
| 170 return mSlot; |
| 171 } |
| 172 |
| 173 public int getPid() { |
| 174 return mPid; |
| 175 } |
| 176 } |
| 177 |
| 178 /** |
| 179 * Spawns and connects to a child process. |
| 180 * May not be called from the main thread. |
| 181 * |
| 182 * @param context context used to obtain the application context. |
| 183 * @param commandLine the child process command line argv. |
| 184 * @return the PID of the started process or 0 if the process could not be s
tarted. |
| 185 */ |
| 186 @CalledByNative |
| 187 private static int launchClient(final Context context, final String[] comman
dLine, |
| 188 final FileDescriptorInfo[] filesToMap) { |
| 189 if (ThreadUtils.runningOnUiThread()) { |
| 190 // This can't be called on the main thread as the native side will b
lock until |
| 191 // onServiceConnected above is called, which cannot happen if the ma
in thread is |
| 192 // blocked. |
| 193 throw new RuntimeException("launchClient cannot be called on the mai
n thread"); |
| 194 } |
| 195 |
| 196 ClientServiceConnection connection = |
| 197 sConnectionAllocator.allocateConnection(commandLine, filesToMap)
; |
| 198 Intent intent = new Intent(); |
| 199 String className = connection.getServiceClassName(); |
| 200 intent.setComponent(new ComponentName(context.getPackageName(), classNam
e)); |
| 201 if (!context.bindService( |
| 202 intent, connection, Context.BIND_AUTO_CREATE | Context.BIND_
IMPORTANT)) { |
| 203 Log.e(TAG, "Failed to bind service: " + context.getPackageName() + "
." + className); |
| 204 sConnectionAllocator.freeConnection(connection); |
| 205 return 0; |
| 206 } |
| 207 |
| 208 connection.waitForConnection(); |
| 209 |
| 210 return connection.getPid(); |
| 211 } |
| 212 |
| 213 /** |
| 214 * Blocks until the main method invoked by a previous call to launchClient t
erminates or until |
| 215 * the specified time-out expires. |
| 216 * Returns immediately if main has already returned. |
| 217 * @param context context used to obtain the application context. |
| 218 * @param pid the process ID that was returned by the call to launchClient |
| 219 * @param timeoutMs the timeout in milliseconds after which the method retur
ns even if main has |
| 220 * not returned. |
| 221 * @return the return code returned by the main method or whether it timed-o
ut. |
| 222 */ |
| 223 @CalledByNative |
| 224 private static MainReturnCodeResult waitForMainToReturn( |
| 225 Context context, int pid, int timeoutMs) { |
| 226 ClientServiceConnection connection = sConnectionAllocator.getConnectionB
yPid(pid); |
| 227 if (connection == null) { |
| 228 Log.e(TAG, "waitForMainToReturn called on unknown connection for pid
" + pid); |
| 229 return null; |
| 230 } |
| 231 try { |
| 232 return connection.getService().waitForMainToReturn(timeoutMs); |
| 233 } catch (RemoteException e) { |
| 234 Log.e(TAG, "Remote call to waitForMainToReturn failed."); |
| 235 return null; |
| 236 } finally { |
| 237 freeConnection(context, connection); |
| 238 } |
| 239 } |
| 240 |
| 241 @CalledByNative |
| 242 private static boolean terminate(Context context, int pid, int exitCode, boo
lean wait) { |
| 243 ClientServiceConnection connection = sConnectionAllocator.getConnectionB
yPid(pid); |
| 244 if (connection == null) { |
| 245 Log.e(TAG, "terminate called on unknown connection for pid " + pid); |
| 246 return false; |
| 247 } |
| 248 try { |
| 249 if (wait) { |
| 250 connection.getService().forceStopSynchronous(exitCode); |
| 251 } else { |
| 252 connection.getService().forceStop(exitCode); |
| 253 } |
| 254 } catch (RemoteException e) { |
| 255 // We expect this failure, since the forceStop's service implementat
ion calls |
| 256 // System.exit(). |
| 257 } finally { |
| 258 freeConnection(context, connection); |
| 259 } |
| 260 return true; |
| 261 } |
| 262 |
| 263 private static void freeConnection(Context context, ClientServiceConnection
connection) { |
| 264 context.unbindService(connection); |
| 265 sConnectionAllocator.freeConnection(connection); |
| 266 } |
| 267 |
| 268 /** Does not take ownership of of fds. */ |
| 269 @CalledByNative |
| 270 private static FileDescriptorInfo[] makeFdInfoArray(int[] keys, int[] fds) { |
| 271 FileDescriptorInfo[] fdInfos = new FileDescriptorInfo[keys.length]; |
| 272 for (int i = 0; i < keys.length; i++) { |
| 273 FileDescriptorInfo fdInfo = makeFdInfo(keys[i], fds[i]); |
| 274 if (fdInfo == null) { |
| 275 Log.e(TAG, "Failed to make file descriptor (" + keys[i] + ", " +
fds[i] + ")."); |
| 276 return null; |
| 277 } |
| 278 fdInfos[i] = fdInfo; |
| 279 } |
| 280 return fdInfos; |
| 281 } |
| 282 |
| 283 private static FileDescriptorInfo makeFdInfo(int id, int fd) { |
| 284 ParcelFileDescriptor parcelableFd = null; |
| 285 try { |
| 286 parcelableFd = ParcelFileDescriptor.fromFd(fd); |
| 287 } catch (IOException e) { |
| 288 Log.e(TAG, "Invalid FD provided for process connection, aborting con
nection.", e); |
| 289 return null; |
| 290 } |
| 291 return new FileDescriptorInfo(id, parcelableFd); |
| 292 } |
| 293 } |
OLD | NEW |