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