blob: 59a7a328e9ae062effb03d89e5872a5b3dec1a6b [file] [log] [blame]
/*
* 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 android.util.ArrayMap
import com.android.systemui.Dumpable
import com.android.systemui.R
import com.android.systemui.dump.DumpManager.Companion.PRIORITY_ARG_CRITICAL
import com.android.systemui.dump.DumpManager.Companion.PRIORITY_ARG_HIGH
import com.android.systemui.dump.DumpManager.Companion.PRIORITY_ARG_NORMAL
import com.android.systemui.log.LogBuffer
import java.io.FileDescriptor
import java.io.PrintWriter
import javax.inject.Inject
import javax.inject.Singleton
/**
* Oversees SystemUI's output during bug reports (and dumpsys in general)
*
* When a bug report is taken, SystemUI dumps various diagnostic information that we hope will be
* useful for the eventual readers of the bug report. Code that wishes to participate in this dump
* should register itself here.
*
* Dump output is split into two sections, CRITICAL and NORMAL. All dumpables registered via
* [registerDumpable] appear in the CRITICAL section, while all [LogBuffer]s appear in the NORMAL
* section (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
* ```
*/
@Singleton
class DumpManager @Inject constructor(
private val context: Context
) {
private val dumpables: MutableMap<String, RegisteredDumpable<Dumpable>> = ArrayMap()
private val buffers: MutableMap<String, RegisteredDumpable<LogBuffer>> = ArrayMap()
/**
* Register a dumpable to be called during a bug report. The dumpable will be called during the
* CRITICAL section of the bug report, so don't dump an excessive amount of stuff here.
*
* @param name The name to register the dumpable under. This is typically the qualified class
* name of the thing being dumped (getClass().getName()), but can be anything as long as it
* doesn't clash with an existing registration.
*/
@Synchronized
fun registerDumpable(name: String, module: Dumpable) {
if (RESERVED_NAMES.contains(name)) {
throw IllegalArgumentException("'$name' is reserved")
}
if (!canAssignToNameLocked(name, module)) {
throw IllegalArgumentException("'$name' is already registered")
}
dumpables[name] = RegisteredDumpable(name, module)
}
/**
* Unregisters a previously-registered dumpable.
*/
@Synchronized
fun unregisterDumpable(name: String) {
dumpables.remove(name)
}
/**
* Register a [LogBuffer] to be dumped during a bug report.
*/
@Synchronized
fun registerBuffer(name: String, buffer: LogBuffer) {
if (!canAssignToNameLocked(name, buffer)) {
throw IllegalArgumentException("'$name' is already registered")
}
buffers[name] = RegisteredDumpable(name, buffer)
}
/**
* Dump the diagnostics! Behavior can be controlled via [args].
*/
@Synchronized
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 -> dumpCriticalLocked(fd, pw, parsedArgs)
PRIORITY_ARG_NORMAL -> dumpNormalLocked(pw, parsedArgs)
else -> dumpParameterizedLocked(fd, pw, parsedArgs)
}
pw.println()
pw.println("Dump took ${SystemClock.uptimeMillis() - start}ms")
Trace.endSection()
}
private fun dumpCriticalLocked(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
dumpDumpablesLocked(fd, pw, args)
dumpConfig(pw)
}
private fun dumpNormalLocked(pw: PrintWriter, args: ParsedArgs) {
dumpBuffersLocked(pw, args)
}
private fun dumpParameterizedLocked(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
when (args.command) {
"bugreport-critical" -> dumpCriticalLocked(fd, pw, args)
"bugreport-normal" -> dumpNormalLocked(pw, args)
"dumpables" -> dumpDumpablesLocked(fd, pw, args)
"buffers" -> dumpBuffersLocked(pw, args)
else -> dumpTargetsLocked(args.nonFlagArgs, fd, pw, args)
}
}
private fun dumpTargetsLocked(
targets: List<String>,
fd: FileDescriptor,
pw: PrintWriter,
args: ParsedArgs
) {
if (targets.isEmpty()) {
pw.println("Nothing to dump :(")
} else {
for (target in targets) {
dumpTarget(target, fd, pw, args)
}
}
}
private fun dumpTarget(
target: String,
fd: FileDescriptor,
pw: PrintWriter,
args: ParsedArgs
) {
if (target == "config") {
dumpConfig(pw)
return
}
for (dumpable in dumpables.values) {
if (dumpable.name.endsWith(target)) {
dumpDumpable(dumpable, fd, pw, args)
return
}
}
for (buffer in buffers.values) {
if (buffer.name.endsWith(target)) {
dumpBuffer(buffer, pw, args)
return
}
}
}
private fun dumpDumpablesLocked(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
for (module in dumpables.values) {
dumpDumpable(module, fd, pw, args)
}
}
private fun dumpBuffersLocked(pw: PrintWriter, args: ParsedArgs) {
for (buffer in buffers.values) {
dumpBuffer(buffer, pw, args)
}
}
private fun dumpDumpable(
dumpable: RegisteredDumpable<Dumpable>,
fd: FileDescriptor,
pw: PrintWriter,
args: ParsedArgs
) {
pw.println()
pw.println("${dumpable.name}:")
pw.println("----------------------------------------------------------------------------")
dumpable.dumpable.dump(fd, pw, args.rawArgs)
}
private fun dumpBuffer(
buffer: RegisteredDumpable<LogBuffer>,
pw: PrintWriter,
args: ParsedArgs
) {
pw.println()
pw.println()
pw.println("BUFFER ${buffer.name}:")
pw.println("============================================================================")
buffer.dumpable.dump(pw, args.tailLength)
}
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 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, "--tail") {
it.toInt()
}
}
else -> {
throw ArgParseException("Unknown flag: $arg")
}
}
}
}
if (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")
}
}
private fun canAssignToNameLocked(name: String, newDumpable: Any): Boolean {
val existingDumpable = dumpables[name]?.dumpable ?: buffers[name]?.dumpable
return existingDumpable == null || newDumpable == existingDumpable
}
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 val RESERVED_NAMES = arrayOf("config", *COMMANDS)
private data class RegisteredDumpable<T>(
val name: String,
val dumpable: T
)
private class ParsedArgs(
val rawArgs: Array<String>,
val nonFlagArgs: List<String>
) {
var dumpPriority: String? = null
var tailLength: Int = 0
var command: String? = null
}
class ArgParseException(message: String) : Exception(message)