1 /*
2  * Copyright (C) 2020 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 
17 package com.android.systemui.dump
18 
19 import android.content.Context
20 import android.os.SystemClock
21 import android.os.Trace
22 import com.android.systemui.R
23 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_CRITICAL
24 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_HIGH
25 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL
26 import com.android.systemui.log.LogBuffer
27 import java.io.FileDescriptor
28 import java.io.PrintWriter
29 import javax.inject.Inject
30 
31 /**
32  * Oversees SystemUI's output during bug reports (and dumpsys in general)
33  *
34  * Dump output is split into two sections, CRITICAL and NORMAL. In general, the CRITICAL section
35  * contains all dumpables that were registered to the [DumpManager], while the NORMAL sections
36  * contains all [LogBuffer]s (due to their length).
37  *
38  * The CRITICAL and NORMAL sections can be found within a bug report by searching for
39  * "SERVICE com.android.systemui/.SystemUIService" and
40  * "SERVICE com.android.systemui/.dump.SystemUIAuxiliaryDumpService", respectively.
41  *
42  * Finally, some or all of the dump can be triggered on-demand via adb (see below).
43  *
44  * ```
45  * # For the following, let <invocation> be:
46  * $ adb shell dumpsys activity service com.android.systemui/.SystemUIService
47  *
48  * # To dump specific target(s), specify one or more registered names:
49  * $ <invocation> NotifCollection
50  * $ <invocation> StatusBar FalsingManager BootCompleteCacheImpl
51  *
52  * # Log buffers can be dumped in the same way (and can even be mixed in with other dump targets,
53  * # although it's not clear why one would want such a thing):
54  * $ <invocation> NotifLog
55  * $ <invocation> StatusBar NotifLog BootCompleteCacheImpl
56  *
57  * # If passing -t or --tail, shows only the last N lines of any log buffers:
58  * $ <invocation> NotifLog --tail 100
59  *
60  * # Dump targets are matched using String.endsWith(), so dumpables that register using their
61  * # fully-qualified class name can still be dumped using their short name:
62  * $ <invocation> com.android.keyguard.KeyguardUpdateMonitor
63  * $ <invocation> keyguard.KeyguardUpdateMonitor
64  * $ <invocation> KeyguardUpdateMonitor
65  *
66  * # To dump all dumpables or all buffers:
67  * $ <invocation> dumpables
68  * $ <invocation> buffers
69  *
70  * # Finally, the following will simulate what we dump during the CRITICAL and NORMAL sections of a
71  * # bug report:
72  * $ <invocation> bugreport-critical
73  * $ <invocation> bugreport-normal
74  *
75  * # And if you need to be reminded of this list of commands:
76  * $ <invocation> -h
77  * $ <invocation> --help
78  * ```
79  */
80 class DumpHandler @Inject constructor(
81     private val context: Context,
82     private val dumpManager: DumpManager,
83     private val logBufferEulogizer: LogBufferEulogizer
84 ) {
85     /**
86      * Dump the diagnostics! Behavior can be controlled via [args].
87      */
dumpnull88     fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) {
89         Trace.beginSection("DumpManager#dump()")
90         val start = SystemClock.uptimeMillis()
91 
92         val parsedArgs = try {
93             parseArgs(args)
94         } catch (e: ArgParseException) {
95             pw.println(e.message)
96             return
97         }
98 
99         when (parsedArgs.dumpPriority) {
100             PRIORITY_ARG_CRITICAL -> dumpCritical(fd, pw, parsedArgs)
101             PRIORITY_ARG_NORMAL -> dumpNormal(pw, parsedArgs)
102             else -> dumpParameterized(fd, pw, parsedArgs)
103         }
104 
105         pw.println()
106         pw.println("Dump took ${SystemClock.uptimeMillis() - start}ms")
107         Trace.endSection()
108     }
109 
dumpParameterizednull110     private fun dumpParameterized(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
111         when (args.command) {
112             "bugreport-critical" -> dumpCritical(fd, pw, args)
113             "bugreport-normal" -> dumpNormal(pw, args)
114             "dumpables" -> dumpDumpables(fd, pw, args)
115             "buffers" -> dumpBuffers(pw, args)
116             "config" -> dumpConfig(pw)
117             "help" -> dumpHelp(pw)
118             else -> dumpTargets(args.nonFlagArgs, fd, pw, args)
119         }
120     }
121 
dumpCriticalnull122     private fun dumpCritical(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
123         dumpManager.dumpDumpables(fd, pw, args.rawArgs)
124         dumpConfig(pw)
125     }
126 
dumpNormalnull127     private fun dumpNormal(pw: PrintWriter, args: ParsedArgs) {
128         dumpManager.dumpBuffers(pw, args.tailLength)
129         logBufferEulogizer.readEulogyIfPresent(pw)
130     }
131 
dumpDumpablesnull132     private fun dumpDumpables(fw: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
133         if (args.listOnly) {
134             dumpManager.listDumpables(pw)
135         } else {
136             dumpManager.dumpDumpables(fw, pw, args.rawArgs)
137         }
138     }
139 
dumpBuffersnull140     private fun dumpBuffers(pw: PrintWriter, args: ParsedArgs) {
141         if (args.listOnly) {
142             dumpManager.listBuffers(pw)
143         } else {
144             dumpManager.dumpBuffers(pw, args.tailLength)
145         }
146     }
147 
dumpTargetsnull148     private fun dumpTargets(
149         targets: List<String>,
150         fd: FileDescriptor,
151         pw: PrintWriter,
152         args: ParsedArgs
153     ) {
154         if (targets.isNotEmpty()) {
155             for (target in targets) {
156                 dumpManager.dumpTarget(target, fd, pw, args.rawArgs, args.tailLength)
157             }
158         } else {
159             if (args.listOnly) {
160                 pw.println("Dumpables:")
161                 dumpManager.listDumpables(pw)
162                 pw.println()
163 
164                 pw.println("Buffers:")
165                 dumpManager.listBuffers(pw)
166             } else {
167                 pw.println("Nothing to dump :(")
168             }
169         }
170     }
171 
dumpConfignull172     private fun dumpConfig(pw: PrintWriter) {
173         pw.println("SystemUiServiceComponents configuration:")
174         pw.print("vendor component: ")
175         pw.println(context.resources.getString(R.string.config_systemUIVendorServiceComponent))
176         dumpServiceList(pw, "global", R.array.config_systemUIServiceComponents)
177         dumpServiceList(pw, "per-user", R.array.config_systemUIServiceComponentsPerUser)
178     }
179 
dumpServiceListnull180     private fun dumpServiceList(pw: PrintWriter, type: String, resId: Int) {
181         val services: Array<String>? = context.resources.getStringArray(resId)
182         pw.print(type)
183         pw.print(": ")
184         if (services == null) {
185             pw.println("N/A")
186             return
187         }
188         pw.print(services.size)
189         pw.println(" services")
190         for (i in services.indices) {
191             pw.print("  ")
192             pw.print(i)
193             pw.print(": ")
194             pw.println(services[i])
195         }
196     }
197 
dumpHelpnull198     private fun dumpHelp(pw: PrintWriter) {
199         pw.println("Let <invocation> be:")
200         pw.println("$ adb shell dumpsys activity service com.android.systemui/.SystemUIService")
201         pw.println()
202 
203         pw.println("Most common usage:")
204         pw.println("$ <invocation> <targets>")
205         pw.println("$ <invocation> NotifLog")
206         pw.println("$ <invocation> StatusBar FalsingManager BootCompleteCacheImpl")
207         pw.println("etc.")
208         pw.println()
209 
210         pw.println("Special commands:")
211         pw.println("$ <invocation> dumpables")
212         pw.println("$ <invocation> buffers")
213         pw.println("$ <invocation> bugreport-critical")
214         pw.println("$ <invocation> bugreport-normal")
215         pw.println()
216 
217         pw.println("Targets can be listed:")
218         pw.println("$ <invocation> --list")
219         pw.println("$ <invocation> dumpables --list")
220         pw.println("$ <invocation> buffers --list")
221         pw.println()
222 
223         pw.println("Show only the most recent N lines of buffers")
224         pw.println("$ <invocation> NotifLog --tail 30")
225     }
226 
parseArgsnull227     private fun parseArgs(args: Array<String>): ParsedArgs {
228         val mutArgs = args.toMutableList()
229         val pArgs = ParsedArgs(args, mutArgs)
230 
231         val iterator = mutArgs.iterator()
232         while (iterator.hasNext()) {
233             val arg = iterator.next()
234             if (arg.startsWith("-")) {
235                 iterator.remove()
236                 when (arg) {
237                     PRIORITY_ARG -> {
238                         pArgs.dumpPriority = readArgument(iterator, PRIORITY_ARG) {
239                             if (PRIORITY_OPTIONS.contains(it)) {
240                                 it
241                             } else {
242                                 throw IllegalArgumentException()
243                             }
244                         }
245                     }
246                     "-t", "--tail" -> {
247                         pArgs.tailLength = readArgument(iterator, arg) {
248                             it.toInt()
249                         }
250                     }
251                     "-l", "--list" -> {
252                         pArgs.listOnly = true
253                     }
254                     "-h", "--help" -> {
255                         pArgs.command = "help"
256                     }
257                     else -> {
258                         throw ArgParseException("Unknown flag: $arg")
259                     }
260                 }
261             }
262         }
263 
264         if (pArgs.command == null && mutArgs.isNotEmpty() && COMMANDS.contains(mutArgs[0])) {
265             pArgs.command = mutArgs.removeAt(0)
266         }
267 
268         return pArgs
269     }
270 
readArgumentnull271     private fun <T> readArgument(
272         iterator: MutableIterator<String>,
273         flag: String,
274         parser: (arg: String) -> T
275     ): T {
276         if (!iterator.hasNext()) {
277             throw ArgParseException("Missing argument for $flag")
278         }
279         val value = iterator.next()
280 
281         return try {
282             parser(value).also { iterator.remove() }
283         } catch (e: Exception) {
284             throw ArgParseException("Invalid argument '$value' for flag $flag")
285         }
286     }
287 
288     companion object {
289         const val PRIORITY_ARG = "--dump-priority"
290         const val PRIORITY_ARG_CRITICAL = "CRITICAL"
291         const val PRIORITY_ARG_HIGH = "HIGH"
292         const val PRIORITY_ARG_NORMAL = "NORMAL"
293     }
294 }
295 
296 private val PRIORITY_OPTIONS =
297         arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_HIGH, PRIORITY_ARG_NORMAL)
298 
299 private val COMMANDS = arrayOf("bugreport-critical", "bugreport-normal", "buffers", "dumpables")
300 
301 private class ParsedArgs(
302     val rawArgs: Array<String>,
303     val nonFlagArgs: List<String>
304 ) {
305     var dumpPriority: String? = null
306     var tailLength: Int = 0
307     var command: String? = null
308     var listOnly = false
309 }
310 
311 class ArgParseException(message: String) : Exception(message)
312