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