Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a001eb70 authored by Evan Laird's avatar Evan Laird
Browse files

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
parent a05a2b4c
Loading
Loading
Loading
Loading
+17 −2
Original line number Diff line number Diff line
@@ -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)) {
+202 −83
Original line number Diff line number Diff line
@@ -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,32 +85,21 @@ 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 {
        val parsedArgs =
            try {
                parseArgs(args)
            } catch (e: ArgParseException) {
                pw.println(e.message)
@@ -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) {
    private fun dumpDumpables(pw: PrintWriter, args: ParsedArgs) =
        dumpManager.getDumpables().run {
            if (args.listOnly) {
            dumpManager.listDumpables(pw)
                listTargetNames(this, pw)
            } else {
            dumpManager.dumpDumpables(pw, args.rawArgs)
                forEach { dumpable -> dumpDumpable(dumpable, pw, args.rawArgs) }
            }
        }

    private fun dumpBuffers(pw: PrintWriter, args: ParsedArgs) {
    private fun dumpBuffers(pw: PrintWriter, args: ParsedArgs) =
        dumpManager.getLogBuffers().run {
            if (args.listOnly) {
            dumpManager.listBuffers(pw)
                listTargetNames(this, pw)
            } else {
            dumpManager.dumpBuffers(pw, args.tailLength)
                forEach { buf -> dumpBuffer(buf, pw, args.tailLength) }
            }
        }

    private fun dumpProtoTargets(
            targets: List<String>,
            fd: FileDescriptor,
            args: ParsedArgs
    ) {
    private fun listTargetNames(targets: Collection<DumpsysEntry>, pw: PrintWriter) {
        for (target in targets) {
            pw.println(target.name)
        }
    }

    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,7 +333,8 @@ class DumpHandler @Inject constructor(
                iterator.remove()
                when (arg) {
                    PRIORITY_ARG -> {
                        pArgs.dumpPriority = readArgument(iterator, PRIORITY_ARG) {
                        pArgs.dumpPriority =
                            readArgument(iterator, PRIORITY_ARG) {
                                if (PRIORITY_OPTIONS.contains(it)) {
                                    it
                                } else {
@@ -300,15 +343,16 @@ class DumpHandler @Inject constructor(
                            }
                    }
                    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 class ParsedArgs(
    val rawArgs: Array<String>,
    val nonFlagArgs: List<String>
) {
private val COMMANDS =
    arrayOf("bugreport-critical", "bugreport-normal", "buffers", "dumpables", "config", "help")

private class ParsedArgs(val rawArgs: Array<String>, val nonFlagArgs: List<String>) {
    var dumpPriority: String? = null
    var tailLength: Int = 0
    var command: String? = null
+18 −187

File changed.

Preview size limit exceeded, changes collapsed.

+49 −0
Original line number Diff line number Diff line
/*
 * 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
}
+23 −20
Original line number Diff line number Diff line
@@ -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,7 +136,8 @@ class LogBufferEulogizer(
    }

    private fun getMillisSinceLastWrite(path: Path): Long {
        val stats = try {
        val stats =
            try {
                files.readAttributes(path, BasicFileAttributes::class.java)
            } catch (e: IOException) {
                // File doesn't exist
Loading