// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.content.browser;

import android.content.Context;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.Surface;

import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.chromium.base.CalledByNative;
import org.chromium.base.JNINamespace;
import org.chromium.base.SysUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.content.app.ChildProcessService;
import org.chromium.content.app.Linker;
import org.chromium.content.app.LinkerParams;
import org.chromium.content.app.PrivilegedProcessService;
import org.chromium.content.app.SandboxedProcessService;
import org.chromium.content.common.IChildProcessCallback;
import org.chromium.content.common.IChildProcessService;

/**
 * This class provides the method to start/stop ChildProcess called by native.
 */
@JNINamespace("content")
public class ChildProcessLauncher {
    private static String TAG = "ChildProcessLauncher";

    private static final int CALLBACK_FOR_UNKNOWN_PROCESS = 0;
    private static final int CALLBACK_FOR_GPU_PROCESS = 1;
    private static final int CALLBACK_FOR_RENDERER_PROCESS = 2;

    private static final String SWITCH_PROCESS_TYPE = "type";
    private static final String SWITCH_PPAPI_BROKER_PROCESS = "ppapi-broker";
    private static final String SWITCH_RENDERER_PROCESS = "renderer";
    private static final String SWITCH_GPU_PROCESS = "gpu-process";

    // The upper limit on the number of simultaneous sandboxed and privileged child service process
    // instances supported. Each limit must not exceed total number of SandboxedProcessServiceX
    // classes and PrivilegedProcessServiceX classes declared in this package and defined as
    // services in the embedding application's manifest file.
    // (See {@link ChildProcessService} for more details on defining the services.)
    /* package */ static final int MAX_REGISTERED_SANDBOXED_SERVICES = 13;
    /* package */ static final int MAX_REGISTERED_PRIVILEGED_SERVICES = 3;

    private static class ChildConnectionAllocator {
        // Connections to services. Indices of the array correspond to the service numbers.
        private ChildProcessConnection[] mChildProcessConnections;

        // The list of free (not bound) service indices. When looking for a free service, the first
        // index in that list should be used. When a service is unbound, its index is added to the
        // end of the list. This is so that we avoid immediately reusing the freed service (see
        // http://crbug.com/164069): the framework might keep a service process alive when it's been
        // unbound for a short time. If a new connection to the same service is bound at that point,
        // the process is reused and bad things happen (mostly static variables are set when we
        // don't expect them to).
        // SHOULD BE ACCESSED WITH mConnectionLock.
        private ArrayList<Integer> mFreeConnectionIndices;
        private final Object mConnectionLock = new Object();

        private Class<? extends ChildProcessService> mChildClass;
        private final boolean mInSandbox;

        public ChildConnectionAllocator(boolean inSandbox) {
            int numChildServices = inSandbox ?
                    MAX_REGISTERED_SANDBOXED_SERVICES : MAX_REGISTERED_PRIVILEGED_SERVICES;
            mChildProcessConnections = new ChildProcessConnection[numChildServices];
            mFreeConnectionIndices = new ArrayList<Integer>(numChildServices);
            for (int i = 0; i < numChildServices; i++) {
                mFreeConnectionIndices.add(i);
            }
            setServiceClass(inSandbox ?
                    SandboxedProcessService.class : PrivilegedProcessService.class);
            mInSandbox = inSandbox;
        }

        public void setServiceClass(Class<? extends ChildProcessService> childClass) {
            mChildClass = childClass;
        }

        public ChildProcessConnection allocate(
                Context context, ChildProcessConnection.DeathCallback deathCallback,
                LinkerParams linkerParams) {
            synchronized(mConnectionLock) {
                if (mFreeConnectionIndices.isEmpty()) {
                    Log.w(TAG, "Ran out of service." );
                    return null;
                }
                int slot = mFreeConnectionIndices.remove(0);
                assert mChildProcessConnections[slot] == null;
                mChildProcessConnections[slot] = new ChildProcessConnection(context, slot,
                        mInSandbox, deathCallback, mChildClass, linkerParams);
                return mChildProcessConnections[slot];
            }
        }

        public void free(ChildProcessConnection connection) {
            synchronized(mConnectionLock) {
                int slot = connection.getServiceNumber();
                if (mChildProcessConnections[slot] != connection) {
                    int occupier = mChildProcessConnections[slot] == null ?
                            -1 : mChildProcessConnections[slot].getServiceNumber();
                    Log.e(TAG, "Unable to find connection to free in slot: " + slot +
                            " already occupied by service: " + occupier);
                    assert false;
                } else {
                    mChildProcessConnections[slot] = null;
                    assert !mFreeConnectionIndices.contains(slot);
                    mFreeConnectionIndices.add(slot);
                }
            }
        }
    }

    // Service class for child process. As the default value it uses SandboxedProcessService0 and
    // PrivilegedProcessService0.
    private static final ChildConnectionAllocator sSandboxedChildConnectionAllocator =
            new ChildConnectionAllocator(true);
    private static final ChildConnectionAllocator sPrivilegedChildConnectionAllocator =
            new ChildConnectionAllocator(false);

    private static boolean sConnectionAllocated = false;

    // Sets service class for sandboxed service and privileged service.
    public static void setChildProcessClass(
            Class<? extends SandboxedProcessService> sandboxedServiceClass,
            Class<? extends PrivilegedProcessService> privilegedServiceClass) {
        // We should guarantee this is called before allocating connection.
        assert !sConnectionAllocated;
        sSandboxedChildConnectionAllocator.setServiceClass(sandboxedServiceClass);
        sPrivilegedChildConnectionAllocator.setServiceClass(privilegedServiceClass);
    }

    private static ChildConnectionAllocator getConnectionAllocator(boolean inSandbox) {
        return inSandbox ?
                sSandboxedChildConnectionAllocator : sPrivilegedChildConnectionAllocator;
    }

    private static ChildProcessConnection allocateConnection(Context context,
            boolean inSandbox, LinkerParams linkerParams) {
        ChildProcessConnection.DeathCallback deathCallback =
            new ChildProcessConnection.DeathCallback() {
                @Override
                public void onChildProcessDied(int pid) {
                    stop(pid);
                }
            };
        sConnectionAllocated = true;
        return getConnectionAllocator(inSandbox).allocate(context, deathCallback, linkerParams);
    }

    private static boolean sLinkerInitialized = false;
    private static long sLinkerLoadAddress = 0;

    private static LinkerParams getLinkerParamsForNewConnection() {
        if (!sLinkerInitialized) {
            if (Linker.isUsed()) {
                sLinkerLoadAddress = Linker.getBaseLoadAddress();
                if (sLinkerLoadAddress == 0) {
                    Log.i(TAG, "Shared RELRO support disabled!");
                }
            }
            sLinkerInitialized = true;
        }

        if (sLinkerLoadAddress == 0)
            return null;

        // Always wait for the shared RELROs in service processes.
        final boolean waitForSharedRelros = true;
        return new LinkerParams(sLinkerLoadAddress,
                                waitForSharedRelros,
                                Linker.getTestRunnerClassName());
    }

    private static ChildProcessConnection allocateBoundConnection(Context context,
            String[] commandLine, boolean inSandbox) {
        LinkerParams linkerParams = getLinkerParamsForNewConnection();
        ChildProcessConnection connection = allocateConnection(context, inSandbox, linkerParams);
        if (connection != null) {
            connection.start(commandLine);
        }
        return connection;
    }

    private static void freeConnection(ChildProcessConnection connection) {
        if (connection == null) {
            return;
        }
        getConnectionAllocator(connection.isInSandbox()).free(connection);
        return;
    }

    // Represents an invalid process handle; same as base/process/process.h kNullProcessHandle.
    private static final int NULL_PROCESS_HANDLE = 0;

    // Map from pid to ChildService connection.
    private static Map<Integer, ChildProcessConnection> sServiceMap =
            new ConcurrentHashMap<Integer, ChildProcessConnection>();

    // A pre-allocated and pre-bound connection ready for connection setup, or null.
    private static ChildProcessConnection sSpareSandboxedConnection = null;

    /**
     * Manages oom bindings used to bound child services. "Oom binding" is a binding that raises the
     * process oom priority so that it shouldn't be killed by the OS out-of-memory killer under
     * normal conditions (it can still be killed under drastic memory pressure).
     *
     * This class serves a proxy between external calls that manipulate the bindings and the
     * connections, allowing to enforce policies such as delayed removal of the bindings.
     */
    static class BindingManager {
        // Delay of 1 second used when removing the initial oom binding of a process.
        private static final long REMOVE_INITIAL_BINDING_DELAY_MILLIS = 1 * 1000;

        // Delay of 5 second used when removing temporary strong binding of a process (only on
        // non-low-memory devices).
        private static final long DETACH_AS_ACTIVE_HIGH_END_DELAY_MILLIS = 5 * 1000;

        // Map from pid to the count of oom bindings bound for the service. Should be accessed with
        // mCountLock.
        private final SparseIntArray mOomBindingCount = new SparseIntArray();

        // Pid of the renderer that was most recently oom bound. This is used on low-memory devices
        // to drop oom bindings of a process when another one acquires them, making sure that only
        // one renderer process at a time is oom bound. Should be accessed with mCountLock.
        private int mLastOomPid = -1;

        // Should be acquired before binding or unbinding the connections and modifying state
        // variables: mOomBindingCount and mLastOomPid.
        private final Object mCountLock = new Object();

        /**
         * Registers an oom binding bound for a child process. Should be called with mCountLock.
         * @param pid handle of the process.
         */
        private void incrementOomCount(int pid) {
            mOomBindingCount.put(pid, mOomBindingCount.get(pid) + 1);
            mLastOomPid = pid;
        }

        /**
         * Registers an oom binding unbound for a child process. Should be called with mCountLock.
         * @param pid handle of the process.
         */
        private void decrementOomCount(int pid) {
            int count = mOomBindingCount.get(pid, -1);
            assert count > 0;
            count--;
            if (count > 0) {
                mOomBindingCount.put(pid, count);
            } else {
                mOomBindingCount.delete(pid);
            }
        }

        /**
         * Drops all oom bindings for the given renderer.
         * @param pid handle of the process.
         */
        private void dropOomBindings(int pid) {
            ChildProcessConnection connection = sServiceMap.get(pid);
            if (connection == null) {
                LogPidWarning(pid, "Tried to drop oom bindings for a non-existent connection");
                return;
            }
            synchronized (mCountLock) {
                connection.dropOomBindings();
                mOomBindingCount.delete(pid);
            }
        }

        /**
         * Registers a freshly started child process. On low-memory devices this will also drop the
         * oom bindings of the last process that was oom-bound. We can do that, because every time a
         * connection is created on the low-end, it is used in foreground (no prerendering, no
         * loading of tabs opened in background).
         * @param pid handle of the process.
         */
        void addNewConnection(int pid) {
            synchronized (mCountLock) {
                if (SysUtils.isLowEndDevice() && mLastOomPid >= 0) {
                    dropOomBindings(mLastOomPid);
                }
                // This will reset the previous entry for the pid in the unlikely event of the OS
                // reusing renderer pids.
                mOomBindingCount.put(pid, 0);
                // Every new connection is bound with initial oom binding.
                incrementOomCount(pid);
            }
        }

        /**
         * Remove the initial binding of the child process. Child processes are bound with initial
         * binding to protect them from getting killed before they are put to use. This method
         * allows to remove the binding once it is no longer needed. The binding is removed after a
         * fixed delay period so that the renderer will not be killed immediately after the call.
         */
        void removeInitialBinding(final int pid) {
            final ChildProcessConnection connection = sServiceMap.get(pid);
            if (connection == null) {
                LogPidWarning(pid, "Tried to remove a binding for a non-existent connection");
                return;
            }
            if (!connection.isInitialBindingBound()) return;
            ThreadUtils.postOnUiThreadDelayed(new Runnable() {
                @Override
                public void run() {
                    synchronized (mCountLock) {
                        if (connection.isInitialBindingBound()) {
                            decrementOomCount(pid);
                            connection.removeInitialBinding();
                        }
                    }
                }
            }, REMOVE_INITIAL_BINDING_DELAY_MILLIS);
        }

        /**
         * Bind a child process as a high priority process so that it has the same priority as the
         * main process. This can be used for the foreground renderer process to distinguish it from
         * the background renderer process.
         * @param pid The process handle of the service connection.
         */
        void bindAsHighPriority(final int pid) {
            ChildProcessConnection connection = sServiceMap.get(pid);
            if (connection == null) {
                LogPidWarning(pid, "Tried to bind a non-existent connection");
                return;
            }
            synchronized (mCountLock) {
                connection.attachAsActive();
                incrementOomCount(pid);
            }
        }

        /**
         * Unbind a high priority process which was previous bound with bindAsHighPriority.
         * @param pid The process handle of the service.
         */
        void unbindAsHighPriority(final int pid) {
            final ChildProcessConnection connection = sServiceMap.get(pid);
            if (connection == null) {
                LogPidWarning(pid, "Tried to unbind non-existent connection");
                return;
            }
            if (!connection.isStrongBindingBound()) return;

            // This runnable performs the actual unbinding. It will be executed synchronously when
            // on low-end devices and posted with a delay otherwise.
            Runnable doUnbind = new Runnable() {
                @Override
                public void run() {
                    synchronized (mCountLock) {
                        if (connection.isStrongBindingBound()) {
                            decrementOomCount(pid);
                            connection.detachAsActive();
                        }
                    }
                }
            };

            if (SysUtils.isLowEndDevice()) {
                doUnbind.run();
            } else {
                ThreadUtils.postOnUiThreadDelayed(doUnbind, DETACH_AS_ACTIVE_HIGH_END_DELAY_MILLIS);
            }
        }

        /**
         * @return True iff the given service process is protected from the out-of-memory killing,
         * or it was protected when it died (either crashed or was closed). This can be used to
         * decide if a disconnection of a renderer was a crash or a probable out-of-memory kill. In
         * the unlikely event of the OS reusing renderer pid, the call will refer to the most recent
         * renderer of the given pid. The binding count is being reset in addNewConnection().
         */
        boolean isOomProtected(int pid) {
            synchronized (mCountLock) {
                return mOomBindingCount.get(pid) > 0;
            }
        }
    }

    private static BindingManager sBindingManager = new BindingManager();

    static BindingManager getBindingManager() {
        return sBindingManager;
    }

    @CalledByNative
    private static boolean isOomProtected(int pid) {
        return sBindingManager.isOomProtected(pid);
    }

    /**
     * Returns the child process service interface for the given pid. This may be called on
     * any thread, but the caller must assume that the service can disconnect at any time. All
     * service calls should catch and handle android.os.RemoteException.
     *
     * @param pid The pid (process handle) of the service obtained from {@link #start}.
     * @return The IChildProcessService or null if the service no longer exists.
     */
    public static IChildProcessService getChildService(int pid) {
        ChildProcessConnection connection = sServiceMap.get(pid);
        if (connection != null) {
            return connection.getService();
        }
        return null;
    }

    /**
     * Should be called early in startup so the work needed to spawn the child process can be done
     * in parallel to other startup work. Must not be called on the UI thread. Spare connection is
     * created in sandboxed child process.
     * @param context the application context used for the connection.
     */
    public static void warmUp(Context context) {
        synchronized (ChildProcessLauncher.class) {
            assert !ThreadUtils.runningOnUiThread();
            if (sSpareSandboxedConnection == null) {
                sSpareSandboxedConnection = allocateBoundConnection(context, null, true);
            }
        }
    }

    private static String getSwitchValue(final String[] commandLine, String switchKey) {
        if (commandLine == null || switchKey == null) {
            return null;
        }
        // This format should be matched with the one defined in command_line.h.
        final String switchKeyPrefix = "--" + switchKey + "=";
        for (String command : commandLine) {
            if (command != null && command.startsWith(switchKeyPrefix)) {
                return command.substring(switchKeyPrefix.length());
            }
        }
        return null;
    }

    /**
     * Spawns and connects to a child process. May be called on any thread. It will not block, but
     * will instead callback to {@link #nativeOnChildProcessStarted} when the connection is
     * established. Note this callback will not necessarily be from the same thread (currently it
     * always comes from the main thread).
     *
     * @param context Context used to obtain the application context.
     * @param commandLine The child process command line argv.
     * @param file_ids The ID that should be used when mapping files in the created process.
     * @param file_fds The file descriptors that should be mapped in the created process.
     * @param file_auto_close Whether the file descriptors should be closed once they were passed to
     * the created process.
     * @param clientContext Arbitrary parameter used by the client to distinguish this connection.
     */
    @CalledByNative
    static void start(
            Context context,
            final String[] commandLine,
            int[] fileIds,
            int[] fileFds,
            boolean[] fileAutoClose,
            final int clientContext) {
        assert fileIds.length == fileFds.length && fileFds.length == fileAutoClose.length;
        FileDescriptorInfo[] filesToBeMapped = new FileDescriptorInfo[fileFds.length];
        for (int i = 0; i < fileFds.length; i++) {
            filesToBeMapped[i] =
                    new FileDescriptorInfo(fileIds[i], fileFds[i], fileAutoClose[i]);
        }
        assert clientContext != 0;

        int callbackType = CALLBACK_FOR_UNKNOWN_PROCESS;
        boolean inSandbox = true;
        String processType = getSwitchValue(commandLine, SWITCH_PROCESS_TYPE);
        if (SWITCH_RENDERER_PROCESS.equals(processType)) {
            callbackType = CALLBACK_FOR_RENDERER_PROCESS;
        } else if (SWITCH_GPU_PROCESS.equals(processType)) {
            callbackType = CALLBACK_FOR_GPU_PROCESS;
        } else if (SWITCH_PPAPI_BROKER_PROCESS.equals(processType)) {
            inSandbox = false;
        }

        ChildProcessConnection allocatedConnection = null;
        synchronized (ChildProcessLauncher.class) {
            if (inSandbox) {
                allocatedConnection = sSpareSandboxedConnection;
                sSpareSandboxedConnection = null;
            }
        }
        if (allocatedConnection == null) {
            allocatedConnection = allocateBoundConnection(context, commandLine, inSandbox);
            if (allocatedConnection == null) {
                // Notify the native code so it can free the heap allocated callback.
                nativeOnChildProcessStarted(clientContext, 0);
                return;
            }
        }
        final ChildProcessConnection connection = allocatedConnection;
        Log.d(TAG, "Setting up connection to process: slot=" + connection.getServiceNumber());

        ChildProcessConnection.ConnectionCallback connectionCallback =
                new ChildProcessConnection.ConnectionCallback() {
            public void onConnected(int pid) {
                Log.d(TAG, "on connect callback, pid=" + pid + " context=" + clientContext);
                if (pid != NULL_PROCESS_HANDLE) {
                    sBindingManager.addNewConnection(pid);
                    sServiceMap.put(pid, connection);
                } else {
                    freeConnection(connection);
                }
                nativeOnChildProcessStarted(clientContext, pid);
            }
        };

        // TODO(sievers): Revisit this as it doesn't correctly handle the utility process
        // assert callbackType != CALLBACK_FOR_UNKNOWN_PROCESS;

        connection.setupConnection(commandLine,
                                   filesToBeMapped,
                                   createCallback(callbackType),
                                   connectionCallback,
                                   Linker.getSharedRelros());
    }

    /**
     * Terminates a child process. This may be called from any thread.
     *
     * @param pid The pid (process handle) of the service connection obtained from {@link #start}.
     */
    @CalledByNative
    static void stop(int pid) {
        Log.d(TAG, "stopping child connection: pid=" + pid);
        ChildProcessConnection connection = sServiceMap.remove(pid);
        if (connection == null) {
            LogPidWarning(pid, "Tried to stop non-existent connection");
            return;
        }
        connection.stop();
        freeConnection(connection);
    }

    /**
     * This implementation is used to receive callbacks from the remote service.
     */
    private static IChildProcessCallback createCallback(final int callbackType) {
        return new IChildProcessCallback.Stub() {
            /**
             * This is called by the remote service regularly to tell us about new values. Note that
             * IPC calls are dispatched through a thread pool running in each process, so the code
             * executing here will NOT be running in our main thread -- so, to update the UI, we
             * need to use a Handler.
             */
            @Override
            public void establishSurfacePeer(
                    int pid, Surface surface, int primaryID, int secondaryID) {
                // Do not allow a malicious renderer to connect to a producer. This is only used
                // from stream textures managed by the GPU process.
                if (callbackType != CALLBACK_FOR_GPU_PROCESS) {
                    Log.e(TAG, "Illegal callback for non-GPU process.");
                    return;
                }

                nativeEstablishSurfacePeer(pid, surface, primaryID, secondaryID);
            }

            @Override
            public Surface getViewSurface(int surfaceId) {
                // Do not allow a malicious renderer to get to our view surface.
                if (callbackType != CALLBACK_FOR_GPU_PROCESS) {
                    Log.e(TAG, "Illegal callback for non-GPU process.");
                    return null;
                }

                return nativeGetViewSurface(surfaceId);
            }
        };
    };

    private static void LogPidWarning(int pid, String message) {
        // This class is effectively a no-op in single process mode, so don't log warnings there.
        if (pid > 0 && !nativeIsSingleProcess()) {
            Log.w(TAG, message + ", pid=" + pid);
        }
    }

    private static native void nativeOnChildProcessStarted(int clientContext, int pid);
    private static native Surface nativeGetViewSurface(int surfaceId);
    private static native void nativeEstablishSurfacePeer(
            int pid, Surface surface, int primaryID, int secondaryID);
    private static native boolean nativeIsSingleProcess();
}
