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