blob: c49e89a080bd15f3f0b4ea9878c3271b3e689009 [file] [log] [blame]
/*
* Copyright (C) 2019 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.cts;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.app.ApplicationExitInfo;
import android.app.Instrumentation;
import android.app.cts.android.app.cts.tools.WatchUidRunner;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.externalservice.common.RunningServiceInfo;
import android.externalservice.common.ServiceMessages;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.DropBoxManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.system.Os;
import android.system.OsConstants;
import android.test.InstrumentationTestCase;
import android.text.TextUtils;
import android.util.DebugUtils;
import android.util.Log;
import com.android.compatibility.common.util.ShellIdentityUtils;
import com.android.compatibility.common.util.SystemUtil;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.MemInfoReader;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.nio.DirectByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public final class ActivityManagerAppExitInfoTest extends InstrumentationTestCase {
private static final String TAG = ActivityManagerAppExitInfoTest.class.getSimpleName();
private static final String STUB_PACKAGE_NAME =
"com.android.cts.launcherapps.simpleapp";
private static final String STUB_SERVICE_NAME =
"com.android.cts.launcherapps.simpleapp.SimpleService4";
private static final String STUB_SERVICE_REMOTE_NAME =
"com.android.cts.launcherapps.simpleapp.SimpleService5";
private static final String STUB_SERVICE_ISOLATED_NAME =
"com.android.cts.launcherapps.simpleapp.SimpleService6";
private static final String STUB_RECEIVER_NAMWE =
"com.android.cts.launcherapps.simpleapp.SimpleReceiver";
private static final String STUB_ROCESS_NAME = STUB_PACKAGE_NAME;
private static final String STUB_REMOTE_ROCESS_NAME = STUB_ROCESS_NAME + ":remote";
private static final String EXIT_ACTION =
"com.android.cts.launchertests.simpleapp.EXIT_ACTION";
private static final String EXTRA_ACTION = "action";
private static final String EXTRA_MESSENGER = "messenger";
private static final String EXTRA_PROCESS_NAME = "process";
private static final String EXTRA_COOKIE = "cookie";
private static final int ACTION_NONE = 0;
private static final int ACTION_FINISH = 1;
private static final int ACTION_EXIT = 2;
private static final int ACTION_ANR = 3;
private static final int ACTION_NATIVE_CRASH = 4;
private static final int ACTION_KILL = 5;
private static final int ACTION_ACQUIRE_STABLE_PROVIDER = 6;
private static final int ACTION_KILL_PROVIDER = 7;
private static final int EXIT_CODE = 123;
private static final int CRASH_SIGNAL = OsConstants.SIGSEGV;
private static final int WAITFOR_MSEC = 5000;
private static final int WAITFOR_SETTLE_DOWN = 2000;
private static final int CMD_PID = 1;
private Context mContext;
private Instrumentation mInstrumentation;
private int mStubPackageUid;
private int mStubPackagePid;
private int mStubPackageRemotePid;
private int mStubPackageOtherUid;
private int mStubPackageOtherUserPid;
private int mStubPackageRemoteOtherUserPid;
private int mStubPackageIsolatedUid;
private int mStubPackageIsolatedPid;
private String mStubPackageIsolatedProcessName;
private WatchUidRunner mWatcher;
private WatchUidRunner mOtherUidWatcher;
private ActivityManager mActivityManager;
private CountDownLatch mLatch;
private UserManager mUserManager;
private HandlerThread mHandlerThread;
private Handler mHandler;
private Messenger mMessenger;
private boolean mSupportMultipleUsers;
private int mCurrentUserId;
private UserHandle mCurrentUserHandle;
private int mOtherUserId;
private UserHandle mOtherUserHandle;
private DropBoxManager.Entry mAnrEntry;
@Override
protected void setUp() throws Exception {
super.setUp();
mInstrumentation = getInstrumentation();
mContext = mInstrumentation.getContext();
mStubPackageUid = mContext.getPackageManager().getPackageUid(STUB_PACKAGE_NAME, 0);
mWatcher = new WatchUidRunner(mInstrumentation, mStubPackageUid, WAITFOR_MSEC);
mActivityManager = mContext.getSystemService(ActivityManager.class);
mUserManager = UserManager.get(mContext);
mCurrentUserId = UserHandle.getUserId(Process.myUid());
mCurrentUserHandle = Process.myUserHandle();
mSupportMultipleUsers = mUserManager.supportsMultipleUsers();
mHandlerThread = new HandlerThread("receiver");
mHandlerThread.start();
mHandler = new H(mHandlerThread.getLooper());
mMessenger = new Messenger(mHandler);
executeShellCmd("cmd deviceidle whitelist +" + STUB_PACKAGE_NAME);
}
private void handleMessagePid(Message msg) {
boolean didSomething = false;
Bundle b = (Bundle) msg.obj;
String processName = b.getString(EXTRA_PROCESS_NAME);
if (STUB_ROCESS_NAME.equals(processName)) {
if (mOtherUserId != 0 && UserHandle.getUserId(msg.arg2) == mOtherUserId) {
mStubPackageOtherUserPid = msg.arg1;
assertTrue(mStubPackageOtherUserPid > 0);
} else {
mStubPackagePid = msg.arg1;
assertTrue(mStubPackagePid > 0);
}
} else if (STUB_REMOTE_ROCESS_NAME.equals(processName)) {
if (mOtherUserId != 0 && UserHandle.getUserId(msg.arg2) == mOtherUserId) {
mStubPackageRemoteOtherUserPid = msg.arg1;
assertTrue(mStubPackageRemoteOtherUserPid > 0);
} else {
mStubPackageRemotePid = msg.arg1;
assertTrue(mStubPackageRemotePid > 0);
}
} else { // must be isolated process
mStubPackageIsolatedPid = msg.arg1;
mStubPackageIsolatedUid = msg.arg2;
mStubPackageIsolatedProcessName = processName;
assertTrue(mStubPackageIsolatedPid > 0);
assertTrue(mStubPackageIsolatedUid > 0);
assertNotNull(processName);
}
if (mLatch != null) {
mLatch.countDown();
}
}
private class H extends Handler {
H(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case CMD_PID:
handleMessagePid(msg);
break;
default:
break;
}
}
}
@Override
protected void tearDown() throws Exception {
mWatcher.finish();
executeShellCmd("cmd deviceidle whitelist -" + STUB_PACKAGE_NAME);
removeTestUserIfNecessary();
mHandlerThread.quitSafely();
}
private int createUser(String name, boolean guest) throws Exception {
final String output = executeShellCmd(
"pm create-user " + (guest ? "--guest " : "") + name);
if (output.startsWith("Success")) {
return Integer.parseInt(output.substring(output.lastIndexOf(" ")).trim());
}
throw new IllegalStateException(String.format("Failed to create user: %s", output));
}
private boolean removeUser(final int userId) throws Exception {
final String output = executeShellCmd(String.format("pm remove-user %s", userId));
if (output.startsWith("Error")) {
return false;
}
return true;
}
private boolean startUser(int userId, boolean waitFlag) throws Exception {
String cmd = "am start-user " + (waitFlag ? "-w " : "") + userId;
final String output = executeShellCmd(cmd);
if (output.startsWith("Error")) {
return false;
}
if (waitFlag) {
String state = executeShellCmd("am get-started-user-state " + userId);
if (!state.contains("RUNNING_UNLOCKED")) {
return false;
}
}
return true;
}
private boolean stopUser(int userId, boolean waitFlag, boolean forceFlag)
throws Exception {
StringBuilder cmd = new StringBuilder("am stop-user ");
if (waitFlag) {
cmd.append("-w ");
}
if (forceFlag) {
cmd.append("-f ");
}
cmd.append(userId);
final String output = executeShellCmd(cmd.toString());
if (output.contains("Error: Can't stop system user")) {
return false;
}
return true;
}
private void installExistingPackageAsUser(String packageName, int userId)
throws Exception {
executeShellCmd(
String.format("pm install-existing --user %d --wait %s", userId, packageName));
}
private String executeShellCmd(String cmd) throws Exception {
final String result = SystemUtil.runShellCommand(mInstrumentation, cmd);
Log.d(TAG, String.format("Output for '%s': %s", cmd, result));
return result;
}
private void awaitForLatch(CountDownLatch latch) {
try {
assertTrue("Timeout for waiting", latch.await(WAITFOR_MSEC, TimeUnit.MILLISECONDS));
} catch (InterruptedException e) {
fail("Interrupted");
}
}
// Start the target package
private void startService(int commandCode, String serviceName, boolean waitForGone,
boolean other) {
startService(commandCode, serviceName, waitForGone, true, other, false, null);
}
private void startService(int commandCode, String serviceName, boolean waitForGone,
boolean waitForIdle, boolean other, boolean includeCookie, byte[] cookie) {
Intent intent = new Intent(EXIT_ACTION);
intent.setClassName(STUB_PACKAGE_NAME, serviceName);
intent.putExtra(EXTRA_ACTION, commandCode);
intent.putExtra(EXTRA_MESSENGER, mMessenger);
if (includeCookie) {
intent.putExtra(EXTRA_COOKIE, cookie);
}
mLatch = new CountDownLatch(1);
UserHandle user = other ? mOtherUserHandle : mCurrentUserHandle;
WatchUidRunner watcher = other ? mOtherUidWatcher : mWatcher;
mContext.startServiceAsUser(intent, user);
if (waitForIdle) {
watcher.waitFor(WatchUidRunner.CMD_IDLE, null);
}
if (waitForGone) {
waitForGone(watcher);
}
awaitForLatch(mLatch);
}
private void startIsolatedService(int commandCode, String serviceName) {
Intent intent = new Intent(EXIT_ACTION);
intent.setClassName(STUB_PACKAGE_NAME, serviceName);
intent.putExtra(EXTRA_ACTION, commandCode);
intent.putExtra(EXTRA_MESSENGER, mMessenger);
mLatch = new CountDownLatch(1);
mContext.startServiceAsUser(intent, mCurrentUserHandle);
awaitForLatch(mLatch);
}
private void waitForGone(WatchUidRunner watcher) {
watcher.waitFor(WatchUidRunner.CMD_GONE, null);
// Give a few seconds to generate the exit report.
sleep(WAITFOR_SETTLE_DOWN);
}
private void clearHistoricalExitInfo() throws Exception {
executeShellCmd("am clear-exit-info --user all " + STUB_PACKAGE_NAME);
}
private void sleep(long timeout) {
try {
Thread.sleep(timeout);
} catch (InterruptedException e) {
}
}
private List<ApplicationExitInfo> getHistoricalProcessExitReasonsAsUser(
final String packageName, final int pid, final int max, final int userId) {
Context context = mContext.createContextAsUser(UserHandle.of(userId), 0);
ActivityManager am = context.getSystemService(ActivityManager.class);
return am.getHistoricalProcessExitReasons(packageName, pid, max);
}
public void testExitCode() throws Exception {
// Remove old records to avoid interference with the test.
clearHistoricalExitInfo();
long now = System.currentTimeMillis();
// Start a process and let it call System.exit() right away.
startService(ACTION_EXIT, STUB_SERVICE_NAME, true, false);
long now2 = System.currentTimeMillis();
// Query with the current package name, but the mStubPackagePid belongs to the
// target package, so the below call should return an empty result.
List<ApplicationExitInfo> list = null;
try {
list = mActivityManager.getHistoricalProcessExitReasons(
STUB_PACKAGE_NAME, mStubPackagePid, 1);
fail("Shouldn't be able to query other package");
} catch (SecurityException e) {
// expected
}
// Now query with the advanced version
try {
list = getHistoricalProcessExitReasonsAsUser(STUB_PACKAGE_NAME,
mStubPackagePid, 1, mCurrentUserId);
fail("Shouldn't be able to query other package");
} catch (SecurityException e) {
// expected
}
list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, mStubPackagePid, 1, mCurrentUserId,
this::getHistoricalProcessExitReasonsAsUser,
android.Manifest.permission.DUMP);
assertTrue(list != null && list.size() == 1);
verify(list.get(0), mStubPackagePid, mStubPackageUid, STUB_PACKAGE_NAME,
ApplicationExitInfo.REASON_EXIT_SELF, EXIT_CODE, null, now, now2);
}
private List<ApplicationExitInfo> fillUpMemoryAndCheck(ArrayList<Long> addresses)
throws Exception {
List<ApplicationExitInfo> list = null;
// Get the meminfo firstly
MemInfoReader reader = new MemInfoReader();
reader.readMemInfo();
long totalMb = (reader.getFreeSizeKb() + reader.getCachedSizeKb()) >> 10;
final int pageSize = 4096;
final int oneMb = 1024 * 1024;
// Create an empty fd -1
FileDescriptor fd = new FileDescriptor();
// Okay now start a loop to allocate 1MB each time and check if our process is gone.
for (long i = 0; i < totalMb; i++) {
long addr = Os.mmap(0, oneMb, OsConstants.PROT_WRITE,
OsConstants.MAP_PRIVATE | OsConstants.MAP_ANONYMOUS, fd, 0);
if (addr == 0) {
break;
}
addresses.add(addr);
// We don't have direct access to Memory.pokeByte() though
DirectByteBuffer buf = new DirectByteBuffer(oneMb, addr, fd, null, false);
// Dirt the buffer
for (int j = 0; j < oneMb; j += pageSize) {
buf.put(j, (byte) 0xf);
}
// Check if we could get the report
list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, mStubPackagePid, 1,
mActivityManager::getHistoricalProcessExitReasons,
android.Manifest.permission.DUMP);
if (list != null && list.size() == 1) {
break;
}
}
return list;
}
public void testLmkdKill() throws Exception {
// Remove old records to avoid interference with the test.
clearHistoricalExitInfo();
long now = System.currentTimeMillis();
boolean lmkdReportSupported = ActivityManager.isLowMemoryKillReportSupported();
// Start a process and do nothing
startService(ACTION_FINISH, STUB_SERVICE_NAME, false, false);
final int oneMb = 1024 * 1024;
ArrayList<Long> addresses = new ArrayList<Long>();
List<ApplicationExitInfo> list = fillUpMemoryAndCheck(addresses);
while (list == null || list.size() == 0) {
// make sure we have cached process killed
String output = executeShellCmd("dumpsys activity lru");
if (output == null && output.indexOf(" cch+") == -1) {
break;
}
// try again since the system might have reclaimed some ram
list = fillUpMemoryAndCheck(addresses);
}
// Free all the buffers firstly
for (int i = addresses.size() - 1; i >= 0; i--) {
Os.munmap(addresses.get(i), oneMb);
}
long now2 = System.currentTimeMillis();
assertTrue(list != null && list.size() == 1);
ApplicationExitInfo info = list.get(0);
assertNotNull(info);
if (lmkdReportSupported) {
verify(info, mStubPackagePid, mStubPackageUid, STUB_PACKAGE_NAME,
ApplicationExitInfo.REASON_LOW_MEMORY, null, null, now, now2);
} else {
verify(info, mStubPackagePid, mStubPackageUid, STUB_PACKAGE_NAME,
ApplicationExitInfo.REASON_SIGNALED, OsConstants.SIGKILL, null, now, now2);
}
}
public void testKillBySignal() throws Exception {
// Remove old records to avoid interference with the test.
clearHistoricalExitInfo();
long now = System.currentTimeMillis();
// Start a process and kill itself
startService(ACTION_KILL, STUB_SERVICE_NAME, true, false);
long now2 = System.currentTimeMillis();
List<ApplicationExitInfo> list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, mStubPackagePid, 1,
mActivityManager::getHistoricalProcessExitReasons,
android.Manifest.permission.DUMP);
assertTrue(list != null && list.size() == 1);
verify(list.get(0), mStubPackagePid, mStubPackageUid, STUB_PACKAGE_NAME,
ApplicationExitInfo.REASON_SIGNALED, OsConstants.SIGKILL, null, now, now2);
}
public void testAnr() throws Exception {
// Remove old records to avoid interference with the test.
clearHistoricalExitInfo();
final DropBoxManager dbox = mContext.getSystemService(DropBoxManager.class);
final CountDownLatch dboxLatch = new CountDownLatch(1);
final BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String tag_anr = "data_app_anr";
if (tag_anr.equals(intent.getStringExtra(DropBoxManager.EXTRA_TAG))) {
mAnrEntry = dbox.getNextEntry(tag_anr, intent.getLongExtra(
DropBoxManager.EXTRA_TIME, 0) - 1);
dboxLatch.countDown();
}
}
};
mContext.registerReceiver(receiver,
new IntentFilter(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED));
final long timeout = Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.BROADCAST_FG_CONSTANTS, 10 * 1000) * 3;
long now = System.currentTimeMillis();
// Start a process and block its main thread
startService(ACTION_ANR, STUB_SERVICE_NAME, false, false);
// Sleep for a while to make sure it's already blocking its main thread.
sleep(WAITFOR_MSEC);
Monitor monitor = new Monitor(mInstrumentation);
Intent intent = new Intent();
intent.setComponent(new ComponentName(STUB_PACKAGE_NAME, STUB_RECEIVER_NAMWE));
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
// This will result an ANR
mContext.sendOrderedBroadcast(intent, null);
// Wait for the early ANR
monitor.waitFor(Monitor.WAIT_FOR_EARLY_ANR, timeout);
// Continue, so we could collect ANR traces
monitor.sendCommand(Monitor.CMD_CONTINUE);
// Wait for the ANR
monitor.waitFor(Monitor.WAIT_FOR_ANR, timeout);
// Kill it
monitor.sendCommand(Monitor.CMD_KILL);
// Wait the process gone
waitForGone(mWatcher);
long now2 = System.currentTimeMillis();
awaitForLatch(dboxLatch);
assertTrue(mAnrEntry != null);
List<ApplicationExitInfo> list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, mStubPackagePid, 1,
mActivityManager::getHistoricalProcessExitReasons,
android.Manifest.permission.DUMP);
assertTrue(list != null && list.size() == 1);
ApplicationExitInfo info = list.get(0);
verify(info, mStubPackagePid, mStubPackageUid, STUB_PACKAGE_NAME,
ApplicationExitInfo.REASON_ANR, null, null, now, now2);
assertEquals(mStubPackageUid, info.getPackageUid());
assertEquals(mStubPackageUid, info.getDefiningUid());
// Verify the traces
// Read from dropbox
final String dboxTrace = mAnrEntry.getText(0x100000 /* 1M */);
assertFalse(TextUtils.isEmpty(dboxTrace));
// Read the input stream from the ApplicationExitInfo
String trace = ShellIdentityUtils.invokeMethodWithShellPermissions(info, (i) -> {
try (BufferedInputStream input = new BufferedInputStream(i.getTraceInputStream())) {
StringBuilder sb = new StringBuilder();
byte[] buf = new byte[8192];
while (true) {
final int len = input.read(buf, 0, buf.length);
if (len <= 0) {
break;
}
sb.append(new String(buf, 0, len));
}
return sb.toString();
} catch (IOException e) {
return null;
}
}, android.Manifest.permission.DUMP);
assertFalse(TextUtils.isEmpty(trace));
assertTrue(trace.indexOf(Integer.toString(info.getPid())) >= 0);
assertTrue(trace.indexOf("Cmd line: " + STUB_PACKAGE_NAME) >= 0);
assertTrue(dboxTrace.indexOf(trace) >= 0);
monitor.finish();
mContext.unregisterReceiver(receiver);
}
public void testOther() throws Exception {
// Remove old records to avoid interference with the test.
clearHistoricalExitInfo();
final String servicePackage = "android.externalservice.service";
final String keyIBinder = "ibinder";
final CountDownLatch latch = new CountDownLatch(1);
final Bundle holder = new Bundle();
final ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
holder.putBinder(keyIBinder, service);
latch.countDown();
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
final Intent intent = new Intent();
intent.setComponent(new ComponentName(servicePackage,
servicePackage + ".ExternalServiceWithZygote"));
// Bind to that external service, which will become an isolated process
// running in the current package user id.
assertTrue(mContext.bindService(intent,
Context.BIND_AUTO_CREATE | Context.BIND_EXTERNAL_SERVICE,
AsyncTask.THREAD_POOL_EXECUTOR, connection));
awaitForLatch(latch);
final IBinder service = holder.getBinder(keyIBinder);
assertNotNull(service);
// Retrieve its uid/pd/package name info.
Messenger remote = new Messenger(service);
RunningServiceInfo id = identifyService(remote);
assertNotNull(id);
assertFalse(id.uid == 0 || id.pid == 0);
assertFalse(Process.myUid() == id.uid);
assertFalse(Process.myPid() == id.pid);
assertEquals(mContext.getPackageName(), id.packageName);
final WatchUidRunner watcher = new WatchUidRunner(mInstrumentation,
id.uid, WAITFOR_MSEC);
long now = System.currentTimeMillis();
// Remove the service connection
mContext.unbindService(connection);
try {
// Isolated process should have been killed as long as its service is done.
waitForGone(watcher);
} finally {
watcher.finish();
}
long now2 = System.currentTimeMillis();
final ActivityManager am = mContext.getSystemService(ActivityManager.class);
final List<ApplicationExitInfo> list = am.getHistoricalProcessExitReasons(null, id.pid, 1);
assertTrue(list != null && list.size() == 1);
ApplicationExitInfo info = list.get(0);
verify(info, id.pid, id.uid, null, ApplicationExitInfo.REASON_OTHER, null,
"isolated not needed", now, now2);
assertEquals(Process.myUid(), info.getPackageUid());
assertEquals(mContext.getPackageManager().getPackageUid(servicePackage, 0),
info.getDefiningUid());
assertEquals(ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE,
info.getImportance());
}
private String extractMemString(String dump, String prefix, char nextSep) {
int start = dump.indexOf(prefix);
assertTrue(start >= 0);
start += prefix.length();
int end = dump.indexOf(nextSep, start);
assertTrue(end > start);
return dump.substring(start, end);
}
public void testPermissionChange() throws Exception {
// Remove old records to avoid interference with the test.
clearHistoricalExitInfo();
// Grant the read calendar permission
mInstrumentation.getUiAutomation().grantRuntimePermission(
STUB_PACKAGE_NAME, android.Manifest.permission.READ_CALENDAR);
long now = System.currentTimeMillis();
// Start a process and do nothing
startService(ACTION_FINISH, STUB_SERVICE_NAME, false, false);
// Enable high frequency memory sampling
executeShellCmd("dumpsys procstats --start-testing");
// Sleep for a while to wait for the sampling of memory info
sleep(10000);
// Stop the high frequency memory sampling
executeShellCmd("dumpsys procstats --stop-testing");
// Get the memory info from it.
String dump = executeShellCmd("dumpsys activity processes " + STUB_PACKAGE_NAME);
assertNotNull(dump);
final String lastPss = extractMemString(dump, " lastPss=", ' ');
final String lastRss = extractMemString(dump, " lastRss=", '\n');
// Revoke the read calendar permission
mInstrumentation.getUiAutomation().revokeRuntimePermission(
STUB_PACKAGE_NAME, android.Manifest.permission.READ_CALENDAR);
waitForGone(mWatcher);
long now2 = System.currentTimeMillis();
List<ApplicationExitInfo> list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, mStubPackagePid, 1,
mActivityManager::getHistoricalProcessExitReasons,
android.Manifest.permission.DUMP);
assertTrue(list != null && list.size() == 1);
ApplicationExitInfo info = list.get(0);
verify(info, mStubPackagePid, mStubPackageUid, STUB_PACKAGE_NAME,
ApplicationExitInfo.REASON_PERMISSION_CHANGE, null, null, now, now2);
// Also verify that we get the expected meminfo
assertEquals(lastPss, DebugUtils.sizeValueToString(
info.getPss() * 1024, new StringBuilder()));
assertEquals(lastRss, DebugUtils.sizeValueToString(
info.getRss() * 1024, new StringBuilder()));
}
public void testCrash() throws Exception {
// Remove old records to avoid interference with the test.
clearHistoricalExitInfo();
long now = System.currentTimeMillis();
// Start a process and do nothing
startService(ACTION_NONE, STUB_SERVICE_NAME, false, false);
// Induce a crash
executeShellCmd("am crash " + STUB_PACKAGE_NAME);
waitForGone(mWatcher);
long now2 = System.currentTimeMillis();
List<ApplicationExitInfo> list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, mStubPackagePid, 1,
mActivityManager::getHistoricalProcessExitReasons,
android.Manifest.permission.DUMP);
assertTrue(list != null && list.size() == 1);
verify(list.get(0), mStubPackagePid, mStubPackageUid, STUB_PACKAGE_NAME,
ApplicationExitInfo.REASON_CRASH, null, null, now, now2);
}
public void testNativeCrash() throws Exception {
// Remove old records to avoid interference with the test.
clearHistoricalExitInfo();
long now = System.currentTimeMillis();
// Start a process and crash it
startService(ACTION_NATIVE_CRASH, STUB_SERVICE_NAME, true, false);
long now2 = System.currentTimeMillis();
List<ApplicationExitInfo> list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, mStubPackagePid, 1,
mActivityManager::getHistoricalProcessExitReasons,
android.Manifest.permission.DUMP);
assertTrue(list != null && list.size() == 1);
verify(list.get(0), mStubPackagePid, mStubPackageUid, STUB_PACKAGE_NAME,
ApplicationExitInfo.REASON_CRASH_NATIVE, null, null, now, now2);
}
public void testUserRequested() throws Exception {
// Remove old records to avoid interference with the test.
clearHistoricalExitInfo();
long now = System.currentTimeMillis();
// Start a process and do nothing
startService(ACTION_NONE, STUB_SERVICE_NAME, false, false);
// Force stop the test package
executeShellCmd("am force-stop " + STUB_PACKAGE_NAME);
// Wait the process gone
waitForGone(mWatcher);
long now2 = System.currentTimeMillis();
List<ApplicationExitInfo> list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, mStubPackagePid, 1,
mActivityManager::getHistoricalProcessExitReasons,
android.Manifest.permission.DUMP);
assertTrue(list != null && list.size() == 1);
verify(list.get(0), mStubPackagePid, mStubPackageUid, STUB_PACKAGE_NAME,
ApplicationExitInfo.REASON_USER_REQUESTED, null, null, now, now2);
}
public void testDependencyDied() throws Exception {
// Remove old records to avoid interference with the test.
clearHistoricalExitInfo();
// Start a process and acquire the provider
startService(ACTION_ACQUIRE_STABLE_PROVIDER, STUB_SERVICE_NAME, false, false);
final ActivityManager am = mContext.getSystemService(ActivityManager.class);
long now = System.currentTimeMillis();
final long timeout = now + WAITFOR_MSEC;
int providerPid = -1;
while (now < timeout && providerPid < 0) {
sleep(1000);
List<RunningAppProcessInfo> list = ShellIdentityUtils.invokeMethodWithShellPermissions(
am, (m) -> m.getRunningAppProcesses(),
android.Manifest.permission.REAL_GET_TASKS);
for (RunningAppProcessInfo info: list) {
if (info.processName.equals(STUB_REMOTE_ROCESS_NAME)) {
providerPid = info.pid;
break;
}
}
now = System.currentTimeMillis();
}
assertTrue(providerPid > 0);
now = System.currentTimeMillis();
// Now let the provider exit itself
startService(ACTION_KILL_PROVIDER, STUB_SERVICE_NAME, false, false, false, false, null);
// Wait for both of the processes gone
waitForGone(mWatcher);
final long now2 = System.currentTimeMillis();
List<ApplicationExitInfo> list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, mStubPackagePid, 1,
mActivityManager::getHistoricalProcessExitReasons,
android.Manifest.permission.DUMP);
assertTrue(list != null && list.size() == 1);
verify(list.get(0), mStubPackagePid, mStubPackageUid, STUB_PACKAGE_NAME,
ApplicationExitInfo.REASON_DEPENDENCY_DIED, null, null, now, now2);
}
public void testMultipleProcess() throws Exception {
// Remove old records to avoid interference with the test.
clearHistoricalExitInfo();
long now = System.currentTimeMillis();
// Start a process and kill itself
startService(ACTION_KILL, STUB_SERVICE_NAME, true, false);
long now2 = System.currentTimeMillis();
// Start a remote process and exit
startService(ACTION_EXIT, STUB_SERVICE_REMOTE_NAME, true, false);
long now3 = System.currentTimeMillis();
// Now to get the two reports
List<ApplicationExitInfo> list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, 0, 2,
mActivityManager::getHistoricalProcessExitReasons,
android.Manifest.permission.DUMP);
assertTrue(list != null && list.size() == 2);
verify(list.get(0), mStubPackageRemotePid, mStubPackageUid, STUB_REMOTE_ROCESS_NAME,
ApplicationExitInfo.REASON_EXIT_SELF, EXIT_CODE, null, now2, now3);
verify(list.get(1), mStubPackagePid, mStubPackageUid, STUB_ROCESS_NAME,
ApplicationExitInfo.REASON_SIGNALED, OsConstants.SIGKILL, null, now, now2);
// If we only retrieve one report
list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, 0, 1,
mActivityManager::getHistoricalProcessExitReasons,
android.Manifest.permission.DUMP);
assertTrue(list != null && list.size() == 1);
verify(list.get(0), mStubPackageRemotePid, mStubPackageUid, STUB_REMOTE_ROCESS_NAME,
ApplicationExitInfo.REASON_EXIT_SELF, EXIT_CODE, null, now2, now3);
}
private RunningServiceInfo identifyService(Messenger service) throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
class IdentifyHandler extends Handler {
IdentifyHandler() {
super(Looper.getMainLooper());
}
RunningServiceInfo mInfo;
@Override
public void handleMessage(Message msg) {
Log.d(TAG, "Received message: " + msg);
switch (msg.what) {
case ServiceMessages.MSG_IDENTIFY_RESPONSE:
msg.getData().setClassLoader(RunningServiceInfo.class.getClassLoader());
mInfo = msg.getData().getParcelable(ServiceMessages.IDENTIFY_INFO);
latch.countDown();
break;
}
super.handleMessage(msg);
}
}
IdentifyHandler handler = new IdentifyHandler();
Messenger local = new Messenger(handler);
Message msg = Message.obtain(null, ServiceMessages.MSG_IDENTIFY);
msg.replyTo = local;
service.send(msg);
awaitForLatch(latch);
return handler.mInfo;
}
private void prepareTestUser() throws Exception {
// Create the test user
mOtherUserId = createUser("TestUser_" + SystemClock.uptimeMillis(), true);
mOtherUserHandle = UserHandle.of(mOtherUserId);
// Start the other user
assertTrue(startUser(mOtherUserId, true));
// Install the test helper APK into the other user
installExistingPackageAsUser(STUB_PACKAGE_NAME, mOtherUserId);
installExistingPackageAsUser(mContext.getPackageName(), mOtherUserId);
mStubPackageOtherUid = mContext.getPackageManager().getPackageUidAsUser(
STUB_PACKAGE_NAME, 0, mOtherUserId);
mOtherUidWatcher = new WatchUidRunner(mInstrumentation, mStubPackageOtherUid,
WAITFOR_MSEC);
}
private void removeTestUserIfNecessary() throws Exception {
if (mSupportMultipleUsers && mOtherUserId > 0) {
// Stop the test user
assertTrue(stopUser(mOtherUserId, true, true));
// Remove the test user
removeUser(mOtherUserId);
mOtherUidWatcher.finish();
mOtherUserId = 0;
mOtherUserHandle = null;
mOtherUidWatcher = null;
}
}
public void testSecondaryUser() throws Exception {
if (!mSupportMultipleUsers) {
return;
}
// Remove old records to avoid interference with the test.
clearHistoricalExitInfo();
// Get the full user permission in order to start service as other user
mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(
android.Manifest.permission.INTERACT_ACROSS_USERS,
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL);
// Create the test user, we'll remove it during tearDown
prepareTestUser();
final byte[] cookie0 = {(byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03,
(byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07};
final byte[] cookie1 = {(byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04,
(byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08};
final byte[] cookie2 = {(byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05,
(byte) 0x06, (byte) 0x07, (byte) 0x08, (byte) 0x01};
final byte[] cookie3 = {(byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06,
(byte) 0x07, (byte) 0x08, (byte) 0x01, (byte) 0x02};
final byte[] cookie4 = {(byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07,
(byte) 0x08, (byte) 0x01, (byte) 0x02, (byte) 0x03};
final byte[] cookie5 = null;
long now = System.currentTimeMillis();
// Start a process and do nothing
startService(ACTION_NONE, STUB_SERVICE_NAME, false, true, false, true, cookie0);
// request to exit by itself with a different cookie
startService(ACTION_EXIT, STUB_SERVICE_NAME, true, false, false, true, cookie1);
long now2 = System.currentTimeMillis();
// Start the process in a secondary user and kill itself
startService(ACTION_KILL, STUB_SERVICE_NAME, true, true, true, true, cookie2);
long now3 = System.currentTimeMillis();
// Start a remote process in a secondary user and exit
startService(ACTION_EXIT, STUB_SERVICE_REMOTE_NAME, true, true, true, true, cookie3);
long now4 = System.currentTimeMillis();
// Start a remote process and kill itself
startService(ACTION_KILL, STUB_SERVICE_REMOTE_NAME, true, true, false, true, cookie4);
long now5 = System.currentTimeMillis();
// drop the permissions
mInstrumentation.getUiAutomation().dropShellPermissionIdentity();
List<ApplicationExitInfo> list = null;
// Now try to query for all users
try {
list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, 0, 0, UserHandle.USER_ALL,
this::getHistoricalProcessExitReasonsAsUser,
android.Manifest.permission.DUMP);
fail("Shouldn't be able to query all users");
} catch (IllegalArgumentException e) {
// expected
}
// Now try to query for "current" user
try {
list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, 0, 0, UserHandle.USER_CURRENT,
this::getHistoricalProcessExitReasonsAsUser,
android.Manifest.permission.DUMP);
fail("Shouldn't be able to query current user, explicit user-Id is expected");
} catch (IllegalArgumentException e) {
// expected
}
// Now only try the current user
list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, 0, 0, mCurrentUserId,
this::getHistoricalProcessExitReasonsAsUser,
android.Manifest.permission.DUMP);
assertTrue(list != null && list.size() == 2);
verify(list.get(0), mStubPackageRemotePid, mStubPackageUid, STUB_REMOTE_ROCESS_NAME,
ApplicationExitInfo.REASON_SIGNALED, OsConstants.SIGKILL, null, now4, now5,
cookie4);
verify(list.get(1), mStubPackagePid, mStubPackageUid, STUB_ROCESS_NAME,
ApplicationExitInfo.REASON_EXIT_SELF, EXIT_CODE, null, now, now2, cookie1);
// Now try the other user
try {
list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, 0, 0, mOtherUserId,
this::getHistoricalProcessExitReasonsAsUser,
android.Manifest.permission.DUMP);
fail("Shouldn't be able to query other users");
} catch (SecurityException e) {
// expected
}
// Now try the other user with proper permissions
list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, 0, 0, mOtherUserId,
this::getHistoricalProcessExitReasonsAsUser,
android.Manifest.permission.DUMP,
android.Manifest.permission.INTERACT_ACROSS_USERS);
assertTrue(list != null && list.size() == 2);
verify(list.get(0), mStubPackageRemoteOtherUserPid, mStubPackageOtherUid,
STUB_REMOTE_ROCESS_NAME, ApplicationExitInfo.REASON_EXIT_SELF, EXIT_CODE,
null, now3, now4, cookie3);
verify(list.get(1), mStubPackageOtherUserPid, mStubPackageOtherUid, STUB_ROCESS_NAME,
ApplicationExitInfo.REASON_SIGNALED, OsConstants.SIGKILL, null,
now2, now3, cookie2);
// Get the full user permission in order to start service as other user
mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(
android.Manifest.permission.INTERACT_ACROSS_USERS,
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL);
// Start the process in a secondary user and do nothing
startService(ACTION_NONE, STUB_SERVICE_NAME, false, true, true, true, cookie5);
// drop the permissions
mInstrumentation.getUiAutomation().dropShellPermissionIdentity();
long now6 = System.currentTimeMillis();
// Stop the test user
assertTrue(stopUser(mOtherUserId, true, true));
// Wait for being killed
waitForGone(mOtherUidWatcher);
long now7 = System.currentTimeMillis();
list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, 0, 1, mOtherUserId,
this::getHistoricalProcessExitReasonsAsUser,
android.Manifest.permission.DUMP,
android.Manifest.permission.INTERACT_ACROSS_USERS);
verify(list.get(0), mStubPackageOtherUserPid, mStubPackageOtherUid, STUB_ROCESS_NAME,
ApplicationExitInfo.REASON_USER_STOPPED, null, null, now6, now7, cookie5);
int otherUserId = mOtherUserId;
// Now remove the other user
removeUser(mOtherUserId);
mOtherUidWatcher.finish();
mOtherUserId = 0;
// Removing user is going take a while, wait for a while before continuing the test.
sleep(15 * 1000);
// Now query the other userId, and it should return nothing.
final Context context = mContext.createPackageContextAsUser("android", 0,
UserHandle.of(otherUserId));
final ActivityManager am = context.getSystemService(ActivityManager.class);
list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, 0, 0,
am::getHistoricalProcessExitReasons,
android.Manifest.permission.DUMP,
android.Manifest.permission.INTERACT_ACROSS_USERS);
assertTrue(list == null || list.size() == 0);
// The current user shouldn't be impacted.
list = ShellIdentityUtils.invokeMethodWithShellPermissions(
STUB_PACKAGE_NAME, 0, 0, mCurrentUserId,
this::getHistoricalProcessExitReasonsAsUser,
android.Manifest.permission.DUMP,
android.Manifest.permission.INTERACT_ACROSS_USERS);
assertTrue(list != null && list.size() == 2);
verify(list.get(0), mStubPackageRemotePid, mStubPackageUid, STUB_REMOTE_ROCESS_NAME,
ApplicationExitInfo.REASON_SIGNALED, OsConstants.SIGKILL, null, now4, now5,
cookie4);
verify(list.get(1), mStubPackagePid, mStubPackageUid, STUB_ROCESS_NAME,
ApplicationExitInfo.REASON_EXIT_SELF, EXIT_CODE, null, now, now2, cookie1);
}
private void verify(ApplicationExitInfo info, int pid, int uid, String processName,
int reason, Integer status, String description, long before, long after) {
verify(info, pid, uid, processName, reason, status, description, before, after, null);
}
private void verify(ApplicationExitInfo info, int pid, int uid, String processName,
int reason, Integer status, String description, long before, long after,
byte[] cookie) {
assertNotNull(info);
assertEquals(pid, info.getPid());
assertEquals(uid, info.getRealUid());
assertEquals(UserHandle.of(UserHandle.getUserId(uid)), info.getUserHandle());
if (processName != null) {
assertEquals(processName, info.getProcessName());
}
assertEquals(reason, info.getReason());
if (status != null) {
assertEquals(status.intValue(), info.getStatus());
}
if (description != null) {
assertEquals(description, info.getDescription());
}
assertTrue(before <= info.getTimestamp());
assertTrue(after >= info.getTimestamp());
assertTrue(ArrayUtils.equals(info.getProcessStateSummary(), cookie,
cookie == null ? 0 : cookie.length));
}
/**
* A utility class interact with "am monitor"
*/
private static class Monitor {
static final String WAIT_FOR_EARLY_ANR = "Waiting after early ANR... available commands:";
static final String WAIT_FOR_ANR = "Waiting after ANR... available commands:";
static final String WAIT_FOR_CRASHED = "Waiting after crash... available commands:";
static final String CMD_CONTINUE = "c";
static final String CMD_KILL = "k";
final Instrumentation mInstrumentation;
final ParcelFileDescriptor mReadFd;
final FileInputStream mReadStream;
final BufferedReader mReadReader;
final ParcelFileDescriptor mWriteFd;
final FileOutputStream mWriteStream;
final PrintWriter mWritePrinter;
final Thread mReaderThread;
final ArrayList<String> mPendingLines = new ArrayList<>();
boolean mStopping;
Monitor(Instrumentation instrumentation) {
mInstrumentation = instrumentation;
ParcelFileDescriptor[] pfds = instrumentation.getUiAutomation()
.executeShellCommandRw("am monitor");
mReadFd = pfds[0];
mReadStream = new ParcelFileDescriptor.AutoCloseInputStream(mReadFd);
mReadReader = new BufferedReader(new InputStreamReader(mReadStream));
mWriteFd = pfds[1];
mWriteStream = new ParcelFileDescriptor.AutoCloseOutputStream(mWriteFd);
mWritePrinter = new PrintWriter(new BufferedOutputStream(mWriteStream));
mReaderThread = new ReaderThread();
mReaderThread.start();
}
void waitFor(String expected, long timeout) {
long waitUntil = SystemClock.uptimeMillis() + timeout;
synchronized (mPendingLines) {
while (true) {
while (mPendingLines.size() == 0) {
long now = SystemClock.uptimeMillis();
if (now >= waitUntil) {
String msg = "Timed out waiting for next line: expected=" + expected;
Log.d(TAG, msg);
throw new IllegalStateException(msg);
}
try {
mPendingLines.wait(waitUntil - now);
} catch (InterruptedException e) {
}
}
String line = mPendingLines.remove(0);
if (TextUtils.equals(line, expected)) {
break;
} else if (TextUtils.equals(line, WAIT_FOR_EARLY_ANR)
|| TextUtils.equals(line, WAIT_FOR_ANR)
|| TextUtils.equals(line, WAIT_FOR_CRASHED)) {
// If we are getting any of the unexpected state,
// for example, get a crash while waiting for an ANR,
// it could be from another unrelated process, kill it directly.
sendCommand(CMD_KILL);
}
}
}
}
void finish() {
synchronized (mPendingLines) {
mStopping = true;
}
mWritePrinter.println("q");
try {
mWriteStream.close();
} catch (IOException e) {
}
try {
mReadStream.close();
} catch (IOException e) {
}
}
void sendCommand(String cmd) {
mWritePrinter.println(cmd);
mWritePrinter.flush();
}
final class ReaderThread extends Thread {
@Override
public void run() {
try {
String line;
while ((line = mReadReader.readLine()) != null) {
// Log.i(TAG, "debug: " + line);
synchronized (mPendingLines) {
mPendingLines.add(line);
mPendingLines.notifyAll();
}
}
} catch (IOException e) {
Log.w(TAG, "Failed reading", e);
}
}
}
}
}