Chromium Code Reviews| 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.app; | 5 package org.chromium.content.app; |
| 6 | 6 |
| 7 import android.app.Service; | 7 import android.app.Service; |
| 8 import android.content.Context; | |
| 9 import android.content.Intent; | 8 import android.content.Intent; |
| 10 import android.graphics.SurfaceTexture; | |
| 11 import android.os.Bundle; | 9 import android.os.Bundle; |
| 12 import android.os.IBinder; | 10 import android.os.IBinder; |
| 13 import android.os.Parcelable; | |
| 14 import android.os.Process; | |
| 15 import android.os.RemoteException; | |
| 16 import android.view.Surface; | |
| 17 | 11 |
| 18 import org.chromium.base.BaseSwitches; | |
| 19 import org.chromium.base.CommandLine; | |
| 20 import org.chromium.base.ContextUtils; | |
| 21 import org.chromium.base.Log; | |
| 22 import org.chromium.base.annotations.CalledByNative; | |
| 23 import org.chromium.base.annotations.JNINamespace; | 12 import org.chromium.base.annotations.JNINamespace; |
| 24 import org.chromium.base.annotations.SuppressFBWarnings; | 13 import org.chromium.base.annotations.SuppressFBWarnings; |
| 25 import org.chromium.base.library_loader.LibraryLoader; | |
| 26 import org.chromium.base.library_loader.Linker; | |
| 27 import org.chromium.base.library_loader.ProcessInitException; | |
| 28 import org.chromium.content.browser.ChildProcessConstants; | |
| 29 import org.chromium.content.browser.ChildProcessCreationParams; | |
| 30 import org.chromium.content.browser.ChildProcessLauncher; | 14 import org.chromium.content.browser.ChildProcessLauncher; |
| 31 import org.chromium.content.browser.FileDescriptorInfo; | |
| 32 import org.chromium.content.common.ContentSwitches; | |
| 33 import org.chromium.content.common.IChildProcessCallback; | |
| 34 import org.chromium.content.common.IChildProcessService; | |
| 35 import org.chromium.content.common.SurfaceWrapper; | |
| 36 | |
| 37 import java.util.concurrent.Semaphore; | |
| 38 import java.util.concurrent.atomic.AtomicReference; | |
| 39 | 15 |
| 40 /** | 16 /** |
| 41 * This is the base class for child services; the [Non]SandboxedProcessService0, 1.. etc | 17 * This is the base class for child services; the [Non]SandboxedProcessService0, 1.. etc |
| 42 * subclasses provide the concrete service entry points, to enable the browser t o connect | 18 * subclasses provide the concrete service entry points, to enable the browser t o connect |
| 43 * to more than one distinct process (i.e. one process per service number, up to limit of N). | 19 * to more than one distinct process (i.e. one process per service number, up to limit of N). |
| 44 * The embedding application must declare these service instances in the applica tion section | 20 * The embedding application must declare these service instances in the applica tion section |
| 45 * of its AndroidManifest.xml, for example with N entries of the form:- | 21 * of its AndroidManifest.xml, for example with N entries of the form:- |
| 46 * <service android:name="org.chromium.content.app.[Non]SandboxedProcessServ iceX" | 22 * <service android:name="org.chromium.content.app.[Non]SandboxedProcessServ iceX" |
| 47 * android:process=":[non]sandboxed_processX" /> | 23 * android:process=":[non]sandboxed_processX" /> |
| 48 * for X in 0...N-1 (where N is {@link ChildProcessLauncher#MAX_REGISTERED_SERVI CES}) | 24 * for X in 0...N-1 (where N is {@link ChildProcessLauncher#MAX_REGISTERED_SERVI CES}) |
| 49 */ | 25 */ |
| 50 @JNINamespace("content") | 26 @JNINamespace("content") |
|
pkotwicz
2016/06/10 21:29:47
Is the JNINamespace annotation needed?
Xi Han
2016/06/13 20:04:11
We have to keep the namespace, since all of its su
| |
| 51 @SuppressWarnings("SynchronizeOnNonFinalField") | 27 @SuppressWarnings("SynchronizeOnNonFinalField") |
|
pkotwicz
2016/06/10 21:29:47
Is this annotation needed?
Xi Han
2016/06/13 20:04:11
Removed.
| |
| 52 public class ChildProcessService extends Service { | 28 public class ChildProcessService extends Service { |
| 53 private static final String MAIN_THREAD_NAME = "ChildProcessMain"; | |
| 54 private static final String TAG = "ChildProcessService"; | 29 private static final String TAG = "ChildProcessService"; |
|
pkotwicz
2016/06/10 21:29:47
Nit: Remove TAG because it is unused
Xi Han
2016/06/13 20:04:11
Done.
| |
| 55 protected static final FileDescriptorInfo[] EMPTY_FILE_DESCRIPTOR_INFO = {}; | |
| 56 private IChildProcessCallback mCallback; | |
| 57 | 30 |
| 58 // This is the native "Main" thread for the renderer / utility process. | 31 private final ChildProcessServiceImpl mChildProcessServiceImpl = |
| 59 private Thread mMainThread; | 32 new ChildProcessServiceImpl(); |
| 60 // Parameters received via IPC, only accessed while holding the mMainThread monitor. | |
| 61 private String[] mCommandLineParams; | |
| 62 private int mCpuCount; | |
| 63 private long mCpuFeatures; | |
| 64 // File descriptors that should be registered natively. | |
| 65 private FileDescriptorInfo[] mFdInfos; | |
| 66 // Linker-specific parameters for this child process service. | |
| 67 private ChromiumLinkerParams mLinkerParams; | |
| 68 // Child library process type. | |
| 69 private int mLibraryProcessType; | |
| 70 | |
| 71 private static AtomicReference<Context> sContext = new AtomicReference<Conte xt>(null); | |
| 72 private boolean mLibraryInitialized = false; | |
| 73 // Becomes true once the service is bound. Access must synchronize around mM ainThread. | |
| 74 private boolean mIsBound = false; | |
| 75 | |
| 76 private final Semaphore mActivitySemaphore = new Semaphore(1); | |
| 77 | |
| 78 // Return a Linker instance. If testing, the Linker needs special setup. | |
| 79 private Linker getLinker() { | |
| 80 if (Linker.areTestsEnabled()) { | |
| 81 // For testing, set the Linker implementation and the test runner | |
| 82 // class name to match those used by the parent. | |
| 83 assert mLinkerParams != null; | |
| 84 Linker.setupForTesting( | |
| 85 mLinkerParams.mLinkerImplementationForTesting, | |
| 86 mLinkerParams.mTestRunnerClassNameForTesting); | |
| 87 } | |
| 88 return Linker.getInstance(); | |
| 89 } | |
| 90 | |
| 91 // Binder object used by clients for this service. | |
| 92 private final IChildProcessService.Stub mBinder = new IChildProcessService.S tub() { | |
| 93 // NOTE: Implement any IChildProcessService methods here. | |
| 94 @Override | |
| 95 public int setupConnection(Bundle args, IChildProcessCallback callback) { | |
| 96 mCallback = callback; | |
| 97 getServiceInfo(args); | |
| 98 return Process.myPid(); | |
| 99 } | |
| 100 | |
| 101 @Override | |
| 102 public void crashIntentionallyForTesting() { | |
| 103 Process.killProcess(Process.myPid()); | |
| 104 } | |
| 105 }; | |
| 106 | |
| 107 /* package */ static Context getContext() { | |
| 108 return sContext.get(); | |
| 109 } | |
| 110 | 33 |
| 111 @Override | 34 @Override |
| 112 public void onCreate() { | 35 public void onCreate() { |
| 113 Log.i(TAG, "Creating new ChildProcessService pid=%d", Process.myPid()); | |
| 114 if (sContext.get() != null) { | |
| 115 throw new RuntimeException("Illegal child process reuse."); | |
| 116 } | |
| 117 sContext.set(this); | |
| 118 super.onCreate(); | 36 super.onCreate(); |
| 119 | 37 mChildProcessServiceImpl.create(getApplicationContext(), |
| 120 ContextUtils.initApplicationContext(getApplicationContext()); | 38 getApplicationContext(), getClassLoader()); |
| 121 | |
| 122 mMainThread = new Thread(new Runnable() { | |
| 123 @Override | |
| 124 @SuppressFBWarnings("DM_EXIT") | |
| 125 public void run() { | |
| 126 try { | |
| 127 // CommandLine must be initialized before everything else. | |
| 128 synchronized (mMainThread) { | |
| 129 while (mCommandLineParams == null) { | |
| 130 mMainThread.wait(); | |
| 131 } | |
| 132 } | |
| 133 CommandLine.init(mCommandLineParams); | |
| 134 | |
| 135 Linker linker = null; | |
| 136 boolean requestedSharedRelro = false; | |
| 137 if (Linker.isUsed()) { | |
| 138 synchronized (mMainThread) { | |
| 139 while (!mIsBound) { | |
| 140 mMainThread.wait(); | |
| 141 } | |
| 142 } | |
| 143 linker = getLinker(); | |
| 144 if (mLinkerParams.mWaitForSharedRelro) { | |
| 145 requestedSharedRelro = true; | |
| 146 linker.initServiceProcess(mLinkerParams.mBaseLoadAdd ress); | |
| 147 } else { | |
| 148 linker.disableSharedRelros(); | |
| 149 } | |
| 150 } | |
| 151 boolean isLoaded = false; | |
| 152 if (CommandLine.getInstance().hasSwitch( | |
| 153 BaseSwitches.RENDERER_WAIT_FOR_JAVA_DEBUGGER)) { | |
| 154 android.os.Debug.waitForDebugger(); | |
| 155 } | |
| 156 | |
| 157 boolean loadAtFixedAddressFailed = false; | |
| 158 try { | |
| 159 LibraryLoader.get(mLibraryProcessType).loadNow(getApplic ationContext()); | |
| 160 isLoaded = true; | |
| 161 } catch (ProcessInitException e) { | |
| 162 if (requestedSharedRelro) { | |
| 163 Log.w(TAG, "Failed to load native library with share d RELRO, " | |
| 164 + "retrying without"); | |
| 165 loadAtFixedAddressFailed = true; | |
| 166 } else { | |
| 167 Log.e(TAG, "Failed to load native library", e); | |
| 168 } | |
| 169 } | |
| 170 if (!isLoaded && requestedSharedRelro) { | |
| 171 linker.disableSharedRelros(); | |
| 172 try { | |
| 173 LibraryLoader.get(mLibraryProcessType).loadNow(getAp plicationContext()); | |
| 174 isLoaded = true; | |
| 175 } catch (ProcessInitException e) { | |
| 176 Log.e(TAG, "Failed to load native library on retry", e); | |
| 177 } | |
| 178 } | |
| 179 if (!isLoaded) { | |
| 180 System.exit(-1); | |
| 181 } | |
| 182 LibraryLoader.get(mLibraryProcessType) | |
| 183 .registerRendererProcessHistogram(requestedSharedRel ro, | |
| 184 loadAtFixedAddressFailed); | |
| 185 LibraryLoader.get(mLibraryProcessType).initialize(); | |
| 186 synchronized (mMainThread) { | |
| 187 mLibraryInitialized = true; | |
| 188 mMainThread.notifyAll(); | |
| 189 while (mFdInfos == null) { | |
| 190 mMainThread.wait(); | |
| 191 } | |
| 192 } | |
| 193 for (FileDescriptorInfo fdInfo : mFdInfos) { | |
| 194 nativeRegisterGlobalFileDescriptor( | |
| 195 fdInfo.mId, fdInfo.mFd.detachFd(), fdInfo.mOffse t, fdInfo.mSize); | |
| 196 } | |
| 197 nativeInitChildProcess(ChildProcessService.this, mCpuCount, mCpuFeatures); | |
| 198 if (mActivitySemaphore.tryAcquire()) { | |
| 199 ContentMain.start(); | |
| 200 nativeExitChildProcess(); | |
| 201 } | |
| 202 } catch (InterruptedException e) { | |
| 203 Log.w(TAG, "%s startup failed: %s", MAIN_THREAD_NAME, e); | |
| 204 } catch (ProcessInitException e) { | |
| 205 Log.w(TAG, "%s startup failed: %s", MAIN_THREAD_NAME, e); | |
| 206 } | |
| 207 } | |
| 208 }, MAIN_THREAD_NAME); | |
| 209 mMainThread.start(); | |
| 210 } | 39 } |
| 211 | 40 |
| 212 @Override | 41 @Override |
| 213 @SuppressFBWarnings("DM_EXIT") | 42 @SuppressFBWarnings("DM_EXIT") |
| 214 public void onDestroy() { | 43 public void onDestroy() { |
| 215 Log.i(TAG, "Destroying ChildProcessService pid=%d", Process.myPid()); | |
| 216 super.onDestroy(); | 44 super.onDestroy(); |
| 217 if (mActivitySemaphore.tryAcquire()) { | 45 mChildProcessServiceImpl.destroy(); |
| 218 // TODO(crbug.com/457406): This is a bit hacky, but there is no know n better solution | |
| 219 // as this service will get reused (at least if not sandboxed). | |
| 220 // In fact, we might really want to always exit() from onDestroy(), not just from | |
| 221 // the early return here. | |
| 222 System.exit(0); | |
| 223 return; | |
| 224 } | |
| 225 synchronized (mMainThread) { | |
| 226 try { | |
| 227 while (!mLibraryInitialized) { | |
| 228 // Avoid a potential race in calling through to native code before the library | |
| 229 // has loaded. | |
| 230 mMainThread.wait(); | |
| 231 } | |
| 232 } catch (InterruptedException e) { | |
| 233 // Ignore | |
| 234 } | |
| 235 } | |
| 236 // Try to shutdown the MainThread gracefully, but it might not | |
| 237 // have chance to exit normally. | |
| 238 nativeShutdownMainThread(); | |
| 239 } | 46 } |
| 240 | 47 |
| 241 @Override | 48 @Override |
| 242 public IBinder onBind(Intent intent) { | 49 public IBinder onBind(Intent intent) { |
| 243 // We call stopSelf() to request that this service be stopped as soon as the client | 50 // We call stopSelf() to request that this service be stopped as soon as the client |
| 244 // unbinds. Otherwise the system may keep it around and available for a reconnect. The | 51 // unbinds. Otherwise the system may keep it around and available for a reconnect. The |
| 245 // child processes do not currently support reconnect; they must be init ialized from | 52 // child processes do not currently support reconnect; they must be init ialized from |
| 246 // scratch every time. | 53 // scratch every time. |
| 247 stopSelf(); | 54 stopSelf(); |
| 248 initializeParams(intent); | 55 return mChildProcessServiceImpl.bind(intent); |
| 249 return mBinder; | |
| 250 } | 56 } |
| 251 | 57 |
| 252 /** | 58 /** |
| 253 * Helper method to initialize the params from intent. | 59 * Helper method to initialize the params from intent. |
| 254 * @param intent Intent to launch the service. | 60 * @param intent Intent to launch the service. |
| 255 */ | 61 */ |
|
pkotwicz
2016/06/10 21:29:47
It feels weird to have initializeParams() and getS
Xi Han
2016/06/13 20:04:11
Done.
| |
| 256 protected void initializeParams(Intent intent) { | 62 protected void initializeParams(Intent intent) { |
| 257 synchronized (mMainThread) { | 63 mChildProcessServiceImpl.initializeParams(intent); |
| 258 mCommandLineParams = | |
| 259 intent.getStringArrayExtra(ChildProcessConstants.EXTRA_COMMA ND_LINE); | |
| 260 // mLinkerParams is never used if Linker.isUsed() returns false. | |
| 261 // See onCreate(). | |
| 262 mLinkerParams = new ChromiumLinkerParams(intent); | |
| 263 mLibraryProcessType = ChildProcessCreationParams.getLibraryProcessTy pe(intent); | |
| 264 mIsBound = true; | |
| 265 mMainThread.notifyAll(); | |
| 266 } | |
| 267 } | 64 } |
| 268 | 65 |
| 269 /** | 66 /** |
| 270 * Helper method to get the information about the service from a given bundl e. | 67 * Helper method to get the information about the service from a given bundl e. |
| 271 * @param bundle Bundle that contains the information to start the service. | 68 * @param bundle Bundle that contains the information to start the service. |
| 272 */ | 69 */ |
| 273 void getServiceInfo(Bundle bundle) { | 70 void getServiceInfo(Bundle bundle) { |
| 274 // Required to unparcel FileDescriptorInfo. | 71 mChildProcessServiceImpl.getServiceInfo(bundle); |
| 275 bundle.setClassLoader(getClassLoader()); | |
| 276 synchronized (mMainThread) { | |
| 277 // Allow the command line to be set via bind() intent or setupConnec tion, but | |
| 278 // the FD can only be transferred here. | |
| 279 if (mCommandLineParams == null) { | |
| 280 mCommandLineParams = | |
| 281 bundle.getStringArray(ChildProcessConstants.EXTRA_COMMAN D_LINE); | |
| 282 } | |
| 283 // We must have received the command line by now | |
| 284 assert mCommandLineParams != null; | |
| 285 mCpuCount = bundle.getInt(ChildProcessConstants.EXTRA_CPU_COUNT); | |
| 286 mCpuFeatures = bundle.getLong(ChildProcessConstants.EXTRA_CPU_FEATUR ES); | |
| 287 assert mCpuCount > 0; | |
| 288 Parcelable[] fdInfosAsParcelable = | |
| 289 bundle.getParcelableArray(ChildProcessConstants.EXTRA_FILES) ; | |
| 290 if (fdInfosAsParcelable != null) { | |
| 291 // For why this arraycopy is necessary: | |
| 292 // http://stackoverflow.com/questions/8745893/i-dont-get-why-thi s-classcastexception-occurs | |
| 293 mFdInfos = new FileDescriptorInfo[fdInfosAsParcelable.length]; | |
| 294 System.arraycopy(fdInfosAsParcelable, 0, mFdInfos, 0, fdInfosAsP arcelable.length); | |
| 295 } else { | |
| 296 String processType = ContentSwitches.getSwitchValue( | |
| 297 mCommandLineParams, ContentSwitches.SWITCH_PROCESS_TYPE) ; | |
| 298 assert ContentSwitches.SWITCH_DOWNLOAD_PROCESS.equals(processTyp e); | |
| 299 mFdInfos = EMPTY_FILE_DESCRIPTOR_INFO; | |
| 300 } | |
| 301 Bundle sharedRelros = bundle.getBundle(Linker.EXTRA_LINKER_SHARED_RE LROS); | |
| 302 if (sharedRelros != null) { | |
| 303 getLinker().useSharedRelros(sharedRelros); | |
| 304 sharedRelros = null; | |
| 305 } | |
| 306 mMainThread.notifyAll(); | |
| 307 } | |
| 308 } | 72 } |
| 309 | |
| 310 /** | |
| 311 * Called from native code to share a surface texture with another child pro cess. | |
| 312 * Through using the callback object the browser is used as a proxy to route the | |
| 313 * call to the correct process. | |
| 314 * | |
| 315 * @param pid Process handle of the child process to share the SurfaceTextur e with. | |
| 316 * @param surfaceObject The Surface or SurfaceTexture to share with the othe r child process. | |
| 317 * @param primaryID Used to route the call to the correct client instance. | |
| 318 * @param secondaryID Used to route the call to the correct client instance. | |
| 319 */ | |
| 320 @SuppressWarnings("unused") | |
| 321 @CalledByNative | |
| 322 private void establishSurfaceTexturePeer( | |
| 323 int pid, Object surfaceObject, int primaryID, int secondaryID) { | |
| 324 if (mCallback == null) { | |
| 325 Log.e(TAG, "No callback interface has been provided."); | |
| 326 return; | |
| 327 } | |
| 328 | |
| 329 Surface surface = null; | |
| 330 boolean needRelease = false; | |
| 331 if (surfaceObject instanceof Surface) { | |
| 332 surface = (Surface) surfaceObject; | |
| 333 } else if (surfaceObject instanceof SurfaceTexture) { | |
| 334 surface = new Surface((SurfaceTexture) surfaceObject); | |
| 335 needRelease = true; | |
| 336 } else { | |
| 337 Log.e(TAG, "Not a valid surfaceObject: %s", surfaceObject); | |
| 338 return; | |
| 339 } | |
| 340 try { | |
| 341 mCallback.establishSurfacePeer(pid, surface, primaryID, secondaryID) ; | |
| 342 } catch (RemoteException e) { | |
| 343 Log.e(TAG, "Unable to call establishSurfaceTexturePeer: %s", e); | |
| 344 return; | |
| 345 } finally { | |
| 346 if (needRelease) { | |
| 347 surface.release(); | |
| 348 } | |
| 349 } | |
| 350 } | |
| 351 | |
| 352 @SuppressWarnings("unused") | |
| 353 @CalledByNative | |
| 354 private Surface getViewSurface(int surfaceId) { | |
| 355 if (mCallback == null) { | |
| 356 Log.e(TAG, "No callback interface has been provided."); | |
| 357 return null; | |
| 358 } | |
| 359 | |
| 360 try { | |
| 361 SurfaceWrapper wrapper = mCallback.getViewSurface(surfaceId); | |
| 362 return wrapper != null ? wrapper.getSurface() : null; | |
| 363 } catch (RemoteException e) { | |
| 364 Log.e(TAG, "Unable to call getViewSurface: %s", e); | |
| 365 return null; | |
| 366 } | |
| 367 } | |
| 368 | |
| 369 @SuppressWarnings("unused") | |
| 370 @CalledByNative | |
| 371 private void createSurfaceTextureSurface( | |
| 372 int surfaceTextureId, int clientId, SurfaceTexture surfaceTexture) { | |
| 373 if (mCallback == null) { | |
| 374 Log.e(TAG, "No callback interface has been provided."); | |
| 375 return; | |
| 376 } | |
| 377 | |
| 378 Surface surface = new Surface(surfaceTexture); | |
| 379 try { | |
| 380 mCallback.registerSurfaceTextureSurface(surfaceTextureId, clientId, surface); | |
| 381 } catch (RemoteException e) { | |
| 382 Log.e(TAG, "Unable to call registerSurfaceTextureSurface: %s", e); | |
| 383 } | |
| 384 surface.release(); | |
| 385 } | |
| 386 | |
| 387 @SuppressWarnings("unused") | |
| 388 @CalledByNative | |
| 389 private void destroySurfaceTextureSurface(int surfaceTextureId, int clientId ) { | |
| 390 if (mCallback == null) { | |
| 391 Log.e(TAG, "No callback interface has been provided."); | |
| 392 return; | |
| 393 } | |
| 394 | |
| 395 try { | |
| 396 mCallback.unregisterSurfaceTextureSurface(surfaceTextureId, clientId ); | |
| 397 } catch (RemoteException e) { | |
| 398 Log.e(TAG, "Unable to call unregisterSurfaceTextureSurface: %s", e); | |
| 399 } | |
| 400 } | |
| 401 | |
| 402 @SuppressWarnings("unused") | |
| 403 @CalledByNative | |
| 404 private Surface getSurfaceTextureSurface(int surfaceTextureId) { | |
| 405 if (mCallback == null) { | |
| 406 Log.e(TAG, "No callback interface has been provided."); | |
| 407 return null; | |
| 408 } | |
| 409 | |
| 410 try { | |
| 411 return mCallback.getSurfaceTextureSurface(surfaceTextureId).getSurfa ce(); | |
| 412 } catch (RemoteException e) { | |
| 413 Log.e(TAG, "Unable to call getSurfaceTextureSurface: %s", e); | |
| 414 return null; | |
| 415 } | |
| 416 } | |
| 417 | |
| 418 /** | |
| 419 * Helper for registering FileDescriptorInfo objects with GlobalFileDescript ors. | |
| 420 * This includes the IPC channel, the crash dump signals and resource relate d | |
| 421 * files. | |
| 422 */ | |
| 423 private static native void nativeRegisterGlobalFileDescriptor( | |
| 424 int id, int fd, long offset, long size); | |
| 425 | |
| 426 /** | |
| 427 * The main entry point for a child process. This should be called from a ne w thread since | |
| 428 * it will not return until the child process exits. See child_process_servi ce.{h,cc} | |
| 429 * | |
| 430 * @param service The current ChildProcessService object. | |
| 431 * renderer. | |
| 432 */ | |
| 433 private static native void nativeInitChildProcess( | |
| 434 ChildProcessService service, int cpuCount, long cpuFeatures); | |
| 435 | |
| 436 /** | |
| 437 * Force the child process to exit. | |
| 438 */ | |
| 439 private static native void nativeExitChildProcess(); | |
| 440 | |
| 441 private native void nativeShutdownMainThread(); | |
| 442 } | 73 } |
| OLD | NEW |