// Copyright 2017 The Chromium Authors
// 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.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;
import org.chromium.base.ApplicationState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.Callback;
import org.chromium.base.ChildBindingState;
import org.chromium.base.ContextUtils;
import org.chromium.base.CpuFeatures;
import org.chromium.base.EarlyTraceEvent;
import org.chromium.base.JavaExceptionReporter;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.LibraryLoader.MultiProcessMediator;
import org.chromium.base.process_launcher.ChildConnectionAllocator;
import org.chromium.base.process_launcher.ChildProcessConnection;
import org.chromium.base.process_launcher.ChildProcessConstants;
import org.chromium.base.process_launcher.ChildProcessLauncher;
import org.chromium.base.process_launcher.FileDescriptorInfo;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.content.app.SandboxedProcessService;
import org.chromium.content.common.ContentSwitchUtils;
import org.chromium.content_public.browser.ChildProcessImportance;
import org.chromium.content_public.browser.ContentFeatureMap;
import org.chromium.content_public.common.ContentFeatures;
import org.chromium.content_public.common.ContentSwitches;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This is the java counterpart to ChildProcessLauncherHelper. It is owned by native side and
* has an explicit destroy method.
* Each public or jni methods should have explicit documentation on what threads they are called.
*/
@JNINamespace("content::internal")
public final class ChildProcessLauncherHelperImpl {
private static final String TAG = "ChildProcLH";
// Manifest values used to specify the service names.
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";
// When decrementing the refcount on bindings, delay the decrement by this amount of time in
// case a new ref count is added in the mean time. This is a heuristic to avoid temporarily
// dropping bindings when inputs to calculating importance change independently.
private static final int REMOVE_BINDING_DELAY_MS = 500;
// To be conservative, only delay removing binding in the initial second of the process.
private static final int TIMEOUT_FOR_DELAY_BINDING_REMOVE_MS = 1000;
// Delay after app is background before reducing process priority.
private static final int REDUCE_PRIORITY_ON_BACKGROUND_DELAY_MS = 9 * 1000;
private static final Runnable sDelayedBackgroundTask =
ChildProcessLauncherHelperImpl::onSentToBackgroundOnLauncherThreadAfterDelay;
// Flag to check if ServiceGroupImportance should be enabled after native is initialized.
private static boolean sCheckedServiceGroupImportance;
// A warmed-up connection to a sandboxed service.
private static SpareChildConnection sSpareSandboxedConnection;
// A warmed-up connection to a privileged service (network service).
private static SpareChildConnection sSparePrivilegedConntection;
// Allocator used for sandboxed services.
private static ChildConnectionAllocator sSandboxedChildConnectionAllocator;
private static ChildProcessRanking sSandboxedChildConnectionRanking;
// Map from PID to ChildProcessLauncherHelper.
private static final Map<Integer, ChildProcessLauncherHelperImpl> sLauncherByPid =
new HashMap<>();
// Allocator used for non-sandboxed services.
private static ChildConnectionAllocator sPrivilegedChildConnectionAllocator;
// Used by tests to override the default sandboxed service allocator settings.
private static ChildConnectionAllocator.ConnectionFactory sSandboxedServiceFactoryForTesting;
private static int sSandboxedServicesCountForTesting = -1;
private static String sSandboxedServicesNameForTesting;
private static boolean sSkipDelayForReducePriorityOnBackgroundForTesting;
private static BindingManager sBindingManager;
// Whether the main application is currently brought to the foreground.
private static boolean sApplicationInForegroundOnUiThread;
// Set on UI thread only, but null-checked on launcher thread as well.
private static ApplicationStatus.ApplicationStateListener sAppStateListener;
// TODO(boliu): These are only set for sandboxed renderer processes. Generalize them for
// all types of processes.
private final ChildProcessRanking mRanking;
private final BindingManager mBindingManager;
// Whether the created process should be sandboxed.
private final boolean mSandboxed;
// Remove strong binding when app is in background.
private final boolean mReducePriorityOnBackground;
// The type of process as determined by the command line.
private final String mProcessType;
// Whether the process can use warmed up connection.
private final boolean mCanUseWarmUpConnection;
// Tracks reporting exception from child process to avoid reporting it more than once.
private boolean mReportedException;
// Enables early Java tracing in child process before native is initialized.
private static final String TRACE_EARLY_JAVA_IN_CHILD_SWITCH =
"--" + EarlyTraceEvent.TRACE_EARLY_JAVA_IN_CHILD_SWITCH;
// The first known App Zygote PID. If the app zygote gets restarted, the new bundles from it
// are not sent further for simplicity. Accessed only on LauncherThread.
private static int sZygotePid;
// The bundle with RELRO FD. For sending to child processes, including the ones that did not
// announce whether they inherit from the app zygote. Declared as volatile to allow sending it
// from different threads.
private static volatile Bundle sZygoteBundle;
private final ChildProcessLauncher.Delegate mLauncherDelegate =
new ChildProcessLauncher.Delegate() {
@Override
public ChildProcessConnection getBoundConnection(
ChildConnectionAllocator connectionAllocator,
ChildProcessConnection.ServiceCallback serviceCallback) {
if (!mCanUseWarmUpConnection) return null;
SpareChildConnection spareConnection =
mSandboxed ? sSpareSandboxedConnection : sSparePrivilegedConntection;
if (spareConnection == null) return null;
return spareConnection.getConnection(connectionAllocator, serviceCallback);
}
@Override
public void onBeforeConnectionAllocated(Bundle bundle) {
populateServiceBundle(bundle);
}
@Override
public void onBeforeConnectionSetup(Bundle connectionBundle) {
// Populate the bundle passed to the service setup call with content specific
// parameters.
connectionBundle.putInt(
ContentChildProcessConstants.EXTRA_CPU_COUNT, CpuFeatures.getCount());
connectionBundle.putLong(
ContentChildProcessConstants.EXTRA_CPU_FEATURES, CpuFeatures.getMask());
if (sZygoteBundle != null) {
connectionBundle.putAll(sZygoteBundle);
} else {
LibraryLoader.getInstance()
.getMediator()
.putSharedRelrosToBundle(connectionBundle);
}
}
@Override
public void onConnectionEstablished(ChildProcessConnection connection) {
assert LauncherThread.runningOnLauncherThread();
int pid = connection.getPid();
if (pid > 0) {
sLauncherByPid.put(pid, ChildProcessLauncherHelperImpl.this);
if (mRanking != null) {
mRanking.addConnection(
connection,
/* visible= */ false,
/* frameDepth= */ 1,
/* intersectsViewport= */ false,
ChildProcessImportance.MODERATE);
if (mBindingManager != null) mBindingManager.rankingChanged();
}
if (mSandboxed) {
ChildProcessConnectionMetrics.getInstance().addConnection(connection);
}
if (mReducePriorityOnBackground
&& !ApplicationStatus.hasVisibleActivities()) {
reducePriorityOnBackgroundOnLauncherThread();
}
}
// Tell native launch result (whether getPid is 0).
if (mNativeChildProcessLauncherHelper != 0) {
ChildProcessLauncherHelperImplJni.get()
.onChildProcessStarted(
mNativeChildProcessLauncherHelper, connection.getPid());
}
mNativeChildProcessLauncherHelper = 0;
}
@Override
public void onReceivedZygoteInfo(
ChildProcessConnection connection, Bundle relroBundle) {
assert LauncherThread.runningOnLauncherThread();
distributeZygoteInfo(connection, relroBundle);
}
@Override
public void onConnectionLost(ChildProcessConnection connection) {
assert LauncherThread.runningOnLauncherThread();
if (connection.getPid() == 0) return;
ChildProcessLauncherHelperImpl result =
sLauncherByPid.remove(connection.getPid());
// Child process might die before onConnectionEstablished.
if (result == null) return;
if (mBindingManager != null) mBindingManager.removeConnection(connection);
if (mRanking != null) {
mRanking.removeConnection(connection);
if (mBindingManager != null) mBindingManager.rankingChanged();
}
if (mSandboxed) {
ChildProcessConnectionMetrics.getInstance().removeConnection(connection);
}
}
};
/**
* Called for every new child connection. Receives a possibly null bundle inherited from the App
* Zygote. Sends the bundle to existing processes that did not have usable bundles or sends
* a previously memoized bundle to the new child.
*
* @param connection the connection to the new child
* @param zygoteBundle the bundle received from the child process, null means that either the
* process did not inherit from the app zygote or the app zygote did not
* produce a usable RELRO region.
*/
private static void distributeZygoteInfo(
ChildProcessConnection connection, @Nullable Bundle zygoteBundle) {
if (LibraryLoader.mainProcessIntendsToProvideRelroFd()) return;
if (!connection.hasUsableZygoteInfo()) {
Log.d(TAG, "Connection likely not created from app zygote");
sendPreviouslySeenZygoteBundle(connection);
return;
}
// If the process was created from the app zygote, but failed to generate the the zygote
// bundle - ignore it.
if (zygoteBundle == null) {
return;
}
if (sZygotePid != 0) {
Log.d(TAG, "Zygote was seen before with a usable RELRO bundle.");
onObtainedUsableZygoteBundle(connection);
return;
}
Log.d(TAG, "Encountered the first usable RELRO bundle.");
sZygotePid = connection.getZygotePid();
sZygoteBundle = zygoteBundle;
// Use the RELRO FD in the current process. Some nontrivial CPU cycles are consumed because
// it needs an mmap+memcmp(5 megs)+mmap+munmap. This happens on the process launcher thread,
// will work correctly on any thread.
LibraryLoader.getInstance().getMediator().takeSharedRelrosFromBundle(zygoteBundle);
// Use the RELRO FD for all processes launched up to now. Non-blocking 'oneway' IPCs are
// used. The CPU time costs in the child process are the same.
sendPreviouslySeenZygoteBundleToExistingConnections(connection.getPid());
}
private static void onObtainedUsableZygoteBundle(ChildProcessConnection connection) {
if (sZygotePid != connection.getZygotePid()) {
Log.d(TAG, "Zygote restarted.");
return;
}
// TODO(pasko): To avoid accumulating open file descriptors close the received RELRO FD
// if it cannot be used.
}
private static void sendPreviouslySeenZygoteBundle(ChildProcessConnection connection) {
if (sZygotePid != 0 && sZygoteBundle != null) {
connection.consumeZygoteBundle(sZygoteBundle);
}
}
private static void sendPreviouslySeenZygoteBundleToExistingConnections(int pid) {
for (var entry : sLauncherByPid.entrySet()) {
int otherPid = entry.getKey();
if (pid != otherPid) {
ChildProcessConnection otherConnection = entry.getValue().mLauncher.getConnection();
if (otherConnection.getZygotePid() == 0) {
// The Zygote PID for each connection must be finalized before the launcher
// thread starts processing the zygote info. Zygote PID being 0 guarantees that
// the zygote did not produce the RELRO region.
otherConnection.consumeZygoteBundle(sZygoteBundle);
}
}
}
}
private final ChildProcessLauncher mLauncher;
private long mNativeChildProcessLauncherHelper;
private long mStartTimeMs;
// This is the current computed importance from all the inputs from setPriority.
// The initial value is MODERATE since a newly created connection has visible bindings.
private @ChildProcessImportance int mEffectiveImportance = ChildProcessImportance.MODERATE;
private boolean mVisible;
private boolean mDroppedStrongBingingDueToBackgrounding;
@CalledByNative
private static FileDescriptorInfo makeFdInfo(
int id, int fd, boolean autoClose, long offset, long size) {
assert LauncherThread.runningOnLauncherThread();
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);
}
@CalledByNative
private static ChildProcessLauncherHelperImpl createAndStart(
long nativePointer,
String[] commandLine,
FileDescriptorInfo[] filesToBeMapped,
boolean canUseWarmUpConnection,
@Nullable IBinder binderBox) {
assert LauncherThread.runningOnLauncherThread();
String processType =
ContentSwitchUtils.getSwitchValue(commandLine, ContentSwitches.SWITCH_PROCESS_TYPE);
if (TraceEvent.enabled()) {
commandLine = Arrays.copyOf(commandLine, commandLine.length + 1);
commandLine[commandLine.length - 1] = TRACE_EARLY_JAVA_IN_CHILD_SWITCH;
}
boolean sandboxed = true;
boolean reducePriorityOnBackground = false;
if (!ContentSwitches.SWITCH_RENDERER_PROCESS.equals(processType)) {
if (ContentSwitches.SWITCH_GPU_PROCESS.equals(processType)) {
sandboxed = false;
reducePriorityOnBackground =
ContentFeatureMap.isEnabled(
ContentFeatures.REDUCE_GPU_PRIORITY_ON_BACKGROUND);
} else {
// We only support sandboxed utility processes now.
assert ContentSwitches.SWITCH_UTILITY_PROCESS.equals(processType);
if (ContentSwitches.NONE_SANDBOX_TYPE.equals(
ContentSwitchUtils.getSwitchValue(
commandLine, ContentSwitches.SWITCH_SERVICE_SANDBOX_TYPE))) {
sandboxed = false;
}
}
}
IBinder binderCallback =
ContentSwitches.SWITCH_GPU_PROCESS.equals(processType)
? new GpuProcessCallback()
: null;
ChildProcessLauncherHelperImpl helper =
new ChildProcessLauncherHelperImpl(
nativePointer,
commandLine,
filesToBeMapped,
sandboxed,
reducePriorityOnBackground,
canUseWarmUpConnection,
binderCallback,
binderBox);
helper.start();
if (sandboxed && !sCheckedServiceGroupImportance) {
sCheckedServiceGroupImportance = true;
if (sSandboxedChildConnectionRanking != null
&& ChildProcessLauncherHelperImplJni.get().serviceGroupImportanceEnabled()) {
sSandboxedChildConnectionRanking.enableServiceGroupImportance();
}
}
return helper;
}
/**
* @see {@link ChildProcessLauncherHelper#warmUp(Context)}.
*/
public static void warmUpOnAnyThread(final Context context, boolean sandboxed) {
LauncherThread.post(
new Runnable() {
@Override
public void run() {
warmUpOnLauncherThread(context, sandboxed);
}
});
}
private static void warmUpOnLauncherThread(Context context, boolean sandboxed) {
SpareChildConnection spareConnection =
sandboxed ? sSpareSandboxedConnection : sSparePrivilegedConntection;
if (spareConnection != null && !spareConnection.isEmpty()) {
return;
}
Bundle serviceBundle = populateServiceBundle(new Bundle());
ChildConnectionAllocator allocator = getConnectionAllocator(context, sandboxed);
if (sandboxed) {
sSpareSandboxedConnection = new SpareChildConnection(context, allocator, serviceBundle);
} else {
sSparePrivilegedConntection =
new SpareChildConnection(context, allocator, serviceBundle);
}
}
/**
* @see {@link ChildProcessLauncherHelper#startBindingManagement(Context)}.
*/
public static void startBindingManagement(final Context context) {
assert ThreadUtils.runningOnUiThread();
LauncherThread.post(
new Runnable() {
@Override
public void run() {
ChildConnectionAllocator allocator =
getConnectionAllocator(context, /* sandboxed= */ true);
if (ChildProcessConnection.supportVariableConnections()) {
sBindingManager =
new BindingManager(
context,
BindingManager.NO_MAX_SIZE,
sSandboxedChildConnectionRanking);
} else {
sBindingManager =
new BindingManager(
context,
allocator.getNumberOfServices(),
sSandboxedChildConnectionRanking);
}
ChildProcessConnectionMetrics.getInstance()
.setBindingManager(sBindingManager);
}
});
}
private static void onSentToBackground() {
assert ThreadUtils.runningOnUiThread();
sApplicationInForegroundOnUiThread = false;
int delay =
sSkipDelayForReducePriorityOnBackgroundForTesting
? 0
: REDUCE_PRIORITY_ON_BACKGROUND_DELAY_MS;
LauncherThread.postDelayed(sDelayedBackgroundTask, delay);
LauncherThread.post(
() -> {
if (sBindingManager != null) sBindingManager.onSentToBackground();
});
}
private static void onSentToBackgroundOnLauncherThreadAfterDelay() {
assert LauncherThread.runningOnLauncherThread();
for (ChildProcessLauncherHelperImpl helper : sLauncherByPid.values()) {
if (!helper.mReducePriorityOnBackground) continue;
helper.reducePriorityOnBackgroundOnLauncherThread();
}
}
private void reducePriorityOnBackgroundOnLauncherThread() {
assert LauncherThread.runningOnLauncherThread();
if (mDroppedStrongBingingDueToBackgrounding) return;
ChildProcessConnection connection = mLauncher.getConnection();
if (!connection.isConnected()) return;
if (connection.isStrongBindingBound()) {
connection.removeStrongBinding();
mDroppedStrongBingingDueToBackgrounding = true;
}
}
private void raisePriorityOnForegroundOnLauncherThread() {
assert LauncherThread.runningOnLauncherThread();
if (!mDroppedStrongBingingDueToBackgrounding) return;
ChildProcessConnection connection = mLauncher.getConnection();
if (!connection.isConnected()) return;
connection.addStrongBinding();
mDroppedStrongBingingDueToBackgrounding = false;
}
private static void onBroughtToForeground() {
assert ThreadUtils.runningOnUiThread();
sApplicationInForegroundOnUiThread = true;
LauncherThread.removeCallbacks(sDelayedBackgroundTask);
LauncherThread.post(
() -> {
for (ChildProcessLauncherHelperImpl helper : sLauncherByPid.values()) {
if (!helper.mReducePriorityOnBackground) continue;
helper.raisePriorityOnForegroundOnLauncherThread();
}
if (sBindingManager != null) sBindingManager.onBroughtToForeground();
});
}
public static void setSandboxServicesSettingsForTesting(
ChildConnectionAllocator.ConnectionFactory factory,
int serviceCount,
String serviceName) {
sSandboxedServiceFactoryForTesting = factory;
sSandboxedServicesCountForTesting = serviceCount;
sSandboxedServicesNameForTesting = serviceName;
}
public static void setSkipDelayForReducePriorityOnBackgroundForTesting() {
sSkipDelayForReducePriorityOnBackgroundForTesting = true;
}
@VisibleForTesting
static ChildConnectionAllocator getConnectionAllocator(Context context, boolean sandboxed) {
assert LauncherThread.runningOnLauncherThread();
boolean bindToCaller = ChildProcessCreationParamsImpl.getBindToCallerCheck();
boolean bindAsExternalService =
sandboxed && ChildProcessCreationParamsImpl.getIsSandboxedServiceExternal();
if (!sandboxed) {
if (sPrivilegedChildConnectionAllocator == null) {
sPrivilegedChildConnectionAllocator =
ChildConnectionAllocator.create(
context,
LauncherThread.getHandler(),
null,
ChildProcessCreationParamsImpl.getPackageNameForPrivilegedService(),
ChildProcessCreationParamsImpl.getPrivilegedServicesName(),
NUM_PRIVILEGED_SERVICES_KEY,
bindToCaller,
bindAsExternalService,
/* useStrongBinding= */ true);
}
return sPrivilegedChildConnectionAllocator;
}
if (sSandboxedChildConnectionAllocator == null) {
final String packageName =
ChildProcessCreationParamsImpl.getPackageNameForSandboxedService();
Log.d(
TAG,
"Create a new ChildConnectionAllocator with package name = %s,"
+ " sandboxed = true",
packageName);
Runnable freeSlotRunnable =
() -> {
ChildProcessConnection lowestRank =
sSandboxedChildConnectionRanking.getLowestRankedConnection();
if (lowestRank != null) {
lowestRank.kill();
}
};
ChildConnectionAllocator connectionAllocator = null;
if (sSandboxedServicesCountForTesting != -1) {
// Testing case where allocator settings are overridden.
String serviceName =
!TextUtils.isEmpty(sSandboxedServicesNameForTesting)
? sSandboxedServicesNameForTesting
: SandboxedProcessService.class.getName();
connectionAllocator =
ChildConnectionAllocator.createFixedForTesting(
freeSlotRunnable,
packageName,
serviceName,
sSandboxedServicesCountForTesting,
bindToCaller,
bindAsExternalService,
/* useStrongBinding= */ false);
} else if (ChildProcessConnection.supportVariableConnections()) {
connectionAllocator =
ChildConnectionAllocator.createVariableSize(
context,
LauncherThread.getHandler(),
freeSlotRunnable,
packageName,
ChildProcessCreationParamsImpl.getSandboxedServicesName(),
bindToCaller,
bindAsExternalService,
/* useStrongBinding= */ false);
} else {
connectionAllocator =
ChildConnectionAllocator.create(
context,
LauncherThread.getHandler(),
freeSlotRunnable,
packageName,
ChildProcessCreationParamsImpl.getSandboxedServicesName(),
NUM_SANDBOXED_SERVICES_KEY,
bindToCaller,
bindAsExternalService,
/* useStrongBinding= */ false);
}
if (sSandboxedServiceFactoryForTesting != null) {
connectionAllocator.setConnectionFactoryForTesting(
sSandboxedServiceFactoryForTesting);
}
sSandboxedChildConnectionAllocator = connectionAllocator;
if (ChildProcessConnection.supportVariableConnections()) {
sSandboxedChildConnectionRanking = new ChildProcessRanking();
} else {
sSandboxedChildConnectionRanking =
new ChildProcessRanking(
sSandboxedChildConnectionAllocator.getNumberOfServices());
}
}
return sSandboxedChildConnectionAllocator;
}
private ChildProcessLauncherHelperImpl(
long nativePointer,
String[] commandLine,
FileDescriptorInfo[] filesToBeMapped,
boolean sandboxed,
boolean reducePriorityOnBackground,
boolean canUseWarmUpConnection,
IBinder binderCallback,
IBinder binderBox) {
assert LauncherThread.runningOnLauncherThread();
mNativeChildProcessLauncherHelper = nativePointer;
mSandboxed = sandboxed;
mReducePriorityOnBackground = reducePriorityOnBackground;
mCanUseWarmUpConnection = canUseWarmUpConnection;
ChildConnectionAllocator connectionAllocator =
getConnectionAllocator(ContextUtils.getApplicationContext(), sandboxed);
mLauncher =
new ChildProcessLauncher(
LauncherThread.getHandler(),
mLauncherDelegate,
commandLine,
filesToBeMapped,
connectionAllocator,
binderCallback == null ? null : Arrays.asList(binderCallback),
binderBox);
mProcessType =
ContentSwitchUtils.getSwitchValue(commandLine, ContentSwitches.SWITCH_PROCESS_TYPE);
if (sandboxed) {
mRanking = sSandboxedChildConnectionRanking;
mBindingManager = sBindingManager;
} else {
mRanking = null;
mBindingManager = null;
}
if (!ApplicationStatus.isInitialized()) return;
if (sAppStateListener != null) return;
PostTask.postTask(
TaskTraits.UI_BEST_EFFORT,
() -> {
if (sAppStateListener != null) return;
sApplicationInForegroundOnUiThread = ApplicationStatus.hasVisibleActivities();
sAppStateListener =
newState -> {
switch (newState) {
case ApplicationState.UNKNOWN:
break;
case ApplicationState.HAS_RUNNING_ACTIVITIES:
case ApplicationState.HAS_PAUSED_ACTIVITIES:
if (!sApplicationInForegroundOnUiThread) {
onBroughtToForeground();
}
break;
default:
if (sApplicationInForegroundOnUiThread) {
onSentToBackground();
}
break;
}
};
ApplicationStatus.registerApplicationStateListener(sAppStateListener);
});
}
private void start() {
mLauncher.start(/* doSetupConnection= */ true, /* queueIfNoFreeConnection= */ true);
mStartTimeMs = System.currentTimeMillis();
}
/**
* @return The type of process as specified in the command line at
* {@link ContentSwitches#SWITCH_PROCESS_TYPE}.
*/
private String getProcessType() {
return TextUtils.isEmpty(mProcessType) ? "" : mProcessType;
}
// Called on client (UI or IO) thread.
@CalledByNative
private void getTerminationInfoAndStop(long terminationInfoPtr) {
ChildProcessConnection connection = mLauncher.getConnection();
// Here we are accessing the connection from a thread other than the launcher thread, but it
// does not change once it's been set. So it is safe to test whether it's null here and
// access it afterwards.
if (connection == null) return;
// Note there is no guarantee that connection lost has happened. However ChildProcessRanking
// is not thread safe, so this is the best we can do.
String exceptionString = connection.getExceptionDuringInit();
if (exceptionString != null && !mReportedException) {
mReportedException = true;
PostTask.postTask(
TaskTraits.UI_BEST_EFFORT,
() -> JavaExceptionReporter.reportStackTrace(exceptionString));
}
ChildProcessLauncherHelperImplJni.get()
.setTerminationInfo(
terminationInfoPtr,
connection.bindingStateCurrentOrWhenDied(),
connection.isKilledByUs(),
connection.hasCleanExit(),
exceptionString != null);
LauncherThread.post(() -> mLauncher.stop());
}
@CalledByNative
private void setPriority(
int pid,
boolean visible,
boolean hasMediaStream,
boolean hasForegroundServiceWorker,
long frameDepth,
boolean intersectsViewport,
boolean boostForPendingViews,
boolean boostForLoading,
@ChildProcessImportance int importance) {
assert LauncherThread.runningOnLauncherThread();
assert mLauncher.getPid() == pid
: "The provided pid ("
+ pid
+ ") did not match the launcher's pid ("
+ mLauncher.getPid()
+ ").";
if (getByPid(pid) == null) {
// Child already disconnected. Ignore any trailing calls.
return;
}
ChildProcessConnection connection = mLauncher.getConnection();
if (ChildProcessCreationParamsImpl.getIgnoreVisibilityForImportance()) {
visible = false;
boostForPendingViews = false;
}
@ChildProcessImportance int newEffectiveImportance;
if ((visible && frameDepth == 0)
|| importance == ChildProcessImportance.IMPORTANT
|| hasMediaStream) {
newEffectiveImportance = ChildProcessImportance.IMPORTANT;
} else if ((visible && frameDepth > 0 && intersectsViewport)
|| boostForPendingViews
|| importance == ChildProcessImportance.MODERATE
|| hasForegroundServiceWorker
|| boostForLoading) {
newEffectiveImportance = ChildProcessImportance.MODERATE;
} else {
newEffectiveImportance = ChildProcessImportance.NORMAL;
}
// Add first and remove second.
if (visible && !mVisible) {
if (mBindingManager != null) mBindingManager.addConnection(connection);
}
mVisible = visible;
if (mEffectiveImportance != newEffectiveImportance) {
switch (newEffectiveImportance) {
case ChildProcessImportance.NORMAL:
// Nothing to add.
break;
case ChildProcessImportance.MODERATE:
connection.addVisibleBinding();
break;
case ChildProcessImportance.IMPORTANT:
connection.addStrongBinding();
break;
default:
assert false;
}
}
if (mRanking != null) {
mRanking.updateConnection(
connection, visible, frameDepth, intersectsViewport, importance);
if (mBindingManager != null) mBindingManager.rankingChanged();
}
if (mEffectiveImportance != newEffectiveImportance
&& mEffectiveImportance != ChildProcessImportance.NORMAL) {
final int existingEffectiveImportance = mEffectiveImportance;
Runnable removeBindingRunnable =
() -> {
switch (existingEffectiveImportance) {
case ChildProcessImportance.NORMAL:
// Nothing to remove.
break;
case ChildProcessImportance.MODERATE:
connection.removeVisibleBinding();
break;
case ChildProcessImportance.IMPORTANT:
connection.removeStrongBinding();
break;
default:
assert false;
}
};
if (System.currentTimeMillis() - mStartTimeMs < TIMEOUT_FOR_DELAY_BINDING_REMOVE_MS) {
LauncherThread.postDelayed(removeBindingRunnable, REMOVE_BINDING_DELAY_MS);
} else {
removeBindingRunnable.run();
}
}
mEffectiveImportance = newEffectiveImportance;
}
@CalledByNative
static void stop(int pid) {
assert LauncherThread.runningOnLauncherThread();
Log.d(TAG, "stopping child connection: pid=%d", pid);
ChildProcessLauncherHelperImpl launcher = getByPid(pid);
// launcher can be null for single process.
if (launcher != null) {
// Can happen for single process.
launcher.mLauncher.stop();
}
}
// Called on client (UI or IO) thread.
@CalledByNative
private @ChildBindingState int getEffectiveChildBindingState() {
ChildProcessConnection connection = mLauncher.getConnection();
// Here we are accessing the connection from a thread other than the launcher thread, but it
// does not change once it's been set. So it is safe to test whether it's null here and
// access it afterwards.
if (connection == null) return ChildBindingState.UNBOUND;
return connection.bindingStateCurrent();
}
/**
* Dumps the stack of the child process with |pid| without crashing it.
* @param pid Process id of the child process.
*/
@CalledByNative
private void dumpProcessStack(int pid) {
assert LauncherThread.runningOnLauncherThread();
ChildProcessLauncherHelperImpl launcher = getByPid(pid);
if (launcher != null) {
ChildProcessConnection connection = launcher.mLauncher.getConnection();
connection.dumpProcessStack();
}
}
private static Bundle populateServiceBundle(Bundle bundle) {
ChildProcessCreationParamsImpl.addIntentExtras(bundle);
bundle.putBoolean(
ChildProcessConstants.EXTRA_BIND_TO_CALLER,
ChildProcessCreationParamsImpl.getBindToCallerCheck());
MultiProcessMediator m = LibraryLoader.getInstance().getMediator();
m.ensureInitializedInMainProcess();
m.putLoadAddressToBundle(bundle);
return bundle;
}
private static ChildProcessLauncherHelperImpl getByPid(int pid) {
return sLauncherByPid.get(pid);
}
/**
* Groups all currently tracked processes by type and returns a map of type -> list of PIDs.
*
* @param callback The callback to notify with the process information. {@code callback} will
* run on the same thread this method is called on. That thread must support a
* {@link android.os.Looper}.
*/
public static void getProcessIdsByType(Callback<Map<String, List<Integer>>> callback) {
final Handler responseHandler = new Handler();
LauncherThread.post(
() -> {
Map<String, List<Integer>> map = new HashMap<>();
for (var entry : sLauncherByPid.entrySet()) {
String type = entry.getValue().getProcessType();
List<Integer> pids = map.get(type);
if (pids == null) {
pids = new ArrayList<>();
map.put(type, pids);
}
pids.add(entry.getKey());
}
responseHandler.post(callback.bind(map));
});
}
// Testing only related methods.
int getPidForTesting() {
assert LauncherThread.runningOnLauncherThread();
return mLauncher.getPid();
}
public static Map<Integer, ChildProcessLauncherHelperImpl> getAllProcessesForTesting() {
return sLauncherByPid;
}
public static ChildProcessLauncherHelperImpl createAndStartForTesting(
String[] commandLine,
FileDescriptorInfo[] filesToBeMapped,
boolean sandboxed,
boolean reducePriorityOnBackground,
boolean canUseWarmUpConnection,
IBinder binderCallback,
boolean doSetupConnection) {
ChildProcessLauncherHelperImpl launcherHelper =
new ChildProcessLauncherHelperImpl(
0L,
commandLine,
filesToBeMapped,
sandboxed,
reducePriorityOnBackground,
canUseWarmUpConnection,
binderCallback,
null);
launcherHelper.mLauncher.start(doSetupConnection, /* queueIfNoFreeConnection= */ true);
return launcherHelper;
}
/** @return the count of services set-up and working. */
static int getConnectedServicesCountForTesting() {
int count =
sPrivilegedChildConnectionAllocator == null
? 0
: sPrivilegedChildConnectionAllocator.allocatedConnectionsCountForTesting();
return count + getConnectedSandboxedServicesCountForTesting();
}
public static int getConnectedSandboxedServicesCountForTesting() {
return sSandboxedChildConnectionAllocator == null
? 0
: sSandboxedChildConnectionAllocator.allocatedConnectionsCountForTesting();
}
@VisibleForTesting
public ChildProcessConnection getChildProcessConnection() {
return mLauncher.getConnection();
}
public ChildConnectionAllocator getChildConnectionAllocatorForTesting() {
return mLauncher.getConnectionAllocator();
}
public static ChildProcessConnection getWarmUpConnectionForTesting(boolean sandboxed) {
SpareChildConnection connection =
sandboxed ? sSpareSandboxedConnection : sSparePrivilegedConntection;
return connection == null ? null : connection.getConnection();
}
@NativeMethods
interface Natives {
// Can be called on a number of threads, including launcher, and binder.
void onChildProcessStarted(long nativeChildProcessLauncherHelper, int pid);
void setTerminationInfo(
long termiantionInfoPtr,
@ChildBindingState int bindingState,
boolean killedByUs,
boolean cleanExit,
boolean exceptionDuringInit);
boolean serviceGroupImportanceEnabled();
}
}