1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.sts.common; 18 19 import com.android.ddmlib.Log; 20 import com.android.tradefed.device.DeviceNotAvailableException; 21 import com.android.tradefed.device.IFileEntry; 22 import com.android.tradefed.device.ITestDevice; 23 import com.android.tradefed.util.CommandResult; 24 import com.android.tradefed.util.CommandStatus; 25 26 import java.util.Arrays; 27 import java.util.HashMap; 28 import java.util.List; 29 import java.util.Map; 30 import java.util.Optional; 31 import java.util.concurrent.TimeoutException; 32 import java.util.regex.Matcher; 33 import java.util.regex.Pattern; 34 import java.util.stream.Collectors; 35 36 /** Various helpers to find, wait, and kill processes on the device */ 37 public final class ProcessUtil { 38 public static class KillException extends Exception { 39 public enum Reason { 40 UNKNOWN, 41 INVALID_SIGNAL, 42 INSUFFICIENT_PERMISSIONS, 43 NO_SUCH_PROCESS; 44 } 45 46 private Reason reason; 47 KillException(String errorMessage, Reason r)48 public KillException(String errorMessage, Reason r) { 49 super(errorMessage); 50 this.reason = r; 51 } 52 getReason()53 public Reason getReason() { 54 return this.reason; 55 } 56 } 57 58 private static final String LOG_TAG = ProcessUtil.class.getSimpleName(); 59 60 public static final long PROCESS_WAIT_TIMEOUT_MS = 10_000; 61 public static final long PROCESS_POLL_PERIOD_MS = 250; 62 public static final String[] INTENT_QUERY_CMDS = { 63 "resolve-activity", "query-activities", "query-services", "query-receivers" 64 }; 65 ProcessUtil()66 private ProcessUtil() {} 67 68 /** 69 * Get the pids matching a pattern passed to `pgrep`. Because /proc/pid/comm is truncated, 70 * `pgrep` is passed with `-f` to check the full command line. 71 * 72 * @param device the device to use 73 * @param pgrepRegex a String representing the regex for pgrep 74 * @return an Optional Map of pid to command line; empty if pgrep did not return EXIT_SUCCESS 75 */ pidsOf(ITestDevice device, String pgrepRegex)76 public static Optional<Map<Integer, String>> pidsOf(ITestDevice device, String pgrepRegex) 77 throws DeviceNotAvailableException { 78 // pgrep is available since 6.0 (Marshmallow) 79 // https://chromium.googlesource.com/aosp/platform/system/core/+/HEAD/shell_and_utilities/README.md 80 CommandResult pgrepRes = 81 device.executeShellV2Command(String.format("pgrep -f -l %s", pgrepRegex)); 82 if (pgrepRes.getStatus() != CommandStatus.SUCCESS) { 83 Log.d( 84 LOG_TAG, 85 String.format( 86 "pgrep '%s' failed with stderr: %s", pgrepRegex, pgrepRes.getStderr())); 87 return Optional.empty(); 88 } 89 Map<Integer, String> pidToCommand = new HashMap<>(); 90 for (String line : pgrepRes.getStdout().split("\n")) { 91 String[] pidComm = line.split(" ", 2); 92 int pid = Integer.valueOf(pidComm[0]); 93 String comm = pidComm[1]; 94 pidToCommand.put(pid, comm); 95 } 96 return Optional.of(pidToCommand); 97 } 98 99 /** 100 * Get a single pid matching a pattern passed to `pgrep`. Throw an {@link 101 * IllegalArgumentException} when there are more than one PID matching the pattern. 102 * 103 * @param device the device to use 104 * @param pgrepRegex a String representing the regex for pgrep 105 * @return an Optional Integer of the pid; empty if pgrep did not return EXIT_SUCCESS 106 */ pidOf(ITestDevice device, String pgrepRegex)107 public static Optional<Integer> pidOf(ITestDevice device, String pgrepRegex) 108 throws DeviceNotAvailableException, IllegalArgumentException { 109 Optional<Map<Integer, String>> pids = pidsOf(device, pgrepRegex); 110 if (!pids.isPresent()) { 111 return Optional.empty(); 112 } else if (pids.get().size() == 1) { 113 return Optional.of(pids.get().keySet().iterator().next()); 114 } else { 115 throw new IllegalArgumentException("More than one process found for: " + pgrepRegex); 116 } 117 } 118 119 /** 120 * Wait until a running process is found for a given regex. 121 * 122 * @param device the device to use 123 * @param pgrepRegex a String representing the regex for pgrep 124 * @return the pid to command map from pidsOf(...) 125 */ waitProcessRunning(ITestDevice device, String pgrepRegex)126 public static Map<Integer, String> waitProcessRunning(ITestDevice device, String pgrepRegex) 127 throws TimeoutException, DeviceNotAvailableException { 128 return waitProcessRunning(device, pgrepRegex, PROCESS_WAIT_TIMEOUT_MS); 129 } 130 131 /** 132 * Wait until a running process is found for a given regex. 133 * 134 * @param device the device to use 135 * @param pgrepRegex a String representing the regex for pgrep 136 * @param timeoutMs how long to wait before throwing a TimeoutException 137 * @return the pid to command map from pidsOf(...) 138 */ waitProcessRunning( ITestDevice device, String pgrepRegex, long timeoutMs)139 public static Map<Integer, String> waitProcessRunning( 140 ITestDevice device, String pgrepRegex, long timeoutMs) 141 throws TimeoutException, DeviceNotAvailableException { 142 long endTime = System.currentTimeMillis() + timeoutMs; 143 while (true) { 144 Optional<Map<Integer, String>> pidToCommand = pidsOf(device, pgrepRegex); 145 if (pidToCommand.isPresent()) { 146 return pidToCommand.get(); 147 } 148 if (System.currentTimeMillis() > endTime) { 149 throw new TimeoutException(); 150 } 151 try { 152 Thread.sleep(PROCESS_POLL_PERIOD_MS); 153 } catch (InterruptedException e) { 154 // don't care, just keep looping until we time out 155 } 156 } 157 } 158 159 /** 160 * Get the contents from /proc/pid/cmdline. 161 * 162 * @param device the device to use 163 * @param pid the id of the process to get the name for 164 * @return an Optional String of the contents of /proc/pid/cmdline; empty if the pid could not 165 * be found 166 */ getProcessName(ITestDevice device, int pid)167 public static Optional<String> getProcessName(ITestDevice device, int pid) 168 throws DeviceNotAvailableException { 169 // /proc/*/comm is truncated, use /proc/*/cmdline instead 170 CommandResult res = 171 device.executeShellV2Command(String.format("cat /proc/%d/cmdline", pid)); 172 if (res.getStatus() != CommandStatus.SUCCESS) { 173 return Optional.empty(); 174 } 175 return Optional.of(res.getStdout()); 176 } 177 178 /** 179 * Wait for a process to be exited. This is not waiting for it to change, but simply be 180 * nonexistent. It is possible, but unlikely, for a pid to be reused between polls 181 * 182 * @param device the device to use 183 * @param pid the id of the process to wait until exited 184 */ waitPidExited(ITestDevice device, int pid)185 public static void waitPidExited(ITestDevice device, int pid) 186 throws TimeoutException, DeviceNotAvailableException, KillException { 187 waitPidExited(device, pid, PROCESS_WAIT_TIMEOUT_MS); 188 } 189 190 /** 191 * Wait for a process to be exited. This is not waiting for it to change, but simply be 192 * nonexistent. It is possible, but unlikely, for a pid to be reused between polls 193 * 194 * @param device the device to use 195 * @param pid the id of the process to wait until exited 196 * @param timeoutMs how long to wait before throwing a TimeoutException 197 */ waitPidExited(ITestDevice device, int pid, long timeoutMs)198 public static void waitPidExited(ITestDevice device, int pid, long timeoutMs) 199 throws TimeoutException, DeviceNotAvailableException, KillException { 200 long endTime = System.currentTimeMillis() + timeoutMs; 201 CommandResult res = null; 202 while (true) { 203 // kill -0 asserts that the process is alive and readable 204 res = device.executeShellV2Command(String.format("kill -0 %d", pid)); 205 if (res.getStatus() != CommandStatus.SUCCESS) { 206 String err = res.getStderr(); 207 if (!err.contains("No such process")) { 208 throw new KillException( 209 "kill -0 returned stderr: " + err, 210 KillException.Reason.NO_SUCH_PROCESS); 211 } 212 // the process is most likely killed 213 return; 214 } 215 if (System.currentTimeMillis() > endTime) { 216 throw new TimeoutException(); 217 } 218 try { 219 Thread.sleep(PROCESS_POLL_PERIOD_MS); 220 } catch (InterruptedException e) { 221 // don't care, just keep looping until we time out 222 } 223 } 224 } 225 226 /** 227 * Send SIGKILL to a process and wait for it to be exited. 228 * 229 * @param device the device to use 230 * @param pid the id of the process to wait until exited 231 * @param timeoutMs how long to wait before throwing a TimeoutException 232 */ killPid(ITestDevice device, int pid, long timeoutMs)233 public static void killPid(ITestDevice device, int pid, long timeoutMs) 234 throws DeviceNotAvailableException, TimeoutException, KillException { 235 killPid(device, pid, 9, timeoutMs); 236 } 237 238 /** 239 * Send a signal to a process and wait for it to be exited. 240 * 241 * @param device the device to use 242 * @param pid the id of the process to wait until exited 243 * @param signal the signal to send to the process 244 * @param timeoutMs how long to wait before throwing a TimeoutException 245 */ killPid(ITestDevice device, int pid, int signal, long timeoutMs)246 public static void killPid(ITestDevice device, int pid, int signal, long timeoutMs) 247 throws DeviceNotAvailableException, TimeoutException, KillException { 248 CommandResult res = device.executeShellV2Command(String.format("kill -%d %d", signal, pid)); 249 if (res.getStatus() != CommandStatus.SUCCESS) { 250 String err = res.getStderr(); 251 if (err.contains("invalid signal specification")) { 252 throw new KillException(err, KillException.Reason.INVALID_SIGNAL); 253 } else if (err.contains("Operation not permitted")) { 254 throw new KillException(err, KillException.Reason.INSUFFICIENT_PERMISSIONS); 255 } else if (err.contains("No such process")) { 256 throw new KillException(err, KillException.Reason.NO_SUCH_PROCESS); 257 } else { 258 throw new KillException(err, KillException.Reason.UNKNOWN); 259 } 260 } 261 waitPidExited(device, pid, timeoutMs); 262 } 263 264 /** 265 * Send SIGKILL to a all processes matching a pattern. 266 * 267 * @param device the device to use 268 * @param pgrepRegex a String representing the regex for pgrep 269 * @param timeoutMs how long to wait before throwing a TimeoutException 270 * @return whether any processes were killed 271 */ killAll(ITestDevice device, String pgrepRegex, long timeoutMs)272 public static boolean killAll(ITestDevice device, String pgrepRegex, long timeoutMs) 273 throws DeviceNotAvailableException, TimeoutException, KillException { 274 return killAll(device, pgrepRegex, timeoutMs, true); 275 } 276 277 /** 278 * Send SIGKILL to a all processes matching a pattern. 279 * 280 * @param device the device to use 281 * @param pgrepRegex a String representing the regex for pgrep 282 * @param timeoutMs how long to wait before throwing a TimeoutException 283 * @param expectExist whether an exception should be thrown when no processes were killed 284 * @param expectExist whether an exception should be thrown when no processes were killed 285 * @return whether any processes were killed 286 */ killAll( ITestDevice device, String pgrepRegex, long timeoutMs, boolean expectExist)287 public static boolean killAll( 288 ITestDevice device, String pgrepRegex, long timeoutMs, boolean expectExist) 289 throws DeviceNotAvailableException, TimeoutException, KillException { 290 Optional<Map<Integer, String>> pids = pidsOf(device, pgrepRegex); 291 if (!pids.isPresent()) { 292 // no pids to kill 293 if (expectExist) { 294 throw new RuntimeException( 295 String.format("Expected to kill processes matching %s", pgrepRegex)); 296 } 297 return false; 298 } 299 300 for (int pid : pids.get().keySet()) { 301 try { 302 killPid(device, pid, timeoutMs); 303 } catch (KillException e) { 304 // ignore pids that do not exist 305 if (e.getReason() != KillException.Reason.NO_SUCH_PROCESS) { 306 throw e; 307 } 308 } 309 } 310 311 return true; 312 } 313 314 /** 315 * Kill a process at the beginning and end of a test. 316 * 317 * @param device the device to use 318 * @param pgrepRegex the name pattern of the process to kill to give to pgrep 319 * @param beforeCloseKill a runnable for any actions that need to cleanup before killing the 320 * process in a normal environment at the end of the test. Can be null. 321 * @return An object that will kill the process again when it is closed 322 */ withProcessKill( final ITestDevice device, final String pgrepRegex, final Runnable beforeCloseKill)323 public static AutoCloseable withProcessKill( 324 final ITestDevice device, final String pgrepRegex, final Runnable beforeCloseKill) 325 throws DeviceNotAvailableException, TimeoutException, KillException { 326 return withProcessKill(device, pgrepRegex, beforeCloseKill, PROCESS_WAIT_TIMEOUT_MS); 327 } 328 329 /** 330 * Kill a process at the beginning and end of a test. 331 * 332 * @param device the device to use 333 * @param pgrepRegex the name pattern of the process to kill to give to pgrep 334 * @param beforeCloseKill a runnable for any actions that need to cleanup before killing the 335 * process in a normal environment at the end of the test. Can be null. 336 * @param timeoutMs how long in milliseconds to wait for the process to kill 337 * @return An object that will kill the process again when it is closed 338 */ withProcessKill( final ITestDevice device, final String pgrepRegex, final Runnable beforeCloseKill, final long timeoutMs)339 public static AutoCloseable withProcessKill( 340 final ITestDevice device, 341 final String pgrepRegex, 342 final Runnable beforeCloseKill, 343 final long timeoutMs) 344 throws DeviceNotAvailableException, TimeoutException, KillException { 345 return new AutoCloseable() { 346 { 347 try { 348 if (!killAll(device, pgrepRegex, timeoutMs, /*expectExist*/ false)) { 349 Log.d( 350 LOG_TAG, 351 String.format("did not kill any processes for %s", pgrepRegex)); 352 } 353 } catch (KillException e) { 354 Log.d(LOG_TAG, "failed to kill a process"); 355 } 356 } 357 358 @Override 359 public void close() throws Exception { 360 if (beforeCloseKill != null) { 361 beforeCloseKill.run(); 362 } 363 try { 364 killAll(device, pgrepRegex, timeoutMs, /*expectExist*/ false); 365 } catch (KillException e) { 366 if (e.getReason() != KillException.Reason.NO_SUCH_PROCESS) { 367 throw e; 368 } 369 } 370 } 371 }; 372 } 373 374 /** 375 * Returns the currently open file names of the specified process. This does not include shared 376 * libraries linked by the linker. 377 * 378 * @param device device to be run on 379 * @param pid the id of the process to search 380 * @return an Optional of the open files; empty if the process wasn't found or the open files 381 * couldn't be read. 382 */ 383 public static Optional<List<String>> listOpenFiles(ITestDevice device, int pid) 384 throws DeviceNotAvailableException { 385 // test if we can access the open files of the specified pid 386 // `test` is available in all relevant Android versions 387 CommandResult fdRes = 388 device.executeShellV2Command(String.format("test -r /proc/%d/fd", pid)); 389 if (fdRes.getStatus() != CommandStatus.SUCCESS) { 390 return Optional.empty(); 391 } 392 // `find` and `realpath` are available since 6.0 (Marshmallow) 393 // https://chromium.googlesource.com/aosp/platform/system/core/+/HEAD/shell_and_utilities/README.md 394 // intentionally not using lsof because of parsing issues 395 // realpath will intentionally fail for non-filesystem file descriptors 396 CommandResult openFilesRes = 397 device.executeShellV2Command( 398 String.format("find /proc/%d/fd -exec realpath {} + 2> /dev/null", pid)); 399 String[] openFilesArray = openFilesRes.getStdout().split("\n"); 400 return Optional.of(Arrays.asList(openFilesArray)); 401 } 402 403 /** 404 * Returns file names of the specified file, loaded by the specified process. This does not 405 * include shared libraries linked. 406 * 407 * @param device device to be run on 408 * @param pid the id of the process to search 409 * @param filePattern a pattern of the file names to return 410 * @return an Optional of the filtered files; empty if the process wasn't found or the open 411 * files couldn't be read. 412 */ 413 public static Optional<List<String>> findFilesLoadedByProcess( 414 ITestDevice device, int pid, Pattern filePattern) throws DeviceNotAvailableException { 415 Optional<List<String>> openFilesOption = listOpenFiles(device, pid); 416 if (!openFilesOption.isPresent()) { 417 return Optional.empty(); 418 } 419 List<String> openFiles = openFilesOption.get(); 420 return Optional.of( 421 openFiles.stream() 422 .filter((f) -> filePattern.matcher(f).matches()) 423 .collect(Collectors.toList())); 424 } 425 426 /** 427 * Returns file entry of the first file loaded by the specified process with specified name. 428 * This includes shared libraries linked. 429 * 430 * @param device device to be run on 431 * @param processPgrepRegex pgrep pattern of process to look for 432 * @param filenamePattern the filename pattern to find 433 * @return an Optional of IFileEntry of the path of the file on the device if exists. 434 */ 435 public static Optional<IFileEntry> findFileLoadedByProcess( 436 ITestDevice device, String processPgrepRegex, Pattern filenamePattern) 437 throws DeviceNotAvailableException { 438 Optional<Map<Integer, String>> pids = pidsOf(device, processPgrepRegex); 439 if (pids.isPresent()) { 440 for (Integer pid : pids.get().keySet()) { 441 String cmd = "lsof -p " + pid.toString() + " | grep -o '/.*'"; 442 String[] openFiles = CommandUtil.runAndCheck(device, cmd).getStdout().split("\n"); 443 for (String f : openFiles) { 444 if (f.contains("Permission denied")) { 445 throw new IllegalStateException( 446 "no permission to read open files for process"); 447 } 448 if (filenamePattern.matcher(f).find()) { 449 return Optional.of(device.getFileEntry(f.trim())); 450 } 451 } 452 } 453 } 454 return Optional.empty(); 455 } 456 457 /* 458 * To get application process pids of all applications that can handle the target intent 459 * @param queryCmd Query command to be used. One of the values present in INTENT_QUERY_CMDS 460 * @param intentOptions Map of intent option to value for target intent 461 * @param device device to be run on 462 * @return Optional Map of pid to process name of application processes that can handle the 463 target intent 464 */ 465 public static Optional<Map<Integer, String>> getAllProcessIdsFromComponents( 466 String queryCmd, Map<String, String> intentOptions, ITestDevice device) 467 throws DeviceNotAvailableException, RuntimeException { 468 if (!Arrays.asList(INTENT_QUERY_CMDS).contains(queryCmd)) { 469 throw new RuntimeException("Unknown command " + queryCmd); 470 } 471 String cmd = "pm " + queryCmd + " "; 472 for (Map.Entry<String, String> entry : intentOptions.entrySet()) { 473 cmd += entry.getKey() + " " + entry.getValue() + " "; 474 } 475 CommandResult result = device.executeShellV2Command(cmd); 476 String resultString = result.getStdout(); 477 Log.i(LOG_TAG, String.format("Executed cmd: %s \nOutput: %s", cmd, resultString)); 478 479 // As target string (process name) is coming from system itself, regex here only checks for 480 // presence of valid characters in process name and not for the actual order of characters 481 Pattern processNamePattern = Pattern.compile("processName=(?<name>[a-zA-Z0-9_\\.:]+)"); 482 Matcher matcher = processNamePattern.matcher(resultString); 483 Map<Integer, String> pidNameMap = new HashMap<Integer, String>(); 484 while (matcher.find()) { 485 String process = matcher.group("name"); 486 pidsOf(device, process) 487 .ifPresent( 488 (pids) -> { 489 pidNameMap.putAll(pids); 490 }); 491 } 492 return pidNameMap.isEmpty() ? Optional.empty() : Optional.of(pidNameMap); 493 } 494 } 495