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