1 /*
<lambda>null2  * 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             val matches =
226                 if (args.matchAll) {
227                     findAllMatchesInCollection(targets, dumpables, buffers, tableBuffers)
228                 } else {
229                     findBestMatchesInCollection(targets, dumpables, buffers, tableBuffers)
230                 }
231             matches.forEach { it.dump(pw, args) }
232         } else {
233             if (args.listOnly) {
234                 val dumpables = dumpManager.getDumpables()
235                 val buffers = dumpManager.getLogBuffers()
236                 val tableBuffers = dumpManager.getTableLogBuffers()
237 
238                 pw.println("Dumpables:")
239                 listTargetNames(dumpables, pw)
240                 pw.println()
241 
242                 pw.println("Buffers:")
243                 listTargetNames(buffers, pw)
244                 pw.println()
245 
246                 pw.println("TableBuffers:")
247                 listTargetNames(tableBuffers, pw)
248             } else {
249                 pw.println("Nothing to dump :(")
250             }
251         }
252     }
253 
254     /** Finds the best match for a particular target */
255     private fun findTargetInCollection(
256         target: String,
257         dumpables: Collection<DumpableEntry>,
258         logBuffers: Collection<LogBufferEntry>,
259         tableBuffers: Collection<TableLogBufferEntry>,
260     ): DumpsysEntry? =
261         sequence {
262                 findBestTargetMatch(dumpables, target)?.let { yield(it) }
263                 findBestTargetMatch(logBuffers, target)?.let { yield(it) }
264                 findBestTargetMatch(tableBuffers, target)?.let { yield(it) }
265             }
266             .sortedBy { it.name }
267             .minByOrNull { it.name.length }
268 
269     /** Finds the best match for each target, if any, in the order of the targets */
270     private fun findBestMatchesInCollection(
271         targets: List<String>,
272         dumpables: Collection<DumpableEntry>,
273         logBuffers: Collection<LogBufferEntry>,
274         tableBuffers: Collection<TableLogBufferEntry>,
275     ): List<DumpsysEntry> =
276         targets.mapNotNull { target ->
277             findTargetInCollection(target, dumpables, logBuffers, tableBuffers)
278         }
279 
280     /** Finds all matches for any target, returning in the --list order. */
281     private fun findAllMatchesInCollection(
282         targets: List<String>,
283         dumpables: Collection<DumpableEntry>,
284         logBuffers: Collection<LogBufferEntry>,
285         tableBuffers: Collection<TableLogBufferEntry>,
286     ): List<DumpsysEntry> =
287         sequence {
288                 yieldAll(dumpables.filter { it.matchesAny(targets) })
289                 yieldAll(logBuffers.filter { it.matchesAny(targets) })
290                 yieldAll(tableBuffers.filter { it.matchesAny(targets) })
291             }
292             .sortedBy { it.name }.toList()
293 
294     private fun dumpConfig(pw: PrintWriter) {
295         config.dump(pw, arrayOf())
296     }
297 
298     private fun dumpHelp(pw: PrintWriter) {
299         pw.println("Let <invocation> be:")
300         pw.println("$ adb shell dumpsys activity service com.android.systemui/.SystemUIService")
301         pw.println()
302 
303         pw.println("Most common usage:")
304         pw.println("$ <invocation> <targets>")
305         pw.println("$ <invocation> NotifLog")
306         pw.println("$ <invocation> StatusBar FalsingManager BootCompleteCacheImpl")
307         pw.println("etc.")
308         pw.println()
309 
310         pw.println("Print all matches, instead of the best match:")
311         pw.println("$ <invocation> --all <targets>")
312         pw.println("$ <invocation> --all Log")
313         pw.println()
314 
315         pw.println("Special commands:")
316         pw.println("$ <invocation> dumpables")
317         pw.println("$ <invocation> buffers")
318         pw.println("$ <invocation> tables")
319         pw.println("$ <invocation> bugreport-critical")
320         pw.println("$ <invocation> bugreport-normal")
321         pw.println("$ <invocation> config")
322         pw.println()
323 
324         pw.println("Targets can be listed:")
325         pw.println("$ <invocation> --list")
326         pw.println("$ <invocation> dumpables --list")
327         pw.println("$ <invocation> buffers --list")
328         pw.println("$ <invocation> tables --list")
329         pw.println()
330 
331         pw.println("Show only the most recent N lines of buffers")
332         pw.println("$ <invocation> NotifLog --tail 30")
333     }
334 
335     private fun parseArgs(args: Array<String>): ParsedArgs {
336         val mutArgs = args.toMutableList()
337         val pArgs = ParsedArgs(args, mutArgs)
338 
339         val iterator = mutArgs.iterator()
340         while (iterator.hasNext()) {
341             val arg = iterator.next()
342             if (arg.startsWith("-")) {
343                 iterator.remove()
344                 when (arg) {
345                     PRIORITY_ARG -> {
346                         pArgs.dumpPriority =
347                             readArgument(iterator, PRIORITY_ARG) {
348                                 if (PRIORITY_OPTIONS.contains(it)) {
349                                     it
350                                 } else {
351                                     throw IllegalArgumentException()
352                                 }
353                             }
354                     }
355                     PROTO -> pArgs.proto = true
356                     "-t",
357                     "--tail" -> {
358                         pArgs.tailLength = readArgument(iterator, arg) { it.toInt() }
359                     }
360                     "-l",
361                     "--list" -> {
362                         pArgs.listOnly = true
363                     }
364                     "-h",
365                     "--help" -> {
366                         pArgs.command = "help"
367                     }
368                     "-a",
369                     "--all" -> {
370                         pArgs.matchAll = true
371                     }
372                     else -> {
373                         throw ArgParseException("Unknown flag: $arg")
374                     }
375                 }
376             }
377         }
378 
379         if (pArgs.command == null && mutArgs.isNotEmpty() && COMMANDS.contains(mutArgs[0])) {
380             pArgs.command = mutArgs.removeAt(0)
381         }
382 
383         return pArgs
384     }
385 
386     private fun <T> readArgument(
387         iterator: MutableIterator<String>,
388         flag: String,
389         parser: (arg: String) -> T
390     ): T {
391         if (!iterator.hasNext()) {
392             throw ArgParseException("Missing argument for $flag")
393         }
394         val value = iterator.next()
395 
396         return try {
397             parser(value).also { iterator.remove() }
398         } catch (e: Exception) {
399             throw ArgParseException("Invalid argument '$value' for flag $flag")
400         }
401     }
402 
403     private fun DumpsysEntry.dump(pw: PrintWriter, args: ParsedArgs) =
404         when (this) {
405             is DumpableEntry -> dumpDumpable(this, pw, args.rawArgs)
406             is LogBufferEntry -> dumpBuffer(this, pw, args.tailLength)
407             is TableLogBufferEntry -> dumpTableBuffer(this, pw, args.rawArgs)
408         }
409 
410     private fun Collection<DumpsysEntry>.listOrDumpEntries(pw: PrintWriter, args: ParsedArgs) =
411         if (args.listOnly) {
412             listTargetNames(this, pw)
413         } else {
414             forEach { it.dump(pw, args) }
415         }
416 
417     companion object {
418         const val PRIORITY_ARG = "--dump-priority"
419         const val PRIORITY_ARG_CRITICAL = "CRITICAL"
420         const val PRIORITY_ARG_NORMAL = "NORMAL"
421         const val PROTO = "--proto"
422 
423         /**
424          * Important: do not change this divider without updating any bug report processing tools
425          * (e.g. ABT), since this divider is used to determine boundaries for bug report views
426          */
427         const val DUMPSYS_DUMPABLE_DIVIDER =
428             "----------------------------------------------------------------------------"
429 
430         private fun DumpsysEntry.matches(target: String) = name.endsWith(target)
431         private fun DumpsysEntry.matchesAny(targets: Collection<String>) =
432             targets.any { matches(it) }
433 
434         private fun findBestTargetMatch(c: Collection<DumpsysEntry>, target: String) =
435             c.asSequence().filter { it.matches(target) }.minByOrNull { it.name.length }
436 
437         private fun findBestProtoTargetMatch(
438             c: Collection<DumpableEntry>,
439             target: String
440         ): ProtoDumpable? =
441             c.asSequence()
442                 .filter { it.matches(target) }
443                 .filter { it.dumpable is ProtoDumpable }
444                 .minByOrNull { it.name.length }
445                 ?.dumpable as? ProtoDumpable
446 
447         private fun PrintWriter.preamble(entry: DumpsysEntry) =
448             when (entry) {
449                 // Historically TableLogBuffer was not separate from dumpables, so they have the
450                 // same header
451                 is DumpableEntry,
452                 is TableLogBufferEntry -> {
453                     println()
454                     println("${entry.name}:")
455                     println(DUMPSYS_DUMPABLE_DIVIDER)
456                 }
457                 is LogBufferEntry -> {
458                     println()
459                     println()
460                     println("BUFFER ${entry.name}:")
461                     println(DUMPSYS_DUMPABLE_DIVIDER)
462                 }
463             }
464 
465         private fun PrintWriter.footer(entry: DumpsysEntry, dumpTimeMillis: Long) {
466             if (entry !is DumpableEntry) return
467             println()
468             print(entry.priority)
469             print(" dump took ")
470             print(dumpTimeMillis)
471             print("ms -- ")
472             print(entry.name)
473             if (entry.priority == DumpPriority.CRITICAL && dumpTimeMillis > 25) {
474                 print(" -- warning: individual dump time exceeds 5% of total CRITICAL dump time!")
475             }
476             println()
477         }
478 
479         private inline fun PrintWriter.wrapSection(entry: DumpsysEntry, block: () -> Unit) {
480             Trace.beginSection(entry.name.take(Trace.MAX_SECTION_NAME_LEN))
481             preamble(entry)
482             val dumpTime = measureTimeMillis(block)
483             footer(entry, dumpTime)
484             Trace.endSection()
485         }
486 
487         /**
488          * Utility to write a [DumpableEntry] to the given [PrintWriter] in a dumpsys-appropriate
489          * format.
490          */
491         private fun dumpDumpable(
492             entry: DumpableEntry,
493             pw: PrintWriter,
494             args: Array<String> = arrayOf(),
495         ) = pw.wrapSection(entry) { entry.dumpable.dump(pw, args) }
496 
497         /**
498          * Utility to write a [LogBufferEntry] to the given [PrintWriter] in a dumpsys-appropriate
499          * format.
500          */
501         private fun dumpBuffer(
502             entry: LogBufferEntry,
503             pw: PrintWriter,
504             tailLength: Int = 0,
505         ) = pw.wrapSection(entry) { entry.buffer.dump(pw, tailLength) }
506 
507         /**
508          * Utility to write a [TableLogBufferEntry] to the given [PrintWriter] in a
509          * dumpsys-appropriate format.
510          */
511         private fun dumpTableBuffer(
512             entry: TableLogBufferEntry,
513             pw: PrintWriter,
514             args: Array<String> = arrayOf(),
515         ) = pw.wrapSection(entry) { entry.table.dump(pw, args) }
516 
517         /**
518          * Zero-arg utility to write a [DumpsysEntry] to the given [PrintWriter] in a
519          * dumpsys-appropriate format.
520          */
521         fun DumpsysEntry.dump(pw: PrintWriter) {
522             when (this) {
523                 is DumpableEntry -> dumpDumpable(this, pw)
524                 is LogBufferEntry -> dumpBuffer(this, pw)
525                 is TableLogBufferEntry -> dumpTableBuffer(this, pw)
526             }
527         }
528 
529         /** Format [entries] in a dumpsys-appropriate way, using [pw] */
530         fun dumpEntries(entries: Collection<DumpsysEntry>, pw: PrintWriter) {
531             entries.forEach { it.dump(pw) }
532         }
533     }
534 }
535 
536 private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
537 private val PRIORITY_OPTIONS = arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_NORMAL)
538 
539 private val COMMANDS =
540     arrayOf(
541         "bugreport-critical",
542         "bugreport-normal",
543         "buffers",
544         "dumpables",
545         "tables",
546         "config",
547         "help"
548     )
549 
550 private class ParsedArgs(val rawArgs: Array<String>, val nonFlagArgs: List<String>) {
551     var dumpPriority: String? = null
552     var tailLength: Int = 0
553     var command: String? = null
554     var listOnly = false
555     var matchAll = false
556     var proto = false
557 }
558 
559 class ArgParseException(message: String) : Exception(message)
560