1 /*
2  * Copyright (C) 2018 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 package com.android.statsd.shelltools;
17 
18 import com.android.os.StatsLog;
19 import com.android.os.StatsLog.ConfigMetricsReportList;
20 import com.android.os.StatsLog.EventMetricData;
21 import com.android.os.StatsLog.StatsLogReport;
22 
23 import com.google.common.io.Files;
24 
25 import java.io.BufferedReader;
26 import java.io.File;
27 import java.io.FileInputStream;
28 import java.io.IOException;
29 import java.io.InputStreamReader;
30 import java.nio.charset.Charset;
31 import java.util.ArrayList;
32 import java.util.Collections;
33 import java.util.Comparator;
34 import java.util.List;
35 import java.util.logging.ConsoleHandler;
36 import java.util.logging.Formatter;
37 import java.util.logging.Level;
38 import java.util.logging.LogRecord;
39 import java.util.logging.Logger;
40 import java.util.regex.Matcher;
41 import java.util.regex.Pattern;
42 
43 import com.android.statsd.shelltools.ExtensionAtomsRegistry;
44 
45 /**
46  * Utilities for local use of statsd.
47  */
48 public class Utils {
49 
50     public static final String CMD_DUMP_REPORT = "cmd stats dump-report";
51     public static final String CMD_LOG_APP_BREADCRUMB = "cmd stats log-app-breadcrumb";
52     public static final String CMD_REMOVE_CONFIG = "cmd stats config remove";
53     public static final String CMD_UPDATE_CONFIG = "cmd stats config update";
54 
55     public static final String SHELL_UID = "2000"; // Use shell, even if rooted.
56 
57     /**
58      * Runs adb shell command with output directed to outputFile if non-null.
59      */
runCommand(File outputFile, Logger logger, String... commands)60     public static void runCommand(File outputFile, Logger logger, String... commands)
61             throws IOException, InterruptedException {
62         ProcessBuilder pb = new ProcessBuilder(commands);
63         if (outputFile != null && outputFile.exists() && outputFile.canWrite()) {
64             pb.redirectOutput(outputFile);
65         }
66         Process process = pb.start();
67 
68         // Capture any errors
69         StringBuilder err = new StringBuilder();
70         BufferedReader br = new BufferedReader(new InputStreamReader(process.getErrorStream()));
71         for (String line = br.readLine(); line != null; line = br.readLine()) {
72             err.append(line).append('\n');
73         }
74         logger.severe(err.toString());
75 
76         final String commandName = commands[0];
77 
78         // Check result
79         if (process.waitFor() == 0) {
80             logger.fine("Command " + commandName + " is successful.");
81         } else {
82             logger.severe(
83                     "Abnormal " + commandName + " termination for: " + String.join(",", commands));
84             throw new RuntimeException("Error running adb command: " + err.toString());
85         }
86     }
87 
88     /**
89      * Dumps the report from the device and converts it to a ConfigMetricsReportList.
90      * Erases the data if clearData is true.
91      *
92      * @param configId    id of the config
93      * @param clearData   whether to erase the report data from statsd after getting the report.
94      * @param useShellUid Pulls data for the {@link SHELL_UID} instead of the caller's uid.
95      * @param logger      Logger to log error messages
96      */
getReportList(long configId, boolean clearData, boolean useShellUid, Logger logger, String deviceSerial)97     public static ConfigMetricsReportList getReportList(long configId, boolean clearData,
98             boolean useShellUid, Logger logger, String deviceSerial)
99             throws IOException, InterruptedException {
100         try {
101             File outputFile = File.createTempFile("statsdret", ".bin");
102             outputFile.deleteOnExit();
103             runCommand(
104                     outputFile,
105                     logger,
106                     "adb",
107                     "-s",
108                     deviceSerial,
109                     "shell",
110                     CMD_DUMP_REPORT,
111                     useShellUid ? SHELL_UID : "",
112                     String.valueOf(configId),
113                     clearData ? "" : "--keep_data",
114                     "--include_current_bucket",
115                     "--proto");
116             ConfigMetricsReportList reportList =
117                     ConfigMetricsReportList.parseFrom(new FileInputStream(outputFile),
118                             ExtensionAtomsRegistry.REGISTRY);
119             return reportList;
120         } catch (com.google.protobuf.InvalidProtocolBufferException e) {
121             logger.severe("Failed to fetch and parse the statsd output report. "
122                     + "Perhaps there is not a valid statsd config for the requested "
123                     + (useShellUid ? ("uid=" + SHELL_UID + ", ") : "")
124                     + "configId=" + configId
125                     + ".");
126             throw (e);
127         }
128     }
129 
130     /**
131      * Logs an AppBreadcrumbReported atom.
132      *
133      * @param label  which label to log for the app breadcrumb atom.
134      * @param state  which state to log for the app breadcrumb atom.
135      * @param logger Logger to log error messages
136      */
logAppBreadcrumb(int label, int state, Logger logger, String deviceSerial)137     public static void logAppBreadcrumb(int label, int state, Logger logger, String deviceSerial)
138             throws IOException, InterruptedException {
139         runCommand(
140                 null,
141                 logger,
142                 "adb",
143                 "-s",
144                 deviceSerial,
145                 "shell",
146                 CMD_LOG_APP_BREADCRUMB,
147                 String.valueOf(label),
148                 String.valueOf(state));
149     }
150 
setUpLogger(Logger logger, boolean debug)151     public static void setUpLogger(Logger logger, boolean debug) {
152         ConsoleHandler handler = new ConsoleHandler();
153         handler.setFormatter(new LocalToolsFormatter());
154         logger.setUseParentHandlers(false);
155         if (debug) {
156             handler.setLevel(Level.ALL);
157             logger.setLevel(Level.ALL);
158         }
159         logger.addHandler(handler);
160     }
161 
162     /**
163      * Attempt to determine whether tool will work with this statsd, i.e. whether statsd is
164      * minCodename or higher.
165      * Algorithm: true if (sdk >= minSdk) || (sdk == minSdk-1 && codeName.startsWith(minCodeName))
166      * If all else fails, assume it will work (letting future commands deal with any errors).
167      */
isAcceptableStatsd(Logger logger, int minSdk, String minCodename, String deviceSerial)168     public static boolean isAcceptableStatsd(Logger logger, int minSdk, String minCodename,
169             String deviceSerial) {
170         BufferedReader in = null;
171         try {
172             File outFileSdk = File.createTempFile("shelltools_sdk", "tmp");
173             outFileSdk.deleteOnExit();
174             runCommand(outFileSdk, logger,
175                     "adb", "-s", deviceSerial, "shell", "getprop", "ro.build.version.sdk");
176             in = new BufferedReader(new InputStreamReader(new FileInputStream(outFileSdk)));
177             // If NullPointerException/NumberFormatException/etc., just catch and return true.
178             int sdk = Integer.parseInt(in.readLine().trim());
179             if (sdk >= minSdk) {
180                 return true;
181             } else if (sdk == minSdk - 1) { // Could be minSdk-1, or could be minSdk development.
182                 in.close();
183                 File outFileCode = File.createTempFile("shelltools_codename", "tmp");
184                 outFileCode.deleteOnExit();
185                 runCommand(outFileCode, logger,
186                         "adb", "-s", deviceSerial, "shell", "getprop", "ro.build.version.codename");
187                 in = new BufferedReader(new InputStreamReader(new FileInputStream(outFileCode)));
188                 return in.readLine().startsWith(minCodename);
189             } else {
190                 return false;
191             }
192         } catch (Exception e) {
193             logger.fine("Could not determine whether statsd version is compatibile "
194                     + "with tool: " + e.toString());
195         } finally {
196             try {
197                 if (in != null) {
198                     in.close();
199                 }
200             } catch (IOException e) {
201                 logger.fine("Could not close temporary file: " + e.toString());
202             }
203         }
204         // Could not determine whether statsd is acceptable version.
205         // Just assume it is; if it isn't, we'll just get future errors via adb and deal with them.
206         return true;
207     }
208 
209     public static class LocalToolsFormatter extends Formatter {
format(LogRecord record)210         public String format(LogRecord record) {
211             return record.getMessage() + "\n";
212         }
213     }
214 
215     /**
216      * Parse the result of "adb devices" to return the list of connected devices.
217      *
218      * @param logger Logger to log error messages
219      * @return List of the serial numbers of the connected devices.
220      */
getDeviceSerials(Logger logger)221     public static List<String> getDeviceSerials(Logger logger) {
222         try {
223             ArrayList<String> devices = new ArrayList<>();
224             File outFile = File.createTempFile("device_serial", "tmp");
225             outFile.deleteOnExit();
226             Utils.runCommand(outFile, logger, "adb", "devices");
227             List<String> outputLines = Files.readLines(outFile, Charset.defaultCharset());
228             Pattern regex = Pattern.compile("^(.*)\tdevice$");
229             for (String line : outputLines) {
230                 Matcher m = regex.matcher(line);
231                 if (m.find()) {
232                     devices.add(m.group(1));
233                 }
234             }
235             return devices;
236         } catch (Exception ex) {
237             logger.log(Level.SEVERE, "Failed to list connected devices: " + ex.getMessage());
238         }
239         return null;
240     }
241 
242     /**
243      * Returns ANDROID_SERIAL environment variable, or null if that is undefined or unavailable.
244      *
245      * @param logger Destination of error messages.
246      * @return String value of ANDROID_SERIAL environment variable, or null.
247      */
getDefaultDevice(Logger logger)248     public static String getDefaultDevice(Logger logger) {
249         try {
250             return System.getenv("ANDROID_SERIAL");
251         } catch (Exception ex) {
252             logger.log(Level.SEVERE, "Failed to check ANDROID_SERIAL environment variable.",
253                     ex);
254         }
255         return null;
256     }
257 
258     /**
259      * Returns the device to use if one can be deduced, or null.
260      *
261      * @param device           Command-line specified device, or null.
262      * @param connectedDevices List of all connected devices.
263      * @param defaultDevice    Environment-variable specified device, or null.
264      * @param logger           Destination of error messages.
265      * @return Device to use, or null.
266      */
chooseDevice(String device, List<String> connectedDevices, String defaultDevice, Logger logger)267     public static String chooseDevice(String device, List<String> connectedDevices,
268             String defaultDevice, Logger logger) {
269         if (connectedDevices == null || connectedDevices.isEmpty()) {
270             logger.severe("No connected device.");
271             return null;
272         }
273         if (device != null) {
274             if (connectedDevices.contains(device)) {
275                 return device;
276             }
277             logger.severe("Device not connected: " + device);
278             return null;
279         }
280         if (connectedDevices.size() == 1) {
281             return connectedDevices.get(0);
282         }
283         if (defaultDevice != null) {
284             if (connectedDevices.contains(defaultDevice)) {
285                 return defaultDevice;
286             } else {
287                 logger.severe("ANDROID_SERIAL device is not connected: " + defaultDevice);
288                 return null;
289             }
290         }
291         logger.severe("More than one device is connected. Choose one"
292                 + " with -s DEVICE_SERIAL or environment variable ANDROID_SERIAL.");
293         return null;
294     }
295 
getEventMetricData(StatsLogReport metric)296     public static List<EventMetricData> getEventMetricData(StatsLogReport metric) {
297         List<EventMetricData> data = new ArrayList<>();
298         for (EventMetricData metricData : metric.getEventMetrics().getDataList()) {
299             if (metricData.hasAtom()) {
300                 data.add(metricData);
301             } else {
302                 data.addAll(backfillAggregatedAtomsInEventMetric(metricData));
303             }
304         }
305         data.sort(Comparator.comparing(EventMetricData::getElapsedTimestampNanos));
306         return data;
307     }
308 
backfillAggregatedAtomsInEventMetric( EventMetricData metricData)309     private static List<EventMetricData> backfillAggregatedAtomsInEventMetric(
310             EventMetricData metricData) {
311         if (!metricData.hasAggregatedAtomInfo()) {
312             return Collections.emptyList();
313         }
314         List<EventMetricData> data = new ArrayList<>();
315         StatsLog.AggregatedAtomInfo atomInfo = metricData.getAggregatedAtomInfo();
316         for (long timestamp : atomInfo.getElapsedTimestampNanosList()) {
317             data.add(EventMetricData.newBuilder()
318                     .setAtom(atomInfo.getAtom())
319                     .setElapsedTimestampNanos(timestamp)
320                     .build());
321         }
322         return data;
323     }
324 }
325