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