1 /*
2  * Copyright (C) 2014 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 android.support.test.aupt;
18 
19 import android.app.UiAutomation;
20 import android.app.ActivityManager.RunningAppProcessInfo;
21 import android.os.ParcelFileDescriptor;
22 import android.util.Log;
23 
24 import java.io.BufferedReader;
25 import java.io.InputStreamReader;
26 import java.io.IOException;
27 import java.util.ArrayList;
28 import java.util.HashMap;
29 import java.util.HashSet;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.regex.Pattern;
33 import java.util.regex.Matcher;
34 import java.util.Set;
35 
36 public class ProcessStatusTracker implements IProcessStatusTracker {
37     private static final String TAG = "ProcessStatusTracker";
38 
39     // Example return line from "adb shell ps" for the pattern below
40     // USER     PID     PPID    VSIZE   RSS     WCHAN       PC          NAME
41     // root     1       0       2216    804     sys_epoll_  00000000 S  com.android.chrome
42     private final String PS_PATTERN =
43             "\\w+\\s+(\\d+)\\s+\\w+\\s+\\d+\\s+\\d+\\s+\\w+\\s+[0-9a-f]+\\s+\\w+\\s+(.+)";
44     private final Pattern PS_PATTERN_MATCH = Pattern.compile(PS_PATTERN);
45     private final int PS_PATTERN_PID_GROUP = 1;
46     private final int PS_PATTERN_PKG_GROUP = 2;
47 
48     private Map<String, Integer> mPidTracker;
49     private Set<String> mPidExclusions;
50 
ProcessStatusTracker(String[] processes)51     public ProcessStatusTracker(String[] processes) {
52         mPidTracker = new HashMap<String, Integer>();
53         mPidExclusions = new HashSet<String>();
54         if (processes == null) return;
55         for (String proc : processes) {
56             addMonitoredProcess(proc);
57         }
58     }
59 
60     @Override
addMonitoredProcess(String processName)61     public void addMonitoredProcess(String processName) {
62         if (mPidTracker.containsKey(processName)) {
63             throw new IllegalArgumentException("Process already being monitored: " + processName);
64         }
65         mPidTracker.put(processName, -1);
66         // don't track right away, until told to
67         mPidExclusions.add(processName);
68     }
69 
70     @Override
getProcessDetails()71     public List<ProcessDetails> getProcessDetails() {
72         if (mPidTracker == null || mPidTracker.isEmpty()) {
73             // nothing to track
74             Log.v(TAG, "getProcessDetails - No pids to track, not doing anything");
75             return null;
76         }
77         List<ProcessDetails> ret = new ArrayList<ProcessDetails>();
78         List<RunningAppProcessInfo> runningApps = null;
79         // Refactored to use shell commands, not ActivityManager
80         runningApps = getRunningAppProcesses();
81         if (runningApps == null || runningApps.isEmpty()) {
82             Log.e(TAG, "Failed to retrieve list of running apps");
83             return ret;
84         }
85         // used for keeping track of which process has died
86         Set<String> deadProcesses = new HashSet<String>(mPidTracker.keySet());
87         Log.i(TAG, "Got running processes");
88         for (RunningAppProcessInfo info : runningApps) {
89             if (mPidTracker.containsKey(info.processName)
90                     && !mPidExclusions.contains(info.processName)) {
91                 ProcessDetails detail = new ProcessDetails();
92                 detail.processName = info.processName;
93                 detail.pid0 = info.pid;
94                 // this is a process that we are monitoring
95                 int pid = mPidTracker.get(info.processName);
96                 if (pid == -1) {
97                     mPidTracker.put(info.processName, info.pid);
98                     Log.d(TAG, String.format(
99                             "pid detected - %s : %d", info.processName, info.pid));
100                     // all good with this process
101                     deadProcesses.remove(info.processName);
102                     detail.processStatus = ProcessStatus.PROC_STARTED;
103                 } else if (pid == info.pid) {
104                     // pid hasn't changed, all good
105                     deadProcesses.remove(info.processName);
106                     detail.processStatus = ProcessStatus.PROC_OK;
107                 } else {
108                     //pid changed
109                     deadProcesses.remove(info.processName);
110                     detail.processStatus = ProcessStatus.PROC_RESTARTED;
111                     detail.pid1 = pid;
112                 }
113                 ret.add(detail);
114             }
115         }
116         for (String proc : deadProcesses) {
117             ProcessDetails detail = new ProcessDetails();
118             detail.processName = proc;
119             // check the remaining ones are dead, or not started
120             int pid = mPidTracker.get(proc);
121             if (pid != -1) {
122                 detail.processStatus = ProcessStatus.PROC_DIED;
123             } else {
124                 detail.processStatus = ProcessStatus.PROC_NOT_STARTED;
125             }
126             ret.add(detail);
127         }
128         return ret;
129     }
130 
131     @Override
setAllowProcessTracking(String processName)132     public void setAllowProcessTracking(String processName) {
133         if (mPidTracker == null || mPidTracker.isEmpty()) {
134             // nothing to track
135             return;
136         }
137         // ignore those not under monitoring
138         if (mPidExclusions.contains(processName)) {
139             mPidExclusions.remove(processName);
140             Log.v(TAG, "Started tracking pid changes: " + processName);
141         }
142         verifyRunningProcess();
143     }
144 
145     @Override
verifyRunningProcess()146     public void verifyRunningProcess() {
147         Log.i(TAG, "Getting process details");
148         List<ProcessDetails> details = getProcessDetails();
149         if (details == null) {
150             return;
151         }
152         for (ProcessDetails d :  details) {
153             Log.v(TAG, String.format("verifyRunningProcess - proc: %s, state: %s",
154                     d.processName, d.processStatus.toString()));
155             switch (d.processStatus) {
156                 case PROC_OK:
157                 case PROC_NOT_STARTED:
158                     // no op
159                     break;
160                 case PROC_STARTED:
161                     Log.v(TAG, String.format("Process started: %s - %d", d.processName, d.pid0));
162                     break;
163                 case PROC_DIED: {
164                     String msg = String.format("Process %s has died.", d.processName);
165                     throw new AuptTerminator(msg);
166                 }
167                 case PROC_RESTARTED: {
168                     String msg = String.format("Process %s restarted: %d -> %d", d.processName,
169                             d.pid1, d.pid0);
170                     throw new AuptTerminator(msg);
171                 }
172                 default:
173                     break;
174             }
175         }
176     }
177 
178     /**
179      * Enumerates the list of processes to be tracked and returns a list of RunningAppProcessInfo
180      * objects with pkg name and pid's set to the process's current information
181      * @result a List of RunningAppProcessInfo objects with pkg and pid set
182      */
getRunningAppProcesses()183     public List<RunningAppProcessInfo> getRunningAppProcesses() {
184         List<RunningAppProcessInfo> results = new ArrayList<RunningAppProcessInfo>();
185 
186         Set<String> procSet = mPidTracker.keySet();
187         // Enumerate status for all currently tracked processes
188         for (String proc : procSet) {
189             // Execute shell command and parse results
190             BufferedReader stream = executeShellCommand("ps");
191             try {
192                 String line;
193                 while ((line = stream.readLine()) != null) {
194                     Matcher matcher = PS_PATTERN_MATCH.matcher(line);
195                     // Find a line that matches the process name exactly
196                     if (matcher.matches() && matcher.group(PS_PATTERN_PKG_GROUP).equals(proc)) {
197                         int pid = Integer.valueOf(matcher.group(PS_PATTERN_PID_GROUP));
198                         results.add(new RunningAppProcessInfo(proc, pid, null));
199                     }
200                 }
201             } catch (IOException exception) {
202                 Log.e(TAG, "Error with buffered reader", exception);
203                 return null;
204             } finally {
205                 try {
206                     if (stream != null) {
207                         stream.close();
208                     }
209                 } catch (IOException exception) {
210                     Log.e(TAG, "Error with closing the stream", exception);
211                 }
212             }
213         }
214 
215         return results;
216     }
217 
218     // TODO: Create subclass for shell commands used by this and GraphicsStatsMonitor
219 
220     /**
221      * UiAutomation is included solely for the purpose of executing shell commands
222      */
223     private UiAutomation mUiAutomation;
224 
225     /**
226      * Executes a shell command through UiAutomation and puts the results in an
227      * InputStreamReader that is returned inside a BufferedReader.
228      * @param command the command to be executed in the adb shell
229      * @result a BufferedReader that reads the command output
230      */
executeShellCommand(String command)231     public BufferedReader executeShellCommand (String command) {
232         ParcelFileDescriptor stdout = getUiAutomation().executeShellCommand(command);
233 
234         BufferedReader stream = new BufferedReader(new InputStreamReader(
235                 new ParcelFileDescriptor.AutoCloseInputStream(stdout)));
236         return stream;
237     }
238 
239     /**
240      * Sets the UiAutomation member for shell execution
241      */
setUiAutomation(UiAutomation uiAutomation)242     public void setUiAutomation (UiAutomation uiAutomation) {
243         mUiAutomation = uiAutomation;
244     }
245 
246     /**
247      * @return UiAutomation instance from Aupt instrumentation
248      */
getUiAutomation()249     public UiAutomation getUiAutomation () {
250         return mUiAutomation;
251     }
252 }
253