blob: d8737e1bb807607316ed390a299c814c602d78f2 [file] [log] [blame]
// 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();
}
}