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