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