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