OLD | NEW |
1 // Copyright 2012 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 package org.chromium.content.browser; | 5 package org.chromium.content.browser; |
6 | 6 |
7 import android.content.ComponentName; | 7 import android.content.ComponentName; |
8 import android.content.Context; | 8 import android.content.Context; |
9 import android.content.pm.ApplicationInfo; | 9 import android.content.pm.ApplicationInfo; |
10 import android.content.pm.PackageManager; | 10 import android.content.pm.PackageManager; |
(...skipping 25 matching lines...) Expand all Loading... |
36 /** | 36 /** |
37 * This class provides the method to start/stop ChildProcess called by native. | 37 * This class provides the method to start/stop ChildProcess called by native. |
38 * | 38 * |
39 * Note about threading. The threading here is complicated and not well document
ed. | 39 * Note about threading. The threading here is complicated and not well document
ed. |
40 * Code can run on these threads: UI, Launcher, async thread pool, binder, and o
ne-off | 40 * Code can run on these threads: UI, Launcher, async thread pool, binder, and o
ne-off |
41 * background threads. | 41 * background threads. |
42 */ | 42 */ |
43 public class ChildProcessLauncher { | 43 public class ChildProcessLauncher { |
44 private static final String TAG = "ChildProcLauncher"; | 44 private static final String TAG = "ChildProcLauncher"; |
45 | 45 |
46 static final int CALLBACK_FOR_UNKNOWN_PROCESS = 0; | |
47 static final int CALLBACK_FOR_GPU_PROCESS = 1; | |
48 static final int CALLBACK_FOR_RENDERER_PROCESS = 2; | |
49 static final int CALLBACK_FOR_UTILITY_PROCESS = 3; | |
50 | |
51 private static class ChildConnectionAllocator { | 46 private static class ChildConnectionAllocator { |
52 // Connections to services. Indices of the array correspond to the servi
ce numbers. | 47 // Connections to services. Indices of the array correspond to the servi
ce numbers. |
53 private final ChildProcessConnection[] mChildProcessConnections; | 48 private final ChildProcessConnection[] mChildProcessConnections; |
54 | 49 |
55 // The list of free (not bound) service indices. | 50 // The list of free (not bound) service indices. |
56 // SHOULD BE ACCESSED WITH mConnectionLock. | 51 // SHOULD BE ACCESSED WITH mConnectionLock. |
57 private final ArrayList<Integer> mFreeConnectionIndices; | 52 private final ArrayList<Integer> mFreeConnectionIndices; |
58 private final Object mConnectionLock = new Object(); | 53 private final Object mConnectionLock = new Object(); |
59 | 54 |
60 private final String mChildClassName; | 55 private final String mChildClassName; |
(...skipping 10 matching lines...) Expand all Loading... |
71 for (int i = 0; i < numChildServices; i++) { | 66 for (int i = 0; i < numChildServices; i++) { |
72 mFreeConnectionIndices.add(i); | 67 mFreeConnectionIndices.add(i); |
73 } | 68 } |
74 mChildClassName = serviceClassName; | 69 mChildClassName = serviceClassName; |
75 mInSandbox = inSandbox; | 70 mInSandbox = inSandbox; |
76 } | 71 } |
77 | 72 |
78 // Allocate or enqueue. If there are no free slots, return null and enqu
eue the spawn data. | 73 // Allocate or enqueue. If there are no free slots, return null and enqu
eue the spawn data. |
79 public ChildProcessConnection allocate(SpawnData spawnData, | 74 public ChildProcessConnection allocate(SpawnData spawnData, |
80 ChildProcessConnection.DeathCallback deathCallback, | 75 ChildProcessConnection.DeathCallback deathCallback, |
81 Bundle childProcessCommonParameters, boolean alwaysInForeground)
{ | 76 Bundle childProcessCommonParameters) { |
82 assert spawnData.inSandbox() == mInSandbox; | 77 assert spawnData.isInSandbox() == mInSandbox; |
83 synchronized (mConnectionLock) { | 78 synchronized (mConnectionLock) { |
84 if (mFreeConnectionIndices.isEmpty()) { | 79 if (mFreeConnectionIndices.isEmpty()) { |
85 Log.d(TAG, "Ran out of services to allocate."); | 80 Log.d(TAG, "Ran out of services to allocate."); |
86 if (!spawnData.forWarmUp()) { | 81 if (!spawnData.isForWarmUp()) { |
87 mPendingSpawnQueue.add(spawnData); | 82 mPendingSpawnQueue.add(spawnData); |
88 } | 83 } |
89 return null; | 84 return null; |
90 } | 85 } |
91 int slot = mFreeConnectionIndices.remove(0); | 86 int slot = mFreeConnectionIndices.remove(0); |
92 assert mChildProcessConnections[slot] == null; | 87 assert mChildProcessConnections[slot] == null; |
93 mChildProcessConnections[slot] = | 88 mChildProcessConnections[slot] = |
94 new ChildProcessConnectionImpl(spawnData.context(), slot
, mInSandbox, | 89 new ChildProcessConnectionImpl(spawnData.getContext(), s
lot, mInSandbox, |
95 deathCallback, mChildClassName, childProcessComm
onParameters, | 90 deathCallback, mChildClassName, childProcessComm
onParameters, |
96 alwaysInForeground, spawnData.getCreationParams(
)); | 91 spawnData.isAlwaysInForeground(), spawnData.getC
reationParams()); |
97 Log.d(TAG, "Allocator allocated a connection, sandbox: %b, slot:
%d", mInSandbox, | 92 Log.d(TAG, "Allocator allocated a connection, sandbox: %b, slot:
%d", mInSandbox, |
98 slot); | 93 slot); |
99 return mChildProcessConnections[slot]; | 94 return mChildProcessConnections[slot]; |
100 } | 95 } |
101 } | 96 } |
102 | 97 |
103 // Also return the first SpawnData in the pending queue, if any. | 98 // Also return the first SpawnData in the pending queue, if any. |
104 public SpawnData free(ChildProcessConnection connection) { | 99 public SpawnData free(ChildProcessConnection connection) { |
105 synchronized (mConnectionLock) { | 100 synchronized (mConnectionLock) { |
106 int slot = connection.getServiceNumber(); | 101 int slot = connection.getServiceNumber(); |
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
153 } | 148 } |
154 } | 149 } |
155 | 150 |
156 private static class SpawnData { | 151 private static class SpawnData { |
157 private final boolean mForWarmup; // Do not queue up if failed. | 152 private final boolean mForWarmup; // Do not queue up if failed. |
158 private final Context mContext; | 153 private final Context mContext; |
159 private final String[] mCommandLine; | 154 private final String[] mCommandLine; |
160 private final int mChildProcessId; | 155 private final int mChildProcessId; |
161 private final FileDescriptorInfo[] mFilesToBeMapped; | 156 private final FileDescriptorInfo[] mFilesToBeMapped; |
162 private final LaunchCallback mLaunchCallback; | 157 private final LaunchCallback mLaunchCallback; |
163 private final int mCallbackType; | 158 private final IBinder mChildProcessCallback; |
164 private final boolean mInSandbox; | 159 private final boolean mInSandbox; |
| 160 private final boolean mAlwaysInForeground; |
165 private final ChildProcessCreationParams mCreationParams; | 161 private final ChildProcessCreationParams mCreationParams; |
166 | 162 |
167 private SpawnData(boolean forWarmUp, Context context, String[] commandLi
ne, | 163 private SpawnData(boolean forWarmUp, Context context, String[] commandLi
ne, |
168 int childProcessId, FileDescriptorInfo[] filesToBeMapped, | 164 int childProcessId, FileDescriptorInfo[] filesToBeMapped, |
169 LaunchCallback launchCallback, int callbackType, boolean inSandb
ox, | 165 LaunchCallback launchCallback, IBinder childProcessCallback, boo
lean inSandbox, |
170 ChildProcessCreationParams creationParams) { | 166 boolean alwaysInForeground, ChildProcessCreationParams creationP
arams) { |
171 mForWarmup = forWarmUp; | 167 mForWarmup = forWarmUp; |
172 mContext = context; | 168 mContext = context; |
173 mCommandLine = commandLine; | 169 mCommandLine = commandLine; |
174 mChildProcessId = childProcessId; | 170 mChildProcessId = childProcessId; |
175 mFilesToBeMapped = filesToBeMapped; | 171 mFilesToBeMapped = filesToBeMapped; |
176 mLaunchCallback = launchCallback; | 172 mLaunchCallback = launchCallback; |
177 mCallbackType = callbackType; | 173 mChildProcessCallback = childProcessCallback; |
178 mInSandbox = inSandbox; | 174 mInSandbox = inSandbox; |
| 175 mAlwaysInForeground = alwaysInForeground; |
179 mCreationParams = creationParams; | 176 mCreationParams = creationParams; |
180 } | 177 } |
181 | 178 |
182 private boolean forWarmUp() { | 179 private boolean isForWarmUp() { |
183 return mForWarmup; | 180 return mForWarmup; |
184 } | 181 } |
185 | 182 private Context getContext() { |
186 private Context context() { | |
187 return mContext; | 183 return mContext; |
188 } | 184 } |
189 private String[] commandLine() { | 185 private String[] getCommandLine() { |
190 return mCommandLine; | 186 return mCommandLine; |
191 } | 187 } |
192 private int childProcessId() { | 188 private int getChildProcessId() { |
193 return mChildProcessId; | 189 return mChildProcessId; |
194 } | 190 } |
195 private FileDescriptorInfo[] filesToBeMapped() { | 191 private FileDescriptorInfo[] getFilesToBeMapped() { |
196 return mFilesToBeMapped; | 192 return mFilesToBeMapped; |
197 } | 193 } |
198 private LaunchCallback launchCallback() { | 194 private LaunchCallback getLaunchCallback() { |
199 return mLaunchCallback; | 195 return mLaunchCallback; |
200 } | 196 } |
201 private int callbackType() { | 197 private IBinder getChildProcessCallback() { |
202 return mCallbackType; | 198 return mChildProcessCallback; |
203 } | 199 } |
204 private boolean inSandbox() { | 200 private boolean isInSandbox() { |
205 return mInSandbox; | 201 return mInSandbox; |
206 } | 202 } |
| 203 private boolean isAlwaysInForeground() { |
| 204 return mAlwaysInForeground; |
| 205 } |
207 private ChildProcessCreationParams getCreationParams() { | 206 private ChildProcessCreationParams getCreationParams() { |
208 return mCreationParams; | 207 return mCreationParams; |
209 } | 208 } |
210 } | 209 } |
211 | 210 |
212 /** | 211 /** |
213 * Implemented by ChildProcessLauncherHelper. | 212 * Implemented by ChildProcessLauncherHelper. |
214 */ | 213 */ |
215 public interface LaunchCallback { void onChildProcessStarted(int pid); } | 214 public interface LaunchCallback { void onChildProcessStarted(int pid); } |
216 | 215 |
217 // Service class for child process. | 216 // Service class for child process. |
218 // Map from package name to ChildConnectionAllocator. | 217 // Map from package name to ChildConnectionAllocator. |
219 private static Map<String, ChildConnectionAllocator> sSandboxedChildConnecti
onAllocatorMap; | 218 private static Map<String, ChildConnectionAllocator> sSandboxedChildConnecti
onAllocatorMap; |
220 // As the default value it uses PrivilegedProcessService0. | 219 // As the default value it uses PrivilegedProcessService0. |
221 private static ChildConnectionAllocator sPrivilegedChildConnectionAllocator; | 220 private static ChildConnectionAllocator sPrivilegedChildConnectionAllocator; |
222 | 221 |
| 222 private static final boolean SPARE_CONNECTION_ALWAYS_IN_FOREGROUND = false; |
| 223 |
223 private static final String NUM_SANDBOXED_SERVICES_KEY = | 224 private static final String NUM_SANDBOXED_SERVICES_KEY = |
224 "org.chromium.content.browser.NUM_SANDBOXED_SERVICES"; | 225 "org.chromium.content.browser.NUM_SANDBOXED_SERVICES"; |
225 private static final String NUM_PRIVILEGED_SERVICES_KEY = | 226 private static final String NUM_PRIVILEGED_SERVICES_KEY = |
226 "org.chromium.content.browser.NUM_PRIVILEGED_SERVICES"; | 227 "org.chromium.content.browser.NUM_PRIVILEGED_SERVICES"; |
227 private static final String SANDBOXED_SERVICES_NAME_KEY = | 228 private static final String SANDBOXED_SERVICES_NAME_KEY = |
228 "org.chromium.content.browser.SANDBOXED_SERVICES_NAME"; | 229 "org.chromium.content.browser.SANDBOXED_SERVICES_NAME"; |
229 // Overrides the number of available sandboxed services. | 230 // Overrides the number of available sandboxed services. |
230 @VisibleForTesting | 231 @VisibleForTesting |
231 public static final String SWITCH_NUM_SANDBOXED_SERVICES_FOR_TESTING = "num-
sandboxed-services"; | 232 public static final String SWITCH_NUM_SANDBOXED_SERVICES_FOR_TESTING = "num-
sandboxed-services"; |
232 public static final String SWITCH_SANDBOXED_SERVICES_NAME_FOR_TESTING = | 233 public static final String SWITCH_SANDBOXED_SERVICES_NAME_FOR_TESTING = |
(...skipping 111 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
344 return sSandboxedChildConnectionAllocatorMap.get(packageName); | 345 return sSandboxedChildConnectionAllocatorMap.get(packageName); |
345 } | 346 } |
346 | 347 |
347 private static ChildConnectionAllocator getAllocatorForTesting( | 348 private static ChildConnectionAllocator getAllocatorForTesting( |
348 Context context, String packageName, boolean inSandbox) { | 349 Context context, String packageName, boolean inSandbox) { |
349 initConnectionAllocatorsIfNecessary(context, inSandbox, packageName); | 350 initConnectionAllocatorsIfNecessary(context, inSandbox, packageName); |
350 return getConnectionAllocator(packageName, inSandbox); | 351 return getConnectionAllocator(packageName, inSandbox); |
351 } | 352 } |
352 | 353 |
353 private static ChildProcessConnection allocateConnection( | 354 private static ChildProcessConnection allocateConnection( |
354 SpawnData spawnData, Bundle childProcessCommonParams, boolean always
InForeground) { | 355 SpawnData spawnData, Bundle childProcessCommonParams) { |
355 ChildProcessConnection.DeathCallback deathCallback = | 356 ChildProcessConnection.DeathCallback deathCallback = |
356 new ChildProcessConnection.DeathCallback() { | 357 new ChildProcessConnection.DeathCallback() { |
357 @Override | 358 @Override |
358 public void onChildProcessDied(ChildProcessConnection connec
tion) { | 359 public void onChildProcessDied(ChildProcessConnection connec
tion) { |
359 if (connection.getPid() != 0) { | 360 if (connection.getPid() != 0) { |
360 stop(connection.getPid()); | 361 stop(connection.getPid()); |
361 } else { | 362 } else { |
362 freeConnection(connection); | 363 freeConnection(connection); |
363 } | 364 } |
364 } | 365 } |
365 }; | 366 }; |
366 final ChildProcessCreationParams creationParams = spawnData.getCreationP
arams(); | 367 final ChildProcessCreationParams creationParams = spawnData.getCreationP
arams(); |
367 final Context context = spawnData.context(); | 368 final Context context = spawnData.getContext(); |
368 final boolean inSandbox = spawnData.inSandbox(); | 369 final boolean inSandbox = spawnData.isInSandbox(); |
369 String packageName = | 370 String packageName = |
370 creationParams != null ? creationParams.getPackageName() : conte
xt.getPackageName(); | 371 creationParams != null ? creationParams.getPackageName() : conte
xt.getPackageName(); |
371 initConnectionAllocatorsIfNecessary(context, inSandbox, packageName); | 372 initConnectionAllocatorsIfNecessary(context, inSandbox, packageName); |
372 return getConnectionAllocator(packageName, inSandbox) | 373 return getConnectionAllocator(packageName, inSandbox) |
373 .allocate(spawnData, deathCallback, childProcessCommonParams, al
waysInForeground); | 374 .allocate(spawnData, deathCallback, childProcessCommonParams); |
374 } | 375 } |
375 | 376 |
376 private static boolean sLinkerInitialized; | 377 private static boolean sLinkerInitialized; |
377 private static long sLinkerLoadAddress; | 378 private static long sLinkerLoadAddress; |
378 | 379 |
379 private static ChromiumLinkerParams getLinkerParamsForNewConnection() { | 380 private static ChromiumLinkerParams getLinkerParamsForNewConnection() { |
380 if (!sLinkerInitialized) { | 381 if (!sLinkerInitialized) { |
381 if (Linker.isUsed()) { | 382 if (Linker.isUsed()) { |
382 sLinkerLoadAddress = Linker.getInstance().getBaseLoadAddress(); | 383 sLinkerLoadAddress = Linker.getInstance().getBaseLoadAddress(); |
383 if (sLinkerLoadAddress == 0) { | 384 if (sLinkerLoadAddress == 0) { |
(...skipping 12 matching lines...) Expand all Loading... |
396 return new ChromiumLinkerParams(sLinkerLoadAddress, | 397 return new ChromiumLinkerParams(sLinkerLoadAddress, |
397 waitForSharedRelros, | 398 waitForSharedRelros, |
398 linker.getTestRunnerClassNameForTest
ing(), | 399 linker.getTestRunnerClassNameForTest
ing(), |
399 linker.getImplementationForTesting()
); | 400 linker.getImplementationForTesting()
); |
400 } else { | 401 } else { |
401 return new ChromiumLinkerParams(sLinkerLoadAddress, | 402 return new ChromiumLinkerParams(sLinkerLoadAddress, |
402 waitForSharedRelros); | 403 waitForSharedRelros); |
403 } | 404 } |
404 } | 405 } |
405 | 406 |
406 private static ChildProcessConnection allocateBoundConnection(SpawnData spaw
nData, | 407 private static ChildProcessConnection allocateBoundConnection( |
407 boolean alwaysInForeground, ChildProcessConnection.StartCallback sta
rtCallback) { | 408 SpawnData spawnData, ChildProcessConnection.StartCallback startCallb
ack) { |
408 final Context context = spawnData.context(); | 409 final Context context = spawnData.getContext(); |
409 final boolean inSandbox = spawnData.inSandbox(); | 410 final boolean inSandbox = spawnData.isInSandbox(); |
410 final ChildProcessCreationParams creationParams = spawnData.getCreationP
arams(); | 411 final ChildProcessCreationParams creationParams = spawnData.getCreationP
arams(); |
411 Bundle commonParams = new Bundle(); | 412 Bundle commonParams = new Bundle(); |
412 commonParams.putParcelable( | 413 commonParams.putParcelable( |
413 ChildProcessConstants.EXTRA_LINKER_PARAMS, getLinkerParamsForNew
Connection()); | 414 ChildProcessConstants.EXTRA_LINKER_PARAMS, getLinkerParamsForNew
Connection()); |
414 ChildProcessConnection connection = | 415 ChildProcessConnection connection = allocateConnection(spawnData, common
Params); |
415 allocateConnection(spawnData, commonParams, alwaysInForeground); | |
416 if (connection != null) { | 416 if (connection != null) { |
417 connection.start(startCallback); | 417 connection.start(startCallback); |
418 | 418 |
419 String packageName = creationParams != null ? creationParams.getPack
ageName() | 419 String packageName = creationParams != null ? creationParams.getPack
ageName() |
420 : context.getPackageName
(); | 420 : context.getPackageName
(); |
421 if (inSandbox | 421 if (inSandbox |
422 && !getConnectionAllocator(packageName, inSandbox) | 422 && !getConnectionAllocator(packageName, inSandbox) |
423 .isFreeConnectionAvailable()) { | 423 .isFreeConnectionAvailable()) { |
424 // Proactively releases all the moderate bindings once all the s
andboxed services | 424 // Proactively releases all the moderate bindings once all the s
andboxed services |
425 // are allocated, which will be very likely to have some of them
killed by OOM | 425 // are allocated, which will be very likely to have some of them
killed by OOM |
(...skipping 18 matching lines...) Expand all Loading... |
444 // variables are set when we don't expect them to). | 444 // variables are set when we don't expect them to). |
445 final ChildProcessConnection conn = connection; | 445 final ChildProcessConnection conn = connection; |
446 ThreadUtils.postOnUiThreadDelayed(new Runnable() { | 446 ThreadUtils.postOnUiThreadDelayed(new Runnable() { |
447 @Override | 447 @Override |
448 public void run() { | 448 public void run() { |
449 final SpawnData pendingSpawn = freeConnectionAndDequeuePending(c
onn); | 449 final SpawnData pendingSpawn = freeConnectionAndDequeuePending(c
onn); |
450 if (pendingSpawn != null) { | 450 if (pendingSpawn != null) { |
451 LauncherThread.post(new Runnable() { | 451 LauncherThread.post(new Runnable() { |
452 @Override | 452 @Override |
453 public void run() { | 453 public void run() { |
454 startInternal(pendingSpawn.context(), pendingSpawn.c
ommandLine(), | 454 startInternal(pendingSpawn.getContext(), pendingSpaw
n.getCommandLine(), |
455 pendingSpawn.childProcessId(), pendingSpawn.
filesToBeMapped(), | 455 pendingSpawn.getChildProcessId(), |
456 pendingSpawn.launchCallback(), pendingSpawn.
callbackType(), | 456 pendingSpawn.getFilesToBeMapped(), |
457 pendingSpawn.inSandbox(), pendingSpawn.getCr
eationParams()); | 457 pendingSpawn.getLaunchCallback(), |
| 458 pendingSpawn.getChildProcessCallback(), |
| 459 pendingSpawn.isInSandbox(), pendingSpawn.isA
lwaysInForeground(), |
| 460 pendingSpawn.getCreationParams()); |
458 } | 461 } |
459 }); | 462 }); |
460 } | 463 } |
461 } | 464 } |
462 }, FREE_CONNECTION_DELAY_MILLIS); | 465 }, FREE_CONNECTION_DELAY_MILLIS); |
463 } | 466 } |
464 | 467 |
465 private static SpawnData freeConnectionAndDequeuePending(ChildProcessConnect
ion conn) { | 468 private static SpawnData freeConnectionAndDequeuePending(ChildProcessConnect
ion conn) { |
466 ChildConnectionAllocator allocator = getConnectionAllocator( | 469 ChildConnectionAllocator allocator = getConnectionAllocator( |
467 conn.getPackageName(), conn.isInSandbox()); | 470 conn.getPackageName(), conn.isInSandbox()); |
(...skipping 110 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
578 synchronized (sSpareConnectionLock) { | 581 synchronized (sSpareConnectionLock) { |
579 sSpareSandboxedConnection = null; | 582 sSpareSandboxedConnection = null; |
580 sSpareConnectionStarting = false; | 583 sSpareConnectionStarting = false; |
581 sSpareConnectionLock.notify(); | 584 sSpareConnectionLock.notify(); |
582 } | 585 } |
583 } | 586 } |
584 }; | 587 }; |
585 SpawnData spawnData = new SpawnData(true /* forWarmUp*/, context
, | 588 SpawnData spawnData = new SpawnData(true /* forWarmUp*/, context
, |
586 null /* commandLine */, -1 /* child process id */, | 589 null /* commandLine */, -1 /* child process id */, |
587 null /* filesToBeMapped */, null /* launchCallback */, | 590 null /* filesToBeMapped */, null /* launchCallback */, |
588 CALLBACK_FOR_RENDERER_PROCESS, true /* inSandbox */, par
ams); | 591 null /* child process callback */, true /* inSandbox */, |
589 sSpareSandboxedConnection = allocateBoundConnection( | 592 SPARE_CONNECTION_ALWAYS_IN_FOREGROUND, params); |
590 spawnData, false /* alwaysInForeground */, startCallback
); | 593 sSpareSandboxedConnection = allocateBoundConnection(spawnData, s
tartCallback); |
591 } | 594 } |
592 } | 595 } |
593 } | 596 } |
594 | 597 |
595 /** | 598 /** |
596 * Spawns and connects to a child process. May be called on any thread. It w
ill not block, but | 599 * Spawns and connects to a child process. May be called on any thread. It w
ill not block, but |
597 * will instead callback to {@link #nativeOnChildProcessStarted} when the co
nnection is | 600 * will instead callback to {@link #nativeOnChildProcessStarted} when the co
nnection is |
598 * established. Note this callback will not necessarily be from the same thr
ead (currently it | 601 * established. Note this callback will not necessarily be from the same thr
ead (currently it |
599 * always comes from the main thread). | 602 * always comes from the main thread). |
600 * | 603 * |
601 * @param context Context used to obtain the application context. | 604 * @param context Context used to obtain the application context. |
602 * @param paramId Key used to retrieve ChildProcessCreationParams. | 605 * @param paramId Key used to retrieve ChildProcessCreationParams. |
603 * @param commandLine The child process command line argv. | 606 * @param commandLine The child process command line argv. |
604 * @param filesToBeMapped File IDs, FDs, offsets, and lengths to pass throug
h. | 607 * @param filesToBeMapped File IDs, FDs, offsets, and lengths to pass throug
h. |
605 */ | 608 */ |
606 // TODO(boliu): All tests should use this over startForTesting. | 609 // TODO(boliu): All tests should use this over startForTesting. |
607 static void start(Context context, int paramId, final String[] commandLine,
int childProcessId, | 610 static void start(Context context, int paramId, final String[] commandLine,
int childProcessId, |
608 FileDescriptorInfo[] filesToBeMapped, LaunchCallback launchCallback)
{ | 611 FileDescriptorInfo[] filesToBeMapped, LaunchCallback launchCallback)
{ |
609 int callbackType = CALLBACK_FOR_UNKNOWN_PROCESS; | 612 IBinder childProcessCallback = null; |
610 boolean inSandbox = true; | 613 boolean inSandbox = true; |
| 614 boolean alwaysInForeground = false; |
611 String processType = | 615 String processType = |
612 ContentSwitches.getSwitchValue(commandLine, ContentSwitches.SWIT
CH_PROCESS_TYPE); | 616 ContentSwitches.getSwitchValue(commandLine, ContentSwitches.SWIT
CH_PROCESS_TYPE); |
613 ChildProcessCreationParams params = ChildProcessCreationParams.get(param
Id); | 617 ChildProcessCreationParams params = ChildProcessCreationParams.get(param
Id); |
614 if (paramId != ChildProcessCreationParams.DEFAULT_ID && params == null)
{ | 618 if (paramId != ChildProcessCreationParams.DEFAULT_ID && params == null)
{ |
615 throw new RuntimeException("CreationParams id " + paramId + " not fo
und"); | 619 throw new RuntimeException("CreationParams id " + paramId + " not fo
und"); |
616 } | 620 } |
617 if (ContentSwitches.SWITCH_RENDERER_PROCESS.equals(processType)) { | 621 if (!ContentSwitches.SWITCH_RENDERER_PROCESS.equals(processType)) { |
618 callbackType = CALLBACK_FOR_RENDERER_PROCESS; | |
619 } else { | |
620 if (params != null && !params.getPackageName().equals(context.getPac
kageName())) { | 622 if (params != null && !params.getPackageName().equals(context.getPac
kageName())) { |
621 // WebViews and WebAPKs have renderer processes running in their
applications. | 623 // WebViews and WebAPKs have renderer processes running in their
applications. |
622 // When launching these renderer processes, {@link ChildProcessC
onnectionImpl} | 624 // When launching these renderer processes, {@link ChildProcessC
onnectionImpl} |
623 // requires the package name of the application which holds the
renderer process. | 625 // requires the package name of the application which holds the
renderer process. |
624 // Therefore, the package name in ChildProcessCreationParams cou
ld be the package | 626 // Therefore, the package name in ChildProcessCreationParams cou
ld be the package |
625 // name of WebViews, WebAPKs, or Chrome, depending on the host a
pplication. | 627 // name of WebViews, WebAPKs, or Chrome, depending on the host a
pplication. |
626 // Except renderer process, all other child processes should use
Chrome's package | 628 // Except renderer process, all other child processes should use
Chrome's package |
627 // name. In WebAPK, ChildProcessCreationParams are initialized w
ith WebAPK's | 629 // name. In WebAPK, ChildProcessCreationParams are initialized w
ith WebAPK's |
628 // package name. Make a copy of the WebAPK's params, but replace
the package with | 630 // package name. Make a copy of the WebAPK's params, but replace
the package with |
629 // Chrome's package to use when initializing a non-renderer proc
esses. | 631 // Chrome's package to use when initializing a non-renderer proc
esses. |
630 // TODO(boliu): Should fold into |paramId|. Investigate why this
is needed. | 632 // TODO(boliu): Should fold into |paramId|. Investigate why this
is needed. |
631 params = new ChildProcessCreationParams(context.getPackageName()
, | 633 params = new ChildProcessCreationParams(context.getPackageName()
, |
632 params.getIsExternalService(), params.getLibraryProcessT
ype()); | 634 params.getIsExternalService(), params.getLibraryProcessT
ype()); |
633 } | 635 } |
634 if (ContentSwitches.SWITCH_GPU_PROCESS.equals(processType)) { | 636 if (ContentSwitches.SWITCH_GPU_PROCESS.equals(processType)) { |
635 callbackType = CALLBACK_FOR_GPU_PROCESS; | 637 childProcessCallback = new GpuProcessCallback(); |
636 inSandbox = false; | 638 inSandbox = false; |
637 } else if (ContentSwitches.SWITCH_UTILITY_PROCESS.equals(processType
)) { | 639 alwaysInForeground = true; |
638 // We only support sandboxed right now. | |
639 callbackType = CALLBACK_FOR_UTILITY_PROCESS; | |
640 } else { | 640 } else { |
641 assert false; | 641 // We only support sandboxed utility processes now. |
| 642 assert ContentSwitches.SWITCH_UTILITY_PROCESS.equals(processType
); |
642 } | 643 } |
643 } | 644 } |
644 | 645 |
645 startInternal(context, commandLine, childProcessId, filesToBeMapped, lau
nchCallback, | 646 startInternal(context, commandLine, childProcessId, filesToBeMapped, lau
nchCallback, |
646 callbackType, inSandbox, params); | 647 childProcessCallback, inSandbox, alwaysInForeground, params); |
647 } | 648 } |
648 | 649 |
649 private static ChildProcessConnection startInternal(final Context context, | 650 private static ChildProcessConnection startInternal(final Context context, |
650 final String[] commandLine, final int childProcessId, | 651 final String[] commandLine, final int childProcessId, |
651 final FileDescriptorInfo[] filesToBeMapped, final LaunchCallback lau
nchCallback, | 652 final FileDescriptorInfo[] filesToBeMapped, final LaunchCallback lau
nchCallback, |
652 final int callbackType, final boolean inSandbox, | 653 final IBinder childProcessCallback, final boolean inSandbox, |
653 final ChildProcessCreationParams creationParams) { | 654 final boolean alwaysInForeground, final ChildProcessCreationParams c
reationParams) { |
654 try { | 655 try { |
655 TraceEvent.begin("ChildProcessLauncher.startInternal"); | 656 TraceEvent.begin("ChildProcessLauncher.startInternal"); |
656 | 657 |
657 ChildProcessConnection allocatedConnection = null; | 658 ChildProcessConnection allocatedConnection = null; |
658 String packageName = creationParams != null ? creationParams.getPack
ageName() | 659 String packageName = creationParams != null ? creationParams.getPack
ageName() |
659 : context.getPackageName(); | 660 : context.getPackageName(); |
660 synchronized (sSpareConnectionLock) { | 661 synchronized (sSpareConnectionLock) { |
661 if (inSandbox && sSpareSandboxedConnection != null | 662 if (inSandbox && sSpareSandboxedConnection != null |
| 663 && SPARE_CONNECTION_ALWAYS_IN_FOREGROUND == alwaysInFore
ground |
662 && sSpareSandboxedConnection.getPackageName().equals(pac
kageName) | 664 && sSpareSandboxedConnection.getPackageName().equals(pac
kageName) |
663 // Object identity check for getDefault should be enough
. The default is | 665 // Object identity check for getDefault should be enough
. The default is |
664 // not supposed to change once set. | 666 // not supposed to change once set. |
665 && creationParams == ChildProcessCreationParams.getDefau
lt()) { | 667 && creationParams == ChildProcessCreationParams.getDefau
lt()) { |
666 while (sSpareConnectionStarting) { | 668 while (sSpareConnectionStarting) { |
667 try { | 669 try { |
668 sSpareConnectionLock.wait(); | 670 sSpareConnectionLock.wait(); |
669 } catch (InterruptedException ex) { | 671 } catch (InterruptedException ex) { |
670 } | 672 } |
671 } | 673 } |
672 allocatedConnection = sSpareSandboxedConnection; | 674 allocatedConnection = sSpareSandboxedConnection; |
673 sSpareSandboxedConnection = null; | 675 sSpareSandboxedConnection = null; |
674 } | 676 } |
675 } | 677 } |
676 if (allocatedConnection == null) { | 678 if (allocatedConnection == null) { |
677 boolean alwaysInForeground = false; | |
678 if (callbackType == CALLBACK_FOR_GPU_PROCESS) alwaysInForeground
= true; | |
679 ChildProcessConnection.StartCallback startCallback = | 679 ChildProcessConnection.StartCallback startCallback = |
680 new ChildProcessConnection.StartCallback() { | 680 new ChildProcessConnection.StartCallback() { |
681 @Override | 681 @Override |
682 public void onChildStarted() {} | 682 public void onChildStarted() {} |
683 | 683 |
684 @Override | 684 @Override |
685 public void onChildStartFailed() { | 685 public void onChildStartFailed() { |
686 Log.e(TAG, "ChildProcessConnection.start failed,
trying again"); | 686 Log.e(TAG, "ChildProcessConnection.start failed,
trying again"); |
687 LauncherThread.post(new Runnable() { | 687 LauncherThread.post(new Runnable() { |
688 @Override | 688 @Override |
689 public void run() { | 689 public void run() { |
690 // The child process may already be boun
d to another client | 690 // The child process may already be boun
d to another client |
691 // (this can happen if multi-process Web
View is used in more | 691 // (this can happen if multi-process Web
View is used in more |
692 // than one process), so try starting th
e process again. | 692 // than one process), so try starting th
e process again. |
693 // This connection that failed to start
has not been freed, | 693 // This connection that failed to start
has not been freed, |
694 // so a new bound connection will be all
ocated. | 694 // so a new bound connection will be all
ocated. |
695 startInternal(context, commandLine, chil
dProcessId, | 695 startInternal(context, commandLine, chil
dProcessId, |
696 filesToBeMapped, launchCallback,
callbackType, | 696 filesToBeMapped, launchCallback, |
697 inSandbox, creationParams); | 697 childProcessCallback, inSandbox,
alwaysInForeground, |
| 698 creationParams); |
698 } | 699 } |
699 }); | 700 }); |
700 } | 701 } |
701 }; | 702 }; |
702 | 703 |
703 SpawnData spawnData = new SpawnData(false /* forWarmUp */, conte
xt, commandLine, | 704 SpawnData spawnData = new SpawnData(false /* forWarmUp */, conte
xt, commandLine, |
704 childProcessId, filesToBeMapped, launchCallback, callbac
kType, inSandbox, | 705 childProcessId, filesToBeMapped, launchCallback, childPr
ocessCallback, |
705 creationParams); | 706 inSandbox, alwaysInForeground, creationParams); |
706 allocatedConnection = | 707 allocatedConnection = allocateBoundConnection(spawnData, startCa
llback); |
707 allocateBoundConnection(spawnData, alwaysInForeground, s
tartCallback); | |
708 if (allocatedConnection == null) { | 708 if (allocatedConnection == null) { |
709 return null; | 709 return null; |
710 } | 710 } |
711 } | 711 } |
712 | 712 |
713 Log.d(TAG, "Setting up connection to process: slot=%d", | 713 Log.d(TAG, "Setting up connection to process: slot=%d", |
714 allocatedConnection.getServiceNumber()); | 714 allocatedConnection.getServiceNumber()); |
715 triggerConnectionSetup(allocatedConnection, commandLine, childProces
sId, | 715 triggerConnectionSetup(allocatedConnection, commandLine, childProces
sId, |
716 filesToBeMapped, callbackType, launchCallback); | 716 filesToBeMapped, childProcessCallback, launchCallback); |
717 return allocatedConnection; | 717 return allocatedConnection; |
718 } finally { | 718 } finally { |
719 TraceEvent.end("ChildProcessLauncher.startInternal"); | 719 TraceEvent.end("ChildProcessLauncher.startInternal"); |
720 } | 720 } |
721 } | 721 } |
722 | 722 |
723 /** | 723 /** |
724 * Create the common bundle to be passed to child processes. | 724 * Create the common bundle to be passed to child processes. |
725 * @param context Application context. | 725 * @param context Application context. |
726 * @param commandLine Command line params to be passed to the service. | 726 * @param commandLine Command line params to be passed to the service. |
727 * @param linkerParams Linker params to start the service. | 727 * @param linkerParams Linker params to start the service. |
728 */ | 728 */ |
729 protected static Bundle createsServiceBundle( | 729 protected static Bundle createsServiceBundle( |
730 String[] commandLine, FileDescriptorInfo[] filesToBeMapped) { | 730 String[] commandLine, FileDescriptorInfo[] filesToBeMapped) { |
731 Bundle bundle = new Bundle(); | 731 Bundle bundle = new Bundle(); |
732 bundle.putStringArray(ChildProcessConstants.EXTRA_COMMAND_LINE, commandL
ine); | 732 bundle.putStringArray(ChildProcessConstants.EXTRA_COMMAND_LINE, commandL
ine); |
733 bundle.putParcelableArray(ChildProcessConstants.EXTRA_FILES, filesToBeMa
pped); | 733 bundle.putParcelableArray(ChildProcessConstants.EXTRA_FILES, filesToBeMa
pped); |
734 bundle.putInt(ChildProcessConstants.EXTRA_CPU_COUNT, CpuFeatures.getCoun
t()); | 734 bundle.putInt(ChildProcessConstants.EXTRA_CPU_COUNT, CpuFeatures.getCoun
t()); |
735 bundle.putLong(ChildProcessConstants.EXTRA_CPU_FEATURES, CpuFeatures.get
Mask()); | 735 bundle.putLong(ChildProcessConstants.EXTRA_CPU_FEATURES, CpuFeatures.get
Mask()); |
736 bundle.putBundle(Linker.EXTRA_LINKER_SHARED_RELROS, Linker.getInstance()
.getSharedRelros()); | 736 bundle.putBundle(Linker.EXTRA_LINKER_SHARED_RELROS, Linker.getInstance()
.getSharedRelros()); |
737 return bundle; | 737 return bundle; |
738 } | 738 } |
739 | 739 |
740 @VisibleForTesting | 740 @VisibleForTesting |
741 static void triggerConnectionSetup(final ChildProcessConnection connection, | 741 static void triggerConnectionSetup(final ChildProcessConnection connection, |
742 String[] commandLine, int childProcessId, FileDescriptorInfo[] files
ToBeMapped, | 742 String[] commandLine, int childProcessId, FileDescriptorInfo[] files
ToBeMapped, |
743 final int callbackType, final LaunchCallback launchCallback) { | 743 final IBinder childProcessCallback, final LaunchCallback launchCallb
ack) { |
744 ChildProcessConnection.ConnectionCallback connectionCallback = | 744 ChildProcessConnection.ConnectionCallback connectionCallback = |
745 new ChildProcessConnection.ConnectionCallback() { | 745 new ChildProcessConnection.ConnectionCallback() { |
746 @Override | 746 @Override |
747 public void onConnected(int pid) { | 747 public void onConnected(int pid) { |
748 Log.d(TAG, "on connect callback, pid=%d callbackType=%d"
, pid, | 748 Log.d(TAG, "on connect callback, pid=%d", pid); |
749 callbackType); | |
750 if (pid != NULL_PROCESS_HANDLE) { | 749 if (pid != NULL_PROCESS_HANDLE) { |
751 sBindingManager.addNewConnection(pid, connection); | 750 sBindingManager.addNewConnection(pid, connection); |
752 sServiceMap.put(pid, connection); | 751 sServiceMap.put(pid, connection); |
753 } | 752 } |
754 // If the connection fails and pid == 0, the Java-side c
leanup was already | 753 // If the connection fails and pid == 0, the Java-side c
leanup was already |
755 // handled by DeathCallback. We still have to call back
to native for | 754 // handled by DeathCallback. We still have to call back
to native for |
756 // cleanup there. | 755 // cleanup there. |
757 if (launchCallback != null) { // Will be null in Java in
strumentation tests. | 756 if (launchCallback != null) { // Will be null in Java in
strumentation tests. |
758 launchCallback.onChildProcessStarted(pid); | 757 launchCallback.onChildProcessStarted(pid); |
759 } | 758 } |
760 } | 759 } |
761 }; | 760 }; |
762 | 761 |
763 assert callbackType != CALLBACK_FOR_UNKNOWN_PROCESS; | 762 connection.setupConnection( |
764 connection.setupConnection(commandLine, filesToBeMapped, | 763 commandLine, filesToBeMapped, childProcessCallback, connectionCa
llback); |
765 createCallback(childProcessId, callbackType), connectionCallback
); | |
766 } | 764 } |
767 | 765 |
768 /** | 766 /** |
769 * Terminates a child process. This may be called from any thread. | 767 * Terminates a child process. This may be called from any thread. |
770 * | 768 * |
771 * @param pid The pid (process handle) of the service connection obtained fr
om {@link #start}. | 769 * @param pid The pid (process handle) of the service connection obtained fr
om {@link #start}. |
772 */ | 770 */ |
773 static void stop(int pid) { | 771 static void stop(int pid) { |
774 Log.d(TAG, "stopping child connection: pid=%d", pid); | 772 Log.d(TAG, "stopping child connection: pid=%d", pid); |
775 ChildProcessConnection connection = sServiceMap.remove(pid); | 773 ChildProcessConnection connection = sServiceMap.remove(pid); |
776 if (connection == null) { | 774 if (connection == null) { |
777 // Can happen for single process. | 775 // Can happen for single process. |
778 return; | 776 return; |
779 } | 777 } |
780 sBindingManager.clearConnection(pid); | 778 sBindingManager.clearConnection(pid); |
781 connection.stop(); | 779 connection.stop(); |
782 freeConnection(connection); | 780 freeConnection(connection); |
783 } | 781 } |
784 | 782 |
785 /** | |
786 * This implementation is used to receive callbacks from the remote service. | |
787 */ | |
788 private static IBinder createCallback(int childProcessId, int callbackType)
{ | |
789 return callbackType == CALLBACK_FOR_GPU_PROCESS ? new GpuProcessCallback
() : null; | |
790 } | |
791 | |
792 @VisibleForTesting | 783 @VisibleForTesting |
793 public static ChildProcessConnection startForTesting(Context context, String
[] commandLine, | 784 public static ChildProcessConnection startForTesting(Context context, String
[] commandLine, |
794 FileDescriptorInfo[] filesToMap, ChildProcessCreationParams params)
{ | 785 FileDescriptorInfo[] filesToMap, ChildProcessCreationParams params)
{ |
795 return startInternal(context, commandLine, 0, filesToMap, null, | 786 return startInternal(context, commandLine, 0 /* childProcessId */, files
ToMap, |
796 CALLBACK_FOR_RENDERER_PROCESS, true, params); | 787 null /* launchCallback */, null /* childProcessCallback */, true
/* inSandbox */, |
| 788 false /* alwaysInForeground */, params); |
797 } | 789 } |
798 | 790 |
799 @VisibleForTesting | 791 @VisibleForTesting |
800 static ChildProcessConnection allocateBoundConnectionForTesting(Context cont
ext, | 792 static ChildProcessConnection allocateBoundConnectionForTesting(Context cont
ext, |
801 ChildProcessCreationParams creationParams) { | 793 ChildProcessCreationParams creationParams) { |
802 return allocateBoundConnection( | 794 return allocateBoundConnection( |
803 new SpawnData(false /* forWarmUp */, context, null /* commandLin
e */, | 795 new SpawnData(false /* forWarmUp */, context, null /* commandLin
e */, |
804 0 /* childProcessId */, null /* filesToBeMapped */, | 796 0 /* childProcessId */, null /* filesToBeMapped */, |
805 null /* LaunchCallback */, CALLBACK_FOR_RENDERER_PROCESS
, | 797 null /* LaunchCallback */, null /* childProcessCallback
*/, |
806 true /* inSandbox */, creationParams), | 798 true /* inSandbox */, false /* alwaysInForeground */, cr
eationParams), |
807 false /* alwaysInForeground */, null); | 799 null /* startCallback */); |
808 } | 800 } |
809 | 801 |
810 @VisibleForTesting | 802 @VisibleForTesting |
811 static ChildProcessConnection allocateConnectionForTesting(Context context, | 803 static ChildProcessConnection allocateConnectionForTesting(Context context, |
812 ChildProcessCreationParams creationParams) { | 804 ChildProcessCreationParams creationParams) { |
813 Bundle commonParams = new Bundle(); | 805 Bundle commonParams = new Bundle(); |
814 commonParams.putParcelable( | 806 commonParams.putParcelable( |
815 ChildProcessConstants.EXTRA_LINKER_PARAMS, getLinkerParamsForNew
Connection()); | 807 ChildProcessConstants.EXTRA_LINKER_PARAMS, getLinkerParamsForNew
Connection()); |
816 return allocateConnection( | 808 return allocateConnection( |
817 new SpawnData(false /* forWarmUp */, context, null /* commandLin
e */, | 809 new SpawnData(false /* forWarmUp */, context, null /* commandLin
e */, |
818 0 /* childProcessId */, null /* filesToBeMapped */, | 810 0 /* childProcessId */, null /* filesToBeMapped */, |
819 null /* LaunchCallback */, CALLBACK_FOR_RENDERER_PROCESS
, | 811 null /* launchCallback */, null /* childProcessCallback
*/, |
820 true /* inSandbox */, creationParams), | 812 true /* inSandbox */, false /* alwaysInForeground */, cr
eationParams), |
821 commonParams, false); | 813 commonParams); |
822 } | 814 } |
823 | 815 |
824 /** | 816 /** |
825 * Queue up a spawn requests for testing. | 817 * Queue up a spawn requests for testing. |
826 */ | 818 */ |
827 @VisibleForTesting | 819 @VisibleForTesting |
828 static void enqueuePendingSpawnForTesting(Context context, String[] commandL
ine, | 820 static void enqueuePendingSpawnForTesting(Context context, String[] commandL
ine, |
829 ChildProcessCreationParams creationParams, boolean inSandbox) { | 821 ChildProcessCreationParams creationParams, boolean inSandbox) { |
830 String packageName = creationParams != null ? creationParams.getPackageN
ame() | 822 String packageName = creationParams != null ? creationParams.getPackageN
ame() |
831 : context.getPackageName(); | 823 : context.getPackageName(); |
832 ChildConnectionAllocator allocator = | 824 ChildConnectionAllocator allocator = |
833 getAllocatorForTesting(context, packageName, inSandbox); | 825 getAllocatorForTesting(context, packageName, inSandbox); |
834 allocator.enqueuePendingQueueForTesting(new SpawnData(false /* forWarmUp
*/, context, | 826 allocator.enqueuePendingQueueForTesting(new SpawnData(false /* forWarmUp
*/, context, |
835 commandLine, 1, new FileDescriptorInfo[0], null, CALLBACK_FOR_RE
NDERER_PROCESS, | 827 commandLine, 1 /* childProcessId */, new FileDescriptorInfo[0], |
836 true, creationParams)); | 828 null /* launchCallback */, null /* childProcessCallback */, true
/* inSandbox */, |
| 829 false /* alwaysInForeground */, creationParams)); |
837 } | 830 } |
838 | 831 |
839 /** | 832 /** |
840 * @return the number of sandboxed connections of given {@link packageName}
managed by the | 833 * @return the number of sandboxed connections of given {@link packageName}
managed by the |
841 * allocator. | 834 * allocator. |
842 */ | 835 */ |
843 @VisibleForTesting | 836 @VisibleForTesting |
844 static int allocatedSandboxedConnectionsCountForTesting(Context context, Str
ing packageName) { | 837 static int allocatedSandboxedConnectionsCountForTesting(Context context, Str
ing packageName) { |
845 initConnectionAllocatorsIfNecessary(context, true, packageName); | 838 initConnectionAllocatorsIfNecessary(context, true, packageName); |
846 return sSandboxedChildConnectionAllocatorMap.get(packageName) | 839 return sSandboxedChildConnectionAllocatorMap.get(packageName) |
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
885 | 878 |
886 try { | 879 try { |
887 ((ChildProcessConnectionImpl) sServiceMap.get(pid)).crashServiceForT
esting(); | 880 ((ChildProcessConnectionImpl) sServiceMap.get(pid)).crashServiceForT
esting(); |
888 } catch (RemoteException ex) { | 881 } catch (RemoteException ex) { |
889 return false; | 882 return false; |
890 } | 883 } |
891 | 884 |
892 return true; | 885 return true; |
893 } | 886 } |
894 } | 887 } |
OLD | NEW |