blob: ddb84de3cc752ac4db9b69d0eccb47ecfc2295fa [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.sts.common;
import com.android.ddmlib.Log;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeoutException;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public final class ProcessUtil {
private static final String LOG_TAG = ProcessUtil.class.getSimpleName();
public static final long PROCESS_WAIT_TIMEOUT_MS = 10_000;
public static final long PROCESS_POLL_PERIOD_MS = 250;
static final Pattern[] mallocDebugErrorPatterns = {
Pattern.compile("^.*HAS A CORRUPTED FRONT GUARD.*$", Pattern.MULTILINE),
Pattern.compile("^.*HAS A CORRUPTED REAR GUARD.*$", Pattern.MULTILINE),
Pattern.compile("^.*USED AFTER FREE.*$", Pattern.MULTILINE),
Pattern.compile("^.*leaked block of size.*$", Pattern.MULTILINE),
Pattern.compile("^.*UNKNOWN POINTER \\(free\\).*$", Pattern.MULTILINE),
Pattern.compile("^.*HAS INVALID TAG.*$", Pattern.MULTILINE),
};
private ProcessUtil() {}
/**
* Get the pids matching a pattern passed to `pgrep`. Because /proc/pid/comm is truncated,
* `pgrep` is passed with `-f` to check the full command line.
*
* @param device the device to use
* @param pgrepRegex a String representing the regex for pgrep
* @return an Optional Map of pid to command line; empty if pgrep did not return EXIT_SUCCESS
*/
public static Optional<Map<Integer, String>> pidsOf(ITestDevice device, String pgrepRegex)
throws DeviceNotAvailableException {
// pgrep is available since 6.0 (Marshmallow)
// https://chromium.googlesource.com/aosp/platform/system/core/+/HEAD/shell_and_utilities/README.md
CommandResult pgrepRes =
device.executeShellV2Command(String.format("pgrep -f -l %s", pgrepRegex));
if (pgrepRes.getStatus() != CommandStatus.SUCCESS) {
Log.d(
LOG_TAG,
String.format(
"pgrep '%s' failed with stderr: %s", pgrepRegex, pgrepRes.getStderr()));
return Optional.empty();
}
Map<Integer, String> pidToCommand = new HashMap<>();
for (String line : pgrepRes.getStdout().split("\n")) {
String[] pidComm = line.split(" ", 2);
int pid = Integer.valueOf(pidComm[0]);
String comm = pidComm[1];
pidToCommand.put(pid, comm);
}
return Optional.of(pidToCommand);
}
/**
* Get a single pid matching a pattern passed to `pgrep`. Throw an {@link
* IllegalArgumentException} when there are more than one PID matching the pattern.
*
* @param device the device to use
* @param pgrepRegex a String representing the regex for pgrep
* @return an Optional Integer of the pid; empty if pgrep did not return EXIT_SUCCESS
*/
public static Optional<Integer> pidOf(ITestDevice device, String pgrepRegex)
throws DeviceNotAvailableException, IllegalArgumentException {
Optional<Map<Integer, String>> pids = pidsOf(device, pgrepRegex);
if (!pids.isPresent()) {
return Optional.empty();
} else if (pids.get().size() == 1) {
return Optional.of(pids.get().keySet().iterator().next());
} else {
throw new IllegalArgumentException("More than one process found for: " + pgrepRegex);
}
}
/**
* Wait until a running process is found for a given regex.
*
* @param device the device to use
* @param pgrepRegex a String representing the regex for pgrep
* @return the pid to command map from pidsOf(...)
*/
public static Map<Integer, String> waitProcessRunning(ITestDevice device, String pgrepRegex)
throws TimeoutException, DeviceNotAvailableException {
return waitProcessRunning(device, pgrepRegex, PROCESS_WAIT_TIMEOUT_MS);
}
/**
* Wait until a running process is found for a given regex.
*
* @param device the device to use
* @param pgrepRegex a String representing the regex for pgrep
* @param timeoutMs how long to wait before throwing a TimeoutException
* @return the pid to command map from pidsOf(...)
*/
public static Map<Integer, String> waitProcessRunning(
ITestDevice device, String pgrepRegex, long timeoutMs)
throws TimeoutException, DeviceNotAvailableException {
long endTime = System.currentTimeMillis() + timeoutMs;
while (true) {
Optional<Map<Integer, String>> pidToCommand = pidsOf(device, pgrepRegex);
if (pidToCommand.isPresent()) {
return pidToCommand.get();
}
if (System.currentTimeMillis() > endTime) {
throw new TimeoutException();
}
try {
Thread.sleep(PROCESS_POLL_PERIOD_MS);
} catch (InterruptedException e) {
// don't care, just keep looping until we time out
}
}
}
/**
* Get the contents from /proc/pid/cmdline.
*
* @param device the device to use
* @param pid the id of the process to get the name for
* @return an Optional String of the contents of /proc/pid/cmdline; empty if the pid could not
* be found
*/
public static Optional<String> getProcessName(ITestDevice device, int pid)
throws DeviceNotAvailableException {
// /proc/*/comm is truncated, use /proc/*/cmdline instead
CommandResult res =
device.executeShellV2Command(String.format("cat /proc/%d/cmdline", pid));
if (res.getStatus() != CommandStatus.SUCCESS) {
return Optional.empty();
}
return Optional.of(res.getStdout());
}
/**
* Wait for a process to be exited. This is not waiting for it to change, but simply be
* nonexistent. It is possible, but unlikely, for a pid to be reused between polls
*
* @param device the device to use
* @param pid the id of the process to wait until exited
*/
public static void waitPidExited(ITestDevice device, int pid)
throws TimeoutException, DeviceNotAvailableException {
waitPidExited(device, pid, PROCESS_WAIT_TIMEOUT_MS);
}
/**
* Wait for a process to be exited. This is not waiting for it to change, but simply be
* nonexistent. It is possible, but unlikely, for a pid to be reused between polls
*
* @param device the device to use
* @param pid the id of the process to wait until exited
* @param timeoutMs how long to wait before throwing a TimeoutException
*/
public static void waitPidExited(ITestDevice device, int pid, long timeoutMs)
throws TimeoutException, DeviceNotAvailableException {
long endTime = System.currentTimeMillis() + timeoutMs;
CommandResult res = null;
while (true) {
// kill -0 asserts that the process is alive and readable
res = device.executeShellV2Command(String.format("kill -0 %d", pid));
if (res.getStatus() != CommandStatus.SUCCESS) {
String err = res.getStderr();
if (!err.contains("No such process")) {
throw new RuntimeException("kill -0 returned stderr: " + err);
}
// the process is most likely killed
return;
}
if (System.currentTimeMillis() > endTime) {
throw new TimeoutException();
}
try {
Thread.sleep(PROCESS_POLL_PERIOD_MS);
} catch (InterruptedException e) {
// don't care, just keep looping until we time out
}
}
}
/**
* Send SIGKILL to a process and wait for it to be exited.
*
* @param device the device to use
* @param pid the id of the process to wait until exited
* @param timeoutMs how long to wait before throwing a TimeoutException
*/
public static void killPid(ITestDevice device, int pid, long timeoutMs)
throws DeviceNotAvailableException, TimeoutException {
killPid(device, pid, 9, timeoutMs);
}
/**
* Send a signal to a process and wait for it to be exited.
*
* @param device the device to use
* @param pid the id of the process to wait until exited
* @param signal the signal to send to the process
* @param timeoutMs how long to wait before throwing a TimeoutException
*/
public static void killPid(ITestDevice device, int pid, int signal, long timeoutMs)
throws DeviceNotAvailableException, TimeoutException {
CommandUtil.runAndCheck(device, String.format("kill -%d %d", signal, pid));
waitPidExited(device, pid, timeoutMs);
}
/**
* Send SIGKILL to a all processes matching a pattern.
*
* @param device the device to use
* @param pgrepRegex a String representing the regex for pgrep
* @param timeoutMs how long to wait before throwing a TimeoutException
* @return whether any processes were killed
*/
public static boolean killAll(ITestDevice device, String pgrepRegex, long timeoutMs)
throws DeviceNotAvailableException, TimeoutException {
return killAll(device, pgrepRegex, timeoutMs, true);
}
/**
* Send SIGKILL to a all processes matching a pattern.
*
* @param device the device to use
* @param pgrepRegex a String representing the regex for pgrep
* @param timeoutMs how long to wait before throwing a TimeoutException
* @param expectExist whether an exception should be thrown when no processes were killed
* @param expectExist whether an exception should be thrown when no processes were killed
* @return whether any processes were killed
*/
public static boolean killAll(
ITestDevice device, String pgrepRegex, long timeoutMs, boolean expectExist)
throws DeviceNotAvailableException, TimeoutException {
Optional<Map<Integer, String>> pids = pidsOf(device, pgrepRegex);
if (!pids.isPresent()) {
// no pids to kill
if (expectExist) {
throw new RuntimeException(
String.format("Expected to kill processes matching %s", pgrepRegex));
}
return false;
}
for (int pid : pids.get().keySet()) {
killPid(device, pid, timeoutMs);
}
return true;
}
/**
* Kill a process at the beginning and end of a test.
*
* @param device the device to use
* @param pid the id of the process to kill
* @param beforeCloseKill a runnable for any actions that need to cleanup before killing the
* process in a normal environment at the end of the test. Can be null.
* @return An object that will kill the process again when it is closed
*/
public static AutoCloseable withProcessKill(
final ITestDevice device, final String pgrepRegex, final Runnable beforeCloseKill)
throws DeviceNotAvailableException, TimeoutException {
return withProcessKill(device, pgrepRegex, beforeCloseKill, PROCESS_WAIT_TIMEOUT_MS);
}
/**
* Kill a process at the beginning and end of a test.
*
* @param device the device to use
* @param pid the id of the process to kill
* @param timeoutMs how long in milliseconds to wait for the process to kill
* @param beforeCloseKill a runnable for any actions that need to cleanup before killing the
* process in a normal environment at the end of the test. Can be null.
* @return An object that will kill the process again when it is closed
*/
public static AutoCloseable withProcessKill(
final ITestDevice device,
final String pgrepRegex,
final Runnable beforeCloseKill,
final long timeoutMs)
throws DeviceNotAvailableException, TimeoutException {
return new AutoCloseable() {
{
if (!killAll(device, pgrepRegex, timeoutMs, /*expectExist*/ false)) {
Log.d(LOG_TAG, String.format("did not kill any processes for %s", pgrepRegex));
}
}
@Override
public void close() throws Exception {
if (beforeCloseKill != null) {
beforeCloseKill.run();
}
killAll(device, pgrepRegex, timeoutMs, /*expectExist*/ false);
}
};
}
/**
* Returns the currently open file names of the specified process.
*
* @param device device to be run on
* @param pid the id of the process to search
* @return an Optional of the open files; empty if the process wasn't found or the open files
* couldn't be read.
*/
public static Optional<List<String>> listOpenFiles(ITestDevice device, int pid)
throws DeviceNotAvailableException {
// test if we can access the open files of the specified pid
// `test` is available in all relevant Android versions
CommandResult fdRes =
device.executeShellV2Command(String.format("test -r /proc/%d/fd", pid));
if (fdRes.getStatus() != CommandStatus.SUCCESS) {
return Optional.empty();
}
// `find` and `realpath` are available since 6.0 (Marshmallow)
// https://chromium.googlesource.com/aosp/platform/system/core/+/HEAD/shell_and_utilities/README.md
// intentionally not using lsof because of parsing issues
// realpath will intentionally fail for non-filesystem file descriptors
CommandResult openFilesRes =
device.executeShellV2Command(
String.format("find /proc/%d/fd -exec realpath {} + 2> /dev/null", pid));
String[] openFilesArray = openFilesRes.getStdout().split("\n");
return Optional.of(Arrays.asList(openFilesArray));
}
/**
* Returns file names of the specified file, loaded by the specified process.
*
* @param device device to be run on
* @param pid the id of the process to search
* @param filePattern a pattern of the file names to return
* @return an Optional of the filtered files; empty if the process wasn't found or the open
* files couldn't be read.
*/
public static Optional<List<String>> findFilesLoadedByProcess(
ITestDevice device, int pid, Pattern filePattern) throws DeviceNotAvailableException {
Optional<List<String>> openFilesOption = listOpenFiles(device, pid);
if (!openFilesOption.isPresent()) {
return Optional.empty();
}
List<String> openFiles = openFilesOption.get();
return Optional.of(
openFiles.stream()
.filter((f) -> filePattern.matcher(f).matches())
.collect(Collectors.toList()));
}
}