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