Rearchitect DumpManager to be registry-only

This CL attempts to rework DumpManager to be a registry-only class,
where DumpsysEntrys can be registered to participate in the system
dumpsys, but the overall collection can be inspected by commands such as
DumpHandler and LogBufferCommand

Test: DumpManagerTest
Test: DumpHandlerTest
Bug: 278094048
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:3737b7e34d74d1c13e1f6f6012d06372d3389caa)
Merged-In: Ic71e03fe54a7e07caaf8679a93d0e7e56606162a

Change-Id: Ic71e03fe54a7e07caaf8679a93d0e7e56606162a
This commit is contained in:
Evan Laird
2023-05-16 16:39:55 -04:00
parent a05a2b4c88
commit a001eb709d
8 changed files with 617 additions and 604 deletions

View File

@@ -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)) {

View File

@@ -20,12 +20,14 @@ 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.nano.SystemUIProtoDump
import com.android.systemui.log.LogBuffer
import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager
import com.google.protobuf.nano.MessageNano
import java.io.BufferedOutputStream
import java.io.FileDescriptor
@@ -41,9 +43,9 @@ import javax.inject.Provider
* 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.
* 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).
*
@@ -83,37 +85,26 @@ import javax.inject.Provider
* $ <invocation> --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<Class<*>, Provider<CoreStartable>>,
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<String>) {
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)
@@ -147,44 +138,69 @@ 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)
}
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().run {
if (args.listOnly) {
listTargetNames(this, pw)
} else {
forEach { dumpable -> dumpDumpable(dumpable, pw, args.rawArgs) }
}
}
private fun dumpBuffers(pw: PrintWriter, args: ParsedArgs) =
dumpManager.getLogBuffers().run {
if (args.listOnly) {
listTargetNames(this, pw)
} else {
forEach { buf -> dumpBuffer(buf, pw, args.tailLength) }
}
}
private fun listTargetNames(targets: Collection<DumpsysEntry>, 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<String>,
fd: FileDescriptor,
args: ParsedArgs
) {
private fun dumpProtoTargets(targets: List<String>, 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,62 @@ class DumpHandler @Inject constructor(
}
}
private fun dumpTargets(
targets: List<String>,
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<String>, 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()
targets.forEach { target ->
findTargetInCollection(target, dumpables, buffers)?.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<DumpableEntry>,
logBuffers: Collection<LogBufferEntry>,
) =
sequence {
findBestTargetMatch(dumpables, target)?.let { yield(it) }
findBestTargetMatch(logBuffers, target)?.let { yield(it) }
}
.sortedBy { it.name }
.minByOrNull { it.name.length }
private fun dumpDumpable(entry: DumpableEntry, pw: PrintWriter, args: Array<String>) {
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 dumpConfig(pw: PrintWriter) {
pw.println("SystemUiServiceComponents configuration:")
pw.print("vendor component: ")
pw.println(context.resources.getString(R.string.config_systemUIVendorServiceComponent))
val services: MutableList<String> = startables.keys
.map({ cls: Class<*> -> cls.simpleName })
.toMutableList()
val services: MutableList<String> =
startables.keys.map({ cls: Class<*> -> cls.simpleName }).toMutableList()
services.add(context.resources.getString(R.string.config_systemUIVendorServiceComponent))
dumpServiceList(pw, "global", services.toTypedArray())
@@ -291,24 +333,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 +389,104 @@ 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)
}
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<DumpsysEntry>, target: String) =
c.asSequence().filter { it.name.endsWith(target) }.minByOrNull { it.name.length }
private fun findBestProtoTargetMatch(
c: Collection<DumpableEntry>,
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 -> {
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 [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)
}
}
/** Format [entries] in a dumpsys-appropriate way, using [pw] */
fun dumpEntries(entries: Collection<DumpsysEntry>, pw: PrintWriter) {
entries.forEach { it.dump(pw) }
}
}
}
private val PRIORITY_OPTIONS = arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_NORMAL)
private val COMMANDS = arrayOf(
"bugreport-critical",
"bugreport-normal",
"buffers",
"dumpables",
"config",
"help"
)
private val COMMANDS =
arrayOf("bugreport-critical", "bugreport-normal", "buffers", "dumpables", "config", "help")
private class ParsedArgs(
val rawArgs: Array<String>,
val nonFlagArgs: List<String>
) {
private class ParsedArgs(val rawArgs: Array<String>, val nonFlagArgs: List<String>) {
var dumpPriority: String? = null
var tailLength: Int = 0
var command: String? = null

View File

@@ -18,9 +18,9 @@ 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.log.LogBuffer
import java.io.PrintWriter
import java.util.TreeMap
import javax.inject.Inject
import javax.inject.Singleton
@@ -37,8 +37,8 @@ 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<String, RegisteredDumpable<Dumpable>> = TreeMap()
private val buffers: MutableMap<String, RegisteredDumpable<LogBuffer>> = TreeMap()
private val dumpables: MutableMap<String, DumpableEntry> = TreeMap()
private val buffers: MutableMap<String, LogBufferEntry> = TreeMap()
/** See [registerCriticalDumpable]. */
fun registerCriticalDumpable(module: Dumpable) {
@@ -77,14 +77,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 +98,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,17 +110,13 @@ 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)) {
@@ -129,198 +125,33 @@ open class DumpManager @Inject constructor() {
// All buffers must be priority NORMAL, not CRITICAL, because they often contain a lot of
// data.
buffers[name] = RegisteredDumpable(name, buffer, DumpPriority.NORMAL)
buffers[name] = LogBufferEntry(buffer, name, DumpPriority.NORMAL)
}
/**
* Dumps the alphabetically first, shortest-named dumpable or buffer whose registered name ends
* with [target].
*/
@Synchronized
fun dumpTarget(
target: String,
pw: PrintWriter,
args: Array<String>,
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<DumpableEntry> = dumpables.values.toList()
@Synchronized
fun dumpProtoTarget(
target: String,
protoDump: SystemUIProtoDump,
args: Array<String>
) {
findBestProtoTargetMatch(dumpables, target)?.let {
dumpProtoDumpable(it, protoDump, args)
}
}
@Synchronized
fun dumpProtoDumpables(
systemUIProtoDump: SystemUIProtoDump,
args: Array<String>
) {
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<String>) {
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<String>, 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<String>) {
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)
}
}
@Synchronized fun getLogBuffers(): Collection<LogBufferEntry> = buffers.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<Dumpable>,
pw: PrintWriter,
args: Array<String>
) {
pw.println()
pw.println("${dumpable.name}:")
pw.println("----------------------------------------------------------------------------")
dumpable.dumpable.dump(pw, args)
}
private fun dumpBuffer(
buffer: RegisteredDumpable<LogBuffer>,
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<String>
) {
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
return existingDumpable == null || newDumpable == existingDumpable
}
private fun <V : Any> findBestTargetMatch(map: Map<String, V>, target: String): V? = map
.asSequence()
.filter { it.key.endsWith(target) }
.minByOrNull { it.key.length }
?.value
private fun findBestProtoTargetMatch(
map: Map<String, RegisteredDumpable<Dumpable>>,
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<T>(
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.

View File

@@ -0,0 +1,49 @@
/*
* 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
/**
* 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,
override val priority: DumpPriority,
) : DumpsysEntry
}

View File

@@ -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)
private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)

View File

@@ -22,7 +22,6 @@ 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.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.google.common.truth.Truth.assertThat
@@ -45,8 +44,6 @@ class DumpHandlerTest : SysuiTestCase() {
@Mock
private lateinit var logBufferEulogizer: LogBufferEulogizer
@Mock
private lateinit var exceptionHandlerManager: UncaughtExceptionPreHandlerManager
@Mock
private lateinit var pw: PrintWriter
@@ -83,16 +80,15 @@ 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)
@@ -113,7 +109,7 @@ class DumpHandlerTest : SysuiTestCase() {
@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")
@@ -183,6 +179,49 @@ class DumpHandlerTest : SysuiTestCase() {
assertThat(stringWriter.toString()).contains(EmptyCoreStartable::class.java.simpleName)
}
@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
val args = arrayOf("buffers", "--tail", "1")
dumpHandler.dump(fd, pw, args)
// 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 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("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())
}
@Test
fun testDumpAllProtoDumpables() {
dumpManager.registerDumpable("protoDumpable1", protoDumpable1)
@@ -207,6 +246,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() {}
}

View File

@@ -20,21 +20,16 @@ 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.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
@@ -50,276 +45,108 @@ 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<String>()
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<String>()
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<String>()
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<String>()
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<String>()
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<String>()
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<String>()
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<String>()
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<String>()
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<String>()
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<String>()
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<String>()
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<String>()
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 registerDumpable_throwsWhenNameCannotBeAssigned() {
// GIVEN dumpable1 and buffer1 are registered
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerBuffer("buffer1", buffer1)
// 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)
}
}
@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)
}
}

View File

@@ -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", DumpPriority.NORMAL)
logBufferEntry2 = DumpsysEntry.LogBufferEntry(logBuffer2, "logbuffer2", DumpPriority.NORMAL)
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")