| // 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.base.process_launcher; |
| |
| 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 java.io.IOException; |
| import java.util.List; |
| |
| /** |
| * 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 abstract static class Delegate { |
| /** |
| * Called when the launcher is about to start. Gives the embedder a chance to provide an |
| * already bound connection if it has one. (allowing for warm-up connections: connections |
| * that are already bound in advance to speed up child process start-up time). |
| * Note that onBeforeConnectionAllocated will not be called if this method returns a |
| * connection. |
| * @param connectionAllocator the allocator the returned connection should have been |
| * allocated of. |
| * @param serviceCallback the service callback that the connection should use. |
| * @return a bound connection to use to connect to the child process service, or null if a |
| * connection should be allocated and bound by the launcher. |
| */ |
| public ChildProcessConnection getBoundConnection( |
| ChildConnectionAllocator connectionAllocator, |
| ChildProcessConnection.ServiceCallback serviceCallback) { |
| return null; |
| } |
| |
| /** |
| * 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. |
| */ |
| public 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. |
| */ |
| public 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. |
| */ |
| public 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. |
| */ |
| public void onConnectionLost(ChildProcessConnection connection) {} |
| } |
| |
| // 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; |
| |
| private final Delegate mDelegate; |
| |
| private final String[] mCommandLine; |
| private final FileDescriptorInfo[] mFilesToBeMapped; |
| |
| // The allocator used to create the connection. |
| private final ChildConnectionAllocator mConnectionAllocator; |
| |
| // The IBinder interfaces provided to the created service. |
| private final List<IBinder> mClientInterfaces; |
| |
| // The actual service connection. Set once we have connected to the service. |
| private ChildProcessConnection mConnection; |
| |
| /** |
| * Constructor. |
| * |
| * @param launcherHandler the handler for the thread where all operations should happen. |
| * @param delegate the delagate that gets notified of the launch progress. |
| * @param commandLine the command line that should be passed to the started process. |
| * @param filesToBeMapped the files that should be passed to the started process. |
| * @param connectionAllocator the allocator used to create connections to the service. |
| * @param clientInterfaces the interfaces that should be passed to the started process so it can |
| * communicate with the parent process. |
| */ |
| public ChildProcessLauncher(Handler launcherHandler, Delegate delegate, String[] commandLine, |
| FileDescriptorInfo[] filesToBeMapped, ChildConnectionAllocator connectionAllocator, |
| List<IBinder> clientInterfaces) { |
| assert connectionAllocator != null; |
| mLauncherHandler = launcherHandler; |
| isRunningOnLauncherThread(); |
| mCommandLine = commandLine; |
| mConnectionAllocator = connectionAllocator; |
| mDelegate = delegate; |
| mFilesToBeMapped = filesToBeMapped; |
| mClientInterfaces = clientInterfaces; |
| } |
| |
| /** |
| * 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(ChildProcessConnection connection) { |
| assert isRunningOnLauncherThread(); |
| assert mConnection == connection; |
| 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 isRunningOnLauncherThread(); |
| assert mConnection == connection; |
| ChildProcessLauncher.this.onChildProcessDied(); |
| } |
| }; |
| mConnection = mDelegate.getBoundConnection(mConnectionAllocator, serviceCallback); |
| if (mConnection != null) { |
| assert mConnectionAllocator.isConnectionFromAllocator(mConnection); |
| setupConnection(); |
| return true; |
| } |
| 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; |
| } |
| mConnectionAllocator.queueAllocation( |
| () -> allocateAndSetupConnection( |
| serviceCallback, setupConnection, queueIfNoFreeConnection)); |
| return false; |
| } |
| |
| if (setupConnection) { |
| setupConnection(); |
| } |
| return true; |
| } |
| |
| private void setupConnection() { |
| ChildProcessConnection.ConnectionCallback connectionCallback = |
| new ChildProcessConnection.ConnectionCallback() { |
| @Override |
| public void onConnected(ChildProcessConnection connection) { |
| onServiceConnected(connection); |
| } |
| }; |
| Bundle connectionBundle = createConnectionBundle(); |
| mDelegate.onBeforeConnectionSetup(connectionBundle); |
| mConnection.setupConnection(connectionBundle, getClientInterfaces(), connectionCallback); |
| } |
| |
| private void onServiceConnected(ChildProcessConnection connection) { |
| assert isRunningOnLauncherThread(); |
| assert mConnection == connection || connection == null; |
| |
| 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 List<IBinder> getClientInterfaces() { |
| return mClientInterfaces; |
| } |
| |
| 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 isRunningOnLauncherThread(); |
| if (getPid() != 0) { |
| mDelegate.onConnectionLost(mConnection); |
| } |
| } |
| |
| public void stop() { |
| assert isRunningOnLauncherThread(); |
| Log.d(TAG, "stopping child connection: pid=%d", mConnection.getPid()); |
| mConnection.stop(); |
| } |
| } |