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