| // Copyright 2017 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.os.Bundle; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Looper; |
| |
| import org.chromium.base.ContextUtils; |
| import org.chromium.base.Log; |
| import org.chromium.base.TraceEvent; |
| import org.chromium.base.annotations.SuppressFBWarnings; |
| import org.chromium.base.process_launcher.ChildProcessConstants; |
| import org.chromium.base.process_launcher.FileDescriptorInfo; |
| |
| import java.io.IOException; |
| |
| /** |
| * This class is used to start a child process by connecting to a ChildProcessService. |
| */ |
| public class ChildProcessLauncher { |
| private static final String TAG = "ChildProcLauncher"; |
| |
| /** Delegate that client should use to customize the process launching. */ |
| public interface Delegate { |
| /** |
| * Called before a connection is allocated. |
| * Note that this is only called if the ChildProcessLauncher is created with |
| * {@link #createWithConnectionAllocator}. |
| * @param serviceBundle the bundle passed in the service intent. Clients can add their own |
| * extras to the bundle. |
| */ |
| void onBeforeConnectionAllocated(Bundle serviceBundle); |
| |
| /** |
| * Called before setup is called on the connection. |
| * @param connectionBundle the bundle passed to the {@link ChildProcessService} in the |
| * setup call. Clients can add their own extras to the bundle. |
| */ |
| void onBeforeConnectionSetup(Bundle connectionBundle); |
| |
| /** |
| * Called when the connection was successfully established, meaning the setup call on the |
| * service was successful. |
| * @param connection the connection over which the setup call was made. |
| */ |
| void onConnectionEstablished(ChildProcessConnection connection); |
| |
| /** |
| * Called when a connection has been disconnected. Only invoked if onConnectionEstablished |
| * was called, meaning the connection was already established. |
| * @param connection the connection that got disconnected. |
| */ |
| void onConnectionLost(ChildProcessConnection connection); |
| } |
| |
| /** |
| * Interface used by clients that already have a bound connection ready when instanciating the |
| * ChildProcessLauncher. |
| */ |
| public interface BoundConnectionProvider { |
| ChildProcessConnection getConnection( |
| ChildProcessConnection.ServiceCallback serviceCallback); |
| } |
| |
| // Represents an invalid process handle; same as base/process/process.h kNullProcessHandle. |
| private static final int NULL_PROCESS_HANDLE = 0; |
| |
| // The handle for the thread we were created on and on which all methods should be called. |
| private final Handler mLauncherHandler = new Handler(); |
| |
| private final Delegate mDelegate; |
| |
| private final String[] mCommandLine; |
| private final FileDescriptorInfo[] mFilesToBeMapped; |
| |
| // The allocator used to create the connection. |
| private final BoundConnectionProvider mConnectionProvider; |
| |
| // The allocator used to create the connection. |
| private final ChildConnectionAllocator mConnectionAllocator; |
| |
| // The IBinder provided to the created service. |
| private final IBinder mIBinderCallback; |
| |
| // The actual service connection. Set once we have connected to the service. |
| private ChildProcessConnection mConnection; |
| |
| /** |
| * Creates a ChildProcessLauncher using the already bound connection provided. |
| * Note that onBeforeConnectionAllocated and onConnectionBound will not be invoked on the |
| * delegate since the connection is already available. |
| */ |
| public static ChildProcessLauncher createWithBoundConnectionProvider(Delegate delegate, |
| String[] commandLine, FileDescriptorInfo[] filesToBeMapped, |
| BoundConnectionProvider connectionProvider, IBinder binderCallback) { |
| return new ChildProcessLauncher(delegate, commandLine, filesToBeMapped, connectionProvider, |
| null /* connectionAllocator */, binderCallback); |
| } |
| |
| /** |
| * Creates a ChildProcessLauncher that will create a connection using the specified |
| * ChildConnectionAllocator. |
| */ |
| public static ChildProcessLauncher createWithConnectionAllocator(Delegate delegate, |
| String[] commandLine, FileDescriptorInfo[] filesToBeMapped, |
| ChildConnectionAllocator connectionAllocator, IBinder binderCallback) { |
| return new ChildProcessLauncher(delegate, commandLine, filesToBeMapped, |
| null /* connection */, connectionAllocator, binderCallback); |
| } |
| |
| @SuppressFBWarnings("EI_EXPOSE_REP2") |
| private ChildProcessLauncher(Delegate delegate, String[] commandLine, |
| FileDescriptorInfo[] filesToBeMapped, BoundConnectionProvider connectionProvider, |
| ChildConnectionAllocator connectionAllocator, IBinder binderCallback) { |
| // Either a bound connection provider or a connection allocator should be provided. |
| assert (connectionProvider == null) != (connectionAllocator == null); |
| mCommandLine = commandLine; |
| mConnectionProvider = connectionProvider; |
| mConnectionAllocator = connectionAllocator; |
| mDelegate = delegate; |
| mFilesToBeMapped = filesToBeMapped; |
| mIBinderCallback = binderCallback; |
| } |
| |
| /** |
| * Starts the child process and calls setup on it if {@param setupConnection} is true. |
| * @param setupConnection whether the setup should be performed on the connection once |
| * established |
| * @param queueIfNoFreeConnection whether to queue that request if no service connection is |
| * available. If the launcher was created with a connection provider, this parameter has no |
| * effect. |
| * @return true if the connection was started or was queued. |
| */ |
| public boolean start(final boolean setupConnection, final boolean queueIfNoFreeConnection) { |
| assert isRunningOnLauncherThread(); |
| try { |
| TraceEvent.begin("ChildProcessLauncher.start"); |
| ChildProcessConnection.ServiceCallback serviceCallback = |
| new ChildProcessConnection.ServiceCallback() { |
| @Override |
| public void onChildStarted() {} |
| |
| @Override |
| public void onChildStartFailed() { |
| assert isRunningOnLauncherThread(); |
| Log.e(TAG, "ChildProcessConnection.start failed, trying again"); |
| mLauncherHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| // The child process may already be bound to another client |
| // (this can happen if multi-process WebView is used in more |
| // than one process), so try starting the process again. |
| // This connection that failed to start has not been freed, |
| // so a new bound connection will be allocated. |
| mConnection = null; |
| start(setupConnection, queueIfNoFreeConnection); |
| } |
| }); |
| } |
| |
| @Override |
| public void onChildProcessDied(ChildProcessConnection connection) { |
| assert LauncherThread.runningOnLauncherThread(); |
| assert mConnection == connection; |
| ChildProcessLauncher.this.onChildProcessDied(); |
| } |
| }; |
| if (mConnectionProvider != null) { |
| mConnection = mConnectionProvider.getConnection(serviceCallback); |
| assert mConnection != null; |
| setupConnection(); |
| } else { |
| assert mConnectionAllocator != null; |
| if (!allocateAndSetupConnection( |
| serviceCallback, setupConnection, queueIfNoFreeConnection) |
| && !queueIfNoFreeConnection) { |
| return false; |
| } |
| } |
| return true; |
| } finally { |
| TraceEvent.end("ChildProcessLauncher.start"); |
| } |
| } |
| |
| public ChildProcessConnection getConnection() { |
| return mConnection; |
| } |
| |
| public ChildConnectionAllocator getConnectionAllocator() { |
| return mConnectionAllocator; |
| } |
| |
| private boolean allocateAndSetupConnection( |
| final ChildProcessConnection.ServiceCallback serviceCallback, |
| final boolean setupConnection, final boolean queueIfNoFreeConnection) { |
| assert mConnection == null; |
| Bundle serviceBundle = new Bundle(); |
| mDelegate.onBeforeConnectionAllocated(serviceBundle); |
| |
| mConnection = mConnectionAllocator.allocate( |
| ContextUtils.getApplicationContext(), serviceBundle, serviceCallback); |
| if (mConnection == null) { |
| if (!queueIfNoFreeConnection) { |
| Log.d(TAG, "Failed to allocate a child connection (no queuing)."); |
| return false; |
| } |
| // No connection is available at this time. Add a listener so when one becomes |
| // available we can create the service. |
| mConnectionAllocator.addListener(new ChildConnectionAllocator.Listener() { |
| @Override |
| public void onConnectionFreed( |
| ChildConnectionAllocator allocator, ChildProcessConnection connection) { |
| assert allocator == mConnectionAllocator; |
| if (!allocator.isFreeConnectionAvailable()) return; |
| allocator.removeListener(this); |
| allocateAndSetupConnection( |
| serviceCallback, setupConnection, queueIfNoFreeConnection); |
| } |
| }); |
| return false; |
| } |
| assert mConnection != null; |
| |
| if (setupConnection) { |
| setupConnection(); |
| } |
| return true; |
| } |
| |
| private void setupConnection() { |
| ChildProcessConnection.ConnectionCallback connectionCallback = |
| new ChildProcessConnection.ConnectionCallback() { |
| @Override |
| public void onConnected(ChildProcessConnection connection) { |
| assert mConnection == connection; |
| onServiceConnected(); |
| } |
| }; |
| Bundle connectionBundle = createConnectionBundle(); |
| mDelegate.onBeforeConnectionSetup(connectionBundle); |
| mConnection.setupConnection(connectionBundle, getIBinderCallback(), connectionCallback); |
| } |
| |
| private void onServiceConnected() { |
| assert isRunningOnLauncherThread(); |
| |
| Log.d(TAG, "on connect callback, pid=%d", mConnection.getPid()); |
| |
| mDelegate.onConnectionEstablished(mConnection); |
| |
| // Proactively close the FDs rather than waiting for the GC to do it. |
| try { |
| for (FileDescriptorInfo fileInfo : mFilesToBeMapped) { |
| fileInfo.fd.close(); |
| } |
| } catch (IOException ioe) { |
| Log.w(TAG, "Failed to close FD.", ioe); |
| } |
| } |
| |
| public int getPid() { |
| assert isRunningOnLauncherThread(); |
| return mConnection == null ? NULL_PROCESS_HANDLE : mConnection.getPid(); |
| } |
| |
| public IBinder getIBinderCallback() { |
| return mIBinderCallback; |
| } |
| |
| private boolean isRunningOnLauncherThread() { |
| return mLauncherHandler.getLooper() == Looper.myLooper(); |
| } |
| |
| private Bundle createConnectionBundle() { |
| Bundle bundle = new Bundle(); |
| bundle.putStringArray(ChildProcessConstants.EXTRA_COMMAND_LINE, mCommandLine); |
| bundle.putParcelableArray(ChildProcessConstants.EXTRA_FILES, mFilesToBeMapped); |
| return bundle; |
| } |
| |
| private void onChildProcessDied() { |
| assert LauncherThread.runningOnLauncherThread(); |
| if (getPid() != 0) { |
| mDelegate.onConnectionLost(mConnection); |
| } |
| } |
| |
| public void stop() { |
| assert isRunningOnLauncherThread(); |
| Log.d(TAG, "stopping child connection: pid=%d", mConnection.getPid()); |
| mConnection.stop(); |
| } |
| } |