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.icu.text.SimpleDateFormat
20 import android.os.SystemClock
21 import android.os.Trace
22 import com.android.systemui.ProtoDumpable
23 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_CRITICAL
24 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL
25 import com.android.systemui.dump.DumpsysEntry.DumpableEntry
26 import com.android.systemui.dump.DumpsysEntry.LogBufferEntry
27 import com.android.systemui.dump.DumpsysEntry.TableLogBufferEntry
28 import com.android.systemui.dump.nano.SystemUIProtoDump
29 import com.android.systemui.log.LogBuffer
30 import com.android.systemui.log.table.TableLogBuffer
31 import com.google.protobuf.nano.MessageNano
32 import java.io.BufferedOutputStream
33 import java.io.FileDescriptor
34 import java.io.FileOutputStream
35 import java.io.PrintWriter
36 import java.util.Locale
37 import javax.inject.Inject
38 import kotlin.system.measureTimeMillis
39 
40 /**
41  * Oversees SystemUI's output during bug reports (and dumpsys in general)
42  *
43  * Dump output is split into two sections, CRITICAL and NORMAL. In general, the CRITICAL section
44  * contains all dumpables that were registered to the [DumpManager], while the NORMAL sections
45  * contains all [LogBuffer]s and [TableLogBuffer]s (due to their length).
46  *
47  * The CRITICAL and NORMAL sections can be found within a bug report by searching for "SERVICE
48  * com.android.systemui/.SystemUIService" and "SERVICE
49  * com.android.systemui/.dump.SystemUIAuxiliaryDumpService", respectively.
50  *
51  * Finally, some or all of the dump can be triggered on-demand via adb (see below).
52  *
53  * ```
54  * # For the following, let <invocation> be:
55  * $ adb shell dumpsys activity service com.android.systemui/.SystemUIService
56  *
57  * # To dump specific target(s), specify one or more registered names:
58  * $ <invocation> NotifCollection
59  * $ <invocation> StatusBar FalsingManager BootCompleteCacheImpl
60  *
61  * # Log buffers can be dumped in the same way (and can even be mixed in with other dump targets,
62  * # although it's not clear why one would want such a thing):
63  * $ <invocation> NotifLog
64  * $ <invocation> StatusBar NotifLog BootCompleteCacheImpl
65  *
66  * # If passing -t or --tail, shows only the last N lines of any log buffers:
67  * $ <invocation> NotifLog --tail 100
68  *
69  * # Dump targets are matched using String.endsWith(), so dumpables that register using their
70  * # fully-qualified class name can still be dumped using their short name:
71  * $ <invocation> com.android.keyguard.KeyguardUpdateMonitor
72  * $ <invocation> keyguard.KeyguardUpdateMonitor
73  * $ <invocation> KeyguardUpdateMonitor
74  *
75  * # To dump all dumpables or all buffers:
76  * $ <invocation> dumpables
77  * $ <invocation> buffers
78  * $ <invocation> tables
79  * $ <invocation> all
80  *
81  * # Finally, the following will simulate what we dump during the CRITICAL and NORMAL sections of a
82  * # bug report:
83  * $ <invocation> bugreport-critical
84  * $ <invocation> bugreport-normal
85  *
86  * # And if you need to be reminded of this list of commands:
87  * $ <invocation> -h
88  * $ <invocation> --help
89  * ```
90  */
91 class DumpHandler
92 @Inject
93 constructor(
94     private val dumpManager: DumpManager,
95     private val logBufferEulogizer: LogBufferEulogizer,
96     private val config: SystemUIConfigDumpable,
97 ) {
98     /** Dump the diagnostics! Behavior can be controlled via [args]. */
99     fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) {
100         Trace.beginSection("DumpManager#dump()")
101         val start = SystemClock.uptimeMillis()
102 
103         val parsedArgs =
104             try {
105                 parseArgs(args)
106             } catch (e: ArgParseException) {
107                 pw.println(e.message)
108                 return
109             }
110 
111         pw.print("Dump starting: ")
112         pw.println(DATE_FORMAT.format(System.currentTimeMillis()))
113         when {
114             parsedArgs.dumpPriority == PRIORITY_ARG_CRITICAL -> dumpCritical(pw, parsedArgs)
115             parsedArgs.dumpPriority == PRIORITY_ARG_NORMAL && !parsedArgs.proto -> {
116                 dumpNormal(pw, parsedArgs)
117             }
118             else -> dumpParameterized(fd, pw, parsedArgs)
119         }
120 
121         pw.println()
122         pw.println("Dump took ${SystemClock.uptimeMillis() - start}ms")
123         Trace.endSection()
124     }
125 
126     private fun dumpParameterized(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
127         when (args.command) {
128             "bugreport-critical" -> dumpCritical(pw, args)
129             "bugreport-normal" -> dumpNormal(pw, args)
130             "dumpables" -> dumpDumpables(pw, args)
131             "buffers" -> dumpBuffers(pw, args)
132             "tables" -> dumpTables(pw, args)
133             "all" -> {
134                 dumpDumpables(pw, args)
135                 dumpBuffers(pw, args)
136                 dumpTables(pw, args)
137             }
138             "config" -> dumpConfig(pw)
139             "help" -> dumpHelp(pw)
140             else -> {
141                 if (args.proto) {
142                     dumpProtoTargets(args.nonFlagArgs, fd, args)
143                 } else {
144                     dumpTargets(args.nonFlagArgs, pw, args)
145                 }
146             }
147         }
148     }
149 
150     private fun dumpCritical(pw: PrintWriter, args: ParsedArgs) {
151         val targets = dumpManager.getDumpables()
152         for (target in targets) {
153             if (target.priority == DumpPriority.CRITICAL) {
154                 dumpDumpable(target, pw, args.rawArgs)
155             }
156         }
157     }
158 
159     private fun dumpNormal(pw: PrintWriter, args: ParsedArgs) {
160         val targets = dumpManager.getDumpables()
161         for (target in targets) {
162             if (target.priority == DumpPriority.NORMAL) {
163                 dumpDumpable(target, pw, args.rawArgs)
164             }
165         }
166 
167         val buffers = dumpManager.getLogBuffers()
168         for (buffer in buffers) {
169             dumpBuffer(buffer, pw, args.tailLength)
170         }
171 
172         val tableBuffers = dumpManager.getTableLogBuffers()
173         for (table in tableBuffers) {
174             dumpTableBuffer(table, pw, args.rawArgs)
175         }
176 
177         logBufferEulogizer.readEulogyIfPresent(pw)
178     }
179 
180     private fun dumpDumpables(pw: PrintWriter, args: ParsedArgs) =
181         dumpManager.getDumpables().listOrDumpEntries(pw, args)
182 
183     private fun dumpBuffers(pw: PrintWriter, args: ParsedArgs) =
184         dumpManager.getLogBuffers().listOrDumpEntries(pw, args)
185 
186     private fun dumpTables(pw: PrintWriter, args: ParsedArgs) =
187         dumpManager.getTableLogBuffers().listOrDumpEntries(pw, args)
188 
189     private fun listTargetNames(targets: Collection<DumpsysEntry>, pw: PrintWriter) {
190         for (target in targets) {
191             pw.println(target.name)
192         }
193     }
194 
195     private fun dumpProtoTargets(targets: List<String>, fd: FileDescriptor, args: ParsedArgs) {
196         val systemUIProto = SystemUIProtoDump()
197         val dumpables = dumpManager.getDumpables()
198         if (targets.isNotEmpty()) {
199             for (target in targets) {
200                 findBestProtoTargetMatch(dumpables, target)?.dumpProto(systemUIProto, args.rawArgs)
201             }
202         } else {
203             // Dump all protos
204             for (dumpable in dumpables) {
205                 (dumpable.dumpable as? ProtoDumpable)?.dumpProto(systemUIProto, args.rawArgs)
206             }
207         }
208 
209         val buffer = BufferedOutputStream(FileOutputStream(fd))
210         buffer.use {
211             it.write(MessageNano.toByteArray(systemUIProto))
212             it.flush()
213         }
214     }
215 
216     // Attempts to dump the target list to the given PrintWriter. Since the arguments come in as
217     // a list of strings, we use the [findBestTargetMatch] method to determine the most-correct
218     // target with the given search string.
219     private fun dumpTargets(targets: List<String>, pw: PrintWriter, args: ParsedArgs) {
220         if (targets.isNotEmpty()) {
221             val dumpables = dumpManager.getDumpables()
222             val buffers = dumpManager.getLogBuffers()
223             val tableBuffers = dumpManager.getTableLogBuffers()
224 
225             targets.forEach { target ->
226                 findTargetInCollection(target, dumpables, buffers, tableBuffers)?.dump(pw, args)
227             }
228         } else {
229             if (args.listOnly) {
230                 val dumpables = dumpManager.getDumpables()
231                 val buffers = dumpManager.getLogBuffers()
232 
233                 pw.println("Dumpables:")
234                 listTargetNames(dumpables, pw)
235                 pw.println()
236 
237                 pw.println("Buffers:")
238                 listTargetNames(buffers, pw)
239             } else {
240                 pw.println("Nothing to dump :(")
241             }
242         }
243     }
244 
245     private fun findTargetInCollection(
246         target: String,
247         dumpables: Collection<DumpableEntry>,
248         logBuffers: Collection<LogBufferEntry>,
249         tableBuffers: Collection<TableLogBufferEntry>,
250     ) =
251         sequence {
252                 findBestTargetMatch(dumpables, target)?.let { yield(it) }
253                 findBestTargetMatch(logBuffers, target)?.let { yield(it) }
254                 findBestTargetMatch(tableBuffers, target)?.let { yield(it) }
255             }
256             .sortedBy { it.name }
257             .minByOrNull { it.name.length }
258 
259     private fun dumpConfig(pw: PrintWriter) {
260         config.dump(pw, arrayOf())
261     }
262 
263     private fun dumpHelp(pw: PrintWriter) {
264         pw.println("Let <invocation> be:")
265         pw.println("$ adb shell dumpsys activity service com.android.systemui/.SystemUIService")
266         pw.println()
267 
268         pw.println("Most common usage:")
269         pw.println("$ <invocation> <targets>")
270         pw.println("$ <invocation> NotifLog")
271         pw.println("$ <invocation> StatusBar FalsingManager BootCompleteCacheImpl")
272         pw.println("etc.")
273         pw.println()
274 
275         pw.println("Special commands:")
276         pw.println("$ <invocation> dumpables")
277         pw.println("$ <invocation> buffers")
278         pw.println("$ <invocation> tables")
279         pw.println("$ <invocation> bugreport-critical")
280         pw.println("$ <invocation> bugreport-normal")
281         pw.println("$ <invocation> config")
282         pw.println()
283 
284         pw.println("Targets can be listed:")
285         pw.println("$ <invocation> --list")
286         pw.println("$ <invocation> dumpables --list")
287         pw.println("$ <invocation> buffers --list")
288         pw.println("$ <invocation> tables --list")
289         pw.println()
290 
291         pw.println("Show only the most recent N lines of buffers")
292         pw.println("$ <invocation> NotifLog --tail 30")
293     }
294 
295     private fun parseArgs(args: Array<String>): ParsedArgs {
296         val mutArgs = args.toMutableList()
297         val pArgs = ParsedArgs(args, mutArgs)
298 
299         val iterator = mutArgs.iterator()
300         while (iterator.hasNext()) {
301             val arg = iterator.next()
302             if (arg.startsWith("-")) {
303                 iterator.remove()
304                 when (arg) {
305                     PRIORITY_ARG -> {
306                         pArgs.dumpPriority =
307                             readArgument(iterator, PRIORITY_ARG) {
308                                 if (PRIORITY_OPTIONS.contains(it)) {
309                                     it
310                                 } else {
311                                     throw IllegalArgumentException()
312                                 }
313                             }
314                     }
315                     PROTO -> pArgs.proto = true
316                     "-t",
317                     "--tail" -> {
318                         pArgs.tailLength = readArgument(iterator, arg) { it.toInt() }
319                     }
320                     "-l",
321                     "--list" -> {
322                         pArgs.listOnly = true
323                     }
324                     "-h",
325                     "--help" -> {
326                         pArgs.command = "help"
327                     }
328                     // This flag is passed as part of the proto dump in Bug reports, we can ignore
329                     // it because this is our default behavior.
330                     "-a" -> {}
331                     else -> {
332                         throw ArgParseException("Unknown flag: $arg")
333                     }
334                 }
335             }
336         }
337 
338         if (pArgs.command == null && mutArgs.isNotEmpty() && COMMANDS.contains(mutArgs[0])) {
339             pArgs.command = mutArgs.removeAt(0)
340         }
341 
342         return pArgs
343     }
344 
345     private fun <T> readArgument(
346         iterator: MutableIterator<String>,
347         flag: String,
348         parser: (arg: String) -> T
349     ): T {
350         if (!iterator.hasNext()) {
351             throw ArgParseException("Missing argument for $flag")
352         }
353         val value = iterator.next()
354 
355         return try {
356             parser(value).also { iterator.remove() }
357         } catch (e: Exception) {
358             throw ArgParseException("Invalid argument '$value' for flag $flag")
359         }
360     }
361 
362     private fun DumpsysEntry.dump(pw: PrintWriter, args: ParsedArgs) =
363         when (this) {
364             is DumpableEntry -> dumpDumpable(this, pw, args.rawArgs)
365             is LogBufferEntry -> dumpBuffer(this, pw, args.tailLength)
366             is TableLogBufferEntry -> dumpTableBuffer(this, pw, args.rawArgs)
367         }
368 
369     private fun Collection<DumpsysEntry>.listOrDumpEntries(pw: PrintWriter, args: ParsedArgs) =
370         if (args.listOnly) {
371             listTargetNames(this, pw)
372         } else {
373             forEach { it.dump(pw, args) }
374         }
375 
376     companion object {
377         const val PRIORITY_ARG = "--dump-priority"
378         const val PRIORITY_ARG_CRITICAL = "CRITICAL"
379         const val PRIORITY_ARG_NORMAL = "NORMAL"
380         const val PROTO = "--proto"
381 
382         /**
383          * Important: do not change this divider without updating any bug report processing tools
384          * (e.g. ABT), since this divider is used to determine boundaries for bug report views
385          */
386         const val DUMPSYS_DUMPABLE_DIVIDER =
387             "----------------------------------------------------------------------------"
388 
389         private fun findBestTargetMatch(c: Collection<DumpsysEntry>, target: String) =
390             c.asSequence().filter { it.name.endsWith(target) }.minByOrNull { it.name.length }
391 
392         private fun findBestProtoTargetMatch(
393             c: Collection<DumpableEntry>,
394             target: String
395         ): ProtoDumpable? =
396             c.asSequence()
397                 .filter { it.name.endsWith(target) }
398                 .filter { it.dumpable is ProtoDumpable }
399                 .minByOrNull { it.name.length }
400                 ?.dumpable as? ProtoDumpable
401 
402         private fun PrintWriter.preamble(entry: DumpsysEntry) =
403             when (entry) {
404                 // Historically TableLogBuffer was not separate from dumpables, so they have the
405                 // same header
406                 is DumpableEntry,
407                 is TableLogBufferEntry -> {
408                     println()
409                     println("${entry.name}:")
410                     println(DUMPSYS_DUMPABLE_DIVIDER)
411                 }
412                 is LogBufferEntry -> {
413                     println()
414                     println()
415                     println("BUFFER ${entry.name}:")
416                     println(DUMPSYS_DUMPABLE_DIVIDER)
417                 }
418             }
419 
420         private fun PrintWriter.footer(entry: DumpsysEntry, dumpTimeMillis: Long) {
421             if (entry !is DumpableEntry) return
422             println()
423             print(entry.priority)
424             print(" dump took ")
425             print(dumpTimeMillis)
426             print("ms -- ")
427             print(entry.name)
428             if (entry.priority == DumpPriority.CRITICAL && dumpTimeMillis > 25) {
429                 print(" -- warning: individual dump time exceeds 5% of total CRITICAL dump time!")
430             }
431             println()
432         }
433 
434         private inline fun PrintWriter.wrapSection(entry: DumpsysEntry, block: () -> Unit) {
435             Trace.beginSection(entry.name)
436             preamble(entry)
437             val dumpTime = measureTimeMillis(block)
438             footer(entry, dumpTime)
439             Trace.endSection()
440         }
441 
442         /**
443          * Utility to write a [DumpableEntry] to the given [PrintWriter] in a
444          * dumpsys-appropriate format.
445          */
446         private fun dumpDumpable(
447                 entry: DumpableEntry,
448                 pw: PrintWriter,
449                 args: Array<String> = arrayOf(),
450         ) = pw.wrapSection(entry) {
451             entry.dumpable.dump(pw, args)
452         }
453 
454         /**
455          * Utility to write a [LogBufferEntry] to the given [PrintWriter] in a
456          * dumpsys-appropriate format.
457          */
458         private fun dumpBuffer(
459                 entry: LogBufferEntry,
460                 pw: PrintWriter,
461                 tailLength: Int = 0,
462         ) = pw.wrapSection(entry) {
463             entry.buffer.dump(pw, tailLength)
464         }
465 
466         /**
467          * Utility to write a [TableLogBufferEntry] to the given [PrintWriter] in a
468          * dumpsys-appropriate format.
469          */
470         private fun dumpTableBuffer(
471                 entry: TableLogBufferEntry,
472                 pw: PrintWriter,
473                 args: Array<String> = arrayOf(),
474         ) = pw.wrapSection(entry) {
475             entry.table.dump(pw, args)
476         }
477 
478         /**
479          * Zero-arg utility to write a [DumpsysEntry] to the given [PrintWriter] in a
480          * dumpsys-appropriate format.
481          */
482         fun DumpsysEntry.dump(pw: PrintWriter) {
483             when (this) {
484                 is DumpableEntry -> dumpDumpable(this, pw)
485                 is LogBufferEntry -> dumpBuffer(this, pw)
486                 is TableLogBufferEntry -> dumpTableBuffer(this, pw)
487             }
488         }
489 
490         /** Format [entries] in a dumpsys-appropriate way, using [pw] */
491         fun dumpEntries(entries: Collection<DumpsysEntry>, pw: PrintWriter) {
492             entries.forEach { it.dump(pw) }
493         }
494     }
495 }
496 
497 private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
498 private val PRIORITY_OPTIONS = arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_NORMAL)
499 
500 private val COMMANDS =
501     arrayOf(
502         "bugreport-critical",
503         "bugreport-normal",
504         "buffers",
505         "dumpables",
506         "tables",
507         "config",
508         "help"
509     )
510 
511 private class ParsedArgs(val rawArgs: Array<String>, val nonFlagArgs: List<String>) {
512     var dumpPriority: String? = null
513     var tailLength: Int = 0
514     var command: String? = null
515     var listOnly = false
516     var proto = false
517 }
518 
519 class ArgParseException(message: String) : Exception(message)
520