ChildProcessLauncher.java revision f2477e01787aa58f445919b809d89e252beef54f
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
5package org.chromium.content.browser;
6
7import android.content.Context;
8import android.util.Log;
9import android.util.SparseIntArray;
10import android.view.Surface;
11
12import org.chromium.base.CalledByNative;
13import org.chromium.base.JNINamespace;
14import org.chromium.base.SysUtils;
15import org.chromium.base.ThreadUtils;
16import org.chromium.content.app.ChildProcessService;
17import org.chromium.content.app.Linker;
18import org.chromium.content.app.LinkerParams;
19import org.chromium.content.app.PrivilegedProcessService;
20import org.chromium.content.app.SandboxedProcessService;
21import org.chromium.content.common.IChildProcessCallback;
22import org.chromium.content.common.IChildProcessService;
23
24import java.util.ArrayList;
25import java.util.Map;
26import java.util.concurrent.ConcurrentHashMap;
27
28/**
29 * This class provides the method to start/stop ChildProcess called by native.
30 */
31@JNINamespace("content")
32public class ChildProcessLauncher {
33    private static final String TAG = "ChildProcessLauncher";
34
35    private static final int CALLBACK_FOR_UNKNOWN_PROCESS = 0;
36    private static final int CALLBACK_FOR_GPU_PROCESS = 1;
37    private static final int CALLBACK_FOR_RENDERER_PROCESS = 2;
38
39    private static final String SWITCH_PROCESS_TYPE = "type";
40    private static final String SWITCH_PPAPI_BROKER_PROCESS = "ppapi-broker";
41    private static final String SWITCH_RENDERER_PROCESS = "renderer";
42    private static final String SWITCH_GPU_PROCESS = "gpu-process";
43
44    // The upper limit on the number of simultaneous sandboxed and privileged child service process
45    // instances supported. Each limit must not exceed total number of SandboxedProcessServiceX
46    // classes and PrivilegedProcessServiceX classes declared in this package and defined as
47    // services in the embedding application's manifest file.
48    // (See {@link ChildProcessService} for more details on defining the services.)
49    /* package */ static final int MAX_REGISTERED_SANDBOXED_SERVICES = 13;
50    /* package */ static final int MAX_REGISTERED_PRIVILEGED_SERVICES = 3;
51
52    private static class ChildConnectionAllocator {
53        // Connections to services. Indices of the array correspond to the service numbers.
54        private final ChildProcessConnection[] mChildProcessConnections;
55
56        // The list of free (not bound) service indices. When looking for a free service, the first
57        // index in that list should be used. When a service is unbound, its index is added to the
58        // end of the list. This is so that we avoid immediately reusing the freed service (see
59        // http://crbug.com/164069): the framework might keep a service process alive when it's been
60        // unbound for a short time. If a new connection to the same service is bound at that point,
61        // the process is reused and bad things happen (mostly static variables are set when we
62        // don't expect them to).
63        // SHOULD BE ACCESSED WITH mConnectionLock.
64        private final ArrayList<Integer> mFreeConnectionIndices;
65        private final Object mConnectionLock = new Object();
66
67        private Class<? extends ChildProcessService> mChildClass;
68        private final boolean mInSandbox;
69
70        public ChildConnectionAllocator(boolean inSandbox) {
71            int numChildServices = inSandbox ?
72                    MAX_REGISTERED_SANDBOXED_SERVICES : MAX_REGISTERED_PRIVILEGED_SERVICES;
73            mChildProcessConnections = new ChildProcessConnection[numChildServices];
74            mFreeConnectionIndices = new ArrayList<Integer>(numChildServices);
75            for (int i = 0; i < numChildServices; i++) {
76                mFreeConnectionIndices.add(i);
77            }
78            setServiceClass(inSandbox ?
79                    SandboxedProcessService.class : PrivilegedProcessService.class);
80            mInSandbox = inSandbox;
81        }
82
83        public void setServiceClass(Class<? extends ChildProcessService> childClass) {
84            mChildClass = childClass;
85        }
86
87        public ChildProcessConnection allocate(
88                Context context, ChildProcessConnection.DeathCallback deathCallback,
89                LinkerParams linkerParams) {
90            synchronized (mConnectionLock) {
91                if (mFreeConnectionIndices.isEmpty()) {
92                    Log.w(TAG, "Ran out of service.");
93                    return null;
94                }
95                int slot = mFreeConnectionIndices.remove(0);
96                assert mChildProcessConnections[slot] == null;
97                mChildProcessConnections[slot] = new ChildProcessConnection(context, slot,
98                        mInSandbox, deathCallback, mChildClass, linkerParams);
99                return mChildProcessConnections[slot];
100            }
101        }
102
103        public void free(ChildProcessConnection connection) {
104            synchronized (mConnectionLock) {
105                int slot = connection.getServiceNumber();
106                if (mChildProcessConnections[slot] != connection) {
107                    int occupier = mChildProcessConnections[slot] == null ?
108                            -1 : mChildProcessConnections[slot].getServiceNumber();
109                    Log.e(TAG, "Unable to find connection to free in slot: " + slot +
110                            " already occupied by service: " + occupier);
111                    assert false;
112                } else {
113                    mChildProcessConnections[slot] = null;
114                    assert !mFreeConnectionIndices.contains(slot);
115                    mFreeConnectionIndices.add(slot);
116                }
117            }
118        }
119    }
120
121    // Service class for child process. As the default value it uses SandboxedProcessService0 and
122    // PrivilegedProcessService0.
123    private static final ChildConnectionAllocator sSandboxedChildConnectionAllocator =
124            new ChildConnectionAllocator(true);
125    private static final ChildConnectionAllocator sPrivilegedChildConnectionAllocator =
126            new ChildConnectionAllocator(false);
127
128    private static boolean sConnectionAllocated = false;
129
130    /**
131     * Sets service class for sandboxed service and privileged service.
132     */
133    public static void setChildProcessClass(
134            Class<? extends SandboxedProcessService> sandboxedServiceClass,
135            Class<? extends PrivilegedProcessService> privilegedServiceClass) {
136        // We should guarantee this is called before allocating connection.
137        assert !sConnectionAllocated;
138        sSandboxedChildConnectionAllocator.setServiceClass(sandboxedServiceClass);
139        sPrivilegedChildConnectionAllocator.setServiceClass(privilegedServiceClass);
140    }
141
142    private static ChildConnectionAllocator getConnectionAllocator(boolean inSandbox) {
143        return inSandbox ?
144                sSandboxedChildConnectionAllocator : sPrivilegedChildConnectionAllocator;
145    }
146
147    private static ChildProcessConnection allocateConnection(Context context,
148            boolean inSandbox, LinkerParams linkerParams) {
149        ChildProcessConnection.DeathCallback deathCallback =
150            new ChildProcessConnection.DeathCallback() {
151                @Override
152                public void onChildProcessDied(int pid) {
153                    stop(pid);
154                }
155            };
156        sConnectionAllocated = true;
157        return getConnectionAllocator(inSandbox).allocate(context, deathCallback, linkerParams);
158    }
159
160    private static boolean sLinkerInitialized = false;
161    private static long sLinkerLoadAddress = 0;
162
163    private static LinkerParams getLinkerParamsForNewConnection() {
164        if (!sLinkerInitialized) {
165            if (Linker.isUsed()) {
166                sLinkerLoadAddress = Linker.getBaseLoadAddress();
167                if (sLinkerLoadAddress == 0) {
168                    Log.i(TAG, "Shared RELRO support disabled!");
169                }
170            }
171            sLinkerInitialized = true;
172        }
173
174        if (sLinkerLoadAddress == 0)
175            return null;
176
177        // Always wait for the shared RELROs in service processes.
178        final boolean waitForSharedRelros = true;
179        return new LinkerParams(sLinkerLoadAddress,
180                                waitForSharedRelros,
181                                Linker.getTestRunnerClassName());
182    }
183
184    private static ChildProcessConnection allocateBoundConnection(Context context,
185            String[] commandLine, boolean inSandbox) {
186        LinkerParams linkerParams = getLinkerParamsForNewConnection();
187        ChildProcessConnection connection = allocateConnection(context, inSandbox, linkerParams);
188        if (connection != null) {
189            connection.start(commandLine);
190        }
191        return connection;
192    }
193
194    private static void freeConnection(ChildProcessConnection connection) {
195        if (connection == null) {
196            return;
197        }
198        getConnectionAllocator(connection.isInSandbox()).free(connection);
199        return;
200    }
201
202    // Represents an invalid process handle; same as base/process/process.h kNullProcessHandle.
203    private static final int NULL_PROCESS_HANDLE = 0;
204
205    // Map from pid to ChildService connection.
206    private static Map<Integer, ChildProcessConnection> sServiceMap =
207            new ConcurrentHashMap<Integer, ChildProcessConnection>();
208
209    // A pre-allocated and pre-bound connection ready for connection setup, or null.
210    private static ChildProcessConnection sSpareSandboxedConnection = null;
211
212    /**
213     * Manages oom bindings used to bound child services. "Oom binding" is a binding that raises the
214     * process oom priority so that it shouldn't be killed by the OS out-of-memory killer under
215     * normal conditions (it can still be killed under drastic memory pressure).
216     *
217     * This class serves a proxy between external calls that manipulate the bindings and the
218     * connections, allowing to enforce policies such as delayed removal of the bindings.
219     */
220    static class BindingManager {
221        // Delay of 1 second used when removing the initial oom binding of a process.
222        private static final long REMOVE_INITIAL_BINDING_DELAY_MILLIS = 1 * 1000;
223
224        // Delay of 1 second used when removing temporary strong binding of a process (only on
225        // non-low-memory devices).
226        private static final long DETACH_AS_ACTIVE_HIGH_END_DELAY_MILLIS = 1 * 1000;
227
228        // Map from pid to the count of oom bindings bound for the service. Should be accessed with
229        // mCountLock.
230        private final SparseIntArray mOomBindingCount = new SparseIntArray();
231
232        // Pid of the renderer that was most recently oom bound. This is used on low-memory devices
233        // to drop oom bindings of a process when another one acquires them, making sure that only
234        // one renderer process at a time is oom bound. Should be accessed with mCountLock.
235        private int mLastOomPid = -1;
236
237        // Pid of the renderer that we bound with a strong binding for the background period. Equals
238        // -1 when the embedder is in foreground.
239        private int mBoundForBackgroundPeriodPid = -1;
240
241        // Should be acquired before binding or unbinding the connections and modifying state
242        // variables: mOomBindingCount and mLastOomPid.
243        private final Object mCountLock = new Object();
244
245        /**
246         * Registers an oom binding bound for a child process. Should be called with mCountLock.
247         * @param pid handle of the process.
248         */
249        private void incrementOomCount(int pid) {
250            mOomBindingCount.put(pid, mOomBindingCount.get(pid) + 1);
251            mLastOomPid = pid;
252        }
253
254        /**
255         * Registers an oom binding unbound for a child process. Should be called with mCountLock.
256         * @param pid handle of the process.
257         */
258        private void decrementOomCount(int pid) {
259            int count = mOomBindingCount.get(pid, -1);
260            assert count > 0;
261            count--;
262            if (count > 0) {
263                mOomBindingCount.put(pid, count);
264            } else {
265                mOomBindingCount.delete(pid);
266            }
267        }
268
269        /**
270         * Drops all oom bindings for the given renderer.
271         * @param pid handle of the process.
272         */
273        private void dropOomBindings(int pid) {
274            ChildProcessConnection connection = sServiceMap.get(pid);
275            if (connection == null) {
276                logPidWarning(pid, "Tried to drop oom bindings for a non-existent connection");
277                return;
278            }
279            synchronized (mCountLock) {
280                connection.dropOomBindings();
281                mOomBindingCount.delete(pid);
282            }
283        }
284
285        /**
286         * Registers a freshly started child process. On low-memory devices this will also drop the
287         * oom bindings of the last process that was oom-bound. We can do that, because every time a
288         * connection is created on the low-end, it is used in foreground (no prerendering, no
289         * loading of tabs opened in background).
290         * @param pid handle of the process.
291         */
292        void addNewConnection(int pid) {
293            synchronized (mCountLock) {
294                if (SysUtils.isLowEndDevice() && mLastOomPid >= 0) {
295                    dropOomBindings(mLastOomPid);
296                }
297                // This will reset the previous entry for the pid in the unlikely event of the OS
298                // reusing renderer pids.
299                mOomBindingCount.put(pid, 0);
300                // Every new connection is bound with initial oom binding.
301                incrementOomCount(pid);
302            }
303        }
304
305        /**
306         * Remove the initial binding of the child process. Child processes are bound with initial
307         * binding to protect them from getting killed before they are put to use. This method
308         * allows to remove the binding once it is no longer needed. The binding is removed after a
309         * fixed delay period so that the renderer will not be killed immediately after the call.
310         */
311        void removeInitialBinding(final int pid) {
312            final ChildProcessConnection connection = sServiceMap.get(pid);
313            if (connection == null) {
314                logPidWarning(pid, "Tried to remove a binding for a non-existent connection");
315                return;
316            }
317            if (!connection.isInitialBindingBound()) return;
318            ThreadUtils.postOnUiThreadDelayed(new Runnable() {
319                @Override
320                public void run() {
321                    synchronized (mCountLock) {
322                        if (connection.isInitialBindingBound()) {
323                            decrementOomCount(pid);
324                            connection.removeInitialBinding();
325                        }
326                    }
327                }
328            }, REMOVE_INITIAL_BINDING_DELAY_MILLIS);
329        }
330
331        /**
332         * Bind a child process as a high priority process so that it has the same priority as the
333         * main process. This can be used for the foreground renderer process to distinguish it from
334         * the background renderer process.
335         * @param pid The process handle of the service connection.
336         */
337        void bindAsHighPriority(final int pid) {
338            ChildProcessConnection connection = sServiceMap.get(pid);
339            if (connection == null) {
340                logPidWarning(pid, "Tried to bind a non-existent connection");
341                return;
342            }
343            synchronized (mCountLock) {
344                connection.attachAsActive();
345                incrementOomCount(pid);
346            }
347        }
348
349        /**
350         * Unbind a high priority process which was previous bound with bindAsHighPriority.
351         * @param pid The process handle of the service.
352         */
353        void unbindAsHighPriority(final int pid) {
354            final ChildProcessConnection connection = sServiceMap.get(pid);
355            if (connection == null) {
356                logPidWarning(pid, "Tried to unbind non-existent connection");
357                return;
358            }
359            if (!connection.isStrongBindingBound()) return;
360
361            // This runnable performs the actual unbinding. It will be executed synchronously when
362            // on low-end devices and posted with a delay otherwise.
363            Runnable doUnbind = new Runnable() {
364                @Override
365                public void run() {
366                    synchronized (mCountLock) {
367                        if (connection.isStrongBindingBound()) {
368                            decrementOomCount(pid);
369                            connection.detachAsActive();
370                        }
371                    }
372                }
373            };
374
375            if (SysUtils.isLowEndDevice()) {
376                doUnbind.run();
377            } else {
378                ThreadUtils.postOnUiThreadDelayed(doUnbind, DETACH_AS_ACTIVE_HIGH_END_DELAY_MILLIS);
379            }
380        }
381
382        /**
383         * @return True iff the given service process is protected from the out-of-memory killing,
384         * or it was protected when it died (either crashed or was closed). This can be used to
385         * decide if a disconnection of a renderer was a crash or a probable out-of-memory kill. In
386         * the unlikely event of the OS reusing renderer pid, the call will refer to the most recent
387         * renderer of the given pid. The binding count is being reset in addNewConnection().
388         */
389        boolean isOomProtected(int pid) {
390            synchronized (mCountLock) {
391                return mOomBindingCount.get(pid) > 0;
392            }
393        }
394
395        /**
396         * Called when the embedding application is sent to background. We want to maintain a strong
397         * binding on the most recently used renderer while the embedder is in background, to
398         * indicate the relative importance of the renderer to system oom killer.
399         *
400         * The embedder needs to ensure that:
401         *  - every onBroughtToForeground() is followed by onSentToBackground()
402         *  - pairs of consecutive onBroughtToForeground() / onSentToBackground() calls do not
403         *    overlap
404         */
405        void onSentToBackground() {
406            assert mBoundForBackgroundPeriodPid == -1;
407            // mLastOomPid can be -1 at this point as the embedding application could be used in
408            // foreground without spawning any renderers.
409            if (mLastOomPid >= 0) {
410                bindAsHighPriority(mLastOomPid);
411                mBoundForBackgroundPeriodPid = mLastOomPid;
412            }
413        }
414
415        /**
416         * Called when the embedding application is brought to foreground. This will drop the strong
417         * binding kept on the main renderer during the background period, so the embedder should
418         * make sure that this is called after the regular strong binding is attached for the
419         * foreground session.
420         */
421        void onBroughtToForeground() {
422            if (mBoundForBackgroundPeriodPid >= 0) {
423                unbindAsHighPriority(mBoundForBackgroundPeriodPid);
424                mBoundForBackgroundPeriodPid = -1;
425            }
426        }
427    }
428
429    private static BindingManager sBindingManager = new BindingManager();
430
431    static BindingManager getBindingManager() {
432        return sBindingManager;
433    }
434
435    @CalledByNative
436    private static boolean isOomProtected(int pid) {
437        return sBindingManager.isOomProtected(pid);
438    }
439
440    /**
441     * Called when the embedding application is sent to background.
442     */
443    public static void onSentToBackground() {
444        sBindingManager.onSentToBackground();
445    }
446
447    /**
448     * Called when the embedding application is brought to foreground.
449     */
450    public static void onBroughtToForeground() {
451        sBindingManager.onBroughtToForeground();
452    }
453
454    /**
455     * Returns the child process service interface for the given pid. This may be called on
456     * any thread, but the caller must assume that the service can disconnect at any time. All
457     * service calls should catch and handle android.os.RemoteException.
458     *
459     * @param pid The pid (process handle) of the service obtained from {@link #start}.
460     * @return The IChildProcessService or null if the service no longer exists.
461     */
462    public static IChildProcessService getChildService(int pid) {
463        ChildProcessConnection connection = sServiceMap.get(pid);
464        if (connection != null) {
465            return connection.getService();
466        }
467        return null;
468    }
469
470    /**
471     * Should be called early in startup so the work needed to spawn the child process can be done
472     * in parallel to other startup work. Must not be called on the UI thread. Spare connection is
473     * created in sandboxed child process.
474     * @param context the application context used for the connection.
475     */
476    public static void warmUp(Context context) {
477        synchronized (ChildProcessLauncher.class) {
478            assert !ThreadUtils.runningOnUiThread();
479            if (sSpareSandboxedConnection == null) {
480                sSpareSandboxedConnection = allocateBoundConnection(context, null, true);
481            }
482        }
483    }
484
485    private static String getSwitchValue(final String[] commandLine, String switchKey) {
486        if (commandLine == null || switchKey == null) {
487            return null;
488        }
489        // This format should be matched with the one defined in command_line.h.
490        final String switchKeyPrefix = "--" + switchKey + "=";
491        for (String command : commandLine) {
492            if (command != null && command.startsWith(switchKeyPrefix)) {
493                return command.substring(switchKeyPrefix.length());
494            }
495        }
496        return null;
497    }
498
499    /**
500     * Spawns and connects to a child process. May be called on any thread. It will not block, but
501     * will instead callback to {@link #nativeOnChildProcessStarted} when the connection is
502     * established. Note this callback will not necessarily be from the same thread (currently it
503     * always comes from the main thread).
504     *
505     * @param context Context used to obtain the application context.
506     * @param commandLine The child process command line argv.
507     * @param fileIds The ID that should be used when mapping files in the created process.
508     * @param fileFds The file descriptors that should be mapped in the created process.
509     * @param fileAutoClose Whether the file descriptors should be closed once they were passed to
510     * the created process.
511     * @param clientContext Arbitrary parameter used by the client to distinguish this connection.
512     */
513    @CalledByNative
514    static void start(
515            Context context,
516            final String[] commandLine,
517            int[] fileIds,
518            int[] fileFds,
519            boolean[] fileAutoClose,
520            final long clientContext) {
521        assert fileIds.length == fileFds.length && fileFds.length == fileAutoClose.length;
522        FileDescriptorInfo[] filesToBeMapped = new FileDescriptorInfo[fileFds.length];
523        for (int i = 0; i < fileFds.length; i++) {
524            filesToBeMapped[i] =
525                    new FileDescriptorInfo(fileIds[i], fileFds[i], fileAutoClose[i]);
526        }
527        assert clientContext != 0;
528
529        int callbackType = CALLBACK_FOR_UNKNOWN_PROCESS;
530        boolean inSandbox = true;
531        String processType = getSwitchValue(commandLine, SWITCH_PROCESS_TYPE);
532        if (SWITCH_RENDERER_PROCESS.equals(processType)) {
533            callbackType = CALLBACK_FOR_RENDERER_PROCESS;
534        } else if (SWITCH_GPU_PROCESS.equals(processType)) {
535            callbackType = CALLBACK_FOR_GPU_PROCESS;
536        } else if (SWITCH_PPAPI_BROKER_PROCESS.equals(processType)) {
537            inSandbox = false;
538        }
539
540        ChildProcessConnection allocatedConnection = null;
541        synchronized (ChildProcessLauncher.class) {
542            if (inSandbox) {
543                allocatedConnection = sSpareSandboxedConnection;
544                sSpareSandboxedConnection = null;
545            }
546        }
547        if (allocatedConnection == null) {
548            allocatedConnection = allocateBoundConnection(context, commandLine, inSandbox);
549            if (allocatedConnection == null) {
550                // Notify the native code so it can free the heap allocated callback.
551                nativeOnChildProcessStarted(clientContext, 0);
552                return;
553            }
554        }
555        final ChildProcessConnection connection = allocatedConnection;
556        Log.d(TAG, "Setting up connection to process: slot=" + connection.getServiceNumber());
557
558        ChildProcessConnection.ConnectionCallback connectionCallback =
559                new ChildProcessConnection.ConnectionCallback() {
560            @Override
561            public void onConnected(int pid) {
562                Log.d(TAG, "on connect callback, pid=" + pid + " context=" + clientContext);
563                if (pid != NULL_PROCESS_HANDLE) {
564                    sBindingManager.addNewConnection(pid);
565                    sServiceMap.put(pid, connection);
566                } else {
567                    freeConnection(connection);
568                }
569                nativeOnChildProcessStarted(clientContext, pid);
570            }
571        };
572
573        // TODO(sievers): Revisit this as it doesn't correctly handle the utility process
574        // assert callbackType != CALLBACK_FOR_UNKNOWN_PROCESS;
575
576        connection.setupConnection(commandLine,
577                                   filesToBeMapped,
578                                   createCallback(callbackType),
579                                   connectionCallback,
580                                   Linker.getSharedRelros());
581    }
582
583    /**
584     * Terminates a child process. This may be called from any thread.
585     *
586     * @param pid The pid (process handle) of the service connection obtained from {@link #start}.
587     */
588    @CalledByNative
589    static void stop(int pid) {
590        Log.d(TAG, "stopping child connection: pid=" + pid);
591        ChildProcessConnection connection = sServiceMap.remove(pid);
592        if (connection == null) {
593            logPidWarning(pid, "Tried to stop non-existent connection");
594            return;
595        }
596        connection.stop();
597        freeConnection(connection);
598    }
599
600    /**
601     * This implementation is used to receive callbacks from the remote service.
602     */
603    private static IChildProcessCallback createCallback(final int callbackType) {
604        return new IChildProcessCallback.Stub() {
605            /**
606             * This is called by the remote service regularly to tell us about new values. Note that
607             * IPC calls are dispatched through a thread pool running in each process, so the code
608             * executing here will NOT be running in our main thread -- so, to update the UI, we
609             * need to use a Handler.
610             */
611            @Override
612            public void establishSurfacePeer(
613                    int pid, Surface surface, int primaryID, int secondaryID) {
614                // Do not allow a malicious renderer to connect to a producer. This is only used
615                // from stream textures managed by the GPU process.
616                if (callbackType != CALLBACK_FOR_GPU_PROCESS) {
617                    Log.e(TAG, "Illegal callback for non-GPU process.");
618                    return;
619                }
620
621                nativeEstablishSurfacePeer(pid, surface, primaryID, secondaryID);
622            }
623
624            @Override
625            public Surface getViewSurface(int surfaceId) {
626                // Do not allow a malicious renderer to get to our view surface.
627                if (callbackType != CALLBACK_FOR_GPU_PROCESS) {
628                    Log.e(TAG, "Illegal callback for non-GPU process.");
629                    return null;
630                }
631
632                return nativeGetViewSurface(surfaceId);
633            }
634        };
635    }
636
637    private static void logPidWarning(int pid, String message) {
638        // This class is effectively a no-op in single process mode, so don't log warnings there.
639        if (pid > 0 && !nativeIsSingleProcess()) {
640            Log.w(TAG, message + ", pid=" + pid);
641        }
642    }
643
644    private static native void nativeOnChildProcessStarted(long clientContext, int pid);
645    private static native Surface nativeGetViewSurface(int surfaceId);
646    private static native void nativeEstablishSurfacePeer(
647            int pid, Surface surface, int primaryID, int secondaryID);
648    private static native boolean nativeIsSingleProcess();
649}
650