diff --git a/api/current.txt b/api/current.txt index 5cd319c84a142..105a6c6456f66 100644 --- a/api/current.txt +++ b/api/current.txt @@ -4049,6 +4049,7 @@ package android.app { method @RequiresPermission(android.Manifest.permission.REORDER_TASKS) public void moveTaskToFront(int, int); method @RequiresPermission(android.Manifest.permission.REORDER_TASKS) public void moveTaskToFront(int, int, android.os.Bundle); method @Deprecated public void restartPackage(String); + method public void setProcessStateSummary(@Nullable byte[]); method public static void setVrThread(int); method public void setWatchHeapLimit(long); field public static final String ACTION_REPORT_HEAP_LIMIT = "android.app.action.REPORT_HEAP_LIMIT"; @@ -4561,12 +4562,14 @@ package android.app { method public int getPackageUid(); method public int getPid(); method @NonNull public String getProcessName(); + method @Nullable public byte[] getProcessStateSummary(); method public long getPss(); method public int getRealUid(); method public int getReason(); method public long getRss(); method public int getStatus(); method public long getTimestamp(); + method @Nullable public java.io.InputStream getTraceInputStream() throws java.io.IOException; method @NonNull public android.os.UserHandle getUserHandle(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator CREATOR; diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index f4ee8faaf9bf3..b3a0be1bec1e9 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -3628,11 +3628,40 @@ public class ActivityManager { } } + /** + * Set custom state data for this process. It will be included in the record of + * {@link ApplicationExitInfo} on the death of the current calling process; the new process + * of the app can retrieve this state data by calling + * {@link ApplicationExitInfo#getProcessStateSummary} on the record returned by + * {@link #getHistoricalProcessExitReasons}. + * + *

This would be useful for the calling app to save its stateful data: if it's + * killed later for any reason, the new process of the app can know what the + * previous process of the app was doing. For instance, you could use this to encode + * the current level in a game, or a set of features/experiments that were enabled. Later you + * could analyze under what circumstances the app tends to crash or use too much memory. + * However, it's not suggested to rely on this to restore the applications previous UI state + * or so, it's only meant for analyzing application healthy status.

+ * + *

System might decide to throttle the calls to this API; so call this API in a reasonable + * manner, excessive calls to this API could result a {@link java.lang.RuntimeException}. + *

+ * + * @param state The state data + */ + public void setProcessStateSummary(@Nullable byte[] state) { + try { + getService().setProcessStateSummary(state); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /* * @return Whether or not the low memory kill will be reported in * {@link #getHistoricalProcessExitReasons}. * - * @see {@link ApplicationExitInfo#REASON_LOW_MEMORY} + * @see ApplicationExitInfo#REASON_LOW_MEMORY */ public static boolean isLowMemoryKillReportSupported() { return SystemProperties.getBoolean("persist.sys.lmk.reportkills", false); diff --git a/core/java/android/app/ApplicationExitInfo.java b/core/java/android/app/ApplicationExitInfo.java index 5df3257f24448..61be01f9f6c01 100644 --- a/core/java/android/app/ApplicationExitInfo.java +++ b/core/java/android/app/ApplicationExitInfo.java @@ -23,7 +23,9 @@ import android.annotation.Nullable; import android.app.ActivityManager.RunningAppProcessInfo.Importance; import android.icu.text.SimpleDateFormat; import android.os.Parcel; +import android.os.ParcelFileDescriptor; import android.os.Parcelable; +import android.os.RemoteException; import android.os.UserHandle; import android.text.TextUtils; import android.util.DebugUtils; @@ -31,12 +33,17 @@ import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; import android.util.proto.WireTypeMismatchException; +import com.android.internal.util.ArrayUtils; + +import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Date; import java.util.Objects; +import java.util.zip.GZIPInputStream; /** * Describes the information of an application process's death. @@ -321,85 +328,105 @@ public final class ApplicationExitInfo implements Parcelable { // be categorized in {@link #REASON_OTHER}, with subreason code starting from 1000. /** - * @see {@link #getPid} + * @see #getPid */ private int mPid; /** - * @see {@link #getRealUid} + * @see #getRealUid */ private int mRealUid; /** - * @see {@link #getPackageUid} + * @see #getPackageUid */ private int mPackageUid; /** - * @see {@link #getDefiningUid} + * @see #getDefiningUid */ private int mDefiningUid; /** - * @see {@link #getProcessName} + * @see #getProcessName */ private String mProcessName; /** - * @see {@link #getReason} + * @see #getReason */ private @Reason int mReason; /** - * @see {@link #getStatus} + * @see #getStatus */ private int mStatus; /** - * @see {@link #getImportance} + * @see #getImportance */ private @Importance int mImportance; /** - * @see {@link #getPss} + * @see #getPss */ private long mPss; /** - * @see {@link #getRss} + * @see #getRss */ private long mRss; /** - * @see {@link #getTimestamp} + * @see #getTimestamp */ private @CurrentTimeMillisLong long mTimestamp; /** - * @see {@link #getDescription} + * @see #getDescription */ private @Nullable String mDescription; /** - * @see {@link #getSubReason} + * @see #getSubReason */ private @SubReason int mSubReason; /** - * @see {@link #getConnectionGroup} + * @see #getConnectionGroup */ private int mConnectionGroup; /** - * @see {@link #getPackageName} + * @see #getPackageName */ private String mPackageName; /** - * @see {@link #getPackageList} + * @see #getPackageList */ private String[] mPackageList; + /** + * @see #getProcessStateSummary + */ + private byte[] mState; + + /** + * The file to the trace file in the storage; + * + * for system internal use only, will not retain across processes. + * + * @see #getTraceInputStream + */ + private File mTraceFile; + + /** + * The Binder interface to retrieve the file descriptor to + * the trace file from the system. + */ + private IAppTraceRetriever mAppTraceRetriever; + /** @hide */ @IntDef(prefix = { "REASON_" }, value = { REASON_UNKNOWN, @@ -556,6 +583,54 @@ public final class ApplicationExitInfo implements Parcelable { return UserHandle.of(UserHandle.getUserId(mRealUid)); } + /** + * Return the state data set by calling {@link ActivityManager#setProcessStateSummary} + * from the process before its death. + * + * @return The process-customized data + * @see ActivityManager#setProcessStateSummary(byte[]) + */ + public @Nullable byte[] getProcessStateSummary() { + return mState; + } + + /** + * Return the InputStream to the traces that was taken by the system + * prior to the death of the process; typically it'll be available when + * the reason is {@link #REASON_ANR}, though if the process gets an ANR + * but recovers, and dies for another reason later, this trace will be included + * in the record of {@link ApplicationExitInfo} still. + * + * @return The input stream to the traces that was taken by the system + * prior to the death of the process. + */ + public @Nullable InputStream getTraceInputStream() throws IOException { + if (mAppTraceRetriever == null) { + return null; + } + try { + final ParcelFileDescriptor fd = mAppTraceRetriever.getTraceFileDescriptor( + mPackageName, mPackageUid, mPid); + if (fd == null) { + return null; + } + return new GZIPInputStream(new ParcelFileDescriptor.AutoCloseInputStream(fd)); + } catch (RemoteException e) { + return null; + } + } + + /** + * Similar to {@link #getTraceInputStream} but return the File object. + * + * For internal use only. + * + * @hide + */ + public @Nullable File getTraceFile() { + return mTraceFile; + } + /** * A subtype reason in conjunction with {@link #mReason}. * @@ -569,7 +644,7 @@ public final class ApplicationExitInfo implements Parcelable { /** * The connection group this process belongs to, if there is any. - * @see {@link android.content.Context#updateServiceGroup}. + * @see android.content.Context#updateServiceGroup * * For internal use only. * @@ -582,8 +657,6 @@ public final class ApplicationExitInfo implements Parcelable { /** * Name of first package running in this process; * - * For system internal use only, will not retain across processes. - * * @hide */ public String getPackageName() { @@ -602,7 +675,7 @@ public final class ApplicationExitInfo implements Parcelable { } /** - * @see {@link #getPid} + * @see #getPid * * @hide */ @@ -611,7 +684,7 @@ public final class ApplicationExitInfo implements Parcelable { } /** - * @see {@link #getRealUid} + * @see #getRealUid * * @hide */ @@ -620,7 +693,7 @@ public final class ApplicationExitInfo implements Parcelable { } /** - * @see {@link #getPackageUid} + * @see #getPackageUid * * @hide */ @@ -629,7 +702,7 @@ public final class ApplicationExitInfo implements Parcelable { } /** - * @see {@link #getDefiningUid} + * @see #getDefiningUid * * @hide */ @@ -638,7 +711,7 @@ public final class ApplicationExitInfo implements Parcelable { } /** - * @see {@link #getProcessName} + * @see #getProcessName * * @hide */ @@ -647,7 +720,7 @@ public final class ApplicationExitInfo implements Parcelable { } /** - * @see {@link #getReason} + * @see #getReason * * @hide */ @@ -656,7 +729,7 @@ public final class ApplicationExitInfo implements Parcelable { } /** - * @see {@link #getStatus} + * @see #getStatus * * @hide */ @@ -665,7 +738,7 @@ public final class ApplicationExitInfo implements Parcelable { } /** - * @see {@link #getImportance} + * @see #getImportance * * @hide */ @@ -674,7 +747,7 @@ public final class ApplicationExitInfo implements Parcelable { } /** - * @see {@link #getPss} + * @see #getPss * * @hide */ @@ -683,7 +756,7 @@ public final class ApplicationExitInfo implements Parcelable { } /** - * @see {@link #getRss} + * @see #getRss * * @hide */ @@ -692,7 +765,7 @@ public final class ApplicationExitInfo implements Parcelable { } /** - * @see {@link #getTimestamp} + * @see #getTimestamp * * @hide */ @@ -701,7 +774,7 @@ public final class ApplicationExitInfo implements Parcelable { } /** - * @see {@link #getDescription} + * @see #getDescription * * @hide */ @@ -710,7 +783,7 @@ public final class ApplicationExitInfo implements Parcelable { } /** - * @see {@link #getSubReason} + * @see #getSubReason * * @hide */ @@ -719,7 +792,7 @@ public final class ApplicationExitInfo implements Parcelable { } /** - * @see {@link #getConnectionGroup} + * @see #getConnectionGroup * * @hide */ @@ -728,7 +801,7 @@ public final class ApplicationExitInfo implements Parcelable { } /** - * @see {@link #getPackageName} + * @see #getPackageName * * @hide */ @@ -737,7 +810,7 @@ public final class ApplicationExitInfo implements Parcelable { } /** - * @see {@link #getPackageList} + * @see #getPackageList * * @hide */ @@ -745,6 +818,33 @@ public final class ApplicationExitInfo implements Parcelable { mPackageList = packageList; } + /** + * @see #getProcessStateSummary + * + * @hide + */ + public void setProcessStateSummary(final byte[] state) { + mState = state; + } + + /** + * @see #getTraceFile + * + * @hide + */ + public void setTraceFile(final File traceFile) { + mTraceFile = traceFile; + } + + /** + * @see #mAppTraceRetriever + * + * @hide + */ + public void setAppTraceRetriever(final IAppTraceRetriever retriever) { + mAppTraceRetriever = retriever; + } + @Override public int describeContents() { return 0; @@ -757,6 +857,7 @@ public final class ApplicationExitInfo implements Parcelable { dest.writeInt(mPackageUid); dest.writeInt(mDefiningUid); dest.writeString(mProcessName); + dest.writeString(mPackageName); dest.writeInt(mConnectionGroup); dest.writeInt(mReason); dest.writeInt(mSubReason); @@ -766,6 +867,13 @@ public final class ApplicationExitInfo implements Parcelable { dest.writeLong(mRss); dest.writeLong(mTimestamp); dest.writeString(mDescription); + dest.writeByteArray(mState); + if (mAppTraceRetriever != null) { + dest.writeInt(1); + dest.writeStrongBinder(mAppTraceRetriever.asBinder()); + } else { + dest.writeInt(0); + } } /** @hide */ @@ -779,6 +887,7 @@ public final class ApplicationExitInfo implements Parcelable { mPackageUid = other.mPackageUid; mDefiningUid = other.mDefiningUid; mProcessName = other.mProcessName; + mPackageName = other.mPackageName; mConnectionGroup = other.mConnectionGroup; mReason = other.mReason; mStatus = other.mStatus; @@ -790,6 +899,9 @@ public final class ApplicationExitInfo implements Parcelable { mDescription = other.mDescription; mPackageName = other.mPackageName; mPackageList = other.mPackageList; + mState = other.mState; + mTraceFile = other.mTraceFile; + mAppTraceRetriever = other.mAppTraceRetriever; } private ApplicationExitInfo(@NonNull Parcel in) { @@ -798,6 +910,7 @@ public final class ApplicationExitInfo implements Parcelable { mPackageUid = in.readInt(); mDefiningUid = in.readInt(); mProcessName = in.readString(); + mPackageName = in.readString(); mConnectionGroup = in.readInt(); mReason = in.readInt(); mSubReason = in.readInt(); @@ -807,6 +920,10 @@ public final class ApplicationExitInfo implements Parcelable { mRss = in.readLong(); mTimestamp = in.readLong(); mDescription = in.readString(); + mState = in.createByteArray(); + if (in.readInt() == 1) { + mAppTraceRetriever = IAppTraceRetriever.Stub.asInterface(in.readStrongBinder()); + } } public @NonNull static final Creator CREATOR = @@ -839,6 +956,9 @@ public final class ApplicationExitInfo implements Parcelable { pw.print(prefix + " pss="); DebugUtils.printSizeValue(pw, mPss << 10); pw.println(); pw.print(prefix + " rss="); DebugUtils.printSizeValue(pw, mRss << 10); pw.println(); pw.println(prefix + " description=" + mDescription); + pw.println(prefix + " state=" + (ArrayUtils.isEmpty(mState) + ? "empty" : Integer.toString(mState.length) + " bytes")); + pw.println(prefix + " trace=" + mTraceFile); } @Override @@ -859,6 +979,9 @@ public final class ApplicationExitInfo implements Parcelable { sb.append(" pss="); DebugUtils.sizeValueToString(mPss << 10, sb); sb.append(" rss="); DebugUtils.sizeValueToString(mRss << 10, sb); sb.append(" description=").append(mDescription); + sb.append(" state=").append(ArrayUtils.isEmpty(mState) + ? "empty" : Integer.toString(mState.length) + " bytes"); + sb.append(" trace=").append(mTraceFile); return sb.toString(); } @@ -961,6 +1084,9 @@ public final class ApplicationExitInfo implements Parcelable { proto.write(ApplicationExitInfoProto.RSS, mRss); proto.write(ApplicationExitInfoProto.TIMESTAMP, mTimestamp); proto.write(ApplicationExitInfoProto.DESCRIPTION, mDescription); + proto.write(ApplicationExitInfoProto.STATE, mState); + proto.write(ApplicationExitInfoProto.TRACE_FILE, + mTraceFile == null ? null : mTraceFile.getAbsolutePath()); proto.end(token); } @@ -1019,6 +1145,15 @@ public final class ApplicationExitInfo implements Parcelable { case (int) ApplicationExitInfoProto.DESCRIPTION: mDescription = proto.readString(ApplicationExitInfoProto.DESCRIPTION); break; + case (int) ApplicationExitInfoProto.STATE: + mState = proto.readBytes(ApplicationExitInfoProto.STATE); + break; + case (int) ApplicationExitInfoProto.TRACE_FILE: + final String path = proto.readString(ApplicationExitInfoProto.TRACE_FILE); + if (!TextUtils.isEmpty(path)) { + mTraceFile = new File(path); + } + break; } } proto.end(token); diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 6f0611e68cda8..b8221b4efa2f2 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -652,4 +652,27 @@ interface IActivityManager { */ void setActivityLocusContext(in ComponentName activity, in LocusId locusId, in IBinder appToken); + + /** + * Set custom state data for this process. It will be included in the record of + * {@link ApplicationExitInfo} on the death of the current calling process; the new process + * of the app can retrieve this state data by calling + * {@link ApplicationExitInfo#getProcessStateSummary} on the record returned by + * {@link #getHistoricalProcessExitReasons}. + * + *

This would be useful for the calling app to save its stateful data: if it's + * killed later for any reason, the new process of the app can know what the + * previous process of the app was doing. For instance, you could use this to encode + * the current level in a game, or a set of features/experiments that were enabled. Later you + * could analyze under what circumstances the app tends to crash or use too much memory. + * However, it's not suggested to rely on this to restore the applications previous UI state + * or so, it's only meant for analyzing application healthy status.

+ * + *

System might decide to throttle the calls to this API; so call this API in a reasonable + * manner, excessive calls to this API could result a {@link java.lang.RuntimeException}. + *

+ * + * @param state The customized state data + */ + void setProcessStateSummary(in byte[] state); } diff --git a/core/java/android/app/IAppTraceRetriever.aidl b/core/java/android/app/IAppTraceRetriever.aidl new file mode 100644 index 0000000000000..1463da7db5d15 --- /dev/null +++ b/core/java/android/app/IAppTraceRetriever.aidl @@ -0,0 +1,38 @@ +/* + * 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 android.app; + +import android.os.ParcelFileDescriptor; + +/** + * An interface that's to be used by {@link ApplicationExitInfo#getTraceFile()} + * to retrieve the actual file descriptor to its trace file. + * + * @hide + */ +interface IAppTraceRetriever { + /** + * Retrieve the trace file with given packageName/uid/pid. + * + * @param packagename The target package name of the trace + * @param uid The target UID of the trace + * @param pid The target PID of the trace + * @return The file descriptor to the trace file, or null if it's not found. + */ + ParcelFileDescriptor getTraceFileDescriptor(in String packageName, + int uid, int pid); +} diff --git a/core/proto/android/app/appexitinfo.proto b/core/proto/android/app/appexitinfo.proto index 66173f60bfd51..4b9444e83e1c4 100644 --- a/core/proto/android/app/appexitinfo.proto +++ b/core/proto/android/app/appexitinfo.proto @@ -42,4 +42,6 @@ message ApplicationExitInfoProto { optional int64 rss = 12; optional int64 timestamp = 13; optional string description = 14; + optional bytes state = 15; + optional string trace_file = 16; } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index ebca1f7e97a41..a45849823a1b8 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -579,6 +579,13 @@ public class ActivityManagerService extends IActivityManager.Stub static final String EXTRA_DESCRIPTION = "android.intent.extra.DESCRIPTION"; static final String EXTRA_BUGREPORT_TYPE = "android.intent.extra.BUGREPORT_TYPE"; + /** + * The maximum number of bytes that {@link #setProcessStateSummary} accepts. + * + * @see {@link android.app.ActivityManager#setProcessStateSummary(byte[])} + */ + static final int MAX_STATE_DATA_SIZE = 128; + /** All system services */ SystemServiceManager mSystemServiceManager; @@ -3202,7 +3209,7 @@ public class ActivityManagerService extends IActivityManager.Stub return mAtmInternal.compatibilityInfoForPackage(ai); } - private void enforceNotIsolatedCaller(String caller) { + /* package */ void enforceNotIsolatedCaller(String caller) { if (UserHandle.isIsolated(Binder.getCallingUid())) { throw new SecurityException("Isolated process not allowed to call " + caller); } @@ -3887,6 +3894,18 @@ public class ActivityManagerService extends IActivityManager.Stub public static File dumpStackTraces(ArrayList firstPids, ProcessCpuTracker processCpuTracker, SparseArray lastPids, ArrayList nativePids, StringWriter logExceptionCreatingFile) { + return dumpStackTraces(firstPids, processCpuTracker, lastPids, nativePids, + logExceptionCreatingFile, null); + } + + /** + * @param firstPidOffsets Optional, when it's set, it receives the start/end offset + * of the very first pid to be dumped. + */ + /* package */ static File dumpStackTraces(ArrayList firstPids, + ProcessCpuTracker processCpuTracker, SparseArray lastPids, + ArrayList nativePids, StringWriter logExceptionCreatingFile, + long[] firstPidOffsets) { ArrayList extraPids = null; Slog.i(TAG, "dumpStackTraces pids=" + lastPids + " nativepids=" + nativePids); @@ -3938,12 +3957,22 @@ public class ActivityManagerService extends IActivityManager.Stub return null; } - dumpStackTraces(tracesFile.getAbsolutePath(), firstPids, nativePids, extraPids); + Pair offsets = dumpStackTraces( + tracesFile.getAbsolutePath(), firstPids, nativePids, extraPids); + if (firstPidOffsets != null) { + if (offsets == null) { + firstPidOffsets[0] = firstPidOffsets[1] = -1; + } else { + firstPidOffsets[0] = offsets.first; // Start offset to the ANR trace file + firstPidOffsets[1] = offsets.second; // End offset to the ANR trace file + } + } return tracesFile; } @GuardedBy("ActivityManagerService.class") private static SimpleDateFormat sAnrFileDateFormat; + static final String ANR_FILE_PREFIX = "anr_"; private static synchronized File createAnrDumpFile(File tracesDir) throws IOException { if (sAnrFileDateFormat == null) { @@ -3951,7 +3980,7 @@ public class ActivityManagerService extends IActivityManager.Stub } final String formattedDate = sAnrFileDateFormat.format(new Date()); - final File anrFile = new File(tracesDir, "anr_" + formattedDate); + final File anrFile = new File(tracesDir, ANR_FILE_PREFIX + formattedDate); if (anrFile.createNewFile()) { FileUtils.setPermissions(anrFile.getAbsolutePath(), 0600, -1, -1); // -rw------- @@ -4020,7 +4049,10 @@ public class ActivityManagerService extends IActivityManager.Stub return SystemClock.elapsedRealtime() - timeStart; } - public static void dumpStackTraces(String tracesFile, ArrayList firstPids, + /** + * @return The start/end offset of the trace of the very first PID + */ + public static Pair dumpStackTraces(String tracesFile, ArrayList firstPids, ArrayList nativePids, ArrayList extraPids) { Slog.i(TAG, "Dumping to " + tracesFile); @@ -4032,21 +4064,39 @@ public class ActivityManagerService extends IActivityManager.Stub // We must complete all stack dumps within 20 seconds. long remainingTime = 20 * 1000; + // As applications are usually interested with the ANR stack traces, but we can't share with + // them the stack traces other than their own stacks. So after the very first PID is + // dumped, remember the current file size. + long firstPidStart = -1; + long firstPidEnd = -1; + // First collect all of the stacks of the most important pids. if (firstPids != null) { int num = firstPids.size(); for (int i = 0; i < num; i++) { - Slog.i(TAG, "Collecting stacks for pid " + firstPids.get(i)); - final long timeTaken = dumpJavaTracesTombstoned(firstPids.get(i), tracesFile, + final int pid = firstPids.get(i); + // We don't copy ANR traces from the system_server intentionally. + final boolean firstPid = i == 0 && MY_PID != pid; + File tf = null; + if (firstPid) { + tf = new File(tracesFile); + firstPidStart = tf.exists() ? tf.length() : 0; + } + + Slog.i(TAG, "Collecting stacks for pid " + pid); + final long timeTaken = dumpJavaTracesTombstoned(pid, tracesFile, remainingTime); remainingTime -= timeTaken; if (remainingTime <= 0) { - Slog.e(TAG, "Aborting stack trace dump (current firstPid=" + firstPids.get(i) + - "); deadline exceeded."); - return; + Slog.e(TAG, "Aborting stack trace dump (current firstPid=" + pid + + "); deadline exceeded."); + return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null; } + if (firstPid) { + firstPidEnd = tf.length(); + } if (DEBUG_ANR) { Slog.d(TAG, "Done with pid " + firstPids.get(i) + " in " + timeTaken + "ms"); } @@ -4068,7 +4118,7 @@ public class ActivityManagerService extends IActivityManager.Stub if (remainingTime <= 0) { Slog.e(TAG, "Aborting stack trace dump (current native pid=" + pid + "); deadline exceeded."); - return; + return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null; } if (DEBUG_ANR) { @@ -4088,7 +4138,7 @@ public class ActivityManagerService extends IActivityManager.Stub if (remainingTime <= 0) { Slog.e(TAG, "Aborting stack trace dump (current extra pid=" + pid + "); deadline exceeded."); - return; + return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null; } if (DEBUG_ANR) { @@ -4097,6 +4147,7 @@ public class ActivityManagerService extends IActivityManager.Stub } } Slog.i(TAG, "Done dumping"); + return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null; } @Override @@ -10280,6 +10331,15 @@ public class ActivityManagerService extends IActivityManager.Stub return new ParceledListSlice(results); } + @Override + public void setProcessStateSummary(@Nullable byte[] state) { + if (state != null && state.length > MAX_STATE_DATA_SIZE) { + throw new IllegalArgumentException("Data size is too large"); + } + mProcessList.mAppExitInfoTracker.setProcessStateSummary(Binder.getCallingUid(), + Binder.getCallingPid(), state); + } + /** * Check if the calling process has the permission to dump given package, * throw SecurityException if it doesn't have the permission. @@ -10287,7 +10347,7 @@ public class ActivityManagerService extends IActivityManager.Stub * @return The UID of the given package, or {@link android.os.Process#INVALID_UID} * if the package is not found. */ - private int enforceDumpPermissionForPackage(String packageName, int userId, int callingUid, + int enforceDumpPermissionForPackage(String packageName, int userId, int callingUid, String function) { long identity = Binder.clearCallingIdentity(); int uid = Process.INVALID_UID; diff --git a/services/core/java/com/android/server/am/AppExitInfoTracker.java b/services/core/java/com/android/server/am/AppExitInfoTracker.java index 028a059c538a6..0c3d02d678bd5 100644 --- a/services/core/java/com/android/server/am/AppExitInfoTracker.java +++ b/services/core/java/com/android/server/am/AppExitInfoTracker.java @@ -17,23 +17,31 @@ package com.android.server.am; import static android.app.ActivityManager.RunningAppProcessInfo.procStateToImportance; +import static android.app.ActivityManagerInternal.ALLOW_NON_FULL; import static android.os.Process.THREAD_PRIORITY_BACKGROUND; 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.annotation.Nullable; import android.app.ApplicationExitInfo; import android.app.ApplicationExitInfo.Reason; import android.app.ApplicationExitInfo.SubReason; +import android.app.IAppTraceRetriever; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.icu.text.SimpleDateFormat; +import android.os.Binder; +import android.os.FileUtils; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.Process; import android.os.SystemProperties; import android.os.UserHandle; import android.system.OsConstants; @@ -52,12 +60,17 @@ import android.util.proto.WireTypeMismatchException; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.ProcessMap; +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.IoThread; import com.android.server.ServiceThread; import com.android.server.SystemServiceManager; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; @@ -68,6 +81,10 @@ import java.util.List; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.zip.GZIPOutputStream; /** * A class to manage all the {@link android.app.ApplicationExitInfo} records. @@ -80,16 +97,21 @@ public final class AppExitInfoTracker { */ private static final long APP_EXIT_INFO_PERSIST_INTERVAL = TimeUnit.MINUTES.toMillis(30); - /** These are actions that the forEachPackage should take after each iteration */ + /** These are actions that the forEach* should take after each iteration */ private static final int FOREACH_ACTION_NONE = 0; - private static final int FOREACH_ACTION_REMOVE_PACKAGE = 1; + private static final int FOREACH_ACTION_REMOVE_ITEM = 1; private static final int FOREACH_ACTION_STOP_ITERATION = 2; private static final int APP_EXIT_RAW_INFO_POOL_SIZE = 8; + @VisibleForTesting + static final String APP_EXIT_STORE_DIR = "procexitstore"; + @VisibleForTesting static final String APP_EXIT_INFO_FILE = "procexitinfo"; + private static final String APP_TRACE_FILE_SUFFIX = ".gz"; + private final Object mLock = new Object(); /** @@ -152,6 +174,13 @@ public final class AppExitInfoTracker { @GuardedBy("mLock") final ArrayList mTmpInfoList2 = new ArrayList(); + /** + * The path to the directory which includes the historical proc exit info file + * as specified in {@link #mProcExitInfoFile}, as well as the associated trace files. + */ + @VisibleForTesting + File mProcExitStoreDir; + /** * The path to the historical proc exit info file, persisted in the storage. */ @@ -176,6 +205,35 @@ public final class AppExitInfoTracker { final AppExitInfoExternalSource mAppExitInfoSourceLmkd = new AppExitInfoExternalSource("lmkd", ApplicationExitInfo.REASON_LOW_MEMORY); + /** + * The active per-UID/PID state data set by + * {@link android.app.ActivityManager#setProcessStateSummary}; + * these state data are to be "claimed" when its process dies, by then the data will be moved + * from this list to the new instance of ApplicationExitInfo. + * + *

The mapping here is UID -> PID -> state

+ * + * @see android.app.ActivityManager#setProcessStateSummary(byte[]) + */ + @GuardedBy("mLock") + final SparseArray> mActiveAppStateSummary = new SparseArray<>(); + + /** + * The active per-UID/PID trace file when an ANR occurs but the process hasn't been killed yet, + * each record is a path to the actual trace file; these files are to be "claimed" + * when its process dies, by then the "ownership" of the files will be transferred + * from this list to the new instance of ApplicationExitInfo. + * + *

The mapping here is UID -> PID -> file

+ */ + @GuardedBy("mLock") + final SparseArray> mActiveAppTraces = new SparseArray<>(); + + /** + * The implementation of the interface IAppTraceRetriever. + */ + final AppTraceRetriever mAppTraceRetriever = new AppTraceRetriever(); + AppExitInfoTracker() { mData = new ProcessMap(); mRawRecordsPool = new SynchronizedPool(APP_EXIT_RAW_INFO_POOL_SIZE); @@ -187,7 +245,13 @@ public final class AppExitInfoTracker { THREAD_PRIORITY_BACKGROUND, true /* allowIo */); thread.start(); mKillHandler = new KillHandler(thread.getLooper()); - mProcExitInfoFile = new File(SystemServiceManager.ensureSystemDir(), APP_EXIT_INFO_FILE); + + mProcExitStoreDir = new File(SystemServiceManager.ensureSystemDir(), APP_EXIT_STORE_DIR); + if (!FileUtils.createDir(mProcExitStoreDir)) { + Slog.e(TAG, "Unable to create " + mProcExitStoreDir); + return; + } + mProcExitInfoFile = new File(mProcExitStoreDir, APP_EXIT_INFO_FILE); mAppExitInfoHistoryListSize = service.mContext.getResources().getInteger( com.android.internal.R.integer.config_app_exit_info_history_list_size); @@ -304,7 +368,7 @@ public final class AppExitInfoTracker { + "(" + raw.getPid() + "/u" + raw.getRealUid() + ")"); } - ApplicationExitInfo info = getExitInfo(raw.getPackageName(), + ApplicationExitInfo info = getExitInfoLocked(raw.getPackageName(), raw.getPackageUid(), raw.getPid()); // query zygote and lmkd to get the exit info, and clear the saved info @@ -312,7 +376,7 @@ public final class AppExitInfoTracker { raw.getPid(), raw.getRealUid()); Pair lmkd = mAppExitInfoSourceLmkd.remove( raw.getPid(), raw.getRealUid()); - mIsolatedUidRecords.removeIsolatedUid(raw.getRealUid()); + mIsolatedUidRecords.removeIsolatedUidLocked(raw.getRealUid()); if (info == null) { info = addExitInfoLocked(raw); @@ -333,7 +397,7 @@ public final class AppExitInfoTracker { @VisibleForTesting @GuardedBy("mLock") void handleNoteAppKillLocked(final ApplicationExitInfo raw) { - ApplicationExitInfo info = getExitInfo( + ApplicationExitInfo info = getExitInfoLocked( raw.getPackageName(), raw.getPackageUid(), raw.getPid()); if (info == null) { @@ -359,7 +423,7 @@ public final class AppExitInfoTracker { final String[] packages = raw.getPackageList(); final int uid = raw.getPackageUid(); for (int i = 0; i < packages.length; i++) { - addExitInfoInner(packages[i], uid, info); + addExitInfoInnerLocked(packages[i], uid, info); } schedulePersistProcessExitInfo(false); @@ -400,39 +464,37 @@ public final class AppExitInfoTracker { * * @return true if a recond is updated */ - private boolean updateExitInfoIfNecessary(int pid, int uid, Integer status, Integer reason) { - synchronized (mLock) { - if (UserHandle.isIsolated(uid)) { - Integer k = mIsolatedUidRecords.getUidByIsolatedUid(uid); - if (k != null) { - uid = k; - } - } - ArrayList tlist = mTmpInfoList; - tlist.clear(); - final int targetUid = uid; - forEachPackage((packageName, records) -> { - AppExitInfoContainer container = records.get(targetUid); - if (container == null) { - return FOREACH_ACTION_NONE; - } - tlist.clear(); - container.getExitInfoLocked(pid, 1, tlist); - if (tlist.size() == 0) { - return FOREACH_ACTION_NONE; - } - ApplicationExitInfo info = tlist.get(0); - if (info.getRealUid() != targetUid) { - tlist.clear(); - return FOREACH_ACTION_NONE; - } - // Okay found it, update its reason. - updateExistingExitInfoRecordLocked(info, status, reason); - - return FOREACH_ACTION_STOP_ITERATION; - }); - return tlist.size() > 0; + @GuardedBy("mLock") + private boolean updateExitInfoIfNecessaryLocked( + int pid, int uid, Integer status, Integer reason) { + Integer k = mIsolatedUidRecords.getUidByIsolatedUid(uid); + if (k != null) { + uid = k; } + ArrayList tlist = mTmpInfoList; + tlist.clear(); + final int targetUid = uid; + forEachPackageLocked((packageName, records) -> { + AppExitInfoContainer container = records.get(targetUid); + if (container == null) { + return FOREACH_ACTION_NONE; + } + tlist.clear(); + container.getExitInfoLocked(pid, 1, tlist); + if (tlist.size() == 0) { + return FOREACH_ACTION_NONE; + } + ApplicationExitInfo info = tlist.get(0); + if (info.getRealUid() != targetUid) { + tlist.clear(); + return FOREACH_ACTION_NONE; + } + // Okay found it, update its reason. + updateExistingExitInfoRecordLocked(info, status, reason); + + return FOREACH_ACTION_STOP_ITERATION; + }); + return tlist.size() > 0; } /** @@ -441,38 +503,43 @@ public final class AppExitInfoTracker { @VisibleForTesting void getExitInfo(final String packageName, final int filterUid, final int filterPid, final int maxNum, final ArrayList results) { - synchronized (mLock) { - boolean emptyPackageName = TextUtils.isEmpty(packageName); - if (!emptyPackageName) { - // fast path - AppExitInfoContainer container = mData.get(packageName, filterUid); - if (container != null) { - container.getExitInfoLocked(filterPid, maxNum, results); - } - } else { - // slow path - final ArrayList list = mTmpInfoList2; - list.clear(); - // get all packages - forEachPackage((name, records) -> { - AppExitInfoContainer container = records.get(filterUid); + long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + boolean emptyPackageName = TextUtils.isEmpty(packageName); + if (!emptyPackageName) { + // fast path + AppExitInfoContainer container = mData.get(packageName, filterUid); if (container != null) { - mTmpInfoList.clear(); - results.addAll(container.toListLocked(mTmpInfoList, filterPid)); + container.getExitInfoLocked(filterPid, maxNum, results); } - return AppExitInfoTracker.FOREACH_ACTION_NONE; - }); + } else { + // slow path + final ArrayList list = mTmpInfoList2; + list.clear(); + // get all packages + forEachPackageLocked((name, records) -> { + AppExitInfoContainer container = records.get(filterUid); + if (container != null) { + mTmpInfoList.clear(); + results.addAll(container.toListLocked(mTmpInfoList, filterPid)); + } + return AppExitInfoTracker.FOREACH_ACTION_NONE; + }); - Collections.sort(list, (a, b) -> (int) (b.getTimestamp() - a.getTimestamp())); - int size = list.size(); - if (maxNum > 0) { - size = Math.min(size, maxNum); + Collections.sort(list, (a, b) -> (int) (b.getTimestamp() - a.getTimestamp())); + int size = list.size(); + if (maxNum > 0) { + size = Math.min(size, maxNum); + } + for (int i = 0; i < size; i++) { + results.add(list.get(i)); + } + list.clear(); } - for (int i = 0; i < size; i++) { - results.add(list.get(i)); - } - list.clear(); } + } finally { + Binder.restoreCallingIdentity(identity); } } @@ -480,17 +547,16 @@ public final class AppExitInfoTracker { * Return the first matching exit info record, for internal use, the parameters are not supposed * to be empty. */ - private ApplicationExitInfo getExitInfo(final String packageName, + @GuardedBy("mLock") + private ApplicationExitInfo getExitInfoLocked(final String packageName, final int filterUid, final int filterPid) { - synchronized (mLock) { - ArrayList list = mTmpInfoList; - list.clear(); - getExitInfo(packageName, filterUid, filterPid, 1, list); + ArrayList list = mTmpInfoList; + list.clear(); + getExitInfo(packageName, filterUid, filterPid, 1, list); - ApplicationExitInfo info = list.size() > 0 ? list.get(0) : null; - list.clear(); - return info; - } + ApplicationExitInfo info = list.size() > 0 ? list.get(0) : null; + list.clear(); + return info; } @VisibleForTesting @@ -498,8 +564,10 @@ public final class AppExitInfoTracker { mAppExitInfoSourceZygote.removeByUserId(userId); mAppExitInfoSourceLmkd.removeByUserId(userId); mIsolatedUidRecords.removeByUserId(userId); - removeByUserId(userId); - schedulePersistProcessExitInfo(true); + synchronized (mLock) { + removeByUserIdLocked(userId); + schedulePersistProcessExitInfo(true); + } } @VisibleForTesting @@ -507,13 +575,16 @@ public final class AppExitInfoTracker { if (packageName != null) { final boolean removeUid = TextUtils.isEmpty( mService.mPackageManagerInt.getNameForUid(uid)); - if (removeUid) { - mAppExitInfoSourceZygote.removeByUid(uid, allUsers); - mAppExitInfoSourceLmkd.removeByUid(uid, allUsers); - mIsolatedUidRecords.removeAppUid(uid, allUsers); + synchronized (mLock) { + if (removeUid) { + mAppExitInfoSourceZygote.removeByUidLocked(uid, allUsers); + mAppExitInfoSourceLmkd.removeByUidLocked(uid, allUsers); + mIsolatedUidRecords.removeAppUid(uid, allUsers); + } + removePackageLocked(packageName, uid, removeUid, + allUsers ? UserHandle.USER_ALL : UserHandle.getUserId(uid)); + schedulePersistProcessExitInfo(true); } - removePackage(packageName, allUsers ? UserHandle.USER_ALL : UserHandle.getUserId(uid)); - schedulePersistProcessExitInfo(true); } } @@ -593,6 +664,7 @@ public final class AppExitInfoTracker { } } synchronized (mLock) { + pruneAnrTracesIfNecessaryLocked(); mAppExitInfoLoaded = true; } } @@ -634,7 +706,7 @@ public final class AppExitInfoTracker { ProtoOutputStream proto = new ProtoOutputStream(out); proto.write(AppsExitInfoProto.LAST_UPDATE_TIMESTAMP, now); synchronized (mLock) { - forEachPackage((packageName, records) -> { + forEachPackageLocked((packageName, records) -> { long token = proto.start(AppsExitInfoProto.PACKAGES); proto.write(AppsExitInfoProto.Package.PACKAGE_NAME, packageName); int uidArraySize = records.size(); @@ -688,6 +760,9 @@ public final class AppExitInfoTracker { mProcExitInfoFile.delete(); } mData.getMap().clear(); + mActiveAppStateSummary.clear(); + mActiveAppTraces.clear(); + pruneAnrTracesIfNecessaryLocked(); } } @@ -695,15 +770,15 @@ public final class AppExitInfoTracker { * Helper function for shell command */ void clearHistoryProcessExitInfo(String packageName, int userId) { - synchronized (mLock) { - if (TextUtils.isEmpty(packageName)) { - if (userId == UserHandle.USER_ALL) { - mData.getMap().clear(); - } else { - removeByUserId(userId); - } - } else { - removePackage(packageName, userId); + if (TextUtils.isEmpty(packageName)) { + synchronized (mLock) { + removeByUserIdLocked(userId); + } + } else { + final int uid = mService.mPackageManagerInt.getPackageUid(packageName, + PackageManager.MATCH_ALL, userId); + synchronized (mLock) { + removePackageLocked(packageName, uid, true, userId); } } schedulePersistProcessExitInfo(true); @@ -716,7 +791,7 @@ public final class AppExitInfoTracker { pw.println("Last Timestamp of Persistence Into Persistent Storage: " + sdf.format(new Date(mLastAppExitInfoPersistTimestamp))); if (TextUtils.isEmpty(packageName)) { - forEachPackage((name, records) -> { + forEachPackageLocked((name, records) -> { dumpHistoryProcessExitInfoLocked(pw, " ", name, records, sdf); return AppExitInfoTracker.FOREACH_ACTION_NONE; }); @@ -741,86 +816,108 @@ public final class AppExitInfoTracker { } } - private void addExitInfoInner(String packageName, int userId, ApplicationExitInfo info) { - synchronized (mLock) { - AppExitInfoContainer container = mData.get(packageName, userId); - if (container == null) { - container = new AppExitInfoContainer(mAppExitInfoHistoryListSize); - if (UserHandle.isIsolated(info.getRealUid())) { - Integer k = mIsolatedUidRecords.getUidByIsolatedUid(info.getRealUid()); - if (k != null) { - container.mUid = k; - } - } else { - container.mUid = info.getRealUid(); + @GuardedBy("mLock") + private void addExitInfoInnerLocked(String packageName, int userId, ApplicationExitInfo info) { + AppExitInfoContainer container = mData.get(packageName, userId); + if (container == null) { + container = new AppExitInfoContainer(mAppExitInfoHistoryListSize); + if (UserHandle.isIsolated(info.getRealUid())) { + Integer k = mIsolatedUidRecords.getUidByIsolatedUid(info.getRealUid()); + if (k != null) { + container.mUid = k; } - mData.put(packageName, userId, container); + } else { + container.mUid = info.getRealUid(); } - container.addExitInfoLocked(info); + mData.put(packageName, userId, container); } + container.addExitInfoLocked(info); } - private void forEachPackage( + @GuardedBy("mLocked") + private void forEachPackageLocked( BiFunction, Integer> callback) { if (callback != null) { - synchronized (mLock) { - ArrayMap> map = mData.getMap(); - for (int i = map.size() - 1; i >= 0; i--) { - switch (callback.apply(map.keyAt(i), map.valueAt(i))) { - case FOREACH_ACTION_REMOVE_PACKAGE: - map.removeAt(i); - break; - case FOREACH_ACTION_STOP_ITERATION: - i = 0; - break; - case FOREACH_ACTION_NONE: - default: - break; - } - } - } - } - } - - private void removePackage(String packageName, int userId) { - synchronized (mLock) { - if (userId == UserHandle.USER_ALL) { - mData.getMap().remove(packageName); - } else { - ArrayMap> map = - mData.getMap(); - SparseArray array = map.get(packageName); - if (array == null) { - return; - } - for (int i = array.size() - 1; i >= 0; i--) { - if (UserHandle.getUserId(array.keyAt(i)) == userId) { - array.removeAt(i); + ArrayMap> map = mData.getMap(); + for (int i = map.size() - 1; i >= 0; i--) { + switch (callback.apply(map.keyAt(i), map.valueAt(i))) { + case FOREACH_ACTION_REMOVE_ITEM: + final SparseArray records = map.valueAt(i); + for (int j = records.size() - 1; j >= 0; j--) { + records.valueAt(j).destroyLocked(); + } + map.removeAt(i); + break; + case FOREACH_ACTION_STOP_ITERATION: + i = 0; + break; + case FOREACH_ACTION_NONE: + default: break; - } - } - if (array.size() == 0) { - map.remove(packageName); } } } } - private void removeByUserId(final int userId) { - if (userId == UserHandle.USER_ALL) { - synchronized (mLock) { - mData.getMap().clear(); + @GuardedBy("mLocked") + private void removePackageLocked(String packageName, int uid, boolean removeUid, int userId) { + if (removeUid) { + mActiveAppStateSummary.remove(uid); + final int idx = mActiveAppTraces.indexOfKey(uid); + if (idx >= 0) { + final SparseArray array = mActiveAppTraces.valueAt(idx); + for (int i = array.size() - 1; i >= 0; i--) { + array.valueAt(i).delete(); + } + mActiveAppTraces.removeAt(idx); } + } + ArrayMap> map = mData.getMap(); + SparseArray array = map.get(packageName); + if (array == null) { return; } - forEachPackage((packageName, records) -> { + if (userId == UserHandle.USER_ALL) { + for (int i = array.size() - 1; i >= 0; i--) { + array.valueAt(i).destroyLocked(); + } + mData.getMap().remove(packageName); + } else { + for (int i = array.size() - 1; i >= 0; i--) { + if (UserHandle.getUserId(array.keyAt(i)) == userId) { + array.valueAt(i).destroyLocked(); + array.removeAt(i); + break; + } + } + if (array.size() == 0) { + map.remove(packageName); + } + } + } + + @GuardedBy("mLocked") + private void removeByUserIdLocked(final int userId) { + if (userId == UserHandle.USER_ALL) { + mData.getMap().clear(); + mActiveAppStateSummary.clear(); + mActiveAppTraces.clear(); + pruneAnrTracesIfNecessaryLocked(); + return; + } + removeFromSparse2dArray(mActiveAppStateSummary, + (v) -> UserHandle.getUserId(v) == userId, null, null); + removeFromSparse2dArray(mActiveAppTraces, + (v) -> UserHandle.getUserId(v) == userId, null, (v) -> v.delete()); + forEachPackageLocked((packageName, records) -> { for (int i = records.size() - 1; i >= 0; i--) { if (UserHandle.getUserId(records.keyAt(i)) == userId) { + records.valueAt(i).destroyLocked(); records.removeAt(i); break; } } - return records.size() == 0 ? FOREACH_ACTION_REMOVE_PACKAGE : FOREACH_ACTION_NONE; + return records.size() == 0 ? FOREACH_ACTION_REMOVE_ITEM : FOREACH_ACTION_NONE; }); } @@ -861,6 +958,262 @@ public final class AppExitInfoTracker { mRawRecordsPool.release(info); } + /** + * Called from {@link ActivityManagerService#setProcessStateSummary}. + */ + @VisibleForTesting + void setProcessStateSummary(int uid, final int pid, final byte[] data) { + synchronized (mLock) { + Integer k = mIsolatedUidRecords.getUidByIsolatedUid(uid); + if (k != null) { + uid = k; + } + putToSparse2dArray(mActiveAppStateSummary, uid, pid, data, SparseArray::new, null); + } + } + + @VisibleForTesting + @Nullable byte[] getProcessStateSummary(int uid, final int pid) { + synchronized (mLock) { + Integer k = mIsolatedUidRecords.getUidByIsolatedUid(uid); + if (k != null) { + uid = k; + } + int index = mActiveAppStateSummary.indexOfKey(uid); + if (index < 0) { + return null; + } + return mActiveAppStateSummary.valueAt(index).get(pid); + } + } + + /** + * Called from ProcessRecord when an ANR occurred and the ANR trace is taken. + */ + void scheduleLogAnrTrace(final int pid, final int uid, final String[] packageList, + final File traceFile, final long startOff, final long endOff) { + mKillHandler.sendMessage(PooledLambda.obtainMessage( + this::handleLogAnrTrace, pid, uid, packageList, + traceFile, startOff, endOff)); + } + + /** + * Copy and compress the given ANR trace file + */ + @VisibleForTesting + void handleLogAnrTrace(final int pid, int uid, final String[] packageList, + final File traceFile, final long startOff, final long endOff) { + if (!traceFile.exists() || ArrayUtils.isEmpty(packageList)) { + return; + } + final long size = traceFile.length(); + final long length = endOff - startOff; + if (startOff >= size || endOff > size || length <= 0) { + return; + } + + final File outFile = new File(mProcExitStoreDir, traceFile.getName() + + APP_TRACE_FILE_SUFFIX); + // Copy & compress + if (copyToGzFile(traceFile, outFile, startOff, length)) { + // Wrote successfully. + synchronized (mLock) { + Integer k = mIsolatedUidRecords.getUidByIsolatedUid(uid); + if (k != null) { + uid = k; + } + if (DEBUG_PROCESSES) { + Slog.i(TAG, "Stored ANR traces of " + pid + "/u" + uid + " in " + outFile); + } + boolean pending = true; + // Unlikely but possible: the app has died + for (int i = 0; i < packageList.length; i++) { + final AppExitInfoContainer container = mData.get(packageList[i], uid); + // Try to see if we could append this trace to an existing record + if (container != null && container.appendTraceIfNecessaryLocked(pid, outFile)) { + // Okay someone took it + pending = false; + } + } + if (pending) { + // Save it into a temporary list for later use (when the app dies). + putToSparse2dArray(mActiveAppTraces, uid, pid, outFile, + SparseArray::new, (v) -> v.delete()); + } + } + } + } + + /** + * Copy the given portion of the file into a gz file. + * + * @param inFile The source file. + * @param outFile The destination file, which will be compressed in gzip format. + * @param start The start offset where the copy should start from. + * @param length The number of bytes that should be copied. + * @return If the copy was successful or not. + */ + private static boolean copyToGzFile(final File inFile, final File outFile, + final long start, final long length) { + long remaining = length; + try ( + BufferedInputStream in = new BufferedInputStream(new FileInputStream(inFile)); + GZIPOutputStream out = new GZIPOutputStream(new BufferedOutputStream( + new FileOutputStream(outFile)))) { + final byte[] buffer = new byte[8192]; + in.skip(start); + while (remaining > 0) { + int t = in.read(buffer, 0, (int) Math.min(buffer.length, remaining)); + if (t < 0) { + break; + } + out.write(buffer, 0, t); + remaining -= t; + } + } catch (IOException e) { + if (DEBUG_PROCESSES) { + Slog.e(TAG, "Error in copying ANR trace from " + inFile + " to " + outFile, e); + } + return false; + } + return remaining == 0 && outFile.exists(); + } + + /** + * In case there is any orphan ANR trace file, remove it. + */ + @GuardedBy("mLock") + private void pruneAnrTracesIfNecessaryLocked() { + final ArraySet allFiles = new ArraySet(); + final File[] files = mProcExitStoreDir.listFiles((f) -> { + final String name = f.getName(); + boolean trace = name.startsWith(ActivityManagerService.ANR_FILE_PREFIX) + && name.endsWith(APP_TRACE_FILE_SUFFIX); + if (trace) { + allFiles.add(name); + } + return trace; + }); + if (ArrayUtils.isEmpty(files)) { + return; + } + // Find out the owners from the existing records + forEachPackageLocked((name, records) -> { + for (int i = records.size() - 1; i >= 0; i--) { + final AppExitInfoContainer container = records.valueAt(i); + container.forEachRecordLocked((pid, info) -> { + final File traceFile = info.getTraceFile(); + if (traceFile != null) { + allFiles.remove(traceFile.getName()); + } + return FOREACH_ACTION_NONE; + }); + } + return AppExitInfoTracker.FOREACH_ACTION_NONE; + }); + // See if there is any active process owns it. + forEachSparse2dArray(mActiveAppTraces, (v) -> allFiles.remove(v.getName())); + + // Remove orphan traces if nobody claims it. + for (int i = allFiles.size() - 1; i >= 0; i--) { + (new File(mProcExitStoreDir, allFiles.valueAt(i))).delete(); + } + } + + /** + * A utility function to add the given value to the given 2d SparseArray + */ + private static , U> void putToSparse2dArray(final SparseArray array, + final int outerKey, final int innerKey, final U value, final Supplier newInstance, + final Consumer actionToOldValue) { + int idx = array.indexOfKey(outerKey); + T innerArray = null; + if (idx < 0) { + innerArray = newInstance.get(); + array.put(outerKey, innerArray); + } else { + innerArray = array.valueAt(idx); + } + idx = innerArray.indexOfKey(innerKey); + if (idx >= 0) { + if (actionToOldValue != null) { + actionToOldValue.accept(innerArray.valueAt(idx)); + } + innerArray.setValueAt(idx, value); + } else { + innerArray.put(innerKey, value); + } + } + + /** + * A utility function to iterate through the given 2d SparseArray + */ + private static , U> void forEachSparse2dArray( + final SparseArray array, final Consumer action) { + if (action != null) { + for (int i = array.size() - 1; i >= 0; i--) { + T innerArray = array.valueAt(i); + if (innerArray == null) { + continue; + } + for (int j = innerArray.size() - 1; j >= 0; j--) { + action.accept(innerArray.valueAt(j)); + } + } + } + } + + /** + * A utility function to remove elements from the given 2d SparseArray + */ + private static , U> void removeFromSparse2dArray( + final SparseArray array, final Predicate outerPredicate, + final Predicate innerPredicate, final Consumer action) { + for (int i = array.size() - 1; i >= 0; i--) { + if (outerPredicate == null || outerPredicate.test(array.keyAt(i))) { + final T innerArray = array.valueAt(i); + if (innerArray == null) { + continue; + } + for (int j = innerArray.size() - 1; j >= 0; j--) { + if (innerPredicate == null || innerPredicate.test(innerArray.keyAt(j))) { + if (action != null) { + action.accept(innerArray.valueAt(j)); + } + innerArray.removeAt(j); + } + } + if (innerArray.size() == 0) { + array.removeAt(i); + } + } + } + } + + /** + * A utility function to find and remove elements from the given 2d SparseArray. + */ + private static , U> U findAndRemoveFromSparse2dArray( + final SparseArray array, final int outerKey, final int innerKey) { + final int idx = array.indexOfKey(outerKey); + if (idx >= 0) { + T p = array.valueAt(idx); + if (p == null) { + return null; + } + final int innerIdx = p.indexOfKey(innerKey); + if (innerIdx >= 0) { + final U ret = p.valueAt(innerIdx); + p.removeAt(innerIdx); + if (p.size() == 0) { + array.removeAt(idx); + } + return ret; + } + } + return null; + } + /** * A container class of {@link android.app.ApplicationExitInfo} */ @@ -934,10 +1287,68 @@ public final class AppExitInfoTracker { } } if (oldestIndex >= 0) { + final File traceFile = mInfos.valueAt(oldestIndex).getTraceFile(); + if (traceFile != null) { + traceFile.delete(); + } mInfos.removeAt(oldestIndex); } } - mInfos.append(info.getPid(), info); + // Claim the state information if there is any + final int uid = info.getPackageUid(); + final int pid = info.getPid(); + info.setProcessStateSummary(findAndRemoveFromSparse2dArray( + mActiveAppStateSummary, uid, pid)); + info.setTraceFile(findAndRemoveFromSparse2dArray(mActiveAppTraces, uid, pid)); + info.setAppTraceRetriever(mAppTraceRetriever); + mInfos.append(pid, info); + } + + @GuardedBy("mLock") + boolean appendTraceIfNecessaryLocked(final int pid, final File traceFile) { + final ApplicationExitInfo r = mInfos.get(pid); + if (r != null) { + r.setTraceFile(traceFile); + r.setAppTraceRetriever(mAppTraceRetriever); + return true; + } + return false; + } + + @GuardedBy("mLock") + void destroyLocked() { + for (int i = mInfos.size() - 1; i >= 0; i--) { + ApplicationExitInfo ai = mInfos.valueAt(i); + final File traceFile = ai.getTraceFile(); + if (traceFile != null) { + traceFile.delete(); + } + ai.setTraceFile(null); + ai.setAppTraceRetriever(null); + } + } + + @GuardedBy("mLock") + void forEachRecordLocked(final BiFunction callback) { + if (callback != null) { + for (int i = mInfos.size() - 1; i >= 0; i--) { + switch (callback.apply(mInfos.keyAt(i), mInfos.valueAt(i))) { + case FOREACH_ACTION_REMOVE_ITEM: + final File traceFile = mInfos.valueAt(i).getTraceFile(); + if (traceFile != null) { + traceFile.delete(); + } + mInfos.removeAt(i); + break; + case FOREACH_ACTION_STOP_ITERATION: + i = 0; + break; + case FOREACH_ACTION_NONE: + default: + break; + } + } + } } @GuardedBy("mLock") @@ -1033,6 +1444,7 @@ public final class AppExitInfoTracker { } } + @GuardedBy("mLock") Integer getUidByIsolatedUid(int isolatedUid) { if (UserHandle.isIsolated(isolatedUid)) { synchronized (mLock) { @@ -1053,6 +1465,7 @@ public final class AppExitInfoTracker { } } + @VisibleForTesting void removeAppUid(int uid, boolean allUsers) { synchronized (mLock) { if (allUsers) { @@ -1071,23 +1484,22 @@ public final class AppExitInfoTracker { } } - int removeIsolatedUid(int isolatedUid) { + @GuardedBy("mLock") + int removeIsolatedUidLocked(int isolatedUid) { if (!UserHandle.isIsolated(isolatedUid)) { return isolatedUid; } - synchronized (mLock) { - int uid = mIsolatedUidToUidMap.get(isolatedUid, -1); - if (uid == -1) { - return isolatedUid; - } - mIsolatedUidToUidMap.remove(isolatedUid); - ArraySet set = mUidToIsolatedUidMap.get(uid); - if (set != null) { - set.remove(isolatedUid); - } - // let the ArraySet stay in the mUidToIsolatedUidMap even if it's empty - return uid; + int uid = mIsolatedUidToUidMap.get(isolatedUid, -1); + if (uid == -1) { + return isolatedUid; } + mIsolatedUidToUidMap.remove(isolatedUid); + ArraySet set = mUidToIsolatedUidMap.get(uid); + if (set != null) { + set.remove(isolatedUid); + } + // let the ArraySet stay in the mUidToIsolatedUidMap even if it's empty + return uid; } void removeByUserId(int userId) { @@ -1193,33 +1605,29 @@ public final class AppExitInfoTracker { mPresetReason = reason; } - void add(int pid, int uid, Object extra) { - if (UserHandle.isIsolated(uid)) { - Integer k = mIsolatedUidRecords.getUidByIsolatedUid(uid); - if (k != null) { - uid = k; - } + @GuardedBy("mLock") + private void addLocked(int pid, int uid, Object extra) { + Integer k = mIsolatedUidRecords.getUidByIsolatedUid(uid); + if (k != null) { + uid = k; } - synchronized (mLock) { - SparseArray> array = mData.get(uid); - if (array == null) { - array = new SparseArray>(); - mData.put(uid, array); - } - array.put(pid, new Pair(System.currentTimeMillis(), extra)); + SparseArray> array = mData.get(uid); + if (array == null) { + array = new SparseArray>(); + mData.put(uid, array); } + array.put(pid, new Pair(System.currentTimeMillis(), extra)); } + @VisibleForTesting Pair remove(int pid, int uid) { - if (UserHandle.isIsolated(uid)) { + synchronized (mLock) { Integer k = mIsolatedUidRecords.getUidByIsolatedUid(uid); if (k != null) { uid = k; } - } - synchronized (mLock) { SparseArray> array = mData.get(uid); if (array != null) { Pair p = array.get(pid); @@ -1228,8 +1636,8 @@ public final class AppExitInfoTracker { return isFresh(p.first) ? p : null; } } + return null; } - return null; } void removeByUserId(int userId) { @@ -1250,7 +1658,8 @@ public final class AppExitInfoTracker { } } - void removeByUid(int uid, boolean allUsers) { + @GuardedBy("mLock") + void removeByUidLocked(int uid, boolean allUsers) { if (UserHandle.isIsolated(uid)) { Integer k = mIsolatedUidRecords.getUidByIsolatedUid(uid); if (k != null) { @@ -1260,17 +1669,13 @@ public final class AppExitInfoTracker { if (allUsers) { uid = UserHandle.getAppId(uid); - synchronized (mLock) { - for (int i = mData.size() - 1; i >= 0; i--) { - if (UserHandle.getAppId(mData.keyAt(i)) == uid) { - mData.removeAt(i); - } + for (int i = mData.size() - 1; i >= 0; i--) { + if (UserHandle.getAppId(mData.keyAt(i)) == uid) { + mData.removeAt(i); } } } else { - synchronized (mLock) { - mData.remove(uid); - } + mData.remove(uid); } } @@ -1292,12 +1697,12 @@ public final class AppExitInfoTracker { // Unlikely but possible: the record has been created // Let's update it if we could find a ApplicationExitInfo record - if (!updateExitInfoIfNecessary(pid, uid, status, mPresetReason)) { - add(pid, uid, status); - } - - // Notify any interesed party regarding the lmkd kills synchronized (mLock) { + if (!updateExitInfoIfNecessaryLocked(pid, uid, status, mPresetReason)) { + addLocked(pid, uid, status); + } + + // Notify any interesed party regarding the lmkd kills final BiConsumer listener = mProcDiedListener; if (listener != null) { mService.mHandler.post(()-> listener.accept(pid, uid)); @@ -1305,4 +1710,51 @@ public final class AppExitInfoTracker { } } } + + /** + * The implementation to the IAppTraceRetriever interface. + */ + @VisibleForTesting + class AppTraceRetriever extends IAppTraceRetriever.Stub { + @Override + public ParcelFileDescriptor getTraceFileDescriptor(final String packageName, + final int uid, final int pid) { + mService.enforceNotIsolatedCaller("getTraceFileDescriptor"); + + if (TextUtils.isEmpty(packageName)) { + throw new IllegalArgumentException("Invalid package name"); + } + final int callingPid = Binder.getCallingPid(); + final int callingUid = Binder.getCallingUid(); + final int callingUserId = UserHandle.getCallingUserId(); + final int userId = UserHandle.getUserId(uid); + + mService.mUserController.handleIncomingUser(callingPid, callingUid, userId, true, + ALLOW_NON_FULL, "getTraceFileDescriptor", null); + if (mService.enforceDumpPermissionForPackage(packageName, userId, + callingUid, "getTraceFileDescriptor") != Process.INVALID_UID) { + synchronized (mLock) { + final ApplicationExitInfo info = getExitInfoLocked(packageName, uid, pid); + if (info == null) { + return null; + } + final File traceFile = info.getTraceFile(); + if (traceFile == null) { + return null; + } + final long identity = Binder.clearCallingIdentity(); + try { + // The fd will be closed after being written into Parcel + return ParcelFileDescriptor.open(traceFile, + ParcelFileDescriptor.MODE_READ_ONLY); + } catch (FileNotFoundException e) { + return null; + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + return null; + } + } } diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 16a7c6b4998e7..0f9d61fe5f0e7 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -71,7 +71,6 @@ import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManagerInternal; -import android.content.pm.ProcessInfo; import android.content.res.Resources; import android.graphics.Point; import android.net.LocalSocket; @@ -99,7 +98,6 @@ import android.provider.DeviceConfig; import android.system.Os; import android.text.TextUtils; import android.util.ArrayMap; -import android.util.ArraySet; import android.util.EventLog; import android.util.LongSparseArray; import android.util.Pair; @@ -138,10 +136,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Set; /** * Activity manager code dealing with processes. @@ -512,13 +508,6 @@ public final class ProcessList { */ private final int[] mZygoteSigChldMessage = new int[3]; - interface LmkdKillListener { - /** - * Called when there is a process kill by lmkd. - */ - void onLmkdKillOccurred(int pid, int uid); - } - final class IsolatedUidRange { @VisibleForTesting public final int mFirstUid; diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index e7f66bb484e02..eec75195c2caa 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -1621,9 +1621,11 @@ class ProcessRecord implements WindowProcessListener { // For background ANRs, don't pass the ProcessCpuTracker to // avoid spending 1/2 second collecting stats to rank lastPids. StringWriter tracesFileException = new StringWriter(); + // To hold the start and end offset to the ANR trace file respectively. + final long[] offsets = new long[2]; File tracesFile = ActivityManagerService.dumpStackTraces(firstPids, (isSilentAnr()) ? null : processCpuTracker, (isSilentAnr()) ? null : lastPids, - nativePids, tracesFileException); + nativePids, tracesFileException, offsets); if (isMonitorCpuUsage()) { mService.updateCpuStatsNow(); @@ -1641,6 +1643,10 @@ class ProcessRecord implements WindowProcessListener { if (tracesFile == null) { // There is no trace file, so dump (only) the alleged culprit's threads to the log Process.sendSignal(pid, Process.SIGNAL_QUIT); + } else if (offsets[1] > 0) { + // We've dumped into the trace file successfully + mService.mProcessList.mAppExitInfoTracker.scheduleLogAnrTrace( + pid, uid, getPackageList(), tracesFile, offsets[0], offsets[1]); } FrameworkStatsLog.write(FrameworkStatsLog.ANR_OCCURRED, uid, processName, diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java index efe8119a62a59..23381ffd4eaaa 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java @@ -46,6 +46,7 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManagerInternal; import android.os.Debug; +import android.os.FileUtils; import android.os.Handler; import android.os.HandlerThread; import android.os.Process; @@ -55,6 +56,7 @@ import android.system.OsConstants; import android.text.TextUtils; import android.util.Pair; +import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import com.android.server.ServiceThread; import com.android.server.appop.AppOpsService; @@ -71,10 +73,17 @@ import org.junit.runners.model.Statement; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Random; +import java.util.zip.GZIPInputStream; /** * Test class for {@link android.app.ApplicationExitInfo}. @@ -119,6 +128,8 @@ public class ApplicationExitInfoTest { setFieldValue(AppExitInfoTracker.class, mAppExitInfoTracker, "mAppExitInfoSourceLmkd", spy(mAppExitInfoTracker.new AppExitInfoExternalSource("lmkd", ApplicationExitInfo.REASON_LOW_MEMORY))); + setFieldValue(AppExitInfoTracker.class, mAppExitInfoTracker, "mAppTraceRetriever", + spy(mAppExitInfoTracker.new AppTraceRetriever())); setFieldValue(ProcessList.class, mProcessList, "mAppExitInfoTracker", mAppExitInfoTracker); mInjector = new TestInjector(mContext); mAms = new ActivityManagerService(mInjector, mServiceThreadRule.getThread()); @@ -169,6 +180,11 @@ public class ApplicationExitInfoTest { public void testApplicationExitInfo() throws Exception { mAppExitInfoTracker.clearProcessExitInfo(true); mAppExitInfoTracker.mAppExitInfoLoaded = true; + mAppExitInfoTracker.mProcExitStoreDir = new File(mContext.getFilesDir(), + AppExitInfoTracker.APP_EXIT_STORE_DIR); + assertTrue(FileUtils.createDir(mAppExitInfoTracker.mProcExitStoreDir)); + mAppExitInfoTracker.mProcExitInfoFile = new File(mAppExitInfoTracker.mProcExitStoreDir, + AppExitInfoTracker.APP_EXIT_INFO_FILE); // Test application calls System.exit() doNothing().when(mAppExitInfoTracker).schedulePersistProcessExitInfo(anyBoolean()); @@ -188,6 +204,10 @@ public class ApplicationExitInfoTest { final long app1Rss3 = 45680; final String app1ProcessName = "com.android.test.stub1:process"; final String app1PackageName = "com.android.test.stub1"; + final byte[] app1Cookie1 = {(byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, + (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08}; + final byte[] app1Cookie2 = {(byte) 0x08, (byte) 0x07, (byte) 0x06, (byte) 0x05, + (byte) 0x04, (byte) 0x03, (byte) 0x02, (byte) 0x01}; final long now1 = System.currentTimeMillis(); ProcessRecord app = makeProcessRecord( @@ -204,6 +224,9 @@ public class ApplicationExitInfoTest { // Case 1: basic System.exit() test int exitCode = 5; + mAppExitInfoTracker.setProcessStateSummary(app1Uid, app1Pid1, app1Cookie1); + assertTrue(ArrayUtils.equals(mAppExitInfoTracker.getProcessStateSummary(app1Uid, + app1Pid1), app1Cookie1, app1Cookie1.length)); doReturn(new Pair(now1, Integer.valueOf(makeExitStatus(exitCode)))) .when(mAppExitInfoTracker.mAppExitInfoSourceZygote) .remove(anyInt(), anyInt()); @@ -235,6 +258,10 @@ public class ApplicationExitInfoTest { IMPORTANCE_CACHED, // importance null); // description + assertTrue(ArrayUtils.equals(info.getProcessStateSummary(), app1Cookie1, + app1Cookie1.length)); + assertEquals(info.getTraceInputStream(), null); + // Case 2: create another app1 process record with a different pid sleep(1); final long now2 = System.currentTimeMillis(); @@ -250,6 +277,12 @@ public class ApplicationExitInfoTest { app1ProcessName, // processName app1PackageName); // packageName exitCode = 6; + + mAppExitInfoTracker.setProcessStateSummary(app1Uid, app1Pid2, app1Cookie1); + // Override with a different cookie + mAppExitInfoTracker.setProcessStateSummary(app1Uid, app1Pid2, app1Cookie2); + assertTrue(ArrayUtils.equals(mAppExitInfoTracker.getProcessStateSummary(app1Uid, + app1Pid2), app1Cookie2, app1Cookie2.length)); doReturn(new Pair(now2, Integer.valueOf(makeExitStatus(exitCode)))) .when(mAppExitInfoTracker.mAppExitInfoSourceZygote) .remove(anyInt(), anyInt()); @@ -280,6 +313,12 @@ public class ApplicationExitInfoTest { IMPORTANCE_SERVICE, // importance null); // description + assertTrue(ArrayUtils.equals(info.getProcessStateSummary(), app1Cookie2, + app1Cookie2.length)); + info = list.get(1); + assertTrue(ArrayUtils.equals(info.getProcessStateSummary(), app1Cookie1, + app1Cookie1.length)); + // Case 3: Create an instance of app1 with different user, and died because of SIGKILL sleep(1); final long now3 = System.currentTimeMillis(); @@ -702,9 +741,19 @@ public class ApplicationExitInfoTest { app1PackageName); // packageName mAppExitInfoTracker.mIsolatedUidRecords.addIsolatedUid(app1IsolatedUid2User2, app1UidUser2); + + // Pretent it gets an ANR trace too (although the reason here should be REASON_ANR) + final File traceFile = new File(mContext.getFilesDir(), "anr_original.txt"); + final int traceSize = 10240; + final int traceStart = 1024; + final int traceEnd = 8192; + createRandomFile(traceFile, traceSize); + assertEquals(traceSize, traceFile.length()); + mAppExitInfoTracker.handleLogAnrTrace(app.pid, app.uid, app.getPackageList(), + traceFile, traceStart, traceEnd); + noteAppKill(app, ApplicationExitInfo.REASON_OTHER, ApplicationExitInfo.SUBREASON_TOO_MANY_EMPTY, app1Description2); - updateExitInfo(app); list.clear(); mAppExitInfoTracker.getExitInfo(app1PackageName, app1UidUser2, app1Pid2User2, 1, list); @@ -729,6 +778,10 @@ public class ApplicationExitInfoTest { IMPORTANCE_CACHED, // importance app1Description2); // description + // Verify if the traceFile get copied into the records correctly. + verifyTraceFile(traceFile, traceStart, info.getTraceFile(), 0, traceEnd - traceStart); + traceFile.delete(); + info.getTraceFile().delete(); // Case 9: User2 gets removed sleep(1); @@ -801,8 +854,6 @@ public class ApplicationExitInfoTest { mAppExitInfoTracker.getExitInfo(null, app1Uid, 0, 0, original); assertTrue(original.size() > 0); - mAppExitInfoTracker.mProcExitInfoFile = new File(mContext.getFilesDir(), - AppExitInfoTracker.APP_EXIT_INFO_FILE); mAppExitInfoTracker.persistProcessExitInfo(); assertTrue(mAppExitInfoTracker.mProcExitInfoFile.exists()); @@ -836,6 +887,37 @@ public class ApplicationExitInfoTest { } } + private static void createRandomFile(File file, int size) throws IOException { + try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file))) { + Random random = new Random(); + byte[] buf = random.ints('a', 'z').limit(size).collect( + StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString().getBytes(); + out.write(buf); + } + } + + private static void verifyTraceFile(File originFile, int originStart, File traceFile, + int traceStart, int length) throws IOException { + assertTrue(originFile.exists()); + assertTrue(traceFile.exists()); + assertTrue(originStart < originFile.length()); + try (GZIPInputStream traceIn = new GZIPInputStream(new FileInputStream(traceFile)); + BufferedInputStream originIn = new BufferedInputStream( + new FileInputStream(originFile))) { + assertEquals(traceStart, traceIn.skip(traceStart)); + assertEquals(originStart, originIn.skip(originStart)); + byte[] buf1 = new byte[8192]; + byte[] buf2 = new byte[8192]; + while (length > 0) { + int len = traceIn.read(buf1, 0, Math.min(buf1.length, length)); + assertEquals(len, originIn.read(buf2, 0, len)); + assertTrue(ArrayUtils.equals(buf1, buf2, len)); + length -= len; + } + } + } + private ProcessRecord makeProcessRecord(int pid, int uid, int packageUid, Integer definingUid, int connectionGroup, int procState, long pss, long rss, String processName, String packageName) {