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.localdrive; 17 18 import com.android.internal.os.StatsdConfigProto.StatsdConfig; 19 import com.android.os.StatsLog.ConfigMetricsReport; 20 import com.android.os.StatsLog.ConfigMetricsReportList; 21 import com.android.statsd.shelltools.Utils; 22 23 import com.google.common.io.Files; 24 import com.google.protobuf.TextFormat; 25 26 import java.io.File; 27 import java.io.FileReader; 28 import java.io.IOException; 29 import java.util.List; 30 import java.util.logging.Logger; 31 32 /** 33 * Tool for using statsd locally. Can upload a config and get the data. Handles 34 * both binary and human-readable protos. 35 * To make: make statsd_localdrive 36 * To run: statsd_localdrive (i.e. ./out/host/linux-x86/bin/statsd_localdrive) 37 */ 38 public class LocalDrive { 39 private static final boolean DEBUG = false; 40 41 public static final int MIN_SDK = 29; 42 public static final String MIN_CODENAME = "Q"; 43 44 public static final long DEFAULT_CONFIG_ID = 56789; 45 46 public static final String BINARY_FLAG = "--binary"; 47 public static final String CLEAR_DATA = "--clear"; 48 public static final String NO_UID_MAP_FLAG = "--no-uid-map"; 49 50 public static final String HELP_STRING = 51 "Usage:\n\n" + 52 53 "statsd_localdrive [-s DEVICE_SERIAL] upload CONFIG_FILE [CONFIG_ID] [--binary]\n" + 54 " Uploads the given statsd config file (in binary or human-readable-text format).\n" + 55 " If a config with this id already exists, removes it first.\n" + 56 " CONFIG_FILE Location of config file on host.\n" + 57 " CONFIG_ID Long ID to associate with this config. If absent, uses " 58 + DEFAULT_CONFIG_ID + ".\n" + 59 " --binary Config is in binary format; otherwise, assumed human-readable text.\n" + 60 // Similar to: adb shell cmd stats config update SHELL_UID CONFIG_ID 61 "\n" + 62 63 "statsd_localdrive [-s DEVICE_SERIAL] update CONFIG_FILE [CONFIG_ID] [--binary]\n" + 64 " Same as upload, but does not remove the old config first (if it already exists).\n" + 65 // Similar to: adb shell cmd stats config update SHELL_UID CONFIG_ID 66 "\n" + 67 68 "statsd_localdrive [-s DEVICE_SERIAL] get-data [CONFIG_ID] [--clear] [--binary] [--no-uid-map]\n" + 69 " Prints the output statslog data (in binary or human-readable-text format).\n" + 70 " CONFIG_ID Long ID of the config. If absent, uses " + DEFAULT_CONFIG_ID + ".\n" + 71 " --binary Output should be in binary, instead of default human-readable text.\n" + 72 " Binary output can be redirected as usual (e.g. > FILENAME).\n" + 73 " --no-uid-map Do not include the uid-map (the very lengthy uid<-->pkgName map).\n" + 74 " --clear Erase the data from statsd afterwards. Does not remove the config.\n" + 75 // Similar to: adb shell cmd stats dump-report SHELL_UID CONFIG_ID [--keep_data] 76 // --include_current_bucket --proto 77 "\n" + 78 79 "statsd_localdrive [-s DEVICE_SERIAL] remove [CONFIG_ID]\n" + 80 " Removes the config.\n" + 81 " CONFIG_ID Long ID of the config. If absent, uses " + DEFAULT_CONFIG_ID + ".\n" + 82 // Equivalent to: adb shell cmd stats config remove SHELL_UID CONFIG_ID 83 "\n" + 84 85 "statsd_localdrive [-s DEVICE_SERIAL] clear [CONFIG_ID]\n" + 86 " Clears the data associated with the config.\n" + 87 " CONFIG_ID Long ID of the config. If absent, uses " + DEFAULT_CONFIG_ID + ".\n" + 88 // Similar to: adb shell cmd stats dump-report SHELL_UID CONFIG_ID 89 // --include_current_bucket --proto 90 ""; 91 92 93 private static final Logger sLogger = Logger.getLogger(LocalDrive.class.getName()); 94 95 /** Usage: make statsd_localdrive && statsd_localdrive */ main(String[] args)96 public static void main(String[] args) { 97 Utils.setUpLogger(sLogger, DEBUG); 98 if (args.length == 0) { 99 printHelp(); 100 return; 101 } 102 103 int remainingArgsLength = args.length; 104 String deviceSerial = null; 105 if (args[0].equals("-s")) { 106 if (args.length == 1) { 107 printHelp(); 108 } 109 deviceSerial = args[1]; 110 remainingArgsLength -= 2; 111 } 112 113 List<String> connectedDevices = Utils.getDeviceSerials(sLogger); 114 deviceSerial = Utils.chooseDevice(deviceSerial, connectedDevices, 115 Utils.getDefaultDevice(sLogger), sLogger); 116 if (deviceSerial == null) { 117 return; 118 } 119 120 if (!Utils.isAcceptableStatsd(sLogger, MIN_SDK, MIN_CODENAME, deviceSerial)) { 121 sLogger.severe("LocalDrive only works with statsd versions for Android " 122 + MIN_CODENAME + " or higher."); 123 return; 124 } 125 126 int idx = args.length - remainingArgsLength; 127 if (remainingArgsLength > 0) { 128 switch (args[idx]) { 129 case "clear": 130 cmdClear(args, idx, deviceSerial); 131 return; 132 case "get-data": 133 cmdGetData(args, idx, deviceSerial); 134 return; 135 case "remove": 136 cmdRemove(args, idx); 137 return; 138 case "update": 139 cmdUpdate(args, idx, deviceSerial); 140 return; 141 case "upload": 142 cmdUpload(args, idx, deviceSerial); 143 return; 144 } 145 } 146 printHelp(); 147 } 148 printHelp()149 private static void printHelp() { 150 sLogger.info(HELP_STRING); 151 } 152 153 // upload CONFIG_FILE [CONFIG_ID] [--binary] cmdUpload(String[] args, int idx, String deviceSerial)154 private static boolean cmdUpload(String[] args, int idx, String deviceSerial) { 155 return updateConfig(args, idx, true, deviceSerial); 156 } 157 158 // update CONFIG_FILE [CONFIG_ID] [--binary] cmdUpdate(String[] args, int idx, String deviceSerial)159 private static boolean cmdUpdate(String[] args, int idx, String deviceSerial) { 160 return updateConfig(args, idx, false, deviceSerial); 161 } 162 updateConfig(String[] args, int idx, boolean removeOldConfig, String deviceSerial)163 private static boolean updateConfig(String[] args, int idx, boolean removeOldConfig, 164 String deviceSerial) { 165 int argCount = args.length - 1 - idx; // Used up one for upload/update. 166 167 // Get CONFIG_FILE 168 if (argCount < 1) { 169 sLogger.severe("No config file provided."); 170 printHelp(); 171 return false; 172 } 173 final String origConfigLocation = args[idx + 1]; 174 if (!new File(origConfigLocation).exists()) { 175 sLogger.severe("Error - Cannot find the provided config file: " + origConfigLocation); 176 return false; 177 } 178 argCount--; 179 180 // Get --binary 181 boolean binary = contains(args, idx + 2, BINARY_FLAG); 182 if (binary) argCount --; 183 184 // Get CONFIG_ID 185 long configId; 186 try { 187 configId = getConfigId(argCount < 1, args, idx + 2); 188 } catch (NumberFormatException e) { 189 sLogger.severe("Invalid config id provided."); 190 printHelp(); 191 return false; 192 } 193 sLogger.fine(String.format("updateConfig with %s %d %b %b", 194 origConfigLocation, configId, binary, removeOldConfig)); 195 196 // Remove the old config. 197 if (removeOldConfig) { 198 try { 199 Utils.runCommand(null, sLogger, "adb", "shell", Utils.CMD_REMOVE_CONFIG, 200 Utils.SHELL_UID, String.valueOf(configId)); 201 Utils.getReportList(configId, true /* clearData */, true /* SHELL_UID */, sLogger, 202 deviceSerial); 203 } catch (InterruptedException | IOException e) { 204 sLogger.severe("Failed to remove config: " + e.getMessage()); 205 return false; 206 } 207 } 208 209 // Upload the config. 210 String configLocation; 211 if (binary) { 212 configLocation = origConfigLocation; 213 } else { 214 StatsdConfig.Builder builder = StatsdConfig.newBuilder(); 215 try { 216 TextFormat.merge(new FileReader(origConfigLocation), builder); 217 } catch (IOException e) { 218 sLogger.severe("Failed to read config file " + origConfigLocation + ": " 219 + e.getMessage()); 220 return false; 221 } 222 223 try { 224 File tempConfigFile = File.createTempFile("statsdconfig", ".config"); 225 tempConfigFile.deleteOnExit(); 226 Files.write(builder.build().toByteArray(), tempConfigFile); 227 configLocation = tempConfigFile.getAbsolutePath(); 228 } catch (IOException e) { 229 sLogger.severe("Failed to write temp config file: " + e.getMessage()); 230 return false; 231 } 232 } 233 String remotePath = "/data/local/tmp/statsdconfig.config"; 234 try { 235 Utils.runCommand(null, sLogger, "adb", "push", configLocation, remotePath); 236 Utils.runCommand(null, sLogger, "adb", "shell", "cat", remotePath, "|", 237 Utils.CMD_UPDATE_CONFIG, Utils.SHELL_UID, String.valueOf(configId)); 238 } catch (InterruptedException | IOException e) { 239 sLogger.severe("Failed to update config: " + e.getMessage()); 240 return false; 241 } 242 return true; 243 } 244 245 // get-data [CONFIG_ID] [--clear] [--binary] [--no-uid-map] cmdGetData(String[] args, int idx, String deviceSerial)246 private static boolean cmdGetData(String[] args, int idx, String deviceSerial) { 247 boolean binary = contains(args, idx + 1, BINARY_FLAG); 248 boolean noUidMap = contains(args, idx + 1, NO_UID_MAP_FLAG); 249 boolean clearData = contains(args, idx + 1, CLEAR_DATA); 250 251 // Get CONFIG_ID 252 int argCount = args.length - 1 - idx; // Used up one for get-data. 253 if (binary) argCount--; 254 if (noUidMap) argCount--; 255 if (clearData) argCount--; 256 long configId; 257 try { 258 configId = getConfigId(argCount < 1, args, idx + 1); 259 } catch (NumberFormatException e) { 260 sLogger.severe("Invalid config id provided."); 261 printHelp(); 262 return false; 263 } 264 sLogger.fine(String.format("cmdGetData with %d %b %b %b", 265 configId, clearData, binary, noUidMap)); 266 267 // Get the StatsLog 268 // Even if the args request no modifications, we still parse it to make sure it's valid. 269 ConfigMetricsReportList reportList; 270 try { 271 reportList = Utils.getReportList(configId, clearData, true /* SHELL_UID */, sLogger, 272 deviceSerial); 273 } catch (IOException | InterruptedException e) { 274 sLogger.severe("Failed to get report list: " + e.getMessage()); 275 return false; 276 } 277 if (noUidMap) { 278 ConfigMetricsReportList.Builder builder 279 = ConfigMetricsReportList.newBuilder(reportList); 280 // Clear the reports, then add them back without their UidMap. 281 builder.clearReports(); 282 for (ConfigMetricsReport report : reportList.getReportsList()) { 283 builder.addReports(ConfigMetricsReport.newBuilder(report).clearUidMap()); 284 } 285 reportList = builder.build(); 286 } 287 288 if (!binary) { 289 sLogger.info(reportList.toString()); 290 } else { 291 try { 292 System.out.write(reportList.toByteArray()); 293 } catch (IOException e) { 294 sLogger.severe("Failed to output binary statslog proto: " 295 + e.getMessage()); 296 return false; 297 } 298 } 299 return true; 300 } 301 302 // clear [CONFIG_ID] cmdClear(String[] args, int idx, String deviceSerial)303 private static boolean cmdClear(String[] args, int idx, String deviceSerial) { 304 // Get CONFIG_ID 305 long configId; 306 try { 307 configId = getConfigId(false, args, idx + 1); 308 } catch (NumberFormatException e) { 309 sLogger.severe("Invalid config id provided."); 310 printHelp(); 311 return false; 312 } 313 sLogger.fine(String.format("cmdClear with %d", configId)); 314 315 try { 316 Utils.getReportList(configId, true /* clearData */, true /* SHELL_UID */, sLogger, 317 deviceSerial); 318 } catch (IOException | InterruptedException e) { 319 sLogger.severe("Failed to get report list: " + e.getMessage()); 320 return false; 321 } 322 return true; 323 } 324 325 // remove [CONFIG_ID] cmdRemove(String[] args, int idx)326 private static boolean cmdRemove(String[] args, int idx) { 327 // Get CONFIG_ID 328 long configId; 329 try { 330 configId = getConfigId(false, args, idx + 1); 331 } catch (NumberFormatException e) { 332 sLogger.severe("Invalid config id provided."); 333 printHelp(); 334 return false; 335 } 336 sLogger.fine(String.format("cmdRemove with %d", configId)); 337 338 try { 339 Utils.runCommand(null, sLogger, "adb", "shell", Utils.CMD_REMOVE_CONFIG, 340 Utils.SHELL_UID, String.valueOf(configId)); 341 } catch (InterruptedException | IOException e) { 342 sLogger.severe("Failed to remove config: " + e.getMessage()); 343 return false; 344 } 345 return true; 346 } 347 348 /** 349 * Searches through the array to see if it contains (precisely) the given value, starting 350 * at the given firstIdx. 351 */ contains(String[] array, int firstIdx, String value)352 private static boolean contains(String[] array, int firstIdx, String value) { 353 if (value == null) return false; 354 if (firstIdx < 0) return false; 355 for (int i = firstIdx; i < array.length; i++) { 356 if (value.equals(array[i])) { 357 return true; 358 } 359 } 360 return false; 361 } 362 363 /** 364 * Gets the config id from args[idx], or returns DEFAULT_CONFIG_ID if args[idx] does not exist. 365 * If justUseDefault, overrides and just uses DEFAULT_CONFIG_ID instead. 366 */ getConfigId(boolean justUseDefault, String[] args, int idx)367 private static long getConfigId(boolean justUseDefault, String[] args, int idx) 368 throws NumberFormatException { 369 if (justUseDefault || args.length <= idx || idx < 0) { 370 return DEFAULT_CONFIG_ID; 371 } 372 try { 373 return Long.valueOf(args[idx]); 374 } catch (NumberFormatException e) { 375 sLogger.severe("Bad config id provided: " + args[idx]); 376 throw e; 377 } 378 } 379 } 380