| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.dump |
| |
| import android.content.Context |
| import android.os.SystemClock |
| import android.os.Trace |
| import com.android.systemui.R |
| import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_CRITICAL |
| import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_HIGH |
| import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL |
| import com.android.systemui.log.LogBuffer |
| import java.io.FileDescriptor |
| import java.io.PrintWriter |
| import javax.inject.Inject |
| |
| /** |
| * Oversees SystemUI's output during bug reports (and dumpsys in general) |
| * |
| * Dump output is split into two sections, CRITICAL and NORMAL. In general, the CRITICAL section |
| * contains all dumpables that were registered to the [DumpManager], while the NORMAL sections |
| * contains all [LogBuffer]s (due to their length). |
| * |
| * The CRITICAL and NORMAL sections can be found within a bug report by searching for |
| * "SERVICE com.android.systemui/.SystemUIService" and |
| * "SERVICE com.android.systemui/.dump.SystemUIAuxiliaryDumpService", respectively. |
| * |
| * Finally, some or all of the dump can be triggered on-demand via adb (see below). |
| * |
| * ``` |
| * # For the following, let <invocation> be: |
| * $ adb shell dumpsys activity service com.android.systemui/.SystemUIService |
| * |
| * # To dump specific target(s), specify one or more registered names: |
| * $ <invocation> NotifCollection |
| * $ <invocation> StatusBar FalsingManager BootCompleteCacheImpl |
| * |
| * # Log buffers can be dumped in the same way (and can even be mixed in with other dump targets, |
| * # although it's not clear why one would want such a thing): |
| * $ <invocation> NotifLog |
| * $ <invocation> StatusBar NotifLog BootCompleteCacheImpl |
| * |
| * # If passing -t or --tail, shows only the last N lines of any log buffers: |
| * $ <invocation> NotifLog --tail 100 |
| * |
| * # Dump targets are matched using String.endsWith(), so dumpables that register using their |
| * # fully-qualified class name can still be dumped using their short name: |
| * $ <invocation> com.android.keyguard.KeyguardUpdateMonitor |
| * $ <invocation> keyguard.KeyguardUpdateMonitor |
| * $ <invocation> KeyguardUpdateMonitor |
| * |
| * # To dump all dumpables or all buffers: |
| * $ <invocation> dumpables |
| * $ <invocation> buffers |
| * |
| * # Finally, the following will simulate what we dump during the CRITICAL and NORMAL sections of a |
| * # bug report: |
| * $ <invocation> bugreport-critical |
| * $ <invocation> bugreport-normal |
| * |
| * # And if you need to be reminded of this list of commands: |
| * $ <invocation> -h |
| * $ <invocation> --help |
| * ``` |
| */ |
| class DumpHandler @Inject constructor( |
| private val context: Context, |
| private val dumpManager: DumpManager, |
| private val logBufferEulogizer: LogBufferEulogizer |
| ) { |
| /** |
| * Dump the diagnostics! Behavior can be controlled via [args]. |
| */ |
| fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) { |
| Trace.beginSection("DumpManager#dump()") |
| val start = SystemClock.uptimeMillis() |
| |
| val parsedArgs = try { |
| parseArgs(args) |
| } catch (e: ArgParseException) { |
| pw.println(e.message) |
| return |
| } |
| |
| when (parsedArgs.dumpPriority) { |
| PRIORITY_ARG_CRITICAL -> dumpCritical(fd, pw, parsedArgs) |
| PRIORITY_ARG_NORMAL -> dumpNormal(pw, parsedArgs) |
| else -> dumpParameterized(fd, pw, parsedArgs) |
| } |
| |
| pw.println() |
| pw.println("Dump took ${SystemClock.uptimeMillis() - start}ms") |
| Trace.endSection() |
| } |
| |
| private fun dumpParameterized(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) { |
| when (args.command) { |
| "bugreport-critical" -> dumpCritical(fd, pw, args) |
| "bugreport-normal" -> dumpNormal(pw, args) |
| "dumpables" -> dumpDumpables(fd, pw, args) |
| "buffers" -> dumpBuffers(pw, args) |
| "config" -> dumpConfig(pw) |
| "help" -> dumpHelp(pw) |
| else -> dumpTargets(args.nonFlagArgs, fd, pw, args) |
| } |
| } |
| |
| private fun dumpCritical(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) { |
| dumpManager.dumpDumpables(fd, pw, args.rawArgs) |
| dumpConfig(pw) |
| } |
| |
| private fun dumpNormal(pw: PrintWriter, args: ParsedArgs) { |
| dumpManager.dumpBuffers(pw, args.tailLength) |
| logBufferEulogizer.readEulogyIfPresent(pw) |
| } |
| |
| private fun dumpDumpables(fw: FileDescriptor, pw: PrintWriter, args: ParsedArgs) { |
| if (args.listOnly) { |
| dumpManager.listDumpables(pw) |
| } else { |
| dumpManager.dumpDumpables(fw, pw, args.rawArgs) |
| } |
| } |
| |
| private fun dumpBuffers(pw: PrintWriter, args: ParsedArgs) { |
| if (args.listOnly) { |
| dumpManager.listBuffers(pw) |
| } else { |
| dumpManager.dumpBuffers(pw, args.tailLength) |
| } |
| } |
| |
| private fun dumpTargets( |
| targets: List<String>, |
| fd: FileDescriptor, |
| pw: PrintWriter, |
| args: ParsedArgs |
| ) { |
| if (targets.isNotEmpty()) { |
| for (target in targets) { |
| dumpManager.dumpTarget(target, fd, pw, args.rawArgs, args.tailLength) |
| } |
| } else { |
| if (args.listOnly) { |
| pw.println("Dumpables:") |
| dumpManager.listDumpables(pw) |
| pw.println() |
| |
| pw.println("Buffers:") |
| dumpManager.listBuffers(pw) |
| } else { |
| pw.println("Nothing to dump :(") |
| } |
| } |
| } |
| |
| private fun dumpConfig(pw: PrintWriter) { |
| pw.println("SystemUiServiceComponents configuration:") |
| pw.print("vendor component: ") |
| pw.println(context.resources.getString(R.string.config_systemUIVendorServiceComponent)) |
| dumpServiceList(pw, "global", R.array.config_systemUIServiceComponents) |
| dumpServiceList(pw, "per-user", R.array.config_systemUIServiceComponentsPerUser) |
| } |
| |
| private fun dumpServiceList(pw: PrintWriter, type: String, resId: Int) { |
| val services: Array<String>? = context.resources.getStringArray(resId) |
| pw.print(type) |
| pw.print(": ") |
| if (services == null) { |
| pw.println("N/A") |
| return |
| } |
| pw.print(services.size) |
| pw.println(" services") |
| for (i in services.indices) { |
| pw.print(" ") |
| pw.print(i) |
| pw.print(": ") |
| pw.println(services[i]) |
| } |
| } |
| |
| private fun dumpHelp(pw: PrintWriter) { |
| pw.println("Let <invocation> be:") |
| pw.println("$ adb shell dumpsys activity service com.android.systemui/.SystemUIService") |
| pw.println() |
| |
| pw.println("Most common usage:") |
| pw.println("$ <invocation> <targets>") |
| pw.println("$ <invocation> NotifLog") |
| pw.println("$ <invocation> StatusBar FalsingManager BootCompleteCacheImpl") |
| pw.println("etc.") |
| pw.println() |
| |
| pw.println("Special commands:") |
| pw.println("$ <invocation> dumpables") |
| pw.println("$ <invocation> buffers") |
| pw.println("$ <invocation> bugreport-critical") |
| pw.println("$ <invocation> bugreport-normal") |
| pw.println() |
| |
| pw.println("Targets can be listed:") |
| pw.println("$ <invocation> --list") |
| pw.println("$ <invocation> dumpables --list") |
| pw.println("$ <invocation> buffers --list") |
| pw.println() |
| |
| pw.println("Show only the most recent N lines of buffers") |
| pw.println("$ <invocation> NotifLog --tail 30") |
| } |
| |
| private fun parseArgs(args: Array<String>): ParsedArgs { |
| val mutArgs = args.toMutableList() |
| val pArgs = ParsedArgs(args, mutArgs) |
| |
| val iterator = mutArgs.iterator() |
| while (iterator.hasNext()) { |
| val arg = iterator.next() |
| if (arg.startsWith("-")) { |
| iterator.remove() |
| when (arg) { |
| PRIORITY_ARG -> { |
| pArgs.dumpPriority = readArgument(iterator, PRIORITY_ARG) { |
| if (PRIORITY_OPTIONS.contains(it)) { |
| it |
| } else { |
| throw IllegalArgumentException() |
| } |
| } |
| } |
| "-t", "--tail" -> { |
| pArgs.tailLength = readArgument(iterator, arg) { |
| it.toInt() |
| } |
| } |
| "-l", "--list" -> { |
| pArgs.listOnly = true |
| } |
| "-h", "--help" -> { |
| pArgs.command = "help" |
| } |
| else -> { |
| throw ArgParseException("Unknown flag: $arg") |
| } |
| } |
| } |
| } |
| |
| if (pArgs.command == null && mutArgs.isNotEmpty() && COMMANDS.contains(mutArgs[0])) { |
| pArgs.command = mutArgs.removeAt(0) |
| } |
| |
| return pArgs |
| } |
| |
| private fun <T> readArgument( |
| iterator: MutableIterator<String>, |
| flag: String, |
| parser: (arg: String) -> T |
| ): T { |
| if (!iterator.hasNext()) { |
| throw ArgParseException("Missing argument for $flag") |
| } |
| val value = iterator.next() |
| |
| return try { |
| parser(value).also { iterator.remove() } |
| } catch (e: Exception) { |
| throw ArgParseException("Invalid argument '$value' for flag $flag") |
| } |
| } |
| |
| companion object { |
| const val PRIORITY_ARG = "--dump-priority" |
| const val PRIORITY_ARG_CRITICAL = "CRITICAL" |
| const val PRIORITY_ARG_HIGH = "HIGH" |
| const val PRIORITY_ARG_NORMAL = "NORMAL" |
| } |
| } |
| |
| private val PRIORITY_OPTIONS = |
| arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_HIGH, PRIORITY_ARG_NORMAL) |
| |
| private val COMMANDS = arrayOf("bugreport-critical", "bugreport-normal", "buffers", "dumpables") |
| |
| private class ParsedArgs( |
| val rawArgs: Array<String>, |
| val nonFlagArgs: List<String> |
| ) { |
| var dumpPriority: String? = null |
| var tailLength: Int = 0 |
| var command: String? = null |
| var listOnly = false |
| } |
| |
| class ArgParseException(message: String) : Exception(message) |