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