diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java index 50e03992df49e..ba3c8602c6ff6 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java @@ -30,8 +30,10 @@ import com.android.internal.os.BinderInternal; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpHandler; +import com.android.systemui.dump.LogBufferEulogizer; import com.android.systemui.dump.LogBufferFreezer; import com.android.systemui.dump.SystemUIAuxiliaryDumpService; +import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager; import com.android.systemui.statusbar.policy.BatteryStateNotifier; import java.io.FileDescriptor; @@ -44,22 +46,29 @@ public class SystemUIService extends Service { private final Handler mMainHandler; private final DumpHandler mDumpHandler; private final BroadcastDispatcher mBroadcastDispatcher; + private final LogBufferEulogizer mLogBufferEulogizer; private final LogBufferFreezer mLogBufferFreezer; private final BatteryStateNotifier mBatteryStateNotifier; + private final UncaughtExceptionPreHandlerManager mUncaughtExceptionPreHandlerManager; + @Inject public SystemUIService( @Main Handler mainHandler, DumpHandler dumpHandler, BroadcastDispatcher broadcastDispatcher, + LogBufferEulogizer logBufferEulogizer, LogBufferFreezer logBufferFreezer, - BatteryStateNotifier batteryStateNotifier) { + BatteryStateNotifier batteryStateNotifier, + UncaughtExceptionPreHandlerManager uncaughtExceptionPreHandlerManager) { super(); mMainHandler = mainHandler; mDumpHandler = dumpHandler; mBroadcastDispatcher = broadcastDispatcher; + mLogBufferEulogizer = logBufferEulogizer; mLogBufferFreezer = logBufferFreezer; mBatteryStateNotifier = batteryStateNotifier; + mUncaughtExceptionPreHandlerManager = uncaughtExceptionPreHandlerManager; } @Override @@ -71,7 +80,13 @@ public class SystemUIService extends Service { // Finish initializing dump logic mLogBufferFreezer.attach(mBroadcastDispatcher); - mDumpHandler.init(); + + // Attempt to dump all LogBuffers for any uncaught exception + mUncaughtExceptionPreHandlerManager.registerHandler((thread, throwable) -> { + if (throwable instanceof Exception) { + mLogBufferEulogizer.record(((Exception) throwable)); + } + }); // If configured, set up a battery notification if (getResources().getBoolean(R.bool.config_showNotificationForUnknownBatteryState)) { diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt index 4b03fd334cb57..75284fc181495 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt @@ -20,12 +20,16 @@ import android.content.Context import android.os.SystemClock import android.os.Trace import com.android.systemui.CoreStartable +import com.android.systemui.ProtoDumpable import com.android.systemui.R import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_CRITICAL import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL +import com.android.systemui.dump.DumpsysEntry.DumpableEntry +import com.android.systemui.dump.DumpsysEntry.LogBufferEntry +import com.android.systemui.dump.DumpsysEntry.TableLogBufferEntry import com.android.systemui.dump.nano.SystemUIProtoDump import com.android.systemui.log.LogBuffer -import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager +import com.android.systemui.log.table.TableLogBuffer import com.google.protobuf.nano.MessageNano import java.io.BufferedOutputStream import java.io.FileDescriptor @@ -39,11 +43,11 @@ import javax.inject.Provider * * 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). + * contains all [LogBuffer]s and [TableLogBuffer]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. + * 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). * @@ -72,6 +76,7 @@ import javax.inject.Provider * # To dump all dumpables or all buffers: * $ dumpables * $ buffers + * $ tables * * # Finally, the following will simulate what we dump during the CRITICAL and NORMAL sections of a * # bug report: @@ -83,37 +88,26 @@ import javax.inject.Provider * $ --help * ``` */ -class DumpHandler @Inject constructor( +class DumpHandler +@Inject +constructor( private val context: Context, private val dumpManager: DumpManager, private val logBufferEulogizer: LogBufferEulogizer, private val startables: MutableMap, Provider>, - private val uncaughtExceptionPreHandlerManager: UncaughtExceptionPreHandlerManager ) { - /** - * Registers an uncaught exception handler - */ - fun init() { - uncaughtExceptionPreHandlerManager.registerHandler { _, e -> - if (e is Exception) { - logBufferEulogizer.record(e) - } - } - } - - /** - * Dump the diagnostics! Behavior can be controlled via [args]. - */ + /** Dump the diagnostics! Behavior can be controlled via [args]. */ fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array) { Trace.beginSection("DumpManager#dump()") val start = SystemClock.uptimeMillis() - val parsedArgs = try { - parseArgs(args) - } catch (e: ArgParseException) { - pw.println(e.message) - return - } + val parsedArgs = + try { + parseArgs(args) + } catch (e: ArgParseException) { + pw.println(e.message) + return + } when { parsedArgs.dumpPriority == PRIORITY_ARG_CRITICAL -> dumpCritical(pw, parsedArgs) @@ -134,6 +128,7 @@ class DumpHandler @Inject constructor( "bugreport-normal" -> dumpNormal(pw, args) "dumpables" -> dumpDumpables(pw, args) "buffers" -> dumpBuffers(pw, args) + "tables" -> dumpTables(pw, args) "config" -> dumpConfig(pw) "help" -> dumpHelp(pw) else -> { @@ -147,44 +142,65 @@ class DumpHandler @Inject constructor( } private fun dumpCritical(pw: PrintWriter, args: ParsedArgs) { - dumpManager.dumpCritical(pw, args.rawArgs) + val targets = dumpManager.getDumpables() + for (target in targets) { + if (target.priority == DumpPriority.CRITICAL) { + dumpDumpable(target, pw, args.rawArgs) + } + } dumpConfig(pw) } private fun dumpNormal(pw: PrintWriter, args: ParsedArgs) { - dumpManager.dumpNormal(pw, args.rawArgs, args.tailLength) + val targets = dumpManager.getDumpables() + for (target in targets) { + if (target.priority == DumpPriority.NORMAL) { + dumpDumpable(target, pw, args.rawArgs) + } + } + + val buffers = dumpManager.getLogBuffers() + for (buffer in buffers) { + dumpBuffer(buffer, pw, args.tailLength) + } + + val tableBuffers = dumpManager.getTableLogBuffers() + for (table in tableBuffers) { + dumpTableBuffer(table, pw, args.rawArgs) + } + logBufferEulogizer.readEulogyIfPresent(pw) } - private fun dumpDumpables(pw: PrintWriter, args: ParsedArgs) { - if (args.listOnly) { - dumpManager.listDumpables(pw) - } else { - dumpManager.dumpDumpables(pw, args.rawArgs) + private fun dumpDumpables(pw: PrintWriter, args: ParsedArgs) = + dumpManager.getDumpables().listOrDumpEntries(pw, args) + + private fun dumpBuffers(pw: PrintWriter, args: ParsedArgs) = + dumpManager.getLogBuffers().listOrDumpEntries(pw, args) + + private fun dumpTables(pw: PrintWriter, args: ParsedArgs) = + dumpManager.getTableLogBuffers().listOrDumpEntries(pw, args) + + private fun listTargetNames(targets: Collection, pw: PrintWriter) { + for (target in targets) { + pw.println(target.name) } } - private fun dumpBuffers(pw: PrintWriter, args: ParsedArgs) { - if (args.listOnly) { - dumpManager.listBuffers(pw) - } else { - dumpManager.dumpBuffers(pw, args.tailLength) - } - } - - private fun dumpProtoTargets( - targets: List, - fd: FileDescriptor, - args: ParsedArgs - ) { + private fun dumpProtoTargets(targets: List, fd: FileDescriptor, args: ParsedArgs) { val systemUIProto = SystemUIProtoDump() + val dumpables = dumpManager.getDumpables() if (targets.isNotEmpty()) { for (target in targets) { - dumpManager.dumpProtoTarget(target, systemUIProto, args.rawArgs) + findBestProtoTargetMatch(dumpables, target)?.dumpProto(systemUIProto, args.rawArgs) } } else { - dumpManager.dumpProtoDumpables(systemUIProto, args.rawArgs) + // Dump all protos + for (dumpable in dumpables) { + (dumpable.dumpable as? ProtoDumpable)?.dumpProto(systemUIProto, args.rawArgs) + } } + val buffer = BufferedOutputStream(FileOutputStream(fd)) buffer.use { it.write(MessageNano.toByteArray(systemUIProto)) @@ -192,36 +208,70 @@ class DumpHandler @Inject constructor( } } - private fun dumpTargets( - targets: List, - pw: PrintWriter, - args: ParsedArgs - ) { + // Attempts to dump the target list to the given PrintWriter. Since the arguments come in as + // a list of strings, we use the [findBestTargetMatch] method to determine the most-correct + // target with the given search string. + private fun dumpTargets(targets: List, pw: PrintWriter, args: ParsedArgs) { if (targets.isNotEmpty()) { - for (target in targets) { - dumpManager.dumpTarget(target, pw, args.rawArgs, args.tailLength) + val dumpables = dumpManager.getDumpables() + val buffers = dumpManager.getLogBuffers() + val tableBuffers = dumpManager.getTableLogBuffers() + + targets.forEach { target -> + findTargetInCollection(target, dumpables, buffers, tableBuffers)?.dump(pw, args) } } else { if (args.listOnly) { + val dumpables = dumpManager.getDumpables() + val buffers = dumpManager.getLogBuffers() + pw.println("Dumpables:") - dumpManager.listDumpables(pw) + listTargetNames(dumpables, pw) pw.println() pw.println("Buffers:") - dumpManager.listBuffers(pw) + listTargetNames(buffers, pw) } else { pw.println("Nothing to dump :(") } } } + private fun findTargetInCollection( + target: String, + dumpables: Collection, + logBuffers: Collection, + tableBuffers: Collection, + ) = + sequence { + findBestTargetMatch(dumpables, target)?.let { yield(it) } + findBestTargetMatch(logBuffers, target)?.let { yield(it) } + findBestTargetMatch(tableBuffers, target)?.let { yield(it) } + } + .sortedBy { it.name } + .minByOrNull { it.name.length } + + private fun dumpDumpable(entry: DumpableEntry, pw: PrintWriter, args: Array) { + pw.preamble(entry) + entry.dumpable.dump(pw, args) + } + + private fun dumpBuffer(entry: LogBufferEntry, pw: PrintWriter, tailLength: Int) { + pw.preamble(entry) + entry.buffer.dump(pw, tailLength) + } + + private fun dumpTableBuffer(buffer: TableLogBufferEntry, pw: PrintWriter, args: Array) { + pw.preamble(buffer) + buffer.table.dump(pw, args) + } + private fun dumpConfig(pw: PrintWriter) { pw.println("SystemUiServiceComponents configuration:") pw.print("vendor component: ") pw.println(context.resources.getString(R.string.config_systemUIVendorServiceComponent)) - val services: MutableList = startables.keys - .map({ cls: Class<*> -> cls.simpleName }) - .toMutableList() + val services: MutableList = + startables.keys.map({ cls: Class<*> -> cls.simpleName }).toMutableList() services.add(context.resources.getString(R.string.config_systemUIVendorServiceComponent)) dumpServiceList(pw, "global", services.toTypedArray()) @@ -265,6 +315,7 @@ class DumpHandler @Inject constructor( pw.println("Special commands:") pw.println("$ dumpables") pw.println("$ buffers") + pw.println("$ tables") pw.println("$ bugreport-critical") pw.println("$ bugreport-normal") pw.println("$ config") @@ -274,6 +325,7 @@ class DumpHandler @Inject constructor( pw.println("$ --list") pw.println("$ dumpables --list") pw.println("$ buffers --list") + pw.println("$ tables --list") pw.println() pw.println("Show only the most recent N lines of buffers") @@ -291,24 +343,26 @@ class DumpHandler @Inject constructor( iterator.remove() when (arg) { PRIORITY_ARG -> { - pArgs.dumpPriority = readArgument(iterator, PRIORITY_ARG) { - if (PRIORITY_OPTIONS.contains(it)) { - it - } else { - throw IllegalArgumentException() + pArgs.dumpPriority = + readArgument(iterator, PRIORITY_ARG) { + if (PRIORITY_OPTIONS.contains(it)) { + it + } else { + throw IllegalArgumentException() + } } - } } PROTO -> pArgs.proto = true - "-t", "--tail" -> { - pArgs.tailLength = readArgument(iterator, arg) { - it.toInt() - } + "-t", + "--tail" -> { + pArgs.tailLength = readArgument(iterator, arg) { it.toInt() } } - "-l", "--list" -> { + "-l", + "--list" -> { pArgs.listOnly = true } - "-h", "--help" -> { + "-h", + "--help" -> { pArgs.command = "help" } // This flag is passed as part of the proto dump in Bug reports, we can ignore @@ -345,29 +399,131 @@ class DumpHandler @Inject constructor( } } + private fun DumpsysEntry.dump(pw: PrintWriter, args: ParsedArgs) = + when (this) { + is DumpableEntry -> dumpDumpable(this, pw, args.rawArgs) + is LogBufferEntry -> dumpBuffer(this, pw, args.tailLength) + is TableLogBufferEntry -> dumpTableBuffer(this, pw, args.rawArgs) + } + + private fun Collection.listOrDumpEntries(pw: PrintWriter, args: ParsedArgs) = + if (args.listOnly) { + listTargetNames(this, pw) + } else { + forEach { it.dump(pw, args) } + } + companion object { const val PRIORITY_ARG = "--dump-priority" const val PRIORITY_ARG_CRITICAL = "CRITICAL" const val PRIORITY_ARG_NORMAL = "NORMAL" const val PROTO = "--proto" + + /** + * Important: do not change this divider without updating any bug report processing tools + * (e.g. ABT), since this divider is used to determine boundaries for bug report views + */ + const val DUMPSYS_DUMPABLE_DIVIDER = + "----------------------------------------------------------------------------" + + /** + * Important: do not change this divider without updating any bug report processing tools + * (e.g. ABT), since this divider is used to determine boundaries for bug report views + */ + const val DUMPSYS_BUFFER_DIVIDER = + "============================================================================" + + private fun findBestTargetMatch(c: Collection, target: String) = + c.asSequence().filter { it.name.endsWith(target) }.minByOrNull { it.name.length } + + private fun findBestProtoTargetMatch( + c: Collection, + target: String + ): ProtoDumpable? = + c.asSequence() + .filter { it.name.endsWith(target) } + .filter { it.dumpable is ProtoDumpable } + .minByOrNull { it.name.length } + ?.dumpable as? ProtoDumpable + + private fun PrintWriter.preamble(entry: DumpsysEntry) = + when (entry) { + // Historically TableLogBuffer was not separate from dumpables, so they have the + // same header + is DumpableEntry, + is TableLogBufferEntry -> { + println() + println(entry.name) + println(DUMPSYS_DUMPABLE_DIVIDER) + } + is LogBufferEntry -> { + println() + println() + println("BUFFER ${entry.name}:") + println(DUMPSYS_BUFFER_DIVIDER) + } + } + + /** + * Zero-arg utility to write a [DumpableEntry] to the given [PrintWriter] in a + * dumpsys-appropriate format. + */ + private fun dumpDumpable(entry: DumpableEntry, pw: PrintWriter) { + pw.preamble(entry) + entry.dumpable.dump(pw, arrayOf()) + } + + /** + * Zero-arg utility to write a [LogBufferEntry] to the given [PrintWriter] in a + * dumpsys-appropriate format. + */ + private fun dumpBuffer(entry: LogBufferEntry, pw: PrintWriter) { + pw.preamble(entry) + entry.buffer.dump(pw, 0) + } + + /** + * Zero-arg utility to write a [TableLogBufferEntry] to the given [PrintWriter] in a + * dumpsys-appropriate format. + */ + private fun dumpTableBuffer(entry: TableLogBufferEntry, pw: PrintWriter) { + pw.preamble(entry) + entry.table.dump(pw, arrayOf()) + } + + /** + * Zero-arg utility to write a [DumpsysEntry] to the given [PrintWriter] in a + * dumpsys-appropriate format. + */ + fun DumpsysEntry.dump(pw: PrintWriter) { + when (this) { + is DumpableEntry -> dumpDumpable(this, pw) + is LogBufferEntry -> dumpBuffer(this, pw) + is TableLogBufferEntry -> dumpTableBuffer(this, pw) + } + } + + /** Format [entries] in a dumpsys-appropriate way, using [pw] */ + fun dumpEntries(entries: Collection, pw: PrintWriter) { + entries.forEach { it.dump(pw) } + } } } private val PRIORITY_OPTIONS = arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_NORMAL) -private val COMMANDS = arrayOf( +private val COMMANDS = + arrayOf( "bugreport-critical", "bugreport-normal", "buffers", "dumpables", + "tables", "config", "help" -) + ) -private class ParsedArgs( - val rawArgs: Array, - val nonFlagArgs: List -) { +private class ParsedArgs(val rawArgs: Array, val nonFlagArgs: List) { var dumpPriority: String? = null var tailLength: Int = 0 var command: String? = null diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt index 2d57633e47a84..c924df6da263c 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt @@ -18,9 +18,11 @@ package com.android.systemui.dump import com.android.systemui.Dumpable import com.android.systemui.ProtoDumpable -import com.android.systemui.dump.nano.SystemUIProtoDump +import com.android.systemui.dump.DumpsysEntry.DumpableEntry +import com.android.systemui.dump.DumpsysEntry.LogBufferEntry +import com.android.systemui.dump.DumpsysEntry.TableLogBufferEntry import com.android.systemui.log.LogBuffer -import java.io.PrintWriter +import com.android.systemui.log.table.TableLogBuffer import java.util.TreeMap import javax.inject.Inject import javax.inject.Singleton @@ -37,8 +39,9 @@ import javax.inject.Singleton @Singleton open class DumpManager @Inject constructor() { // NOTE: Using TreeMap ensures that iteration is in a predictable & alphabetical order. - private val dumpables: MutableMap> = TreeMap() - private val buffers: MutableMap> = TreeMap() + private val dumpables: MutableMap = TreeMap() + private val buffers: MutableMap = TreeMap() + private val tableLogBuffers: MutableMap = TreeMap() /** See [registerCriticalDumpable]. */ fun registerCriticalDumpable(module: Dumpable) { @@ -77,14 +80,14 @@ open class DumpManager @Inject constructor() { * Register a dumpable to be called during a bug report. * * @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. + * name of the thing being dumped (getClass().getName()), but can be anything as long as it + * doesn't clash with an existing registration. * @param priority the priority level of this dumpable, which affects at what point in the bug - * report this gets dump. By default, the dumpable will be called during the CRITICAL section of - * the bug report, so don't dump an excessive amount of stuff here. + * report this gets dump. By default, the dumpable will be called during the CRITICAL section + * of the bug report, so don't dump an excessive amount of stuff here. * * TODO(b/259973758): Replace all calls to this method with calls to [registerCriticalDumpable] - * or [registerNormalDumpable] instead. + * or [registerNormalDumpable] instead. */ @Synchronized @JvmOverloads @@ -98,7 +101,7 @@ open class DumpManager @Inject constructor() { throw IllegalArgumentException("'$name' is already registered") } - dumpables[name] = RegisteredDumpable(name, module, priority) + dumpables[name] = DumpableEntry(module, name, priority) } /** @@ -110,217 +113,62 @@ open class DumpManager @Inject constructor() { registerDumpable(module::class.java.simpleName, module) } - /** - * Unregisters a previously-registered dumpable. - */ + /** Unregisters a previously-registered dumpable. */ @Synchronized fun unregisterDumpable(name: String) { dumpables.remove(name) } - /** - * Register a [LogBuffer] to be dumped during a bug report. - */ + /** 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] = LogBufferEntry(buffer, name) + } + + /** Register a [TableLogBuffer] to be dumped during a bugreport */ + @Synchronized + fun registerTableLogBuffer(name: String, buffer: TableLogBuffer) { + if (!canAssignToNameLocked(name, buffer)) { + throw IllegalArgumentException("'$name' is already registered") + } + // All buffers must be priority NORMAL, not CRITICAL, because they often contain a lot of // data. - buffers[name] = RegisteredDumpable(name, buffer, DumpPriority.NORMAL) + tableLogBuffers[name] = TableLogBufferEntry(buffer, name) } - /** - * Dumps the alphabetically first, shortest-named dumpable or buffer whose registered name ends - * with [target]. - */ - @Synchronized - fun dumpTarget( - target: String, - pw: PrintWriter, - args: Array, - tailLength: Int, - ) { - sequence { - findBestTargetMatch(dumpables, target)?.let { - yield(it.name to { dumpDumpable(it, pw, args) }) - } - findBestTargetMatch(buffers, target)?.let { - yield(it.name to { dumpBuffer(it, pw, tailLength) }) - } - }.sortedBy { it.first }.minByOrNull { it.first.length }?.second?.invoke() - } + @Synchronized fun getDumpables(): Collection = dumpables.values.toList() + + @Synchronized fun getLogBuffers(): Collection = buffers.values.toList() @Synchronized - fun dumpProtoTarget( - target: String, - protoDump: SystemUIProtoDump, - args: Array - ) { - findBestProtoTargetMatch(dumpables, target)?.let { - dumpProtoDumpable(it, protoDump, args) - } - } - - @Synchronized - fun dumpProtoDumpables( - systemUIProtoDump: SystemUIProtoDump, - args: Array - ) { - for (dumpable in dumpables.values) { - if (dumpable.dumpable is ProtoDumpable) { - dumpProtoDumpable( - dumpable.dumpable, - systemUIProtoDump, - args - ) - } - } - } - - /** - * Dumps all registered dumpables with critical priority to [pw] - */ - @Synchronized - fun dumpCritical(pw: PrintWriter, args: Array) { - for (dumpable in dumpables.values) { - if (dumpable.priority == DumpPriority.CRITICAL) { - dumpDumpable(dumpable, pw, args) - } - } - } - - /** - * To [pw], dumps (1) all registered dumpables with normal priority; and (2) all [LogBuffer]s. - */ - @Synchronized - fun dumpNormal(pw: PrintWriter, args: Array, tailLength: Int = 0) { - for (dumpable in dumpables.values) { - if (dumpable.priority == DumpPriority.NORMAL) { - dumpDumpable(dumpable, pw, args) - } - } - - for (buffer in buffers.values) { - dumpBuffer(buffer, pw, tailLength) - } - } - - /** - * Dump all the instances of [Dumpable]. - */ - @Synchronized - fun dumpDumpables(pw: PrintWriter, args: Array) { - for (module in dumpables.values) { - dumpDumpable(module, pw, args) - } - } - - /** - * Dumps the names of all registered dumpables (one per line) - */ - @Synchronized - fun listDumpables(pw: PrintWriter) { - for (module in dumpables.values) { - pw.println(module.name) - } - } - - /** - * Dumps all registered [LogBuffer]s to [pw] - */ - @Synchronized - fun dumpBuffers(pw: PrintWriter, tailLength: Int) { - for (buffer in buffers.values) { - dumpBuffer(buffer, pw, tailLength) - } - } - - /** - * Dumps the names of all registered buffers (one per line) - */ - @Synchronized - fun listBuffers(pw: PrintWriter) { - for (buffer in buffers.values) { - pw.println(buffer.name) - } - } + fun getTableLogBuffers(): Collection = tableLogBuffers.values.toList() @Synchronized fun freezeBuffers() { for (buffer in buffers.values) { - buffer.dumpable.freeze() + buffer.buffer.freeze() } } @Synchronized fun unfreezeBuffers() { for (buffer in buffers.values) { - buffer.dumpable.unfreeze() + buffer.buffer.unfreeze() } } - private fun dumpDumpable( - dumpable: RegisteredDumpable, - pw: PrintWriter, - args: Array - ) { - pw.println() - pw.println("${dumpable.name}:") - pw.println("----------------------------------------------------------------------------") - dumpable.dumpable.dump(pw, args) - } - - private fun dumpBuffer( - buffer: RegisteredDumpable, - pw: PrintWriter, - tailLength: Int - ) { - pw.println() - pw.println() - pw.println("BUFFER ${buffer.name}:") - pw.println("============================================================================") - buffer.dumpable.dump(pw, tailLength) - } - - private fun dumpProtoDumpable( - protoDumpable: ProtoDumpable, - systemUIProtoDump: SystemUIProtoDump, - args: Array - ) { - protoDumpable.dumpProto(systemUIProtoDump, args) - } - private fun canAssignToNameLocked(name: String, newDumpable: Any): Boolean { - val existingDumpable = dumpables[name]?.dumpable ?: buffers[name]?.dumpable + val existingDumpable = + dumpables[name]?.dumpable ?: buffers[name]?.buffer ?: tableLogBuffers[name]?.table return existingDumpable == null || newDumpable == existingDumpable } - - private fun findBestTargetMatch(map: Map, target: String): V? = map - .asSequence() - .filter { it.key.endsWith(target) } - .minByOrNull { it.key.length } - ?.value - - private fun findBestProtoTargetMatch( - map: Map>, - target: String - ): ProtoDumpable? = map - .asSequence() - .filter { it.key.endsWith(target) } - .filter { it.value.dumpable is ProtoDumpable } - .minByOrNull { it.key.length } - ?.value?.dumpable as? ProtoDumpable } -private data class RegisteredDumpable( - val name: String, - val dumpable: T, - val priority: DumpPriority, -) - /** * The priority level for a given dumpable, which affects at what point in the bug report this gets * dumped. diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpsysEntry.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpsysEntry.kt new file mode 100644 index 0000000000000..cd3e1bb7acaca --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpsysEntry.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2023 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 com.android.systemui.Dumpable +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.table.TableLogBuffer + +/** + * A DumpsysEntry is a named, registered entry tracked by [DumpManager] which can be addressed and + * used both in a bugreport / dumpsys invocation or in an individual CLI implementation. + * + * The idea here is that we define every type that [DumpManager] knows about and defines the minimum + * shared interface between each type. So far, just [name] and [priority]. This way, [DumpManager] + * can just store them in separate maps and do the minimal amount of work to discriminate between + * them. + * + * Individual consumers can request these participants in a list via the relevant get* methods on + * [DumpManager] + */ +sealed interface DumpsysEntry { + val name: String + val priority: DumpPriority + + data class DumpableEntry( + val dumpable: Dumpable, + override val name: String, + override val priority: DumpPriority, + ) : DumpsysEntry + + data class LogBufferEntry( + val buffer: LogBuffer, + override val name: String, + ) : DumpsysEntry { + // All buffers must be priority NORMAL, not CRITICAL, because they often contain a lot of + // data. + override val priority: DumpPriority = DumpPriority.NORMAL + } + + data class TableLogBufferEntry( + val table: TableLogBuffer, + override val name: String, + ) : DumpsysEntry { + // All buffers must be priority NORMAL, not CRITICAL, because they often contain a lot of + // data. + override val priority: DumpPriority = DumpPriority.NORMAL + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt index 2d5c9ae2e641b..25b90bebf0d12 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt @@ -20,6 +20,7 @@ import android.content.Context import android.icu.text.SimpleDateFormat import android.util.Log import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpHandler.Companion.dumpEntries import com.android.systemui.log.LogBuffer import com.android.systemui.util.io.Files import com.android.systemui.util.time.SystemClock @@ -48,20 +49,21 @@ class LogBufferEulogizer( private val files: Files, private val logPath: Path, private val minWriteGap: Long, - private val maxLogAgeToDump: Long + private val maxLogAgeToDump: Long, ) { - @Inject constructor( + @Inject + constructor( context: Context, dumpManager: DumpManager, systemClock: SystemClock, - files: Files + files: Files, ) : this( dumpManager, systemClock, files, Paths.get(context.filesDir.toPath().toString(), "log_buffers.txt"), MIN_WRITE_GAP, - MAX_AGE_TO_DUMP + MAX_AGE_TO_DUMP, ) /** @@ -91,7 +93,8 @@ class LogBufferEulogizer( pw.println() pw.println("Dump triggered by exception:") reason.printStackTrace(pw) - dumpManager.dumpBuffers(pw, 0) + val buffers = dumpManager.getLogBuffers() + dumpEntries(buffers, pw) duration = systemClock.uptimeMillis() - start pw.println() pw.println("Buffer eulogy took ${duration}ms") @@ -105,16 +108,17 @@ class LogBufferEulogizer( return reason } - /** - * If a eulogy file is present, writes its contents to [pw]. - */ + /** If a eulogy file is present, writes its contents to [pw]. */ fun readEulogyIfPresent(pw: PrintWriter) { try { val millisSinceLastWrite = getMillisSinceLastWrite(logPath) if (millisSinceLastWrite > maxLogAgeToDump) { - Log.i(TAG, "Not eulogizing buffers; they are " + + Log.i( + TAG, + "Not eulogizing buffers; they are " + TimeUnit.HOURS.convert(millisSinceLastWrite, TimeUnit.MILLISECONDS) + - " hours old") + " hours old" + ) return } @@ -122,9 +126,7 @@ class LogBufferEulogizer( pw.println() pw.println() pw.println("=============== BUFFERS FROM MOST RECENT CRASH ===============") - s.forEach { line -> - pw.println(line) - } + s.forEach { line -> pw.println(line) } } } catch (e: IOException) { // File doesn't exist, okay @@ -134,12 +136,13 @@ class LogBufferEulogizer( } private fun getMillisSinceLastWrite(path: Path): Long { - val stats = try { - files.readAttributes(path, BasicFileAttributes::class.java) - } catch (e: IOException) { - // File doesn't exist - null - } + val stats = + try { + files.readAttributes(path, BasicFileAttributes::class.java) + } catch (e: IOException) { + // File doesn't exist + null + } return systemClock.currentTimeMillis() - (stats?.lastModifiedTime()?.toMillis() ?: 0) } } @@ -147,4 +150,4 @@ class LogBufferEulogizer( private const val TAG = "BufferEulogizer" private val MIN_WRITE_GAP = TimeUnit.MINUTES.toMillis(5) private val MAX_AGE_TO_DUMP = TimeUnit.HOURS.toMillis(48) -private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US) \ No newline at end of file +private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US) diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt index 19e112487c463..1e2f71f01c352 100644 --- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt @@ -61,7 +61,7 @@ constructor( bgDispatcher, coroutineScope, ) - dumpManager.registerNormalDumpable(name, tableBuffer) + dumpManager.registerTableLogBuffer(name, tableBuffer) tableBuffer.init() return tableBuffer } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt index e8cbdf3db3274..2830476874ed4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt @@ -22,7 +22,7 @@ import com.android.systemui.Dumpable import com.android.systemui.ProtoDumpable import com.android.systemui.SysuiTestCase import com.android.systemui.log.LogBuffer -import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.google.common.truth.Truth.assertThat @@ -45,8 +45,6 @@ class DumpHandlerTest : SysuiTestCase() { @Mock private lateinit var logBufferEulogizer: LogBufferEulogizer - @Mock - private lateinit var exceptionHandlerManager: UncaughtExceptionPreHandlerManager @Mock private lateinit var pw: PrintWriter @@ -70,6 +68,11 @@ class DumpHandlerTest : SysuiTestCase() { @Mock private lateinit var buffer2: LogBuffer + @Mock + private lateinit var table1: TableLogBuffer + @Mock + private lateinit var table2: TableLogBuffer + private val dumpManager = DumpManager() @Before @@ -83,21 +86,22 @@ class DumpHandlerTest : SysuiTestCase() { mutableMapOf( EmptyCoreStartable::class.java to Provider { EmptyCoreStartable() } ), - exceptionHandlerManager ) } @Test fun testDumpablesCanBeDumpedSelectively() { // GIVEN a variety of registered dumpables and buffers - dumpManager.registerDumpable("dumpable1", dumpable1) - dumpManager.registerDumpable("dumpable2", dumpable2) - dumpManager.registerDumpable("dumpable3", dumpable3) + dumpManager.registerCriticalDumpable("dumpable1", dumpable1) + dumpManager.registerCriticalDumpable("dumpable2", dumpable2) + dumpManager.registerCriticalDumpable("dumpable3", dumpable3) dumpManager.registerBuffer("buffer1", buffer1) dumpManager.registerBuffer("buffer2", buffer2) + dumpManager.registerTableLogBuffer("table1", table1) + dumpManager.registerTableLogBuffer("table2", table2) // WHEN some of them are dumped explicitly - val args = arrayOf("dumpable1", "dumpable3", "buffer2") + val args = arrayOf("dumpable1", "dumpable3", "buffer2", "table2") dumpHandler.dump(fd, pw, args) // THEN only the requested ones have their dump() method called @@ -108,12 +112,14 @@ class DumpHandlerTest : SysuiTestCase() { verify(dumpable3).dump(pw, args) verify(buffer1, never()).dump(any(PrintWriter::class.java), anyInt()) verify(buffer2).dump(pw, 0) + verify(table1, never()).dump(any(), any()) + verify(table2).dump(pw, args) } @Test fun testDumpableMatchingIsBasedOnEndOfTag() { // GIVEN a dumpable registered to the manager - dumpManager.registerDumpable("com.android.foo.bar.dumpable1", dumpable1) + dumpManager.registerCriticalDumpable("com.android.foo.bar.dumpable1", dumpable1) // WHEN that module is dumped val args = arrayOf("dumpable1") @@ -131,6 +137,8 @@ class DumpHandlerTest : SysuiTestCase() { dumpManager.registerNormalDumpable("dumpable3", dumpable3) dumpManager.registerBuffer("buffer1", buffer1) dumpManager.registerBuffer("buffer2", buffer2) + dumpManager.registerTableLogBuffer("table1", table1) + dumpManager.registerTableLogBuffer("table2", table2) // WHEN a critical dump is requested val args = arrayOf("--dump-priority", "CRITICAL") @@ -144,6 +152,8 @@ class DumpHandlerTest : SysuiTestCase() { any(Array::class.java)) verify(buffer1, never()).dump(any(PrintWriter::class.java), anyInt()) verify(buffer2, never()).dump(any(PrintWriter::class.java), anyInt()) + verify(table1, never()).dump(any(), any()) + verify(table2, never()).dump(any(), any()) } @Test @@ -154,6 +164,8 @@ class DumpHandlerTest : SysuiTestCase() { dumpManager.registerNormalDumpable("dumpable3", dumpable3) dumpManager.registerBuffer("buffer1", buffer1) dumpManager.registerBuffer("buffer2", buffer2) + dumpManager.registerTableLogBuffer("table1", table1) + dumpManager.registerTableLogBuffer("table2", table2) // WHEN a normal dump is requested val args = arrayOf("--dump-priority", "NORMAL") @@ -169,6 +181,8 @@ class DumpHandlerTest : SysuiTestCase() { verify(dumpable3).dump(pw, args) verify(buffer1).dump(pw, 0) verify(buffer2).dump(pw, 0) + verify(table1).dump(pw, args) + verify(table2).dump(pw, args) } @Test @@ -183,6 +197,81 @@ class DumpHandlerTest : SysuiTestCase() { assertThat(stringWriter.toString()).contains(EmptyCoreStartable::class.java.simpleName) } + @Test + fun testDumpBuffers() { + // GIVEN a variety of registered dumpables and buffers and tables + dumpManager.registerCriticalDumpable("dumpable1", dumpable1) + dumpManager.registerCriticalDumpable("dumpable2", dumpable2) + dumpManager.registerNormalDumpable("dumpable3", dumpable3) + dumpManager.registerBuffer("buffer1", buffer1) + dumpManager.registerBuffer("buffer2", buffer2) + dumpManager.registerTableLogBuffer("table1", table1) + dumpManager.registerTableLogBuffer("table2", table2) + + // WHEN a buffer dump is requested + val args = arrayOf("buffers", "--tail", "1") + dumpHandler.dump(fd, pw, args) + + // THEN all buffers are dumped (and no dumpables or tables) + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2, never()).dump(any(), any()) + verify(dumpable3, never()).dump(any(), any()) + verify(buffer1).dump(pw, tailLength = 1) + verify(buffer2).dump(pw, tailLength = 1) + verify(table1, never()).dump(any(), any()) + verify(table2, never()).dump(any(), any()) + } + + @Test + fun testDumpDumpables() { + // GIVEN a variety of registered dumpables and buffers and tables + dumpManager.registerCriticalDumpable("dumpable1", dumpable1) + dumpManager.registerCriticalDumpable("dumpable2", dumpable2) + dumpManager.registerNormalDumpable("dumpable3", dumpable3) + dumpManager.registerBuffer("buffer1", buffer1) + dumpManager.registerBuffer("buffer2", buffer2) + dumpManager.registerTableLogBuffer("table1", table1) + dumpManager.registerTableLogBuffer("table2", table2) + + // WHEN a dumpable dump is requested + val args = arrayOf("dumpables") + dumpHandler.dump(fd, pw, args) + + // THEN all dumpables are dumped (both critical and normal) (and no dumpables) + verify(dumpable1).dump(pw, args) + verify(dumpable2).dump(pw, args) + verify(dumpable3).dump(pw, args) + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2, never()).dump(any(), anyInt()) + verify(table1, never()).dump(any(), any()) + verify(table2, never()).dump(any(), any()) + } + + @Test + fun testDumpTables() { + // GIVEN a variety of registered dumpables and buffers and tables + dumpManager.registerCriticalDumpable("dumpable1", dumpable1) + dumpManager.registerCriticalDumpable("dumpable2", dumpable2) + dumpManager.registerNormalDumpable("dumpable3", dumpable3) + dumpManager.registerBuffer("buffer1", buffer1) + dumpManager.registerBuffer("buffer2", buffer2) + dumpManager.registerTableLogBuffer("table1", table1) + dumpManager.registerTableLogBuffer("table2", table2) + + // WHEN a dumpable dump is requested + val args = arrayOf("tables") + dumpHandler.dump(fd, pw, args) + + // THEN all dumpables are dumped (both critical and normal) (and no dumpables) + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2, never()).dump(any(), any()) + verify(dumpable3, never()).dump(any(), any()) + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2, never()).dump(any(), anyInt()) + verify(table1).dump(pw, args) + verify(table2).dump(pw, args) + } + @Test fun testDumpAllProtoDumpables() { dumpManager.registerDumpable("protoDumpable1", protoDumpable1) @@ -207,6 +296,123 @@ class DumpHandlerTest : SysuiTestCase() { verify(protoDumpable2, never()).dumpProto(any(), any()) } + @Test + fun testDumpTarget_selectsShortestNamedDumpable() { + // GIVEN a variety of registered dumpables and buffers + dumpManager.registerCriticalDumpable("first-dumpable", dumpable1) + dumpManager.registerCriticalDumpable("scnd-dumpable", dumpable2) + dumpManager.registerCriticalDumpable("third-dumpable", dumpable3) + + // WHEN a dumpable is dumped by a suffix that matches multiple options + val args = arrayOf("dumpable") + dumpHandler.dump(fd, pw, args) + + // THEN the matching dumpable with the shorter name is dumped + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2).dump(pw, args) + verify(dumpable3, never()).dump(any(), any()) + } + + @Test + fun testDumpTarget_selectsShortestNamedBuffer() { + // GIVEN a variety of registered dumpables and buffers + dumpManager.registerBuffer("first-buffer", buffer1) + dumpManager.registerBuffer("scnd-buffer", buffer2) + + // WHEN a dumpable is dumped by a suffix that matches multiple options + val args = arrayOf("buffer", "--tail", "14") + dumpHandler.dump(fd, pw, args) + + // THEN the matching buffer with the shorter name is dumped + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2).dump(pw, tailLength = 14) + } + + @Test + fun testDumpTarget_selectsShortestNamedMatch_dumpable() { + // GIVEN a variety of registered dumpables and buffers + dumpManager.registerCriticalDumpable("dumpable1", dumpable1) + dumpManager.registerCriticalDumpable("dumpable2", dumpable2) + dumpManager.registerCriticalDumpable("dumpable3", dumpable3) + dumpManager.registerBuffer("big-buffer1", buffer1) + dumpManager.registerBuffer("big-buffer2", buffer2) + + // WHEN a dumpable is dumped by a suffix that matches multiple options + val args = arrayOf("2") + dumpHandler.dump(fd, pw, args) + + // THEN the matching dumpable with the shorter name is dumped + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2).dump(pw, args) + verify(dumpable3, never()).dump(any(), any()) + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2, never()).dump(any(), anyInt()) + } + + @Test + fun testDumpTarget_selectsShortestNamedMatch_buffer() { + // GIVEN a variety of registered dumpables and buffers + dumpManager.registerCriticalDumpable("dumpable1", dumpable1) + dumpManager.registerCriticalDumpable("dumpable2", dumpable2) + dumpManager.registerCriticalDumpable("dumpable3", dumpable3) + dumpManager.registerBuffer("buffer1", buffer1) + dumpManager.registerBuffer("buffer2", buffer2) + + // WHEN a dumpable is dumped by a suffix that matches multiple options + val args = arrayOf("2", "--tail", "14") + dumpHandler.dump(fd, pw, args) + + // THEN the matching buffer with the shorter name is dumped + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2, never()).dump(any(), any()) + verify(dumpable3, never()).dump(any(), any()) + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2).dump(pw, tailLength = 14) + } + + @Test + fun testDumpTarget_selectsTheAlphabeticallyFirstShortestMatch_dumpable() { + // GIVEN a variety of registered dumpables and buffers + dumpManager.registerCriticalDumpable("d1x", dumpable1) + dumpManager.registerCriticalDumpable("d2x", dumpable2) + dumpManager.registerCriticalDumpable("a3x", dumpable3) + dumpManager.registerBuffer("ab1x", buffer1) + dumpManager.registerBuffer("b2x", buffer2) + + // WHEN a dumpable is dumped by a suffix that matches multiple options + val args = arrayOf("x") + dumpHandler.dump(fd, pw, args) + + // THEN the alphabetically first dumpable/buffer (of the 3 letter names) is dumped + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2, never()).dump(any(), any()) + verify(dumpable3).dump(pw, args) + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2, never()).dump(any(), anyInt()) + } + + @Test + fun testDumpTarget_selectsTheAlphabeticallyFirstShortestMatch_buffer() { + // GIVEN a variety of registered dumpables and buffers + dumpManager.registerCriticalDumpable("d1x", dumpable1) + dumpManager.registerCriticalDumpable("d2x", dumpable2) + dumpManager.registerCriticalDumpable("az1x", dumpable3) + dumpManager.registerBuffer("b1x", buffer1) + dumpManager.registerBuffer("b2x", buffer2) + + // WHEN a dumpable is dumped by a suffix that matches multiple options + val args = arrayOf("x", "--tail", "14") + dumpHandler.dump(fd, pw, args) + + // THEN the alphabetically first dumpable/buffer (of the 3 letter names) is dumped + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2, never()).dump(any(), any()) + verify(dumpable3, never()).dump(any(), any()) + verify(buffer1).dump(pw, tailLength = 14) + verify(buffer2, never()).dump(any(), anyInt()) + } + + private class EmptyCoreStartable : CoreStartable { override fun start() {} } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt index 02555cfa783a0..6d5226f35e975 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt @@ -20,21 +20,17 @@ import androidx.test.filters.SmallTest import com.android.systemui.Dumpable import com.android.systemui.SysuiTestCase import com.android.systemui.log.LogBuffer -import com.android.systemui.util.mockito.any -import java.io.PrintWriter +import com.android.systemui.log.table.TableLogBuffer +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test import org.mockito.Mock -import org.mockito.Mockito.anyInt -import org.mockito.Mockito.never -import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest class DumpManagerTest : SysuiTestCase() { - @Mock private lateinit var pw: PrintWriter - @Mock private lateinit var dumpable1: Dumpable @Mock private lateinit var dumpable2: Dumpable @Mock private lateinit var dumpable3: Dumpable @@ -42,6 +38,9 @@ class DumpManagerTest : SysuiTestCase() { @Mock private lateinit var buffer1: LogBuffer @Mock private lateinit var buffer2: LogBuffer + @Mock private lateinit var table1: TableLogBuffer + @Mock private lateinit var table2: TableLogBuffer + private val dumpManager = DumpManager() @Before @@ -50,276 +49,144 @@ class DumpManagerTest : SysuiTestCase() { } @Test - fun testDumpTarget_dumpable() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerCriticalDumpable("dumpable1", dumpable1) - dumpManager.registerCriticalDumpable("dumpable2", dumpable2) - dumpManager.registerCriticalDumpable("dumpable3", dumpable3) - dumpManager.registerBuffer("buffer1", buffer1) - dumpManager.registerBuffer("buffer2", buffer2) - - // WHEN a dumpable is dumped explicitly - val args = arrayOf() - dumpManager.dumpTarget("dumpable2", pw, args, tailLength = 0) - - // THEN only the requested one has their dump() method called - verify(dumpable1, never()).dump(any(), any()) - verify(dumpable2).dump(pw, args) - verify(dumpable3, never()).dump(any(), any()) - verify(buffer1, never()).dump(any(), anyInt()) - verify(buffer2, never()).dump(any(), anyInt()) - } - - @Test - fun testDumpTarget_buffer() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerCriticalDumpable("dumpable1", dumpable1) - dumpManager.registerCriticalDumpable("dumpable2", dumpable2) - dumpManager.registerCriticalDumpable("dumpable3", dumpable3) - dumpManager.registerBuffer("buffer1", buffer1) - dumpManager.registerBuffer("buffer2", buffer2) - - // WHEN a buffer is dumped explicitly - val args = arrayOf() - dumpManager.dumpTarget("buffer1", pw, args, tailLength = 14) - - // THEN only the requested one has their dump() method called - verify(dumpable1, never()).dump(any(), any()) - verify(dumpable2, never()).dump(any(), any()) - verify(dumpable3, never()).dump(any(), any()) - verify(buffer1).dump(pw, tailLength = 14) - verify(buffer2, never()).dump(any(), anyInt()) - } - - @Test - fun testDumpableMatchingIsBasedOnEndOfTag() { - // GIVEN a dumpable registered to the manager - dumpManager.registerCriticalDumpable("com.android.foo.bar.dumpable1", dumpable1) - - // WHEN that module is dumped - val args = arrayOf() - dumpManager.dumpTarget("dumpable1", pw, arrayOf(), tailLength = 14) - - // THEN its dump() method is called - verify(dumpable1).dump(pw, args) - } - - @Test - fun testDumpTarget_selectsShortestNamedDumpable() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerCriticalDumpable("first-dumpable", dumpable1) - dumpManager.registerCriticalDumpable("scnd-dumpable", dumpable2) - dumpManager.registerCriticalDumpable("third-dumpable", dumpable3) - - // WHEN a dumpable is dumped by a suffix that matches multiple options - val args = arrayOf() - dumpManager.dumpTarget("dumpable", pw, args, tailLength = 0) - - // THEN the matching dumpable with the shorter name is dumped - verify(dumpable1, never()).dump(any(), any()) - verify(dumpable2).dump(pw, args) - verify(dumpable3, never()).dump(any(), any()) - } - - @Test - fun testDumpTarget_selectsShortestNamedBuffer() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerBuffer("first-buffer", buffer1) - dumpManager.registerBuffer("scnd-buffer", buffer2) - - // WHEN a dumpable is dumped by a suffix that matches multiple options - val args = arrayOf() - dumpManager.dumpTarget("buffer", pw, args, tailLength = 14) - - // THEN the matching buffer with the shorter name is dumped - verify(buffer1, never()).dump(any(), anyInt()) - verify(buffer2).dump(pw, tailLength = 14) - } - - @Test - fun testDumpTarget_selectsShortestNamedMatch_dumpable() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerCriticalDumpable("dumpable1", dumpable1) - dumpManager.registerCriticalDumpable("dumpable2", dumpable2) - dumpManager.registerCriticalDumpable("dumpable3", dumpable3) - dumpManager.registerBuffer("big-buffer1", buffer1) - dumpManager.registerBuffer("big-buffer2", buffer2) - - // WHEN a dumpable is dumped by a suffix that matches multiple options - val args = arrayOf() - dumpManager.dumpTarget("2", pw, args, tailLength = 14) - - // THEN the matching dumpable with the shorter name is dumped - verify(dumpable1, never()).dump(any(), any()) - verify(dumpable2).dump(pw, args) - verify(dumpable3, never()).dump(any(), any()) - verify(buffer1, never()).dump(any(), anyInt()) - verify(buffer2, never()).dump(any(), anyInt()) - } - - @Test - fun testDumpTarget_selectsShortestNamedMatch_buffer() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerCriticalDumpable("dumpable1", dumpable1) - dumpManager.registerCriticalDumpable("dumpable2", dumpable2) - dumpManager.registerCriticalDumpable("dumpable3", dumpable3) - dumpManager.registerBuffer("buffer1", buffer1) - dumpManager.registerBuffer("buffer2", buffer2) - - // WHEN a dumpable is dumped by a suffix that matches multiple options - val args = arrayOf() - dumpManager.dumpTarget("2", pw, args, tailLength = 14) - - // THEN the matching buffer with the shorter name is dumped - verify(dumpable1, never()).dump(any(), any()) - verify(dumpable2, never()).dump(any(), any()) - verify(dumpable3, never()).dump(any(), any()) - verify(buffer1, never()).dump(any(), anyInt()) - verify(buffer2).dump(pw, tailLength = 14) - } - - @Test - fun testDumpTarget_selectsTheAlphabeticallyFirstShortestMatch_dumpable() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerCriticalDumpable("d1x", dumpable1) - dumpManager.registerCriticalDumpable("d2x", dumpable2) - dumpManager.registerCriticalDumpable("a3x", dumpable3) - dumpManager.registerBuffer("ab1x", buffer1) - dumpManager.registerBuffer("b2x", buffer2) - - // WHEN a dumpable is dumped by a suffix that matches multiple options - val args = arrayOf() - dumpManager.dumpTarget("x", pw, args, tailLength = 14) - - // THEN the alphabetically first dumpable/buffer (of the 3 letter names) is dumped - verify(dumpable1, never()).dump(any(), any()) - verify(dumpable2, never()).dump(any(), any()) - verify(dumpable3).dump(pw, args) - verify(buffer1, never()).dump(any(), anyInt()) - verify(buffer2, never()).dump(any(), anyInt()) - } - - @Test - fun testDumpTarget_selectsTheAlphabeticallyFirstShortestMatch_buffer() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerCriticalDumpable("d1x", dumpable1) - dumpManager.registerCriticalDumpable("d2x", dumpable2) - dumpManager.registerCriticalDumpable("az1x", dumpable3) - dumpManager.registerBuffer("b1x", buffer1) - dumpManager.registerBuffer("b2x", buffer2) - - // WHEN a dumpable is dumped by a suffix that matches multiple options - val args = arrayOf() - dumpManager.dumpTarget("x", pw, args, tailLength = 14) - - // THEN the alphabetically first dumpable/buffer (of the 3 letter names) is dumped - verify(dumpable1, never()).dump(any(), any()) - verify(dumpable2, never()).dump(any(), any()) - verify(dumpable3, never()).dump(any(), any()) - verify(buffer1).dump(pw, tailLength = 14) - verify(buffer2, never()).dump(any(), anyInt()) - } - - @Test - fun testDumpDumpables() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerCriticalDumpable("dumpable1", dumpable1) - dumpManager.registerCriticalDumpable("dumpable2", dumpable2) - dumpManager.registerNormalDumpable("dumpable3", dumpable3) - dumpManager.registerBuffer("buffer1", buffer1) - dumpManager.registerBuffer("buffer2", buffer2) - - // WHEN a dumpable dump is requested - val args = arrayOf() - dumpManager.dumpDumpables(pw, args) - - // THEN all dumpables are dumped (both critical and normal) (and no dumpables) - verify(dumpable1).dump(pw, args) - verify(dumpable2).dump(pw, args) - verify(dumpable3).dump(pw, args) - verify(buffer1, never()).dump(any(), anyInt()) - verify(buffer2, never()).dump(any(), anyInt()) - } - - @Test - fun testDumpBuffers() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerCriticalDumpable("dumpable1", dumpable1) - dumpManager.registerCriticalDumpable("dumpable2", dumpable2) - dumpManager.registerNormalDumpable("dumpable3", dumpable3) - dumpManager.registerBuffer("buffer1", buffer1) - dumpManager.registerBuffer("buffer2", buffer2) - - // WHEN a buffer dump is requested - dumpManager.dumpBuffers(pw, tailLength = 1) - - // THEN all buffers are dumped (and no dumpables) - verify(dumpable1, never()).dump(any(), any()) - verify(dumpable2, never()).dump(any(), any()) - verify(dumpable3, never()).dump(any(), any()) - verify(buffer1).dump(pw, tailLength = 1) - verify(buffer2).dump(pw, tailLength = 1) - } - - @Test - fun testCriticalDump() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerCriticalDumpable("dumpable1", dumpable1) - dumpManager.registerCriticalDumpable("dumpable2", dumpable2) - dumpManager.registerNormalDumpable("dumpable3", dumpable3) - dumpManager.registerBuffer("buffer1", buffer1) - dumpManager.registerBuffer("buffer2", buffer2) - - // WHEN a critical dump is requested - val args = arrayOf() - dumpManager.dumpCritical(pw, args) - - // THEN only critical modules are dumped (and no buffers) - verify(dumpable1).dump(pw, args) - verify(dumpable2).dump(pw, args) - verify(dumpable3, never()).dump(any(), any()) - verify(buffer1, never()).dump(any(), anyInt()) - verify(buffer2, never()).dump(any(), anyInt()) - } - - @Test - fun testNormalDump() { - // GIVEN a variety of registered dumpables and buffers - dumpManager.registerCriticalDumpable("dumpable1", dumpable1) - dumpManager.registerCriticalDumpable("dumpable2", dumpable2) - dumpManager.registerNormalDumpable("dumpable3", dumpable3) - dumpManager.registerBuffer("buffer1", buffer1) - dumpManager.registerBuffer("buffer2", buffer2) - - // WHEN a normal dump is requested - val args = arrayOf() - dumpManager.dumpNormal(pw, args, tailLength = 2) - - // THEN the normal module and all buffers are dumped - verify(dumpable1, never()).dump(any(), any()) - verify(dumpable2, never()).dump(any(), any()) - verify(dumpable3).dump(pw, args) - verify(buffer1).dump(pw, tailLength = 2) - verify(buffer2).dump(pw, tailLength = 2) - } - - @Test - fun testUnregister() { - // GIVEN a variety of registered dumpables and buffers + fun testRegisterUnregister_dumpables() { + // GIVEN a variety of registered dumpables dumpManager.registerCriticalDumpable("dumpable1", dumpable1) dumpManager.registerCriticalDumpable("dumpable2", dumpable2) dumpManager.registerNormalDumpable("dumpable3", dumpable3) + // WHEN the collection is requested + var dumpables = dumpManager.getDumpables().map { it.dumpable } + + // THEN it contains the registered entries + assertThat(dumpables).containsExactly(dumpable1, dumpable2, dumpable3) + + // WHEN the dumpables are unregistered dumpManager.unregisterDumpable("dumpable2") dumpManager.unregisterDumpable("dumpable3") - // WHEN a dumpables dump is requested - val args = arrayOf() - dumpManager.dumpDumpables(pw, args) + // WHEN the dumpable collection is requests + dumpables = dumpManager.getDumpables().map { it.dumpable } - // THEN the unregistered dumpables (both normal and critical) are not dumped - verify(dumpable1).dump(pw, args) - verify(dumpable2, never()).dump(any(), any()) - verify(dumpable3, never()).dump(any(), any()) + // THEN it contains only the currently-registered entry + assertThat(dumpables).containsExactly(dumpable1) + } + + @Test + fun testRegister_buffers() { + // GIVEN a set of registered buffers + dumpManager.registerBuffer("buffer1", buffer1) + dumpManager.registerBuffer("buffer2", buffer2) + + // WHEN the collection is requested + val dumpables = dumpManager.getLogBuffers().map { it.buffer } + + // THEN it contains the registered entries + assertThat(dumpables).containsExactly(buffer1, buffer2) + } + + @Test + fun testRegister_tableLogBuffers() { + // GIVEN a set of registered buffers + dumpManager.registerTableLogBuffer("table1", table1) + dumpManager.registerTableLogBuffer("table2", table2) + + // WHEN the collection is requested + val tables = dumpManager.getTableLogBuffers().map { it.table } + + // THEN it contains the registered entries + assertThat(tables).containsExactly(table1, table2) + } + + @Test + fun registerDumpable_throwsWhenNameCannotBeAssigned() { + // GIVEN dumpable1 and buffer1 and table1 are registered + dumpManager.registerCriticalDumpable("dumpable1", dumpable1) + dumpManager.registerBuffer("buffer1", buffer1) + dumpManager.registerTableLogBuffer("table1", table1) + + // THEN an exception is thrown when trying to re-register a new dumpable under the same key + assertThrows(IllegalArgumentException::class.java) { + dumpManager.registerCriticalDumpable("dumpable1", dumpable2) + } + assertThrows(IllegalArgumentException::class.java) { + dumpManager.registerBuffer("buffer1", buffer2) + } + assertThrows(IllegalArgumentException::class.java) { + dumpManager.registerTableLogBuffer("table1", table2) + } + } + + @Test + fun registerDumpable_doesNotThrowWhenReRegistering() { + // GIVEN dumpable1 and buffer1 are registered + dumpManager.registerCriticalDumpable("dumpable1", dumpable1) + dumpManager.registerBuffer("buffer1", buffer1) + + // THEN no exception is thrown when trying to re-register a new dumpable under the same key + dumpManager.registerCriticalDumpable("dumpable1", dumpable1) + dumpManager.registerBuffer("buffer1", buffer1) + + // No exception thrown + } + + @Test + fun getDumpables_returnsSafeCollection() { + // GIVEN a variety of registered dumpables + dumpManager.registerCriticalDumpable("dumpable1", dumpable1) + dumpManager.registerCriticalDumpable("dumpable2", dumpable2) + dumpManager.registerNormalDumpable("dumpable3", dumpable3) + + // WHEN the collection is retrieved + val dumpables = dumpManager.getDumpables() + + // WHEN the collection changes from underneath + dumpManager.unregisterDumpable("dumpable1") + dumpManager.unregisterDumpable("dumpable2") + dumpManager.unregisterDumpable("dumpable3") + + // THEN new collections are empty + assertThat(dumpManager.getDumpables()).isEmpty() + + // AND the collection is still safe to use + assertThat(dumpables).hasSize(3) + } + + @Test + fun getBuffers_returnsSafeCollection() { + // GIVEN a set of registered buffers + dumpManager.registerBuffer("buffer1", buffer1) + dumpManager.registerBuffer("buffer2", buffer2) + + // WHEN the collection is requested + val buffers = dumpManager.getLogBuffers() + + // WHEN the collection changes + dumpManager.registerBuffer("buffer3", buffer1) + + // THEN the new entry is represented + assertThat(dumpManager.getLogBuffers()).hasSize(3) + + // AND the previous collection is unchanged + assertThat(buffers).hasSize(2) + } + + @Test + fun getTableBuffers_returnsSafeCollection() { + // GIVEN a set of registered buffers + dumpManager.registerTableLogBuffer("table1", table1) + dumpManager.registerTableLogBuffer("table2", table2) + + // WHEN the collection is requested + val tables = dumpManager.getTableLogBuffers() + + // WHEN the collection changes + dumpManager.registerTableLogBuffer("table3", table1) + + // THEN the new entry is represented + assertThat(dumpManager.getTableLogBuffers()).hasSize(3) + + // AND the previous collection is unchanged + assertThat(tables).hasSize(2) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt index cb38846a0514e..3ff72028d5ca7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt @@ -18,20 +18,14 @@ package com.android.systemui.dump import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpHandler.Companion.dump +import com.android.systemui.log.LogBuffer import com.android.systemui.util.io.FakeBasicFileAttributes import com.android.systemui.util.io.Files import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.mockito.Mock -import org.mockito.Mockito -import org.mockito.Mockito.never -import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations import java.io.BufferedWriter import java.io.ByteArrayOutputStream import java.io.IOException @@ -42,17 +36,29 @@ import java.nio.file.OpenOption import java.nio.file.Paths import java.nio.file.attribute.BasicFileAttributes import java.util.Arrays +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations @SmallTest class LogEulogizerTest : SysuiTestCase() { lateinit var eulogizer: LogBufferEulogizer - @Mock - lateinit var dumpManager: DumpManager + @Mock lateinit var dumpManager: DumpManager + @Mock lateinit var logBuffer1: LogBuffer + lateinit var logBufferEntry1: DumpsysEntry.LogBufferEntry + @Mock lateinit var logBuffer2: LogBuffer + lateinit var logBufferEntry2: DumpsysEntry.LogBufferEntry - @Mock - lateinit var files: Files + @Mock lateinit var files: Files private val clock = FakeSystemClock() @@ -67,37 +73,47 @@ class LogEulogizerTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) + logBufferEntry1 = DumpsysEntry.LogBufferEntry(logBuffer1, "logbuffer1") + logBufferEntry2 = DumpsysEntry.LogBufferEntry(logBuffer2, "logbuffer2") - eulogizer = - LogBufferEulogizer(dumpManager, clock, files, path, MIN_WRITE_GAP, MAX_READ_AGE) + eulogizer = LogBufferEulogizer(dumpManager, clock, files, path, MIN_WRITE_GAP, MAX_READ_AGE) Mockito.`when`(files.newBufferedWriter(eq(path), any(OpenOption::class.java))) - .thenReturn(fileWriter) + .thenReturn(fileWriter) Mockito.`when`( - files.readAttributes(eq(path), - eq(BasicFileAttributes::class.java), - any(LinkOption::class.java)) - ).thenReturn(fileAttrs) + files.readAttributes( + eq(path), + eq(BasicFileAttributes::class.java), + any(LinkOption::class.java) + ) + ) + .thenReturn(fileAttrs) Mockito.`when`(files.lines(eq(path))).thenReturn(Arrays.stream(FAKE_LINES)) + + whenever(dumpManager.getLogBuffers()).thenReturn(listOf(logBufferEntry1, logBufferEntry2)) } @Test fun testFileIsCreated() { // GIVEN that the log file doesn't already exist Mockito.`when`( - files.readAttributes(eq(path), - eq(BasicFileAttributes::class.java), - any(LinkOption::class.java)) - ).thenThrow(IOException("File not found")) + files.readAttributes( + eq(path), + eq(BasicFileAttributes::class.java), + any(LinkOption::class.java) + ) + ) + .thenThrow(IOException("File not found")) // WHEN .record() is called val exception = RuntimeException("Something bad happened") assertEquals(exception, eulogizer.record(exception)) // THEN the buffers are dumped to the file - verify(dumpManager).dumpBuffers(any(PrintWriter::class.java), Mockito.anyInt()) + verify(logBuffer1).dump(any(PrintWriter::class.java), anyInt()) + verify(logBuffer2).dump(any(PrintWriter::class.java), anyInt()) assertTrue(fileStream.toString().isNotEmpty()) } @@ -111,7 +127,8 @@ class LogEulogizerTest : SysuiTestCase() { assertEquals(exception, eulogizer.record(exception)) // THEN the buffers are dumped to the file - verify(dumpManager).dumpBuffers(any(PrintWriter::class.java), Mockito.anyInt()) + verify(logBuffer1).dump(any(PrintWriter::class.java), anyInt()) + verify(logBuffer2).dump(any(PrintWriter::class.java), anyInt()) assertTrue(fileStream.toString().isNotEmpty()) } @@ -125,7 +142,8 @@ class LogEulogizerTest : SysuiTestCase() { assertEquals(exception, eulogizer.record(exception)) // THEN the file isn't written to - verify(dumpManager, never()).dumpBuffers(any(PrintWriter::class.java), Mockito.anyInt()) + verify(logBuffer1, never()).dump(any(PrintWriter::class.java), anyInt()) + verify(logBuffer2, never()).dump(any(PrintWriter::class.java), anyInt()) assertTrue(fileStream.toString().isEmpty()) } @@ -161,9 +179,4 @@ class LogEulogizerTest : SysuiTestCase() { private const val MIN_WRITE_GAP = 10L private const val MAX_READ_AGE = 100L -private val FAKE_LINES = - arrayOf( - "First line", - "Second line", - "Third line" - ) +private val FAKE_LINES = arrayOf("First line", "Second line", "Third line")