blob: 6a74480b505e8dadb6e1b9eab8431571dfdcbf3d [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.statsd.shelltools;
import com.android.os.StatsLog.ConfigMetricsReportList;
import com.google.common.io.Files;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.ConsoleHandler;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utilities for local use of statsd.
*/
public class Utils {
public static final String CMD_DUMP_REPORT = "cmd stats dump-report";
public static final String CMD_LOG_APP_BREADCRUMB = "cmd stats log-app-breadcrumb";
public static final String CMD_REMOVE_CONFIG = "cmd stats config remove";
public static final String CMD_UPDATE_CONFIG = "cmd stats config update";
public static final String SHELL_UID = "2000"; // Use shell, even if rooted.
/**
* Runs adb shell command with output directed to outputFile if non-null.
*/
public static void runCommand(File outputFile, Logger logger, String... commands)
throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder(commands);
if (outputFile != null && outputFile.exists() && outputFile.canWrite()) {
pb.redirectOutput(outputFile);
}
Process process = pb.start();
// Capture any errors
StringBuilder err = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(process.getErrorStream()));
for (String line = br.readLine(); line != null; line = br.readLine()) {
err.append(line).append('\n');
}
logger.severe(err.toString());
// Check result
if (process.waitFor() == 0) {
logger.fine("Adb command successful.");
} else {
logger.severe("Abnormal adb shell termination for: " + String.join(",", commands));
throw new RuntimeException("Error running adb command: " + err.toString());
}
}
/**
* Dumps the report from the device and converts it to a ConfigMetricsReportList.
* Erases the data if clearData is true.
* @param configId id of the config
* @param clearData whether to erase the report data from statsd after getting the report.
* @param useShellUid Pulls data for the {@link SHELL_UID} instead of the caller's uid.
* @param logger Logger to log error messages
* @return
* @throws IOException
* @throws InterruptedException
*/
public static ConfigMetricsReportList getReportList(long configId, boolean clearData,
boolean useShellUid, Logger logger, String deviceSerial)
throws IOException, InterruptedException {
try {
File outputFile = File.createTempFile("statsdret", ".bin");
outputFile.deleteOnExit();
runCommand(
outputFile,
logger,
"adb",
"-s",
deviceSerial,
"shell",
CMD_DUMP_REPORT,
useShellUid ? SHELL_UID : "",
String.valueOf(configId),
clearData ? "" : "--keep_data",
"--include_current_bucket",
"--proto");
ConfigMetricsReportList reportList =
ConfigMetricsReportList.parseFrom(new FileInputStream(outputFile));
return reportList;
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
logger.severe("Failed to fetch and parse the statsd output report. "
+ "Perhaps there is not a valid statsd config for the requested "
+ (useShellUid ? ("uid=" + SHELL_UID + ", ") : "")
+ "configId=" + configId
+ ".");
throw (e);
}
}
/**
* Logs an AppBreadcrumbReported atom.
* @param label which label to log for the app breadcrumb atom.
* @param state which state to log for the app breadcrumb atom.
* @param logger Logger to log error messages
*
* @throws IOException
* @throws InterruptedException
*/
public static void logAppBreadcrumb(int label, int state, Logger logger, String deviceSerial)
throws IOException, InterruptedException {
runCommand(
null,
logger,
"adb",
"-s",
deviceSerial,
"shell",
CMD_LOG_APP_BREADCRUMB,
String.valueOf(label),
String.valueOf(state));
}
public static void setUpLogger(Logger logger, boolean debug) {
ConsoleHandler handler = new ConsoleHandler();
handler.setFormatter(new LocalToolsFormatter());
logger.setUseParentHandlers(false);
if (debug) {
handler.setLevel(Level.ALL);
logger.setLevel(Level.ALL);
}
logger.addHandler(handler);
}
/**
* Attempt to determine whether tool will work with this statsd, i.e. whether statsd is
* minCodename or higher.
* Algorithm: true if (sdk >= minSdk) || (sdk == minSdk-1 && codeName.startsWith(minCodeName))
* If all else fails, assume it will work (letting future commands deal with any errors).
*/
public static boolean isAcceptableStatsd(Logger logger, int minSdk, String minCodename,
String deviceSerial) {
BufferedReader in = null;
try {
File outFileSdk = File.createTempFile("shelltools_sdk", "tmp");
outFileSdk.deleteOnExit();
runCommand(outFileSdk, logger,
"adb", "-s", deviceSerial, "shell", "getprop", "ro.build.version.sdk");
in = new BufferedReader(new InputStreamReader(new FileInputStream(outFileSdk)));
// If NullPointerException/NumberFormatException/etc., just catch and return true.
int sdk = Integer.parseInt(in.readLine().trim());
if (sdk >= minSdk) {
return true;
} else if (sdk == minSdk - 1) { // Could be minSdk-1, or could be minSdk development.
in.close();
File outFileCode = File.createTempFile("shelltools_codename", "tmp");
outFileCode.deleteOnExit();
runCommand(outFileCode, logger,
"adb", "-s", deviceSerial, "shell", "getprop", "ro.build.version.codename");
in = new BufferedReader(new InputStreamReader(new FileInputStream(outFileCode)));
return in.readLine().startsWith(minCodename);
} else {
return false;
}
} catch (Exception e) {
logger.fine("Could not determine whether statsd version is compatibile "
+ "with tool: " + e.toString());
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
logger.fine("Could not close temporary file: " + e.toString());
}
}
// Could not determine whether statsd is acceptable version.
// Just assume it is; if it isn't, we'll just get future errors via adb and deal with them.
return true;
}
public static class LocalToolsFormatter extends Formatter {
public String format(LogRecord record) {
return record.getMessage() + "\n";
}
}
/**
* Parse the result of "adb devices" to return the list of connected devices.
* @param logger Logger to log error messages
* @return List of the serial numbers of the connected devices.
*/
public static List<String> getDeviceSerials(Logger logger) {
try {
ArrayList<String> devices = new ArrayList<>();
File outFile = File.createTempFile("device_serial", "tmp");
outFile.deleteOnExit();
Utils.runCommand(outFile, logger, "adb", "devices");
List<String> outputLines = Files.readLines(outFile, Charset.defaultCharset());
Pattern regex = Pattern.compile("^(.*)\tdevice$");
for (String line : outputLines) {
Matcher m = regex.matcher(line);
if (m.find()) {
devices.add(m.group(1));
}
}
return devices;
} catch (Exception ex) {
logger.log(Level.SEVERE, "Failed to list connected devices: " + ex.getMessage());
}
return null;
}
/**
* Returns ANDROID_SERIAL environment variable, or null if that is undefined or unavailable.
* @param logger Destination of error messages.
* @return String value of ANDROID_SERIAL environment variable, or null.
*/
public static String getDefaultDevice(Logger logger) {
try {
return System.getenv("ANDROID_SERIAL");
} catch (Exception ex) {
logger.log(Level.SEVERE, "Failed to check ANDROID_SERIAL environment variable.",
ex);
}
return null;
}
/**
* Returns the device to use if one can be deduced, or null.
* @param device Command-line specified device, or null.
* @param connectedDevices List of all connected devices.
* @param defaultDevice Environment-variable specified device, or null.
* @param logger Destination of error messages.
* @return Device to use, or null.
*/
public static String chooseDevice(String device, List<String> connectedDevices,
String defaultDevice, Logger logger) {
if (connectedDevices == null || connectedDevices.isEmpty()) {
logger.severe("No connected device.");
return null;
}
if (device != null) {
if (connectedDevices.contains(device)) {
return device;
}
logger.severe("Device not connected: " + device);
return null;
}
if (connectedDevices.size() == 1) {
return connectedDevices.get(0);
}
if (defaultDevice != null) {
if (connectedDevices.contains(defaultDevice)) {
return defaultDevice;
} else {
logger.severe("ANDROID_SERIAL device is not connected: " + defaultDevice);
return null;
}
}
logger.severe("More than one device is connected. Choose one"
+ " with -s DEVICE_SERIAL or environment variable ANDROID_SERIAL.");
return null;
}
}