diff options
Diffstat (limited to 'base/android/java/src/org/chromium/base/process_launcher/ChildProcessLauncher.java')
-rw-r--r-- | base/android/java/src/org/chromium/base/process_launcher/ChildProcessLauncher.java | 278 |
1 files changed, 278 insertions, 0 deletions
diff --git a/base/android/java/src/org/chromium/base/process_launcher/ChildProcessLauncher.java b/base/android/java/src/org/chromium/base/process_launcher/ChildProcessLauncher.java new file mode 100644 index 0000000000..7cdc8528bd --- /dev/null +++ b/base/android/java/src/org/chromium/base/process_launcher/ChildProcessLauncher.java @@ -0,0 +1,278 @@ +// 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) { + assert mConnection == connection; + onServiceConnected(); + } + }; + Bundle connectionBundle = createConnectionBundle(); + mDelegate.onBeforeConnectionSetup(connectionBundle); + mConnection.setupConnection(connectionBundle, getClientInterfaces(), 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 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(); + } +} |