1 | // Copyright 2012 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.content.browser; |
6 | |
7 | import android.content.Context; |
8 | import android.util.Log; |
9 | import android.util.SparseIntArray; |
10 | import android.view.Surface; |
11 | |
12 | import java.util.ArrayList; |
13 | import java.util.Map; |
14 | import java.util.concurrent.ConcurrentHashMap; |
15 | |
16 | import org.chromium.base.CalledByNative; |
17 | import org.chromium.base.JNINamespace; |
18 | import org.chromium.base.ThreadUtils; |
19 | import org.chromium.content.app.ChildProcessService; |
20 | import org.chromium.content.app.PrivilegedProcessService; |
21 | import org.chromium.content.app.SandboxedProcessService; |
22 | import org.chromium.content.common.IChildProcessCallback; |
23 | import org.chromium.content.common.IChildProcessService; |
24 | |
25 | /** |
26 | * This class provides the method to start/stop ChildProcess called by native. |
27 | */ |
28 | @JNINamespace("content") |
29 | public class ChildProcessLauncher { |
30 | private static String TAG = "ChildProcessLauncher"; |
31 | |
32 | private static final int CALLBACK_FOR_UNKNOWN_PROCESS = 0; |
33 | private static final int CALLBACK_FOR_GPU_PROCESS = 1; |
34 | private static final int CALLBACK_FOR_RENDERER_PROCESS = 2; |
35 | |
36 | private static final String SWITCH_PROCESS_TYPE = "type"; |
37 | private static final String SWITCH_PPAPI_BROKER_PROCESS = "ppapi-broker"; |
38 | private static final String SWITCH_RENDERER_PROCESS = "renderer"; |
39 | private static final String SWITCH_GPU_PROCESS = "gpu-process"; |
40 | |
41 | // The upper limit on the number of simultaneous sandboxed and privileged child service process |
42 | // instances supported. Each limit must not exceed total number of SandboxedProcessServiceX |
43 | // classes and PrivilegedProcessServiceX classes declared in this package and defined as |
44 | // services in the embedding application's manifest file. |
45 | // (See {@link ChildProcessService} for more details on defining the services.) |
46 | /* package */ static final int MAX_REGISTERED_SANDBOXED_SERVICES = 13; |
47 | /* package */ static final int MAX_REGISTERED_PRIVILEGED_SERVICES = 3; |
48 | |
49 | private static class ChildConnectionAllocator { |
50 | // Connections to services. Indices of the array correspond to the service numbers. |
51 | private ChildProcessConnection[] mChildProcessConnections; |
52 | |
53 | // The list of free (not bound) service indices. When looking for a free service, the first |
54 | // index in that list should be used. When a service is unbound, its index is added to the |
55 | // end of the list. This is so that we avoid immediately reusing the freed service (see |
56 | // http://crbug.com/164069): the framework might keep a service process alive when it's been |
57 | // unbound for a short time. If a new connection to the same service is bound at that point, |
58 | // the process is reused and bad things happen (mostly static variables are set when we |
59 | // don't expect them to). |
60 | // SHOULD BE ACCESSED WITH mConnectionLock. |
61 | private ArrayList<Integer> mFreeConnectionIndices; |
62 | private final Object mConnectionLock = new Object(); |
63 | |
64 | private Class<? extends ChildProcessService> mChildClass; |
65 | private final boolean mInSandbox; |
66 | |
67 | public ChildConnectionAllocator(boolean inSandbox) { |
68 | int numChildServices = inSandbox ? |
69 | MAX_REGISTERED_SANDBOXED_SERVICES : MAX_REGISTERED_PRIVILEGED_SERVICES; |
70 | mChildProcessConnections = new ChildProcessConnection[numChildServices]; |
71 | mFreeConnectionIndices = new ArrayList<Integer>(numChildServices); |
72 | for (int i = 0; i < numChildServices; i++) { |
73 | mFreeConnectionIndices.add(i); |
74 | } |
75 | setServiceClass(inSandbox ? |
76 | SandboxedProcessService.class : PrivilegedProcessService.class); |
77 | mInSandbox = inSandbox; |
78 | } |
79 | |
80 | public void setServiceClass(Class<? extends ChildProcessService> childClass) { |
81 | mChildClass = childClass; |
82 | } |
83 | |
84 | public ChildProcessConnection allocate( |
85 | Context context, ChildProcessConnection.DeathCallback deathCallback) { |
86 | synchronized(mConnectionLock) { |
87 | if (mFreeConnectionIndices.isEmpty()) { |
88 | Log.w(TAG, "Ran out of service." ); |
89 | return null; |
90 | } |
91 | int slot = mFreeConnectionIndices.remove(0); |
92 | assert mChildProcessConnections[slot] == null; |
93 | mChildProcessConnections[slot] = new ChildProcessConnection(context, slot, |
94 | mInSandbox, deathCallback, mChildClass); |
95 | return mChildProcessConnections[slot]; |
96 | } |
97 | } |
98 | |
99 | public void free(ChildProcessConnection connection) { |
100 | synchronized(mConnectionLock) { |
101 | int slot = connection.getServiceNumber(); |
102 | if (mChildProcessConnections[slot] != connection) { |
103 | int occupier = mChildProcessConnections[slot] == null ? |
104 | -1 : mChildProcessConnections[slot].getServiceNumber(); |
105 | Log.e(TAG, "Unable to find connection to free in slot: " + slot + |
106 | " already occupied by service: " + occupier); |
107 | assert false; |
108 | } else { |
109 | mChildProcessConnections[slot] = null; |
110 | assert !mFreeConnectionIndices.contains(slot); |
111 | mFreeConnectionIndices.add(slot); |
112 | } |
113 | } |
114 | } |
115 | } |
116 | |
117 | // Service class for child process. As the default value it uses SandboxedProcessService0 and |
118 | // PrivilegedProcessService0. |
119 | private static final ChildConnectionAllocator sSandboxedChildConnectionAllocator = |
120 | new ChildConnectionAllocator(true); |
121 | private static final ChildConnectionAllocator sPrivilegedChildConnectionAllocator = |
122 | new ChildConnectionAllocator(false); |
123 | |
124 | private static boolean sConnectionAllocated = false; |
125 | |
126 | // Sets service class for sandboxed service and privileged service. |
127 | public static void setChildProcessClass( |
128 | Class<? extends SandboxedProcessService> sandboxedServiceClass, |
129 | Class<? extends PrivilegedProcessService> privilegedServiceClass) { |
130 | // We should guarantee this is called before allocating connection. |
131 | assert !sConnectionAllocated; |
132 | sSandboxedChildConnectionAllocator.setServiceClass(sandboxedServiceClass); |
133 | sPrivilegedChildConnectionAllocator.setServiceClass(privilegedServiceClass); |
134 | } |
135 | |
136 | private static ChildConnectionAllocator getConnectionAllocator(boolean inSandbox) { |
137 | return inSandbox ? |
138 | sSandboxedChildConnectionAllocator : sPrivilegedChildConnectionAllocator; |
139 | } |
140 | |
141 | private static ChildProcessConnection allocateConnection(Context context, |
142 | boolean inSandbox) { |
143 | ChildProcessConnection.DeathCallback deathCallback = |
144 | new ChildProcessConnection.DeathCallback() { |
145 | @Override |
146 | public void onChildProcessDied(int pid) { |
147 | stop(pid); |
148 | } |
149 | }; |
150 | sConnectionAllocated = true; |
151 | return getConnectionAllocator(inSandbox).allocate(context, deathCallback); |
152 | } |
153 | |
154 | private static ChildProcessConnection allocateBoundConnection(Context context, |
155 | String[] commandLine, boolean inSandbox) { |
156 | ChildProcessConnection connection = allocateConnection(context, inSandbox); |
157 | if (connection != null) { |
158 | connection.start(commandLine); |
159 | } |
160 | return connection; |
161 | } |
162 | |
163 | private static void freeConnection(ChildProcessConnection connection) { |
164 | if (connection == null) { |
165 | return; |
166 | } |
167 | getConnectionAllocator(connection.isInSandbox()).free(connection); |
168 | return; |
169 | } |
170 | |
171 | // Represents an invalid process handle; same as base/process/process.h kNullProcessHandle. |
172 | private static final int NULL_PROCESS_HANDLE = 0; |
173 | |
174 | // Map from pid to ChildService connection. |
175 | private static Map<Integer, ChildProcessConnection> sServiceMap = |
176 | new ConcurrentHashMap<Integer, ChildProcessConnection>(); |
177 | |
178 | // Map from pid to the count of oom bindings. "Oom binding" is a binding that raises the process |
179 | // oom priority so that it shouldn't be killed by the OS out-of-memory killer under normal |
180 | // conditions (it can still be killed under drastic memory pressure). |
181 | private static SparseIntArray sOomBindingCount = new SparseIntArray(); |
182 | |
183 | // A pre-allocated and pre-bound connection ready for connection setup, or null. |
184 | private static ChildProcessConnection sSpareSandboxedConnection = null; |
185 | |
186 | /** |
187 | * Returns the child process service interface for the given pid. This may be called on |
188 | * any thread, but the caller must assume that the service can disconnect at any time. All |
189 | * service calls should catch and handle android.os.RemoteException. |
190 | * |
191 | * @param pid The pid (process handle) of the service obtained from {@link #start}. |
192 | * @return The IChildProcessService or null if the service no longer exists. |
193 | */ |
194 | public static IChildProcessService getChildService(int pid) { |
195 | ChildProcessConnection connection = sServiceMap.get(pid); |
196 | if (connection != null) { |
197 | return connection.getService(); |
198 | } |
199 | return null; |
200 | } |
201 | |
202 | /** |
203 | * Should be called early in startup so the work needed to spawn the child process can be done |
204 | * in parallel to other startup work. Must not be called on the UI thread. Spare connection is |
205 | * created in sandboxed child process. |
206 | * @param context the application context used for the connection. |
207 | */ |
208 | public static void warmUp(Context context) { |
209 | synchronized (ChildProcessLauncher.class) { |
210 | assert !ThreadUtils.runningOnUiThread(); |
211 | if (sSpareSandboxedConnection == null) { |
212 | sSpareSandboxedConnection = allocateBoundConnection(context, null, true); |
213 | } |
214 | } |
215 | } |
216 | |
217 | private static String getSwitchValue(final String[] commandLine, String switchKey) { |
218 | if (commandLine == null || switchKey == null) { |
219 | return null; |
220 | } |
221 | // This format should be matched with the one defined in command_line.h. |
222 | final String switchKeyPrefix = "--" + switchKey + "="; |
223 | for (String command : commandLine) { |
224 | if (command != null && command.startsWith(switchKeyPrefix)) { |
225 | return command.substring(switchKeyPrefix.length()); |
226 | } |
227 | } |
228 | return null; |
229 | } |
230 | |
231 | /** |
232 | * Spawns and connects to a child process. May be called on any thread. It will not block, but |
233 | * will instead callback to {@link #nativeOnChildProcessStarted} when the connection is |
234 | * established. Note this callback will not necessarily be from the same thread (currently it |
235 | * always comes from the main thread). |
236 | * |
237 | * @param context Context used to obtain the application context. |
238 | * @param commandLine The child process command line argv. |
239 | * @param file_ids The ID that should be used when mapping files in the created process. |
240 | * @param file_fds The file descriptors that should be mapped in the created process. |
241 | * @param file_auto_close Whether the file descriptors should be closed once they were passed to |
242 | * the created process. |
243 | * @param clientContext Arbitrary parameter used by the client to distinguish this connection. |
244 | */ |
245 | @CalledByNative |
246 | static void start( |
247 | Context context, |
248 | final String[] commandLine, |
249 | int[] fileIds, |
250 | int[] fileFds, |
251 | boolean[] fileAutoClose, |
252 | final int clientContext) { |
253 | assert fileIds.length == fileFds.length && fileFds.length == fileAutoClose.length; |
254 | FileDescriptorInfo[] filesToBeMapped = new FileDescriptorInfo[fileFds.length]; |
255 | for (int i = 0; i < fileFds.length; i++) { |
256 | filesToBeMapped[i] = |
257 | new FileDescriptorInfo(fileIds[i], fileFds[i], fileAutoClose[i]); |
258 | } |
259 | assert clientContext != 0; |
260 | |
261 | int callbackType = CALLBACK_FOR_UNKNOWN_PROCESS; |
262 | boolean inSandbox = true; |
263 | String processType = getSwitchValue(commandLine, SWITCH_PROCESS_TYPE); |
264 | if (SWITCH_RENDERER_PROCESS.equals(processType)) { |
265 | callbackType = CALLBACK_FOR_RENDERER_PROCESS; |
266 | } else if (SWITCH_GPU_PROCESS.equals(processType)) { |
267 | callbackType = CALLBACK_FOR_GPU_PROCESS; |
268 | } else if (SWITCH_PPAPI_BROKER_PROCESS.equals(processType)) { |
269 | inSandbox = false; |
270 | } |
271 | |
272 | ChildProcessConnection allocatedConnection = null; |
273 | synchronized (ChildProcessLauncher.class) { |
274 | if (inSandbox) { |
275 | allocatedConnection = sSpareSandboxedConnection; |
276 | sSpareSandboxedConnection = null; |
277 | } |
278 | } |
279 | if (allocatedConnection == null) { |
280 | allocatedConnection = allocateBoundConnection(context, commandLine, inSandbox); |
281 | if (allocatedConnection == null) { |
282 | // Notify the native code so it can free the heap allocated callback. |
283 | nativeOnChildProcessStarted(clientContext, 0); |
284 | return; |
285 | } |
286 | } |
287 | final ChildProcessConnection connection = allocatedConnection; |
288 | Log.d(TAG, "Setting up connection to process: slot=" + connection.getServiceNumber()); |
289 | |
290 | ChildProcessConnection.ConnectionCallbacks connectionCallbacks = |
291 | new ChildProcessConnection.ConnectionCallbacks() { |
292 | public void onConnected(int pid, int oomBindingCount) { |
293 | Log.d(TAG, "on connect callback, pid=" + pid + " context=" + clientContext); |
294 | if (pid != NULL_PROCESS_HANDLE) { |
295 | sOomBindingCount.put(pid, oomBindingCount); |
296 | sServiceMap.put(pid, connection); |
297 | } else { |
298 | freeConnection(connection); |
299 | } |
300 | nativeOnChildProcessStarted(clientContext, pid); |
301 | } |
302 | |
303 | public void onOomBindingAdded(int pid) { |
304 | if (pid != NULL_PROCESS_HANDLE) { |
305 | sOomBindingCount.put(pid, sOomBindingCount.get(pid) + 1); |
306 | } |
307 | } |
308 | |
309 | public void onOomBindingRemoved(int pid) { |
310 | if (pid != NULL_PROCESS_HANDLE) { |
311 | int count = sOomBindingCount.get(pid, -1); |
312 | assert count > 0; |
313 | count--; |
314 | if (count > 0) { |
315 | sOomBindingCount.put(pid, count); |
316 | } else { |
317 | sOomBindingCount.delete(pid); |
318 | } |
319 | } |
320 | } |
321 | }; |
322 | |
323 | // TODO(sievers): Revisit this as it doesn't correctly handle the utility process |
324 | // assert callbackType != CALLBACK_FOR_UNKNOWN_PROCESS; |
325 | |
326 | connection.setupConnection(commandLine, filesToBeMapped, createCallback(callbackType), |
327 | connectionCallbacks); |
328 | } |
329 | |
330 | /** |
331 | * Terminates a child process. This may be called from any thread. |
332 | * |
333 | * @param pid The pid (process handle) of the service connection obtained from {@link #start}. |
334 | */ |
335 | @CalledByNative |
336 | static void stop(int pid) { |
337 | Log.d(TAG, "stopping child connection: pid=" + pid); |
338 | ChildProcessConnection connection = sServiceMap.remove(pid); |
339 | if (connection == null) { |
340 | LogPidWarning(pid, "Tried to stop non-existent connection"); |
341 | return; |
342 | } |
343 | connection.stop(); |
344 | freeConnection(connection); |
345 | } |
346 | |
347 | /** |
348 | * Remove the initial child process binding. Child processes are bound with initial binding to |
349 | * protect them from getting killed before they are put to use. This method allows to remove the |
350 | * binding once it is no longer needed. |
351 | */ |
352 | static void removeInitialBinding(int pid) { |
353 | ChildProcessConnection connection = sServiceMap.get(pid); |
354 | if (connection == null) { |
355 | LogPidWarning(pid, "Tried to remove a binding for a non-existent connection"); |
356 | return; |
357 | } |
358 | connection.removeInitialBinding(); |
359 | } |
360 | |
361 | /** |
362 | * Bind a child process as a high priority process so that it has the same priority as the main |
363 | * process. This can be used for the foreground renderer process to distinguish it from the the |
364 | * background renderer process. |
365 | * |
366 | * @param pid The process handle of the service connection obtained from {@link #start}. |
367 | */ |
368 | static void bindAsHighPriority(int pid) { |
369 | ChildProcessConnection connection = sServiceMap.get(pid); |
370 | if (connection == null) { |
371 | LogPidWarning(pid, "Tried to bind a non-existent connection"); |
372 | return; |
373 | } |
374 | connection.attachAsActive(); |
375 | } |
376 | |
377 | /** |
378 | * Unbind a high priority process which is bound by {@link #bindAsHighPriority}. |
379 | * |
380 | * @param pid The process handle of the service obtained from {@link #start}. |
381 | */ |
382 | static void unbindAsHighPriority(int pid) { |
383 | ChildProcessConnection connection = sServiceMap.get(pid); |
384 | if (connection == null) { |
385 | LogPidWarning(pid, "Tried to unbind non-existent connection"); |
386 | return; |
387 | } |
388 | connection.detachAsActive(); |
389 | } |
390 | |
391 | /** |
392 | * @return True iff the given service process is protected from the out-of-memory killing, or it |
393 | * was protected from it when it died. |
394 | */ |
395 | static boolean isOomProtected(int pid) { |
396 | return sOomBindingCount.get(pid) > 0; |
397 | } |
398 | |
399 | /** |
400 | * This implementation is used to receive callbacks from the remote service. |
401 | */ |
402 | private static IChildProcessCallback createCallback(final int callbackType) { |
403 | return new IChildProcessCallback.Stub() { |
404 | /** |
405 | * This is called by the remote service regularly to tell us about new values. Note that |
406 | * IPC calls are dispatched through a thread pool running in each process, so the code |
407 | * executing here will NOT be running in our main thread -- so, to update the UI, we |
408 | * need to use a Handler. |
409 | */ |
410 | @Override |
411 | public void establishSurfacePeer( |
412 | int pid, Surface surface, int primaryID, int secondaryID) { |
413 | // Do not allow a malicious renderer to connect to a producer. This is only used |
414 | // from stream textures managed by the GPU process. |
415 | if (callbackType != CALLBACK_FOR_GPU_PROCESS) { |
416 | Log.e(TAG, "Illegal callback for non-GPU process."); |
417 | return; |
418 | } |
419 | |
420 | nativeEstablishSurfacePeer(pid, surface, primaryID, secondaryID); |
421 | } |
422 | |
423 | @Override |
424 | public Surface getViewSurface(int surfaceId) { |
425 | // Do not allow a malicious renderer to get to our view surface. |
426 | if (callbackType != CALLBACK_FOR_GPU_PROCESS) { |
427 | Log.e(TAG, "Illegal callback for non-GPU process."); |
428 | return null; |
429 | } |
430 | |
431 | return nativeGetViewSurface(surfaceId); |
432 | } |
433 | }; |
434 | }; |
435 | |
436 | private static void LogPidWarning(int pid, String message) { |
437 | // This class is effectively a no-op in single process mode, so don't log warnings there. |
438 | if (pid > 0 && !nativeIsSingleProcess()) { |
439 | Log.w(TAG, message + ", pid=" + pid); |
440 | } |
441 | } |
442 | |
443 | private static native void nativeOnChildProcessStarted(int clientContext, int pid); |
444 | private static native Surface nativeGetViewSurface(int surfaceId); |
445 | private static native void nativeEstablishSurfacePeer( |
446 | int pid, Surface surface, int primaryID, int secondaryID); |
447 | private static native boolean nativeIsSingleProcess(); |
448 | } |