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 java.io.BufferedReader; 21 import java.io.File; 22 import java.io.FileInputStream; 23 import java.io.IOException; 24 import java.io.InputStreamReader; 25 import java.util.logging.ConsoleHandler; 26 import java.util.logging.Formatter; 27 import java.util.logging.Level; 28 import java.util.logging.LogRecord; 29 import java.util.logging.Logger; 30 31 /** 32 * Utilities for local use of statsd. 33 */ 34 public class Utils { 35 36 public static final String CMD_DUMP_REPORT = "cmd stats dump-report"; 37 public static final String CMD_LOG_APP_BREADCRUMB = "cmd stats log-app-breadcrumb"; 38 public static final String CMD_REMOVE_CONFIG = "cmd stats config remove"; 39 public static final String CMD_UPDATE_CONFIG = "cmd stats config update"; 40 41 public static final String SHELL_UID = "2000"; // Use shell, even if rooted. 42 43 /** 44 * Runs adb shell command with output directed to outputFile if non-null. 45 */ runCommand(File outputFile, Logger logger, String... commands)46 public static void runCommand(File outputFile, Logger logger, String... commands) 47 throws IOException, InterruptedException { 48 ProcessBuilder pb = new ProcessBuilder(commands); 49 if (outputFile != null && outputFile.exists() && outputFile.canWrite()) { 50 pb.redirectOutput(outputFile); 51 } 52 Process process = pb.start(); 53 54 // Capture any errors 55 StringBuilder err = new StringBuilder(); 56 BufferedReader br = new BufferedReader(new InputStreamReader(process.getErrorStream())); 57 for (String line = br.readLine(); line != null; line = br.readLine()) { 58 err.append(line).append('\n'); 59 } 60 logger.severe(err.toString()); 61 62 // Check result 63 if (process.waitFor() == 0) { 64 logger.fine("Adb command successful."); 65 } else { 66 logger.severe("Abnormal adb shell termination for: " + String.join(",", commands)); 67 throw new RuntimeException("Error running adb command: " + err.toString()); 68 } 69 } 70 71 /** 72 * Dumps the report from the device and converts it to a ConfigMetricsReportList. 73 * Erases the data if clearData is true. 74 * @param configId id of the config 75 * @param clearData whether to erase the report data from statsd after getting the report. 76 * @param useShellUid Pulls data for the {@link SHELL_UID} instead of the caller's uid. 77 * @param logger Logger to log error messages 78 * @return 79 * @throws IOException 80 * @throws InterruptedException 81 */ getReportList(long configId, boolean clearData, boolean useShellUid, Logger logger)82 public static ConfigMetricsReportList getReportList(long configId, boolean clearData, 83 boolean useShellUid, Logger logger) throws IOException, InterruptedException { 84 try { 85 File outputFile = File.createTempFile("statsdret", ".bin"); 86 outputFile.deleteOnExit(); 87 runCommand( 88 outputFile, 89 logger, 90 "adb", 91 "shell", 92 CMD_DUMP_REPORT, 93 useShellUid ? SHELL_UID : "", 94 String.valueOf(configId), 95 clearData ? "" : "--keep_data", 96 "--include_current_bucket", 97 "--proto"); 98 ConfigMetricsReportList reportList = 99 ConfigMetricsReportList.parseFrom(new FileInputStream(outputFile)); 100 return reportList; 101 } catch (com.google.protobuf.InvalidProtocolBufferException e) { 102 logger.severe("Failed to fetch and parse the statsd output report. " 103 + "Perhaps there is not a valid statsd config for the requested " 104 + (useShellUid ? ("uid=" + SHELL_UID + ", ") : "") 105 + "configId=" + configId 106 + "."); 107 throw (e); 108 } 109 } 110 111 /** 112 * Logs an AppBreadcrumbReported atom. 113 * @param label which label to log for the app breadcrumb atom. 114 * @param state which state to log for the app breadcrumb atom. 115 * @param logger Logger to log error messages 116 * 117 * @throws IOException 118 * @throws InterruptedException 119 */ logAppBreadcrumb(int label, int state, Logger logger)120 public static void logAppBreadcrumb(int label, int state, Logger logger) 121 throws IOException, InterruptedException { 122 runCommand( 123 null, 124 logger, 125 "adb", 126 "shell", 127 CMD_LOG_APP_BREADCRUMB, 128 String.valueOf(label), 129 String.valueOf(state)); 130 } setUpLogger(Logger logger, boolean debug)131 public static void setUpLogger(Logger logger, boolean debug) { 132 ConsoleHandler handler = new ConsoleHandler(); 133 handler.setFormatter(new LocalToolsFormatter()); 134 logger.setUseParentHandlers(false); 135 if (debug) { 136 handler.setLevel(Level.ALL); 137 logger.setLevel(Level.ALL); 138 } 139 logger.addHandler(handler); 140 } 141 142 /** 143 * Attempt to determine whether tool will work with this statsd, i.e. whether statsd is 144 * minCodename or higher. 145 * Algorithm: true if (sdk >= minSdk) || (sdk == minSdk-1 && codeName.startsWith(minCodeName)) 146 * If all else fails, assume it will work (letting future commands deal with any errors). 147 */ isAcceptableStatsd(Logger logger, int minSdk, String minCodename)148 public static boolean isAcceptableStatsd(Logger logger, int minSdk, String minCodename) { 149 BufferedReader in = null; 150 try { 151 File outFileSdk = File.createTempFile("shelltools_sdk", "tmp"); 152 outFileSdk.deleteOnExit(); 153 runCommand(outFileSdk, logger, 154 "adb", "shell", "getprop", "ro.build.version.sdk"); 155 in = new BufferedReader(new InputStreamReader(new FileInputStream(outFileSdk))); 156 // If NullPointerException/NumberFormatException/etc., just catch and return true. 157 int sdk = Integer.parseInt(in.readLine().trim()); 158 if (sdk >= minSdk) { 159 return true; 160 } else if (sdk == minSdk - 1) { // Could be minSdk-1, or could be minSdk development. 161 in.close(); 162 File outFileCode = File.createTempFile("shelltools_codename", "tmp"); 163 outFileCode.deleteOnExit(); 164 runCommand(outFileCode, logger, 165 "adb", "shell", "getprop", "ro.build.version.codename"); 166 in = new BufferedReader(new InputStreamReader(new FileInputStream(outFileCode))); 167 return in.readLine().startsWith(minCodename); 168 } else { 169 return false; 170 } 171 } catch (Exception e) { 172 logger.fine("Could not determine whether statsd version is compatibile " 173 + "with tool: " + e.toString()); 174 } finally { 175 try { 176 if (in != null) { 177 in.close(); 178 } 179 } catch (IOException e) { 180 logger.fine("Could not close temporary file: " + e.toString()); 181 } 182 } 183 // Could not determine whether statsd is acceptable version. 184 // Just assume it is; if it isn't, we'll just get future errors via adb and deal with them. 185 return true; 186 } 187 188 public static class LocalToolsFormatter extends Formatter { format(LogRecord record)189 public String format(LogRecord record) { 190 return record.getMessage() + "\n"; 191 } 192 } 193 } 194