Merge "Track the child processes that are forked by app processes"
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index efea953..d7393ca 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -32,6 +32,7 @@
import libcore.io.IoUtils;
import java.io.FileDescriptor;
+import java.io.IOException;
import java.util.Map;
import java.util.concurrent.TimeoutException;
@@ -1317,33 +1318,16 @@
*/
public static void waitForProcessDeath(int pid, int timeout)
throws InterruptedException, TimeoutException {
- FileDescriptor pidfd = null;
- if (sPidFdSupported == PIDFD_UNKNOWN) {
- int fd = -1;
+ boolean fallback = supportsPidFd();
+ if (!fallback) {
+ FileDescriptor pidfd = null;
try {
- fd = nativePidFdOpen(pid, 0);
- sPidFdSupported = PIDFD_SUPPORTED;
- } catch (ErrnoException e) {
- sPidFdSupported = e.errno != OsConstants.ENOSYS
- ? PIDFD_SUPPORTED : PIDFD_UNSUPPORTED;
- } finally {
+ final int fd = nativePidFdOpen(pid, 0);
if (fd >= 0) {
pidfd = new FileDescriptor();
pidfd.setInt$(fd);
- }
- }
- }
- boolean fallback = sPidFdSupported == PIDFD_UNSUPPORTED;
- if (!fallback) {
- try {
- if (pidfd == null) {
- int fd = nativePidFdOpen(pid, 0);
- if (fd >= 0) {
- pidfd = new FileDescriptor();
- pidfd.setInt$(fd);
- } else {
- fallback = true;
- }
+ } else {
+ fallback = true;
}
if (pidfd != null) {
StructPollfd[] fds = new StructPollfd[] {
@@ -1392,5 +1376,59 @@
throw new TimeoutException();
}
+ /**
+ * Determine whether the system supports pidfd APIs
+ *
+ * @return Returns true if the system supports pidfd APIs
+ * @hide
+ */
+ public static boolean supportsPidFd() {
+ if (sPidFdSupported == PIDFD_UNKNOWN) {
+ int fd = -1;
+ try {
+ fd = nativePidFdOpen(myPid(), 0);
+ sPidFdSupported = PIDFD_SUPPORTED;
+ } catch (ErrnoException e) {
+ sPidFdSupported = e.errno != OsConstants.ENOSYS
+ ? PIDFD_SUPPORTED : PIDFD_UNSUPPORTED;
+ } finally {
+ if (fd >= 0) {
+ final FileDescriptor f = new FileDescriptor();
+ f.setInt$(fd);
+ IoUtils.closeQuietly(f);
+ }
+ }
+ }
+ return sPidFdSupported == PIDFD_SUPPORTED;
+ }
+
+ /**
+ * Open process file descriptor for given pid.
+ *
+ * @param pid The process ID to open for
+ * @param flags Reserved, unused now, must be 0
+ * @return The process file descriptor for given pid
+ * @throws IOException if it can't be opened
+ *
+ * @hide
+ */
+ public static @Nullable FileDescriptor openPidFd(int pid, int flags) throws IOException {
+ if (!supportsPidFd()) {
+ return null;
+ }
+ if (flags != 0) {
+ throw new IllegalArgumentException();
+ }
+ try {
+ FileDescriptor pidfd = new FileDescriptor();
+ pidfd.setInt$(nativePidFdOpen(pid, flags));
+ return pidfd;
+ } catch (ErrnoException e) {
+ IOException ex = new IOException();
+ ex.initCause(e);
+ throw ex;
+ }
+ }
+
private static native int nativePidFdOpen(int pid, int flags) throws ErrnoException;
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index fede1d2..48055b5 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -26,6 +26,7 @@
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
+import android.os.Message;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.OnPropertiesChangedListener;
import android.provider.DeviceConfig.Properties;
@@ -124,6 +125,7 @@
private static final long DEFAULT_TOP_TO_FGS_GRACE_DURATION = 15 * 1000;
private static final int DEFAULT_PENDINGINTENT_WARNING_THRESHOLD = 2000;
private static final int DEFAULT_MIN_CRASH_INTERVAL = 2 * 60 * 1000;
+ private static final int DEFAULT_MAX_PHANTOM_PROCESSES = 32;
// Flag stored in the DeviceConfig API.
@@ -133,6 +135,11 @@
private static final String KEY_MAX_CACHED_PROCESSES = "max_cached_processes";
/**
+ * Maximum number of cached processes.
+ */
+ private static final String KEY_MAX_PHANTOM_PROCESSES = "max_phantom_processes";
+
+ /**
* Default value for mFlagBackgroundActivityStartsEnabled if not explicitly set in
* Settings.Global. This allows it to be set experimentally unless it has been
* enabled/disabled in developer options. Defaults to false.
@@ -364,6 +371,11 @@
*/
public final ArraySet<ComponentName> KEEP_WARMING_SERVICES = new ArraySet<ComponentName>();
+ /**
+ * Maximum number of phantom processes.
+ */
+ public int MAX_PHANTOM_PROCESSES = DEFAULT_MAX_PHANTOM_PROCESSES;
+
private List<String> mDefaultImperceptibleKillExemptPackages;
private List<Integer> mDefaultImperceptibleKillExemptProcStates;
@@ -481,6 +493,9 @@
case KEY_BINDER_HEAVY_HITTER_AUTO_SAMPLER_THRESHOLD:
updateBinderHeavyHitterWatcher();
break;
+ case KEY_MAX_PHANTOM_PROCESSES:
+ updateMaxPhantomProcesses();
+ break;
default:
break;
}
@@ -599,6 +614,8 @@
// with defaults.
Slog.e("ActivityManagerConstants", "Bad activity manager config settings", e);
}
+ final long currentPowerCheckInterval = POWER_CHECK_INTERVAL;
+
BACKGROUND_SETTLE_TIME = mParser.getLong(KEY_BACKGROUND_SETTLE_TIME,
DEFAULT_BACKGROUND_SETTLE_TIME);
FGSERVICE_MIN_SHOWN_TIME = mParser.getLong(KEY_FGSERVICE_MIN_SHOWN_TIME,
@@ -664,6 +681,13 @@
PENDINGINTENT_WARNING_THRESHOLD = mParser.getInt(KEY_PENDINGINTENT_WARNING_THRESHOLD,
DEFAULT_PENDINGINTENT_WARNING_THRESHOLD);
+ if (POWER_CHECK_INTERVAL != currentPowerCheckInterval) {
+ mService.mHandler.removeMessages(
+ ActivityManagerService.CHECK_EXCESSIVE_POWER_USE_MSG);
+ final Message msg = mService.mHandler.obtainMessage(
+ ActivityManagerService.CHECK_EXCESSIVE_POWER_USE_MSG);
+ mService.mHandler.sendMessageDelayed(msg, POWER_CHECK_INTERVAL);
+ }
// For new flags that are intended for server-side experiments, please use the new
// DeviceConfig package.
}
@@ -811,6 +835,16 @@
mService.scheduleUpdateBinderHeavyHitterWatcherConfig();
}
+ private void updateMaxPhantomProcesses() {
+ final int oldVal = MAX_PHANTOM_PROCESSES;
+ MAX_PHANTOM_PROCESSES = DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, KEY_MAX_PHANTOM_PROCESSES,
+ DEFAULT_MAX_PHANTOM_PROCESSES);
+ if (oldVal > MAX_PHANTOM_PROCESSES) {
+ mService.mHandler.post(mService.mPhantomProcessList::trimPhantomProcessesIfNecessary);
+ }
+ }
+
void dump(PrintWriter pw) {
pw.println("ACTIVITY MANAGER SETTINGS (dumpsys activity settings) "
+ Settings.Global.ACTIVITY_MANAGER_CONSTANTS + ":");
@@ -897,6 +931,8 @@
pw.println(BINDER_HEAVY_HITTER_AUTO_SAMPLER_BATCHSIZE);
pw.print(" "); pw.print(KEY_BINDER_HEAVY_HITTER_AUTO_SAMPLER_THRESHOLD); pw.print("=");
pw.println(BINDER_HEAVY_HITTER_AUTO_SAMPLER_THRESHOLD);
+ pw.print(" "); pw.print(KEY_MAX_PHANTOM_PROCESSES); pw.print("=");
+ pw.println(MAX_PHANTOM_PROCESSES);
pw.println();
if (mOverrideMaxCachedProcesses >= 0) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b55d555..b1b4018 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -635,6 +635,12 @@
final ProcessList mProcessList;
/**
+ * The list of phantom processes.
+ * @see PhantomProcessRecord
+ */
+ final PhantomProcessList mPhantomProcessList;
+
+ /**
* Tracking long-term execution of processes to look for abuse and other
* bad app behavior.
*/
@@ -1996,6 +2002,7 @@
mProcessList = injector.getProcessList(this);
mProcessList.init(this, activeUids, mPlatformCompat);
mAppProfiler = new AppProfiler(this, BackgroundThread.getHandler().getLooper(), null);
+ mPhantomProcessList = new PhantomProcessList(this);
mOomAdjuster = hasHandlerThread
? new OomAdjuster(this, mProcessList, activeUids, handlerThread) : null;
@@ -2053,6 +2060,7 @@
mProcessList.init(this, activeUids, mPlatformCompat);
mAppProfiler = new AppProfiler(this, BackgroundThread.getHandler().getLooper(),
new LowMemDetector(this));
+ mPhantomProcessList = new PhantomProcessList(this);
mOomAdjuster = new OomAdjuster(this, mProcessList, activeUids);
// Broadcast policy parameters
@@ -9209,6 +9217,10 @@
}
}
+ if (dumpAll) {
+ mPhantomProcessList.dump(pw, " ");
+ }
+
if (mImportantProcesses.size() > 0) {
synchronized (mPidsSelfLocked) {
boolean printed = false;
@@ -14832,44 +14844,24 @@
int i = mProcessList.mLruProcesses.size();
while (i > 0) {
i--;
- ProcessRecord app = mProcessList.mLruProcesses.get(i);
+ final ProcessRecord app = mProcessList.mLruProcesses.get(i);
if (app.setProcState >= ActivityManager.PROCESS_STATE_HOME) {
- if (app.lastCpuTime <= 0) {
- continue;
+ int cpuLimit;
+ long checkDur = curUptime - app.getWhenUnimportant();
+ if (checkDur <= mConstants.POWER_CHECK_INTERVAL) {
+ cpuLimit = mConstants.POWER_CHECK_MAX_CPU_1;
+ } else if (checkDur <= (mConstants.POWER_CHECK_INTERVAL * 2)
+ || app.setProcState <= ActivityManager.PROCESS_STATE_HOME) {
+ cpuLimit = mConstants.POWER_CHECK_MAX_CPU_2;
+ } else if (checkDur <= (mConstants.POWER_CHECK_INTERVAL * 3)) {
+ cpuLimit = mConstants.POWER_CHECK_MAX_CPU_3;
+ } else {
+ cpuLimit = mConstants.POWER_CHECK_MAX_CPU_4;
}
- long cputimeUsed = app.curCpuTime - app.lastCpuTime;
- if (DEBUG_POWER) {
- StringBuilder sb = new StringBuilder(128);
- sb.append("CPU for ");
- app.toShortString(sb);
- sb.append(": over ");
- TimeUtils.formatDuration(uptimeSince, sb);
- sb.append(" used ");
- TimeUtils.formatDuration(cputimeUsed, sb);
- sb.append(" (");
- sb.append((cputimeUsed * 100) / uptimeSince);
- sb.append("%)");
- Slog.i(TAG_POWER, sb.toString());
- }
- // If the process has used too much CPU over the last duration, the
- // user probably doesn't want this, so kill!
- if (doCpuKills && uptimeSince > 0) {
- // What is the limit for this process?
- int cpuLimit;
- long checkDur = curUptime - app.getWhenUnimportant();
- if (checkDur <= mConstants.POWER_CHECK_INTERVAL) {
- cpuLimit = mConstants.POWER_CHECK_MAX_CPU_1;
- } else if (checkDur <= (mConstants.POWER_CHECK_INTERVAL * 2)
- || app.setProcState <= ActivityManager.PROCESS_STATE_HOME) {
- cpuLimit = mConstants.POWER_CHECK_MAX_CPU_2;
- } else if (checkDur <= (mConstants.POWER_CHECK_INTERVAL * 3)) {
- cpuLimit = mConstants.POWER_CHECK_MAX_CPU_3;
- } else {
- cpuLimit = mConstants.POWER_CHECK_MAX_CPU_4;
- }
- if (((cputimeUsed * 100) / uptimeSince) >= cpuLimit) {
- mBatteryStatsService.reportExcessiveCpu(app.info.uid, app.processName,
- uptimeSince, cputimeUsed);
+ if (app.lastCpuTime > 0) {
+ final long cputimeUsed = app.curCpuTime - app.lastCpuTime;
+ if (checkExcessivePowerUsageLocked(uptimeSince, doCpuKills, cputimeUsed,
+ app.processName, app.toShortString(), cpuLimit, app)) {
app.kill("excessive cpu " + cputimeUsed + " during " + uptimeSince
+ " dur=" + checkDur + " limit=" + cpuLimit,
ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE,
@@ -14878,23 +14870,73 @@
synchronized (mProcessStats.mLock) {
app.baseProcessTracker.reportExcessiveCpu(app.pkgList.mPkgList);
}
- for (int ipkg = app.pkgList.size() - 1; ipkg >= 0; ipkg--) {
- ProcessStats.ProcessStateHolder holder = app.pkgList.valueAt(ipkg);
- FrameworkStatsLog.write(
- FrameworkStatsLog.EXCESSIVE_CPU_USAGE_REPORTED,
- app.info.uid,
- holder.state.getName(),
- holder.state.getPackage(),
- holder.appVersion);
- }
}
}
+
app.lastCpuTime = app.curCpuTime;
+
+ // Also check the phantom processes if there is any
+ final long chkDur = checkDur;
+ final int cpuLmt = cpuLimit;
+ final boolean doKill = doCpuKills;
+ mPhantomProcessList.forEachPhantomProcessOfApp(app, r -> {
+ if (r.mLastCputime > 0) {
+ final long cputimeUsed = r.mCurrentCputime - r.mLastCputime;
+ if (checkExcessivePowerUsageLocked(uptimeSince, doKill, cputimeUsed,
+ app.processName, r.toString(), cpuLimit, app)) {
+ mPhantomProcessList.killPhantomProcessGroupLocked(app, r,
+ ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE,
+ ApplicationExitInfo.SUBREASON_EXCESSIVE_CPU,
+ "excessive cpu " + cputimeUsed + " during "
+ + uptimeSince + " dur=" + chkDur + " limit=" + cpuLmt);
+ return false;
+ }
+ }
+ r.mLastCputime = r.mCurrentCputime;
+ return true;
+ });
}
}
}
}
+ private boolean checkExcessivePowerUsageLocked(final long uptimeSince, boolean doCpuKills,
+ final long cputimeUsed, final String processName, final String description,
+ final int cpuLimit, final ProcessRecord app) {
+ if (DEBUG_POWER) {
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("CPU for ");
+ sb.append(description);
+ sb.append(": over ");
+ TimeUtils.formatDuration(uptimeSince, sb);
+ sb.append(" used ");
+ TimeUtils.formatDuration(cputimeUsed, sb);
+ sb.append(" (");
+ sb.append((cputimeUsed * 100.0) / uptimeSince);
+ sb.append("%)");
+ Slog.i(TAG_POWER, sb.toString());
+ }
+ // If the process has used too much CPU over the last duration, the
+ // user probably doesn't want this, so kill!
+ if (doCpuKills && uptimeSince > 0) {
+ if (((cputimeUsed * 100) / uptimeSince) >= cpuLimit) {
+ mBatteryStatsService.reportExcessiveCpu(app.info.uid, app.processName,
+ uptimeSince, cputimeUsed);
+ for (int ipkg = app.pkgList.size() - 1; ipkg >= 0; ipkg--) {
+ ProcessStats.ProcessStateHolder holder = app.pkgList.valueAt(ipkg);
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.EXCESSIVE_CPU_USAGE_REPORTED,
+ app.info.uid,
+ processName,
+ holder.state.getPackage(),
+ holder.appVersion);
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
final void setProcessTrackerStateLocked(ProcessRecord proc, int memFactor, long now) {
synchronized (mProcessStats.mLock) {
if (proc.thread != null && proc.baseProcessTracker != null) {
diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java
index 0b5d585..31ffb35 100644
--- a/services/core/java/com/android/server/am/AppProfiler.java
+++ b/services/core/java/com/android/server/am/AppProfiler.java
@@ -1257,6 +1257,10 @@
}
}
+ if (haveNewCpuStats) {
+ mService.mPhantomProcessList.updateProcessCpuStatesLocked(mProcessCpuTracker);
+ }
+
final BatteryStatsImpl bstats = mService.mBatteryStatsService.getActiveStatistics();
synchronized (bstats) {
if (haveNewCpuStats) {
diff --git a/services/core/java/com/android/server/am/PhantomProcessList.java b/services/core/java/com/android/server/am/PhantomProcessList.java
new file mode 100644
index 0000000..e2fcf08
--- /dev/null
+++ b/services/core/java/com/android/server/am/PhantomProcessList.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+
+import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PROCESSES;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
+import android.app.ApplicationExitInfo.Reason;
+import android.app.ApplicationExitInfo.SubReason;
+import android.os.Handler;
+import android.os.Process;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.ProcessCpuTracker;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.function.Function;
+
+/**
+ * Activity manager code dealing with phantom processes.
+ */
+public final class PhantomProcessList {
+ static final String TAG = TAG_WITH_CLASS_NAME ? "PhantomProcessList" : TAG_AM;
+
+ final Object mLock = new Object();
+
+ /**
+ * All of the phantom process record we track, key is the pid of the process.
+ */
+ @GuardedBy("mLock")
+ final SparseArray<PhantomProcessRecord> mPhantomProcesses = new SparseArray<>();
+
+ /**
+ * The mapping between app processes and their phantom processess, outer key is the pid of
+ * the app process, while the inner key is the pid of the phantom process.
+ */
+ @GuardedBy("mLock")
+ final SparseArray<SparseArray<PhantomProcessRecord>> mAppPhantomProcessMap =
+ new SparseArray<>();
+
+ /**
+ * The mapping of the pidfd to PhantomProcessRecord.
+ */
+ @GuardedBy("mLock")
+ final SparseArray<PhantomProcessRecord> mPhantomProcessesPidFds = new SparseArray<>();
+
+ /**
+ * The list of phantom processes tha's being signaled to be killed but still undead yet.
+ */
+ @GuardedBy("mLock")
+ final SparseArray<PhantomProcessRecord> mZombiePhantomProcesses = new SparseArray<>();
+
+ @GuardedBy("mLock")
+ private final ArrayList<PhantomProcessRecord> mTempPhantomProcesses = new ArrayList<>();
+
+ @GuardedBy("mLock")
+ private boolean mTrimPhantomProcessScheduled = false;
+
+ @GuardedBy("mLock")
+ int mUpdateSeq;
+
+ private final ActivityManagerService mService;
+ private final Handler mKillHandler;
+
+ PhantomProcessList(final ActivityManagerService service) {
+ mService = service;
+ mKillHandler = service.mProcessList.sKillHandler;
+ }
+
+ /**
+ * Get the existing phantom process record, or create if it's not existing yet;
+ * however, before creating it, we'll check if this is really a phantom process
+ * and we'll return null if it's not.
+ */
+ @GuardedBy("mLock")
+ PhantomProcessRecord getOrCreatePhantomProcessIfNeededLocked(final String processName,
+ final int uid, final int pid) {
+ // First check if it's actually an app process we know
+ if (isAppProcess(pid)) {
+ return null;
+ }
+
+ // Have we already been aware of this?
+ final int index = mPhantomProcesses.indexOfKey(pid);
+ if (index >= 0) {
+ final PhantomProcessRecord proc = mPhantomProcesses.valueAt(index);
+ if (proc.equals(processName, uid, pid)) {
+ return proc;
+ }
+ // Somehow our record doesn't match, remove it anyway
+ Slog.w(TAG, "Stale " + proc + ", removing");
+ mPhantomProcesses.removeAt(index);
+ } else {
+ // Is this one of the zombie processes we've known?
+ final int idx = mZombiePhantomProcesses.indexOfKey(pid);
+ if (idx >= 0) {
+ final PhantomProcessRecord proc = mZombiePhantomProcesses.valueAt(idx);
+ if (proc.equals(processName, uid, pid)) {
+ return proc;
+ }
+ // Our zombie process information is outdated, let's remove this one, it shoud
+ // have been gone.
+ mZombiePhantomProcesses.removeAt(idx);
+ }
+ }
+
+ int ppid = getParentPid(pid);
+
+ // Walk through its parents and see if it could be traced back to an app process.
+ while (ppid > 1) {
+ if (isAppProcess(ppid)) {
+ // It's a phantom process, bookkeep it
+ try {
+ final PhantomProcessRecord proc = new PhantomProcessRecord(
+ processName, uid, pid, ppid, mService,
+ this::onPhantomProcessKilledLocked);
+ proc.mUpdateSeq = mUpdateSeq;
+ mPhantomProcesses.put(pid, proc);
+ SparseArray<PhantomProcessRecord> array = mAppPhantomProcessMap.get(ppid);
+ if (array == null) {
+ array = new SparseArray<>();
+ mAppPhantomProcessMap.put(ppid, array);
+ }
+ array.put(pid, proc);
+ if (proc.mPidFd != null) {
+ mKillHandler.getLooper().getQueue().addOnFileDescriptorEventListener(
+ proc.mPidFd, EVENT_INPUT | EVENT_ERROR,
+ this::onPhantomProcessFdEvent);
+ mPhantomProcessesPidFds.put(proc.mPidFd.getInt$(), proc);
+ }
+ scheduleTrimPhantomProcessesLocked();
+ return proc;
+ } catch (IllegalStateException e) {
+ return null;
+ }
+ }
+
+ ppid = getParentPid(ppid);
+ }
+ return null;
+ }
+
+ private static int getParentPid(int pid) {
+ try {
+ return Process.getParentPid(pid);
+ } catch (Exception e) {
+ }
+ return -1;
+ }
+
+ private boolean isAppProcess(int pid) {
+ synchronized (mService.mPidsSelfLocked) {
+ return mService.mPidsSelfLocked.get(pid) != null;
+ }
+ }
+
+ private int onPhantomProcessFdEvent(FileDescriptor fd, int events) {
+ synchronized (mLock) {
+ final PhantomProcessRecord proc = mPhantomProcessesPidFds.get(fd.getInt$());
+ if ((events & EVENT_INPUT) != 0) {
+ proc.onProcDied(true);
+ } else {
+ // EVENT_ERROR, kill the process
+ proc.killLocked("Process error", true);
+ }
+ }
+ return 0;
+ }
+
+ @GuardedBy("mLock")
+ private void onPhantomProcessKilledLocked(final PhantomProcessRecord proc) {
+ if (proc.mPidFd != null && proc.mPidFd.valid()) {
+ mKillHandler.getLooper().getQueue()
+ .removeOnFileDescriptorEventListener(proc.mPidFd);
+ mPhantomProcessesPidFds.remove(proc.mPidFd.getInt$());
+ IoUtils.closeQuietly(proc.mPidFd);
+ }
+ mPhantomProcesses.remove(proc.mPid);
+ final int index = mAppPhantomProcessMap.indexOfKey(proc.mPpid);
+ if (index < 0) {
+ return;
+ }
+ SparseArray<PhantomProcessRecord> array = mAppPhantomProcessMap.valueAt(index);
+ array.remove(proc.mPid);
+ if (array.size() == 0) {
+ mAppPhantomProcessMap.removeAt(index);
+ }
+ if (proc.mZombie) {
+ // If it's not really dead, bookkeep it
+ mZombiePhantomProcesses.put(proc.mPid, proc);
+ } else {
+ // In case of race condition, let's try to remove it from zombie list
+ mZombiePhantomProcesses.remove(proc.mPid);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void scheduleTrimPhantomProcessesLocked() {
+ if (!mTrimPhantomProcessScheduled) {
+ mTrimPhantomProcessScheduled = true;
+ mService.mHandler.post(this::trimPhantomProcessesIfNecessary);
+ }
+ }
+
+ /**
+ * Clamp the number of phantom processes to
+ * {@link ActivityManagerConstants#MAX_PHANTOM_PROCESSE}, kills those surpluses in the
+ * order of the oom adjs of their parent process.
+ */
+ void trimPhantomProcessesIfNecessary() {
+ synchronized (mLock) {
+ mTrimPhantomProcessScheduled = false;
+ if (mService.mConstants.MAX_PHANTOM_PROCESSES < mPhantomProcesses.size()) {
+ for (int i = mPhantomProcesses.size() - 1; i >= 0; i--) {
+ mTempPhantomProcesses.add(mPhantomProcesses.valueAt(i));
+ }
+ synchronized (mService.mPidsSelfLocked) {
+ Collections.sort(mTempPhantomProcesses, (a, b) -> {
+ final ProcessRecord ra = mService.mPidsSelfLocked.get(a.mPpid);
+ if (ra == null) {
+ // parent is gone, this process should have been killed too
+ return 1;
+ }
+ final ProcessRecord rb = mService.mPidsSelfLocked.get(b.mPpid);
+ if (rb == null) {
+ // parent is gone, this process should have been killed too
+ return -1;
+ }
+ if (ra.curAdj != rb.curAdj) {
+ return ra.curAdj - rb.curAdj;
+ }
+ if (a.mKnownSince != b.mKnownSince) {
+ // In case of identical oom adj, younger one first
+ return a.mKnownSince < b.mKnownSince ? 1 : -1;
+ }
+ return 0;
+ });
+ }
+ for (int i = mTempPhantomProcesses.size() - 1;
+ i >= mService.mConstants.MAX_PHANTOM_PROCESSES; i--) {
+ final PhantomProcessRecord proc = mTempPhantomProcesses.get(i);
+ proc.killLocked("Trimming phantom processes", true);
+ }
+ mTempPhantomProcesses.clear();
+ }
+ }
+ }
+
+ /**
+ * Remove all entries with outdated seq num.
+ */
+ @GuardedBy("mLock")
+ void pruneStaleProcessesLocked() {
+ for (int i = mPhantomProcesses.size() - 1; i >= 0; i--) {
+ final PhantomProcessRecord proc = mPhantomProcesses.valueAt(i);
+ if (proc.mUpdateSeq < mUpdateSeq) {
+ if (DEBUG_PROCESSES) {
+ Slog.v(TAG, "Pruning " + proc + " as it should have been dead.");
+ }
+ proc.killLocked("Stale process", true);
+ }
+ }
+ for (int i = mZombiePhantomProcesses.size() - 1; i >= 0; i--) {
+ final PhantomProcessRecord proc = mZombiePhantomProcesses.valueAt(i);
+ if (proc.mUpdateSeq < mUpdateSeq) {
+ if (DEBUG_PROCESSES) {
+ Slog.v(TAG, "Pruning " + proc + " as it should have been dead.");
+ }
+ }
+ }
+ }
+
+ /**
+ * Kill the given phantom process, all its siblings (if any) and their parent process
+ */
+ @GuardedBy("mService")
+ void killPhantomProcessGroupLocked(ProcessRecord app, PhantomProcessRecord proc,
+ @Reason int reasonCode, @SubReason int subReason, String msg) {
+ synchronized (mLock) {
+ int index = mAppPhantomProcessMap.indexOfKey(proc.mPpid);
+ if (index >= 0) {
+ final SparseArray<PhantomProcessRecord> array =
+ mAppPhantomProcessMap.valueAt(index);
+ for (int i = array.size() - 1; i >= 0; i--) {
+ final PhantomProcessRecord r = array.valueAt(i);
+ if (r == proc) {
+ r.killLocked(msg, true);
+ } else {
+ r.killLocked("Caused by siling process: " + msg, false);
+ }
+ }
+ }
+ }
+ // Lastly, kill the parent process too
+ app.kill("Caused by child process: " + msg, reasonCode, subReason, true);
+ }
+
+ /**
+ * Iterate all phantom process belonging to the given app, and invokve callback
+ * for each of them.
+ */
+ void forEachPhantomProcessOfApp(final ProcessRecord app,
+ final Function<PhantomProcessRecord, Boolean> callback) {
+ synchronized (mLock) {
+ int index = mAppPhantomProcessMap.indexOfKey(app.pid);
+ if (index >= 0) {
+ final SparseArray<PhantomProcessRecord> array =
+ mAppPhantomProcessMap.valueAt(index);
+ for (int i = array.size() - 1; i >= 0; i--) {
+ final PhantomProcessRecord r = array.valueAt(i);
+ if (!callback.apply(r)) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ @GuardedBy("tracker")
+ void updateProcessCpuStatesLocked(ProcessCpuTracker tracker) {
+ synchronized (mLock) {
+ // refresh the phantom process list with the latest cpu stats results.
+ mUpdateSeq++;
+ for (int i = tracker.countStats() - 1; i >= 0; i--) {
+ final ProcessCpuTracker.Stats st = tracker.getStats(i);
+ final PhantomProcessRecord r =
+ getOrCreatePhantomProcessIfNeededLocked(st.name, st.uid, st.pid);
+ if (r != null) {
+ r.mUpdateSeq = mUpdateSeq;
+ r.mCurrentCputime += st.rel_utime + st.rel_stime;
+ if (r.mLastCputime == 0) {
+ r.mLastCputime = r.mCurrentCputime;
+ }
+ r.updateAdjLocked();
+ }
+ }
+ // remove the stale ones
+ pruneStaleProcessesLocked();
+ }
+ }
+
+ void dump(PrintWriter pw, String prefix) {
+ synchronized (mLock) {
+ dumpPhantomeProcessLocked(pw, prefix, "All Active App Child Processes:",
+ mPhantomProcesses);
+ dumpPhantomeProcessLocked(pw, prefix, "All Zombie App Child Processes:",
+ mZombiePhantomProcesses);
+ }
+ }
+
+ void dumpPhantomeProcessLocked(PrintWriter pw, String prefix, String headline,
+ SparseArray<PhantomProcessRecord> list) {
+ final int size = list.size();
+ if (size == 0) {
+ return;
+ }
+ pw.println();
+ pw.print(prefix);
+ pw.println(headline);
+ for (int i = 0; i < size; i++) {
+ final PhantomProcessRecord proc = list.valueAt(i);
+ pw.print(prefix);
+ pw.print(" proc #");
+ pw.print(i);
+ pw.print(": ");
+ pw.println(proc.toString());
+ proc.dump(pw, prefix + " ");
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/am/PhantomProcessRecord.java b/services/core/java/com/android/server/am/PhantomProcessRecord.java
new file mode 100644
index 0000000..0156ee5
--- /dev/null
+++ b/services/core/java/com/android/server/am/PhantomProcessRecord.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import static android.os.Process.PROC_NEWLINE_TERM;
+import static android.os.Process.PROC_OUT_LONG;
+
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
+import android.os.Handler;
+import android.os.Process;
+import android.os.StrictMode;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.EventLog;
+import android.util.Slog;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+/**
+ * The "phantom" app processes, which are forked by app processes so we are not aware of
+ * them until we walk through the process list in /proc.
+ */
+public final class PhantomProcessRecord {
+ static final String TAG = TAG_WITH_CLASS_NAME ? "PhantomProcessRecord" : TAG_AM;
+
+ static final long[] LONG_OUT = new long[1];
+ static final int[] LONG_FORMAT = new int[] {PROC_NEWLINE_TERM | PROC_OUT_LONG};
+
+ final String mProcessName; // name of the process
+ final int mUid; // uid of the process
+ final int mPid; // The id of the process
+ final int mPpid; // Ancestor (managed app process) pid of the process
+ final long mKnownSince; // The timestamp when we're aware of the process
+ final FileDescriptor mPidFd; // The fd to monitor the termination of this process
+
+ long mLastCputime; // How long proc has run CPU at last check
+ long mCurrentCputime; // How long proc has run CPU most recently
+ int mUpdateSeq; // Seq no, indicating the last check on this process
+ int mAdj; // The last known oom adj score
+ boolean mKilled; // Whether it has been killed by us or not
+ boolean mZombie; // Whether it was signaled to be killed but timed out
+ String mStringName; // Caching of the toString() result
+
+ final ActivityManagerService mService;
+ final Object mLock;
+ final Consumer<PhantomProcessRecord> mOnKillListener;
+ final Handler mKillHandler;
+
+ PhantomProcessRecord(final String processName, final int uid, final int pid,
+ final int ppid, final ActivityManagerService service,
+ final Consumer<PhantomProcessRecord> onKillListener) throws IllegalStateException {
+ mProcessName = processName;
+ mUid = uid;
+ mPid = pid;
+ mPpid = ppid;
+ mKilled = false;
+ mAdj = ProcessList.NATIVE_ADJ;
+ mKnownSince = SystemClock.elapsedRealtime();
+ mService = service;
+ mLock = service.mPhantomProcessList.mLock;
+ mOnKillListener = onKillListener;
+ mKillHandler = service.mProcessList.sKillHandler;
+ if (Process.supportsPidFd()) {
+ StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
+ try {
+ mPidFd = Process.openPidFd(pid, 0);
+ if (mPidFd == null) {
+ throw new IllegalStateException();
+ }
+ } catch (IOException e) {
+ // Maybe a race condition, the process is gone.
+ Slog.w(TAG, "Unable to open process " + pid + ", it might be gone");
+ IllegalStateException ex = new IllegalStateException();
+ ex.initCause(e);
+ throw ex;
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
+ } else {
+ mPidFd = null;
+ }
+ }
+
+ @GuardedBy("mLock")
+ void killLocked(String reason, boolean noisy) {
+ if (!mKilled) {
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "kill");
+ if (noisy || mUid == mService.mCurOomAdjUid) {
+ mService.reportUidInfoMessageLocked(TAG,
+ "Killing " + toString() + ": " + reason, mUid);
+ }
+ if (mPid > 0) {
+ EventLog.writeEvent(EventLogTags.AM_KILL, UserHandle.getUserId(mUid),
+ mPid, mProcessName, mAdj, reason);
+ if (!Process.supportsPidFd()) {
+ onProcDied(false);
+ } else {
+ // We'll notify the listener when we're notified it's dead.
+ // Meanwhile, we'd also need handle the case of zombie processes.
+ mKillHandler.postDelayed(mProcKillTimer, this,
+ ProcessList.PROC_KILL_TIMEOUT);
+ }
+ Process.killProcessQuiet(mPid);
+ ProcessList.killProcessGroup(mUid, mPid);
+ }
+ mKilled = true;
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ }
+ }
+
+ private Runnable mProcKillTimer = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mLock) {
+ // The process is maybe in either D or Z state.
+ Slog.w(TAG, "Process " + toString() + " is still alive after "
+ + ProcessList.PROC_KILL_TIMEOUT + "ms");
+ // Force a cleanup as we can't keep the fd open forever
+ mZombie = true;
+ onProcDied(false);
+ // But still bookkeep it, so it won't be added as a new one if it's spotted again.
+ }
+ }
+ };
+
+ @GuardedBy("mLock")
+ void updateAdjLocked() {
+ if (Process.readProcFile("/proc/" + mPid + "/oom_score_adj",
+ LONG_FORMAT, null, LONG_OUT, null)) {
+ mAdj = (int) LONG_OUT[0];
+ }
+ }
+
+ @GuardedBy("mLock")
+ void onProcDied(boolean reallyDead) {
+ if (reallyDead) {
+ Slog.i(TAG, "Process " + toString() + " died");
+ }
+ mKillHandler.removeCallbacks(mProcKillTimer, this);
+ if (mOnKillListener != null) {
+ mOnKillListener.accept(this);
+ }
+ }
+
+ @Override
+ public String toString() {
+ if (mStringName != null) {
+ return mStringName;
+ }
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("PhantomProcessRecord {");
+ sb.append(Integer.toHexString(System.identityHashCode(this)));
+ sb.append(' ');
+ sb.append(mPid);
+ sb.append(':');
+ sb.append(mPpid);
+ sb.append(':');
+ sb.append(mProcessName);
+ sb.append('/');
+ if (mUid < Process.FIRST_APPLICATION_UID) {
+ sb.append(mUid);
+ } else {
+ sb.append('u');
+ sb.append(UserHandle.getUserId(mUid));
+ int appId = UserHandle.getAppId(mUid);
+ if (appId >= Process.FIRST_APPLICATION_UID) {
+ sb.append('a');
+ sb.append(appId - Process.FIRST_APPLICATION_UID);
+ } else {
+ sb.append('s');
+ sb.append(appId);
+ }
+ if (appId >= Process.FIRST_ISOLATED_UID && appId <= Process.LAST_ISOLATED_UID) {
+ sb.append('i');
+ sb.append(appId - Process.FIRST_ISOLATED_UID);
+ }
+ }
+ sb.append('}');
+ return mStringName = sb.toString();
+ }
+
+ void dump(PrintWriter pw, String prefix) {
+ final long now = SystemClock.elapsedRealtime();
+ pw.print(prefix);
+ pw.print("user #");
+ pw.print(UserHandle.getUserId(mUid));
+ pw.print(" uid=");
+ pw.print(mUid);
+ pw.print(" pid=");
+ pw.print(mPid);
+ pw.print(" ppid=");
+ pw.print(mPpid);
+ pw.print(" knownSince=");
+ TimeUtils.formatDuration(mKnownSince, now, pw);
+ pw.print(" killed=");
+ pw.println(mKilled);
+ pw.print(prefix);
+ pw.print("lastCpuTime=");
+ pw.print(mLastCputime);
+ if (mLastCputime > 0) {
+ pw.print(" timeUsed=");
+ TimeUtils.formatDuration(mCurrentCputime - mLastCputime, pw);
+ }
+ pw.print(" oom adj=");
+ pw.print(mAdj);
+ pw.print(" seq=");
+ pw.println(mUpdateSeq);
+ }
+
+ boolean equals(final String processName, final int uid, final int pid) {
+ return mUid == uid && mPid == pid && TextUtils.equals(mProcessName, processName);
+ }
+}
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index ced2f0f..5e65563 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -329,7 +329,7 @@
/**
* How long between a process kill and we actually receive its death recipient
*/
- private static final int PROC_KILL_TIMEOUT = 2000; // 2 seconds;
+ static final int PROC_KILL_TIMEOUT = 2000; // 2 seconds;
/**
* Native heap allocations will now have a non-zero tag in the most significant byte.
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/AppChildProcessTest.java b/services/tests/mockingservicestests/src/com/android/server/am/AppChildProcessTest.java
new file mode 100644
index 0000000..04e8b63
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/am/AppChildProcessTest.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManagerInternal;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.platform.test.annotations.Presubmit;
+import android.util.ArraySet;
+
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.server.LocalServices;
+import com.android.server.ServiceThread;
+import com.android.server.am.ActivityManagerService.Injector;
+import com.android.server.appop.AppOpsService;
+import com.android.server.wm.ActivityTaskManagerService;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.quality.Strictness;
+
+import java.io.File;
+
+@Presubmit
+public class AppChildProcessTest {
+ private static final String TAG = AppChildProcessTest.class.getSimpleName();
+
+ @Rule public ServiceThreadRule mServiceThreadRule = new ServiceThreadRule();
+ @Mock private AppOpsService mAppOpsService;
+ @Mock private PackageManagerInternal mPackageManagerInt;
+ private StaticMockitoSession mMockitoSession;
+
+ private Context mContext = getInstrumentation().getTargetContext();
+ private TestInjector mInjector;
+ private ActivityManagerService mAms;
+ private ProcessList mProcessList;
+ private PhantomProcessList mPhantomProcessList;
+ private Handler mHandler;
+ private HandlerThread mHandlerThread;
+
+ @BeforeClass
+ public static void setUpOnce() {
+ System.setProperty("dexmaker.share_classloader", "true");
+ }
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mMockitoSession = mockitoSession()
+ .spyStatic(Process.class)
+ .strictness(Strictness.LENIENT)
+ .startMocking();
+
+ mHandlerThread = new HandlerThread(TAG);
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ final ProcessList pList = new ProcessList();
+ mProcessList = spy(pList);
+
+ mInjector = new TestInjector(mContext);
+ mAms = new ActivityManagerService(mInjector, mServiceThreadRule.getThread());
+ mAms.mActivityTaskManager = new ActivityTaskManagerService(mContext);
+ mAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper());
+ mAms.mAtmInternal = spy(mAms.mActivityTaskManager.getAtmInternal());
+ mAms.mPackageManagerInt = mPackageManagerInt;
+ pList.mService = mAms;
+ mPhantomProcessList = mAms.mPhantomProcessList;
+ doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent();
+ doReturn(false).when(() -> Process.supportsPidFd());
+ // Remove stale instance of PackageManagerInternal if there is any
+ LocalServices.removeServiceForTest(PackageManagerInternal.class);
+ LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt);
+ }
+
+ @After
+ public void tearDown() {
+ LocalServices.removeServiceForTest(PackageManagerInternal.class);
+ mMockitoSession.finishMocking();
+ mHandlerThread.quit();
+ }
+
+ @Test
+ public void testManageAppChildProcesses() throws Exception {
+ final int initPid = 1;
+ final int rootUid = 0;
+ final int zygote64Pid = 100;
+ final int zygote32Pid = 101;
+ final int app1Pid = 200;
+ final int app2Pid = 201;
+ final int app1Uid = 10000;
+ final int app2Uid = 10001;
+ final int child1Pid = 300;
+ final int child2Pid = 301;
+ final int nativePid = 400;
+ final String zygote64ProcessName = "zygote64";
+ final String zygote32ProcessName = "zygote32";
+ final String app1ProcessName = "test1";
+ final String app2ProcessName = "test2";
+ final String child1ProcessName = "test1_child1";
+ final String child2ProcessName = "test1_child1_child2";
+ final String nativeProcessName = "test_native";
+
+ makeParent(zygote64Pid, initPid);
+ makeParent(zygote32Pid, initPid);
+
+ makeAppProcess(app1Pid, app1Uid, app1ProcessName, app1ProcessName);
+ makeParent(app1Pid, zygote64Pid);
+ makeAppProcess(app2Pid, app2Uid, app2ProcessName, app2ProcessName);
+ makeParent(app2Pid, zygote64Pid);
+
+ assertEquals(0, mPhantomProcessList.mPhantomProcesses.size());
+
+ // Verify zygote itself isn't a phantom process
+ assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
+ zygote64ProcessName, rootUid, zygote64Pid));
+ assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
+ zygote32ProcessName, rootUid, zygote32Pid));
+ // Verify none of the app isn't a phantom process
+ assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
+ app1ProcessName, app1Uid, app1Pid));
+ assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
+ app2ProcessName, app2Uid, app2Pid));
+
+ // "Fork" an app child process
+ makeParent(child1Pid, app1Pid);
+ PhantomProcessRecord pr = mPhantomProcessList
+ .getOrCreatePhantomProcessIfNeededLocked(child1ProcessName, app1Uid, child1Pid);
+ assertTrue(pr != null);
+ assertEquals(1, mPhantomProcessList.mPhantomProcesses.size());
+ assertEquals(pr, mPhantomProcessList.mPhantomProcesses.valueAt(0));
+ verifyPhantomProcessRecord(pr, child1ProcessName, app1Uid, child1Pid);
+
+ // Create another native process from init
+ makeParent(nativePid, initPid);
+ assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
+ nativeProcessName, rootUid, nativePid));
+ assertEquals(1, mPhantomProcessList.mPhantomProcesses.size());
+ assertEquals(pr, mPhantomProcessList.mPhantomProcesses.valueAt(0));
+
+ // "Fork" another app child process
+ makeParent(child2Pid, child1Pid);
+ PhantomProcessRecord pr2 = mPhantomProcessList
+ .getOrCreatePhantomProcessIfNeededLocked(child2ProcessName, app1Uid, child2Pid);
+ assertTrue(pr2 != null);
+ assertEquals(2, mPhantomProcessList.mPhantomProcesses.size());
+ verifyPhantomProcessRecord(pr2, child2ProcessName, app1Uid, child2Pid);
+
+ ArraySet<PhantomProcessRecord> set = new ArraySet<>();
+ set.add(pr);
+ set.add(pr2);
+ for (int i = mPhantomProcessList.mPhantomProcesses.size() - 1; i >= 0; i--) {
+ set.remove(mPhantomProcessList.mPhantomProcesses.valueAt(i));
+ }
+ assertEquals(0, set.size());
+ }
+
+ private void verifyPhantomProcessRecord(PhantomProcessRecord pr,
+ String processName, int uid, int pid) {
+ assertEquals(processName, pr.mProcessName);
+ assertEquals(uid, pr.mUid);
+ assertEquals(pid, pr.mPid);
+ }
+
+ private void makeAppProcess(int pid, int uid, String packageName, String processName) {
+ ApplicationInfo ai = new ApplicationInfo();
+ ai.packageName = packageName;
+ ProcessRecord app = new ProcessRecord(mAms, ai, processName, uid);
+ app.pid = pid;
+ mAms.mPidsSelfLocked.doAddInternal(app);
+ }
+
+ private void makeParent(int pid, int ppid) {
+ doReturn(ppid).when(() -> Process.getParentPid(eq(pid)));
+ }
+
+ private class TestInjector extends Injector {
+ TestInjector(Context context) {
+ super(context);
+ }
+
+ @Override
+ public AppOpsService getAppOpsService(File file, Handler handler) {
+ return mAppOpsService;
+ }
+
+ @Override
+ public Handler getUiHandler(ActivityManagerService service) {
+ return mHandler;
+ }
+
+ @Override
+ public ProcessList getProcessList(ActivityManagerService service) {
+ return mProcessList;
+ }
+ }
+
+ static class ServiceThreadRule implements TestRule {
+ private ServiceThread mThread;
+
+ ServiceThread getThread() {
+ return mThread;
+ }
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ mThread = new ServiceThread("TestServiceThread",
+ Process.THREAD_PRIORITY_DEFAULT, true /* allowIo */);
+ mThread.start();
+ try {
+ base.evaluate();
+ } finally {
+ mThread.getThreadHandler().runWithScissors(mThread::quit, 0 /* timeout */);
+ }
+ }
+ };
+ }
+ }
+
+}