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

  static final int CALLBACK_FOR_UNKNOWN_PROCESS = 0;
  static final int CALLBACK_FOR_GPU_PROCESS = 1;
  static final int CALLBACK_FOR_RENDERER_PROCESS = 2;
  static final int CALLBACK_FOR_UTILITY_PROCESS = 3;

  private static final String SWITCH_PROCESS_TYPE = "type";
  private static final String SWITCH_RENDERER_PROCESS = "renderer";
  private static final String SWITCH_UTILITY_PROCESS = "utility";
  private static final String SWITCH_GPU_PROCESS = "gpu-process";

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

    // The list of free (not bound) service indices.
    // SHOULD BE ACCESSED WITH mConnectionLock.
    private final ArrayList<Integer> mFreeConnectionIndices;
    private final Object mConnectionLock = new Object();

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

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

    public ChildProcessConnection allocate(
        Context context,
        ChildProcessConnection.DeathCallback deathCallback,
        ChromiumLinkerParams chromiumLinkerParams,
        boolean alwaysInForeground) {
      synchronized (mConnectionLock) {
        if (mFreeConnectionIndices.isEmpty()) {
          Log.d(TAG, "Ran out of services to allocate.");
          return null;
        }
        int slot = mFreeConnectionIndices.remove(0);
        assert mChildProcessConnections[slot] == null;
        mChildProcessConnections[slot] =
            new ChildProcessConnectionImpl(
                context,
                slot,
                mInSandbox,
                deathCallback,
                mChildClass,
                chromiumLinkerParams,
                alwaysInForeground);
        Log.d(TAG, "Allocator allocated a connection, sandbox: " + mInSandbox + ", slot: " + slot);
        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);
          Log.d(TAG, "Allocator freed a connection, sandbox: " + mInSandbox + ", slot: " + slot);
        }
      }
    }

    /** @return the count of connections managed by the allocator */
    @VisibleForTesting
    int allocatedConnectionsCountForTesting() {
      return mChildProcessConnections.length - mFreeConnectionIndices.size();
    }
  }

  private static class PendingSpawnData {
    private final Context mContext;
    private final String[] mCommandLine;
    private final int mChildProcessId;
    private final FileDescriptorInfo[] mFilesToBeMapped;
    private final long mClientContext;
    private final int mCallbackType;
    private final boolean mInSandbox;

    private PendingSpawnData(
        Context context,
        String[] commandLine,
        int childProcessId,
        FileDescriptorInfo[] filesToBeMapped,
        long clientContext,
        int callbackType,
        boolean inSandbox) {
      mContext = context;
      mCommandLine = commandLine;
      mChildProcessId = childProcessId;
      mFilesToBeMapped = filesToBeMapped;
      mClientContext = clientContext;
      mCallbackType = callbackType;
      mInSandbox = inSandbox;
    }

    private Context context() {
      return mContext;
    }

    private String[] commandLine() {
      return mCommandLine;
    }

    private int childProcessId() {
      return mChildProcessId;
    }

    private FileDescriptorInfo[] filesToBeMapped() {
      return mFilesToBeMapped;
    }

    private long clientContext() {
      return mClientContext;
    }

    private int callbackType() {
      return mCallbackType;
    }

    private boolean inSandbox() {
      return mInSandbox;
    }
  }

  private static class PendingSpawnQueue {
    // The list of pending process spawn requests and its lock.
    private static Queue<PendingSpawnData> sPendingSpawns = new LinkedList<PendingSpawnData>();
    static final Object sPendingSpawnsLock = new Object();

    /**
     * Queue up a spawn requests to be processed once a free service is available. Called when a
     * spawn is requested while we are at the capacity.
     */
    public void enqueue(final PendingSpawnData pendingSpawn) {
      synchronized (sPendingSpawnsLock) {
        sPendingSpawns.add(pendingSpawn);
      }
    }

    /**
     * Pop the next request from the queue. Called when a free service is available.
     *
     * @return the next spawn request waiting in the queue.
     */
    public PendingSpawnData dequeue() {
      synchronized (sPendingSpawnsLock) {
        return sPendingSpawns.poll();
      }
    }

    /** @return the count of pending spawns in the queue */
    public int size() {
      synchronized (sPendingSpawnsLock) {
        return sPendingSpawns.size();
      }
    }
  }

  private static final PendingSpawnQueue sPendingSpawnQueue = new PendingSpawnQueue();

  // Service class for child process. As the default value it uses SandboxedProcessService0 and
  // PrivilegedProcessService0.
  private static ChildConnectionAllocator sSandboxedChildConnectionAllocator;
  private static ChildConnectionAllocator sPrivilegedChildConnectionAllocator;

  private static final String NUM_SANDBOXED_SERVICES_KEY =
      "org.chromium.content.browser.NUM_SANDBOXED_SERVICES";
  private static final String NUM_PRIVILEGED_SERVICES_KEY =
      "org.chromium.content.browser.NUM_PRIVILEGED_SERVICES";

  private static int getNumberOfServices(Context context, boolean inSandbox) {
    try {
      PackageManager packageManager = context.getPackageManager();
      ApplicationInfo appInfo =
          packageManager.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
      int numServices =
          appInfo.metaData.getInt(
              inSandbox ? NUM_SANDBOXED_SERVICES_KEY : NUM_PRIVILEGED_SERVICES_KEY);
      if (numServices <= 0) {
        throw new RuntimeException("Illegal meta data value for number of child services");
      }
      return numServices;
    } catch (PackageManager.NameNotFoundException e) {
      throw new RuntimeException("Could not get application info");
    }
  }

  private static void initConnectionAllocatorsIfNecessary(Context context) {
    synchronized (ChildProcessLauncher.class) {
      if (sSandboxedChildConnectionAllocator == null) {
        sSandboxedChildConnectionAllocator =
            new ChildConnectionAllocator(true, getNumberOfServices(context, true));
      }
      if (sPrivilegedChildConnectionAllocator == null) {
        sPrivilegedChildConnectionAllocator =
            new ChildConnectionAllocator(false, getNumberOfServices(context, false));
      }
    }
  }

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

  private static ChildProcessConnection allocateConnection(
      Context context,
      boolean inSandbox,
      ChromiumLinkerParams chromiumLinkerParams,
      boolean alwaysInForeground) {
    ChildProcessConnection.DeathCallback deathCallback =
        new ChildProcessConnection.DeathCallback() {
          @Override
          public void onChildProcessDied(ChildProcessConnection connection) {
            if (connection.getPid() != 0) {
              stop(connection.getPid());
            } else {
              freeConnection(connection);
            }
          }
        };
    initConnectionAllocatorsIfNecessary(context);
    return getConnectionAllocator(inSandbox)
        .allocate(context, deathCallback, chromiumLinkerParams, alwaysInForeground);
  }

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

  private static ChromiumLinkerParams 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 ChromiumLinkerParams(
        sLinkerLoadAddress, waitForSharedRelros, Linker.getTestRunnerClassName());
  }

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

  private static final long FREE_CONNECTION_DELAY_MILLIS = 1;

  private static void freeConnection(ChildProcessConnection connection) {
    // Freeing a service should be delayed. 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).
    final ChildProcessConnection conn = connection;
    ThreadUtils.postOnUiThreadDelayed(
        new Runnable() {
          @Override
          public void run() {
            getConnectionAllocator(conn.isInSandbox()).free(conn);

            final PendingSpawnData pendingSpawn = sPendingSpawnQueue.dequeue();
            if (pendingSpawn != null) {
              new Thread(
                      new Runnable() {
                        @Override
                        public void run() {
                          startInternal(
                              pendingSpawn.context(),
                              pendingSpawn.commandLine(),
                              pendingSpawn.childProcessId(),
                              pendingSpawn.filesToBeMapped(),
                              pendingSpawn.clientContext(),
                              pendingSpawn.callbackType(),
                              pendingSpawn.inSandbox());
                        }
                      })
                  .start();
            }
          }
        },
        FREE_CONNECTION_DELAY_MILLIS);
  }

  // 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 bind chind services.
  private static BindingManager sBindingManager = BindingManagerImpl.createBindingManager();

  // Map from surface id to Surface.
  private static Map<Integer, Surface> sViewSurfaceMap = new ConcurrentHashMap<Integer, Surface>();

  // Map from surface texture id to Surface.
  private static Map<Pair<Integer, Integer>, Surface> sSurfaceTextureSurfaceMap =
      new ConcurrentHashMap<Pair<Integer, Integer>, Surface>();

  // Whether the main application is currently brought to the foreground.
  private static boolean sApplicationInForeground = true;

  @VisibleForTesting
  public static void setBindingManagerForTesting(BindingManager manager) {
    sBindingManager = manager;
  }

  /** @return true iff the child process is protected from out-of-memory killing */
  @CalledByNative
  private static boolean isOomProtected(int pid) {
    return sBindingManager.isOomProtected(pid);
  }

  @CalledByNative
  private static void registerViewSurface(int surfaceId, Surface surface) {
    sViewSurfaceMap.put(surfaceId, surface);
  }

  @CalledByNative
  private static void unregisterViewSurface(int surfaceId) {
    sViewSurfaceMap.remove(surfaceId);
  }

  private static void registerSurfaceTextureSurface(
      int surfaceTextureId, int clientId, Surface surface) {
    Pair<Integer, Integer> key = new Pair<Integer, Integer>(surfaceTextureId, clientId);
    sSurfaceTextureSurfaceMap.put(key, surface);
  }

  private static void unregisterSurfaceTextureSurface(int surfaceTextureId, int clientId) {
    Pair<Integer, Integer> key = new Pair<Integer, Integer>(surfaceTextureId, clientId);
    Surface surface = sSurfaceTextureSurfaceMap.remove(key);
    if (surface == null) return;

    assert surface.isValid();
    surface.release();
  }

  @CalledByNative
  private static void createSurfaceTextureSurface(
      int surfaceTextureId, int clientId, SurfaceTexture surfaceTexture) {
    registerSurfaceTextureSurface(surfaceTextureId, clientId, new Surface(surfaceTexture));
  }

  @CalledByNative
  private static void destroySurfaceTextureSurface(int surfaceTextureId, int clientId) {
    unregisterSurfaceTextureSurface(surfaceTextureId, clientId);
  }

  @CalledByNative
  private static SurfaceWrapper getSurfaceTextureSurface(int surfaceTextureId, int clientId) {
    Pair<Integer, Integer> key = new Pair<Integer, Integer>(surfaceTextureId, clientId);

    Surface surface = sSurfaceTextureSurfaceMap.get(key);
    if (surface == null) {
      Log.e(TAG, "Invalid Id for surface texture.");
      return null;
    }
    assert surface.isValid();
    return new SurfaceWrapper(surface);
  }

  /**
   * Sets the visibility of the child process when it changes or when it is determined for the first
   * time.
   */
  @CalledByNative
  public static void setInForeground(int pid, boolean inForeground) {
    sBindingManager.setInForeground(pid, inForeground);
  }

  /**
   * Called when the renderer commits a navigation. This signals a time at which it is safe to rely
   * on renderer visibility signalled through setInForeground. See http://crbug.com/421041.
   */
  public static void determinedVisibility(int pid) {
    sBindingManager.determinedVisibility(pid);
  }

  /** Called when the embedding application is sent to background. */
  public static void onSentToBackground() {
    sApplicationInForeground = false;
    sBindingManager.onSentToBackground();
  }

  /** Called when the embedding application is brought to foreground. */
  public static void onBroughtToForeground() {
    sApplicationInForeground = true;
    sBindingManager.onBroughtToForeground();
  }

  /** Returns whether the application is currently in the foreground. */
  static boolean isApplicationInForeground() {
    return sApplicationInForeground;
  }

  /**
   * 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, false);
      }
    }
  }

  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;
  }

  @CalledByNative
  private static FileDescriptorInfo makeFdInfo(
      int id, int fd, boolean autoClose, long offset, long size) {
    ParcelFileDescriptor pFd;
    if (autoClose) {
      // Adopt the FD, it will be closed when we close the ParcelFileDescriptor.
      pFd = ParcelFileDescriptor.adoptFd(fd);
    } else {
      try {
        pFd = ParcelFileDescriptor.fromFd(fd);
      } catch (IOException e) {
        Log.e(TAG, "Invalid FD provided for process connection, aborting connection.", e);
        return null;
      }
    }
    return new FileDescriptorInfo(id, pFd, offset, size);
  }

  /**
   * 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 filesToBeMapped File IDs, FDs, offsets, and lengths to pass through.
   * @param clientContext Arbitrary parameter used by the client to distinguish this connection.
   */
  @CalledByNative
  private static void start(
      Context context,
      final String[] commandLine,
      int childProcessId,
      FileDescriptorInfo[] filesToBeMapped,
      long clientContext) {
    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;
      inSandbox = false;
    } else if (SWITCH_UTILITY_PROCESS.equals(processType)) {
      // We only support sandboxed right now.
      callbackType = CALLBACK_FOR_UTILITY_PROCESS;
    } else {
      assert false;
    }

    startInternal(
        context,
        commandLine,
        childProcessId,
        filesToBeMapped,
        clientContext,
        callbackType,
        inSandbox);
  }

  private static void startInternal(
      Context context,
      final String[] commandLine,
      int childProcessId,
      FileDescriptorInfo[] filesToBeMapped,
      long clientContext,
      int callbackType,
      boolean inSandbox) {
    try {
      TraceEvent.begin("ChildProcessLauncher.startInternal");

      ChildProcessConnection allocatedConnection = null;
      synchronized (ChildProcessLauncher.class) {
        if (inSandbox) {
          allocatedConnection = sSpareSandboxedConnection;
          sSpareSandboxedConnection = null;
        }
      }
      if (allocatedConnection == null) {
        boolean alwaysInForeground = false;
        if (callbackType == CALLBACK_FOR_GPU_PROCESS) alwaysInForeground = true;
        allocatedConnection =
            allocateBoundConnection(context, commandLine, inSandbox, alwaysInForeground);
        if (allocatedConnection == null) {
          Log.d(TAG, "Allocation of new service failed. Queuing up pending spawn.");
          sPendingSpawnQueue.enqueue(
              new PendingSpawnData(
                  context,
                  commandLine,
                  childProcessId,
                  filesToBeMapped,
                  clientContext,
                  callbackType,
                  inSandbox));
          return;
        }
      }

      Log.d(
          TAG, "Setting up connection to process: slot=" + allocatedConnection.getServiceNumber());
      triggerConnectionSetup(
          allocatedConnection,
          commandLine,
          childProcessId,
          filesToBeMapped,
          callbackType,
          clientContext);
    } finally {
      TraceEvent.end("ChildProcessLauncher.startInternal");
    }
  }

  @VisibleForTesting
  static void triggerConnectionSetup(
      final ChildProcessConnection connection,
      String[] commandLine,
      int childProcessId,
      FileDescriptorInfo[] filesToBeMapped,
      final int callbackType,
      final long clientContext) {
    ChildProcessConnection.ConnectionCallback connectionCallback =
        new ChildProcessConnection.ConnectionCallback() {
          @Override
          public void onConnected(int pid) {
            Log.d(
                TAG,
                "on connect callback, pid="
                    + pid
                    + " context="
                    + clientContext
                    + " callbackType="
                    + callbackType);
            if (pid != NULL_PROCESS_HANDLE) {
              sBindingManager.addNewConnection(pid, connection);
              sServiceMap.put(pid, connection);
            }
            // If the connection fails and pid == 0, the Java-side cleanup was already
            // handled by DeathCallback. We still have to call back to native for
            // cleanup there.
            if (clientContext != 0) { // Will be 0 in Java instrumentation tests.
              nativeOnChildProcessStarted(clientContext, pid);
            }
          }
        };

    assert callbackType != CALLBACK_FOR_UNKNOWN_PROCESS;
    connection.setupConnection(
        commandLine,
        filesToBeMapped,
        createCallback(childProcessId, 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;
    }
    sBindingManager.clearConnection(pid);
    connection.stop();
    freeConnection(connection);
  }

  /** This implementation is used to receive callbacks from the remote service. */
  private static IChildProcessCallback createCallback(
      final int childProcessId, 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 SurfaceWrapper 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;
        }

        Surface surface = sViewSurfaceMap.get(surfaceId);
        if (surface == null) {
          Log.e(TAG, "Invalid surfaceId.");
          return null;
        }
        assert surface.isValid();
        return new SurfaceWrapper(surface);
      }

      @Override
      public void registerSurfaceTextureSurface(
          int surfaceTextureId, int clientId, Surface surface) {
        if (callbackType != CALLBACK_FOR_GPU_PROCESS) {
          Log.e(TAG, "Illegal callback for non-GPU process.");
          return;
        }

        ChildProcessLauncher.registerSurfaceTextureSurface(surfaceTextureId, clientId, surface);
      }

      @Override
      public void unregisterSurfaceTextureSurface(int surfaceTextureId, int clientId) {
        if (callbackType != CALLBACK_FOR_GPU_PROCESS) {
          Log.e(TAG, "Illegal callback for non-GPU process.");
          return;
        }

        ChildProcessLauncher.unregisterSurfaceTextureSurface(surfaceTextureId, clientId);
      }

      @Override
      public SurfaceWrapper getSurfaceTextureSurface(int surfaceTextureId) {
        if (callbackType != CALLBACK_FOR_RENDERER_PROCESS) {
          Log.e(TAG, "Illegal callback for non-renderer process.");
          return null;
        }

        return ChildProcessLauncher.getSurfaceTextureSurface(surfaceTextureId, childProcessId);
      }
    };
  }

  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);
    }
  }

  @VisibleForTesting
  static ChildProcessConnection allocateBoundConnectionForTesting(Context context) {
    return allocateBoundConnection(context, null, true, false);
  }

  /** Queue up a spawn requests for testing. */
  @VisibleForTesting
  static void enqueuePendingSpawnForTesting(Context context) {
    sPendingSpawnQueue.enqueue(
        new PendingSpawnData(
            context,
            new String[0],
            1,
            new FileDescriptorInfo[0],
            0,
            CALLBACK_FOR_RENDERER_PROCESS,
            true));
  }

  /** @return the count of sandboxed connections managed by the allocator */
  @VisibleForTesting
  static int allocatedConnectionsCountForTesting(Context context) {
    initConnectionAllocatorsIfNecessary(context);
    return sSandboxedChildConnectionAllocator.allocatedConnectionsCountForTesting();
  }

  /** @return the count of services set up and working */
  @VisibleForTesting
  static int connectedServicesCountForTesting() {
    return sServiceMap.size();
  }

  /** @return the count of pending spawns in the queue */
  @VisibleForTesting
  static int pendingSpawnsCountForTesting() {
    return sPendingSpawnQueue.size();
  }

  /**
   * Kills the child process for testing.
   *
   * @return true iff the process was killed as expected
   */
  @VisibleForTesting
  public static boolean crashProcessForTesting(int pid) {
    if (sServiceMap.get(pid) == null) return false;

    try {
      ((ChildProcessConnectionImpl) sServiceMap.get(pid)).crashServiceForTesting();
    } catch (RemoteException ex) {
      return false;
    }

    return true;
  }

  private static native void nativeOnChildProcessStarted(long clientContext, int pid);

  private static native void nativeEstablishSurfacePeer(
      int pid, Surface surface, int primaryID, int secondaryID);

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

  static final int CALLBACK_FOR_UNKNOWN_PROCESS = 0;
  static final int CALLBACK_FOR_GPU_PROCESS = 1;
  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 = 2;
  /* 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 final 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 final 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 ChildProcessConnectionImpl[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,
        ChromiumLinkerParams chromiumLinkerParams) {
      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 ChildProcessConnectionImpl(
                context, slot, mInSandbox, deathCallback, mChildClass, chromiumLinkerParams);
        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);
        }
      }
    }

    /** @return the count of connections managed by the allocator */
    @VisibleForTesting
    int allocatedConnectionsCountForTesting() {
      return mChildProcessConnections.length - mFreeConnectionIndices.size();
    }
  }

  // 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, ChromiumLinkerParams chromiumLinkerParams) {
    ChildProcessConnection.DeathCallback deathCallback =
        new ChildProcessConnection.DeathCallback() {
          @Override
          public void onChildProcessDied(ChildProcessConnection connection) {
            if (connection.getPid() != 0) {
              stop(connection.getPid());
            } else {
              freeConnection(connection);
            }
          }
        };
    sConnectionAllocated = true;
    return getConnectionAllocator(inSandbox).allocate(context, deathCallback, chromiumLinkerParams);
  }

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

  private static ChromiumLinkerParams 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 ChromiumLinkerParams(
        sLinkerLoadAddress, waitForSharedRelros, Linker.getTestRunnerClassName());
  }

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

  private static void freeConnection(ChildProcessConnection connection) {
    getConnectionAllocator(connection.isInSandbox()).free(connection);
  }

  // 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 bind chind services.
  private static BindingManager sBindingManager = BindingManagerImpl.createBindingManager();

  // Map from surface id to Surface.
  private static Map<Integer, Surface> sViewSurfaceMap = new ConcurrentHashMap<Integer, Surface>();

  // Map from surface texture id to Surface.
  private static Map<Pair<Integer, Integer>, Surface> sSurfaceTextureSurfaceMap =
      new ConcurrentHashMap<Pair<Integer, Integer>, Surface>();

  @VisibleForTesting
  public static void setBindingManagerForTesting(BindingManager manager) {
    sBindingManager = manager;
  }

  /** @return true iff the child process is protected from out-of-memory killing */
  @CalledByNative
  private static boolean isOomProtected(int pid) {
    return sBindingManager.isOomProtected(pid);
  }

  @CalledByNative
  private static void registerViewSurface(int surfaceId, Surface surface) {
    sViewSurfaceMap.put(surfaceId, surface);
  }

  @CalledByNative
  private static void unregisterViewSurface(int surfaceId) {
    sViewSurfaceMap.remove(surfaceId);
  }

  @CalledByNative
  private static void registerSurfaceTexture(
      int surfaceTextureId, int childProcessId, SurfaceTexture surfaceTexture) {
    Pair<Integer, Integer> key = new Pair<Integer, Integer>(surfaceTextureId, childProcessId);
    sSurfaceTextureSurfaceMap.put(key, new Surface(surfaceTexture));
  }

  @CalledByNative
  private static void unregisterSurfaceTexture(int surfaceTextureId, int childProcessId) {
    Pair<Integer, Integer> key = new Pair<Integer, Integer>(surfaceTextureId, childProcessId);
    sSurfaceTextureSurfaceMap.remove(key);
  }

  /**
   * Sets the visibility of the child process when it changes or when it is determined for the first
   * time.
   */
  @CalledByNative
  public static void setInForeground(int pid, boolean inForeground) {
    sBindingManager.setInForeground(pid, inForeground);
  }

  /** Called when the embedding application is sent to background. */
  public static void onSentToBackground() {
    sBindingManager.onSentToBackground();
  }

  /** Called when the embedding application is brought to foreground. */
  public static void onBroughtToForeground() {
    sBindingManager.onBroughtToForeground();
  }

  /**
   * 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 fileIds The ID that should be used when mapping files in the created process.
   * @param fileFds The file descriptors that should be mapped in the created process.
   * @param fileAutoClose 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 childProcessId,
      int[] fileIds,
      int[] fileFds,
      boolean[] fileAutoClose,
      long clientContext) {
    TraceEvent.begin();
    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);
        Log.e(TAG, "Allocation of new service failed.");
        TraceEvent.end();
        return;
      }
    }

    Log.d(TAG, "Setting up connection to process: slot=" + allocatedConnection.getServiceNumber());
    triggerConnectionSetup(
        allocatedConnection,
        commandLine,
        childProcessId,
        filesToBeMapped,
        callbackType,
        clientContext);
    TraceEvent.end();
  }

  @VisibleForTesting
  static void triggerConnectionSetup(
      final ChildProcessConnection connection,
      String[] commandLine,
      int childProcessId,
      FileDescriptorInfo[] filesToBeMapped,
      int callbackType,
      final long clientContext) {
    ChildProcessConnection.ConnectionCallback connectionCallback =
        new ChildProcessConnection.ConnectionCallback() {
          @Override
          public void onConnected(int pid) {
            Log.d(TAG, "on connect callback, pid=" + pid + " context=" + clientContext);
            if (pid != NULL_PROCESS_HANDLE) {
              sBindingManager.addNewConnection(pid, connection);
              sServiceMap.put(pid, connection);
            }
            // If the connection fails and pid == 0, the Java-side cleanup was already
            // handled by DeathCallback. We still have to call back to native for
            // cleanup there.
            if (clientContext != 0) { // Will be 0 in Java instrumentation tests.
              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(childProcessId, 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;
    }
    sBindingManager.clearConnection(pid);
    connection.stop();
    freeConnection(connection);
  }

  /** This implementation is used to receive callbacks from the remote service. */
  private static IChildProcessCallback createCallback(
      final int childProcessId, 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 SurfaceWrapper 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;
        }

        Surface surface = sViewSurfaceMap.get(surfaceId);
        if (surface == null) {
          Log.e(TAG, "Invalid surfaceId.");
          return null;
        }
        assert surface.isValid();
        return new SurfaceWrapper(surface);
      }

      @Override
      public SurfaceWrapper getSurfaceTextureSurface(int primaryId, int secondaryId) {
        if (callbackType != CALLBACK_FOR_RENDERER_PROCESS) {
          Log.e(TAG, "Illegal callback for non-renderer process.");
          return null;
        }

        if (secondaryId != childProcessId) {
          Log.e(TAG, "Illegal secondaryId for renderer process.");
          return null;
        }

        Pair<Integer, Integer> key = new Pair<Integer, Integer>(primaryId, secondaryId);
        // Note: This removes the surface and passes the ownership to the caller.
        Surface surface = sSurfaceTextureSurfaceMap.remove(key);
        if (surface == null) {
          Log.e(TAG, "Invalid Id for surface texture.");
          return null;
        }
        assert surface.isValid();
        return new SurfaceWrapper(surface);
      }
    };
  }

  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);
    }
  }

  @VisibleForTesting
  static ChildProcessConnection allocateBoundConnectionForTesting(Context context) {
    return allocateBoundConnection(context, null, true);
  }

  /** @return the count of sandboxed connections managed by the allocator */
  @VisibleForTesting
  static int allocatedConnectionsCountForTesting() {
    return sSandboxedChildConnectionAllocator.allocatedConnectionsCountForTesting();
  }

  /** @return the count of services set up and working */
  @VisibleForTesting
  static int connectedServicesCountForTesting() {
    return sServiceMap.size();
  }

  private static native void nativeOnChildProcessStarted(long clientContext, int pid);

  private static native void nativeEstablishSurfacePeer(
      int pid, Surface surface, int primaryID, int secondaryID);

  private static native boolean nativeIsSingleProcess();
}