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