summaryrefslogtreecommitdiff
path: root/libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java
blob: d8fd05fda9b3bf311cad44405efa46d9c4c57e02 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
/*
 * 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.IFileEntry;
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.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/** Various helpers to find, wait, and kill processes on the device */
public final class ProcessUtil {
    public static class KillException extends Exception {
        public enum Reason {
            UNKNOWN,
            INVALID_SIGNAL,
            INSUFFICIENT_PERMISSIONS,
            NO_SUCH_PROCESS;
        }

        private Reason reason;

        public KillException(String errorMessage, Reason r) {
            super(errorMessage);
            this.reason = r;
        }

        public Reason getReason() {
            return this.reason;
        }
    }

    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;
    public static final String[] INTENT_QUERY_CMDS = {
        "resolve-activity", "query-activities", "query-services", "query-receivers"
    };

    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, KillException {
        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, KillException {
        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 KillException(
                            "kill -0 returned stderr: " + err,
                            KillException.Reason.NO_SUCH_PROCESS);
                }
                // 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, KillException {
        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, KillException {
        CommandResult res = device.executeShellV2Command(String.format("kill -%d %d", signal, pid));
        if (res.getStatus() != CommandStatus.SUCCESS) {
            String err = res.getStderr();
            if (err.contains("invalid signal specification")) {
                throw new KillException(err, KillException.Reason.INVALID_SIGNAL);
            } else if (err.contains("Operation not permitted")) {
                throw new KillException(err, KillException.Reason.INSUFFICIENT_PERMISSIONS);
            } else if (err.contains("No such process")) {
                throw new KillException(err, KillException.Reason.NO_SUCH_PROCESS);
            } else {
                throw new KillException(err, KillException.Reason.UNKNOWN);
            }
        }
        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, KillException {
        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, KillException {
        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()) {
            try {
                killPid(device, pid, timeoutMs);
            } catch (KillException e) {
                // ignore pids that do not exist
                if (e.getReason() != KillException.Reason.NO_SUCH_PROCESS) {
                    throw e;
                }
            }
        }

        return true;
    }

    /**
     * Kill a process at the beginning and end of a test.
     *
     * @param device the device to use
     * @param pgrepRegex the name pattern of the process to kill to give to pgrep
     * @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, KillException {
        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 pgrepRegex the name pattern of the process to kill to give to pgrep
     * @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.
     * @param timeoutMs how long in milliseconds to wait for the process to kill
     * @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, KillException {
        return new AutoCloseable() {
            {
                try {
                    if (!killAll(device, pgrepRegex, timeoutMs, /*expectExist*/ false)) {
                        Log.d(
                                LOG_TAG,
                                String.format("did not kill any processes for %s", pgrepRegex));
                    }
                } catch (KillException e) {
                    Log.d(LOG_TAG, "failed to kill a process");
                }
            }

            @Override
            public void close() throws Exception {
                if (beforeCloseKill != null) {
                    beforeCloseKill.run();
                }
                try {
                    killAll(device, pgrepRegex, timeoutMs, /*expectExist*/ false);
                } catch (KillException e) {
                    if (e.getReason() != KillException.Reason.NO_SUCH_PROCESS) {
                        throw e;
                    }
                }
            }
        };
    }

    /**
     * Returns the currently open file names of the specified process. This does not include shared
     * libraries linked by the linker.
     *
     * @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. This does not
     * include shared libraries linked.
     *
     * @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()));
    }

    /**
     * Returns file entry of the first file loaded by the specified process with specified name.
     * This includes shared libraries linked.
     *
     * @param device device to be run on
     * @param process pgrep pattern of process to look for
     * @param filenamePattern the filename pattern to find
     * @return an Opotional of IFileEntry of the path of the file on the device if exists.
     */
    public static Optional<IFileEntry> findFileLoadedByProcess(
            ITestDevice device, String process, Pattern filenamePattern)
            throws DeviceNotAvailableException {
        Optional<Integer> pid = ProcessUtil.pidOf(device, process);
        if (pid.isPresent()) {
            String cmd = "lsof -p " + pid.get().toString() + " | grep -o '/.*'";
            String[] openFiles = CommandUtil.runAndCheck(device, cmd).getStdout().split("\n");
            for (String f : openFiles) {
                if (f.contains("Permission denied")) {
                    throw new IllegalStateException("no permission to read open files for process");
                }
                if (filenamePattern.matcher(f).find()) {
                    return Optional.of(device.getFileEntry(f.trim()));
                }
            }
        }
        return Optional.empty();
    }

    /*
    * To get application process pids of all applications that can handle the target intent
    * @param queryCmd Query command to be used. One of the values present in INTENT_QUERY_CMDS
    * @param intentOptions Map of intent option to value for target intent
    * @param device device to be run on
    * @return Optional Map of pid to process name of application processes that can handle the
           target intent
    */
    public static Optional<Map<Integer, String>> getAllProcessIdsFromComponents(
            String queryCmd, Map<String, String> intentOptions, ITestDevice device)
            throws DeviceNotAvailableException, RuntimeException {
        if (!Arrays.asList(INTENT_QUERY_CMDS).contains(queryCmd)) {
            throw new RuntimeException("Unknown command " + queryCmd);
        }
        String cmd = "pm " + queryCmd + " ";
        for (Map.Entry<String, String> entry : intentOptions.entrySet()) {
            cmd += entry.getKey() + " " + entry.getValue() + " ";
        }
        CommandResult result = device.executeShellV2Command(cmd);
        String resultString = result.getStdout();
        Log.i(LOG_TAG, String.format("Executed cmd: %s \nOutput: %s", cmd, resultString));

        // As target string (process name) is coming from system itself, regex here only checks for
        // presence of valid characters in process name and not for the actual order of characters
        Pattern processNamePattern = Pattern.compile("processName=(?<name>[a-zA-Z0-9_\\.:]+)");
        Matcher matcher = processNamePattern.matcher(resultString);
        Map<Integer, String> pidNameMap = new HashMap<Integer, String>();
        while (matcher.find()) {
            String process = matcher.group("name");
            pidsOf(device, process)
                    .ifPresent(
                            (pids) -> {
                                pidNameMap.putAll(pids);
                            });
        }
        return pidNameMap.isEmpty() ? Optional.empty() : Optional.of(pidNameMap);
    }
}