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

Commit 08403647 authored by Ned Burns's avatar Ned Burns
Browse files

Split DumpManager into two

Splits DumpManager into two pieces:
[1] DumpHandler, which is responsible for parsing input args from either
    of our dumpable services and triggering the right kind of dumps.
[2] DumpManager, which is now largely just a registry of dumpables that
    exposes methods for dumping those dumpables.

This change is necessary to allow other clients to use the DumpManager
to dump to things other than bug reports.

Test: manual, atest
Bug: 112656837, 151317347
Change-Id: I6dcef9773c204c50fb4101f8fc355767b54f39bc
parent b90e71d4
Loading
Loading
Loading
Loading
+7 −7
Original line number Diff line number Diff line
@@ -28,7 +28,7 @@ import android.util.Slog;

import com.android.internal.os.BinderInternal;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.dump.DumpHandler;
import com.android.systemui.dump.SystemUIAuxiliaryDumpService;

import java.io.FileDescriptor;
@@ -39,15 +39,15 @@ import javax.inject.Inject;
public class SystemUIService extends Service {

    private final Handler mMainHandler;
    private final DumpManager mDumpManager;
    private final DumpHandler mDumpHandler;

    @Inject
    public SystemUIService(
            @Main Handler mainHandler,
            DumpManager dumpManager) {
            DumpHandler dumpHandler) {
        super();
        mMainHandler = mainHandler;
        mDumpManager = dumpManager;
        mDumpHandler = dumpHandler;
    }

    @Override
@@ -94,10 +94,10 @@ public class SystemUIService extends Service {
        String[] massagedArgs = args;
        if (args.length == 0) {
            massagedArgs = new String[] {
                    DumpManager.PRIORITY_ARG,
                    DumpManager.PRIORITY_ARG_CRITICAL};
                    DumpHandler.PRIORITY_ARG,
                    DumpHandler.PRIORITY_ARG_CRITICAL};
        }

        mDumpManager.dump(fd, pw, massagedArgs);
        mDumpHandler.dump(fd, pw, massagedArgs);
    }
}
+309 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.dump

import android.content.Context
import android.os.SystemClock
import android.os.Trace
import com.android.systemui.R
import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_CRITICAL
import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_HIGH
import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL
import com.android.systemui.log.LogBuffer
import java.io.FileDescriptor
import java.io.PrintWriter
import javax.inject.Inject

/**
 * Oversees SystemUI's output during bug reports (and dumpsys in general)
 *
 * Dump output is split into two sections, CRITICAL and NORMAL. In general, the CRITICAL section
 * contains all dumpables that were registered to the [DumpManager], while the NORMAL sections
 * contains all [LogBuffer]s (due to their length).
 *
 * The CRITICAL and NORMAL sections can be found within a bug report by searching for
 * "SERVICE com.android.systemui/.SystemUIService" and
 * "SERVICE com.android.systemui/.dump.SystemUIAuxiliaryDumpService", respectively.
 *
 * Finally, some or all of the dump can be triggered on-demand via adb (see below).
 *
 * ```
 * # For the following, let <invocation> be:
 * $ adb shell dumpsys activity service com.android.systemui/.SystemUIService
 *
 * # To dump specific target(s), specify one or more registered names:
 * $ <invocation> NotifCollection
 * $ <invocation> StatusBar FalsingManager BootCompleteCacheImpl
 *
 * # Log buffers can be dumped in the same way (and can even be mixed in with other dump targets,
 * # although it's not clear why one would want such a thing):
 * $ <invocation> NotifLog
 * $ <invocation> StatusBar NotifLog BootCompleteCacheImpl
 *
 * # If passing -t or --tail, shows only the last N lines of any log buffers:
 * $ <invocation> NotifLog --tail 100
 *
 * # Dump targets are matched using String.endsWith(), so dumpables that register using their
 * # fully-qualified class name can still be dumped using their short name:
 * $ <invocation> com.android.keyguard.KeyguardUpdateMonitor
 * $ <invocation> keyguard.KeyguardUpdateMonitor
 * $ <invocation> KeyguardUpdateMonitor
 *
 * # To dump all dumpables or all buffers:
 * $ <invocation> dumpables
 * $ <invocation> buffers
 *
 * # Finally, the following will simulate what we dump during the CRITICAL and NORMAL sections of a
 * # bug report:
 * $ <invocation> bugreport-critical
 * $ <invocation> bugreport-normal
 *
 * # And if you need to be reminded of this list of commands:
 * $ <invocation> -h
 * $ <invocation> --help
 * ```
 */
class DumpHandler @Inject constructor(
    private val context: Context,
    private val dumpManager: DumpManager
) {
    /**
     * Dump the diagnostics! Behavior can be controlled via [args].
     */
    fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) {
        Trace.beginSection("DumpManager#dump()")
        val start = SystemClock.uptimeMillis()

        val parsedArgs = try {
            parseArgs(args)
        } catch (e: ArgParseException) {
            pw.println(e.message)
            return
        }

        when (parsedArgs.dumpPriority) {
            PRIORITY_ARG_CRITICAL -> dumpCritical(fd, pw, parsedArgs)
            PRIORITY_ARG_NORMAL -> dumpNormal(pw, parsedArgs)
            else -> dumpParameterized(fd, pw, parsedArgs)
        }

        pw.println()
        pw.println("Dump took ${SystemClock.uptimeMillis() - start}ms")
        Trace.endSection()
    }

    private fun dumpParameterized(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
        when (args.command) {
            "bugreport-critical" -> dumpCritical(fd, pw, args)
            "bugreport-normal" -> dumpNormal(pw, args)
            "dumpables" -> dumpDumpables(fd, pw, args)
            "buffers" -> dumpBuffers(pw, args)
            "config" -> dumpConfig(pw)
            "help" -> dumpHelp(pw)
            else -> dumpTargets(args.nonFlagArgs, fd, pw, args)
        }
    }

    private fun dumpCritical(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
        dumpManager.dumpDumpables(fd, pw, args.rawArgs)
        dumpConfig(pw)
    }

    private fun dumpNormal(pw: PrintWriter, args: ParsedArgs) {
        dumpManager.dumpBuffers(pw, args.tailLength)
    }

    private fun dumpDumpables(fw: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
        if (args.listOnly) {
            dumpManager.listDumpables(pw)
        } else {
            dumpManager.dumpDumpables(fw, pw, args.rawArgs)
        }
    }

    private fun dumpBuffers(pw: PrintWriter, args: ParsedArgs) {
        if (args.listOnly) {
            dumpManager.listBuffers(pw)
        } else {
            dumpManager.dumpBuffers(pw, args.tailLength)
        }
    }

    private fun dumpTargets(
        targets: List<String>,
        fd: FileDescriptor,
        pw: PrintWriter,
        args: ParsedArgs
    ) {
        if (targets.isNotEmpty()) {
            for (target in targets) {
                dumpManager.dumpTarget(target, fd, pw, args.rawArgs, args.tailLength)
            }
        } else {
            if (args.listOnly) {
                pw.println("Dumpables:")
                dumpManager.listDumpables(pw)
                pw.println()

                pw.println("Buffers:")
                dumpManager.listBuffers(pw)
            } else {
                pw.println("Nothing to dump :(")
            }
        }
    }

    private fun dumpConfig(pw: PrintWriter) {
        pw.println("SystemUiServiceComponents configuration:")
        pw.print("vendor component: ")
        pw.println(context.resources.getString(R.string.config_systemUIVendorServiceComponent))
        dumpServiceList(pw, "global", R.array.config_systemUIServiceComponents)
        dumpServiceList(pw, "per-user", R.array.config_systemUIServiceComponentsPerUser)
    }

    private fun dumpServiceList(pw: PrintWriter, type: String, resId: Int) {
        val services: Array<String>? = context.resources.getStringArray(resId)
        pw.print(type)
        pw.print(": ")
        if (services == null) {
            pw.println("N/A")
            return
        }
        pw.print(services.size)
        pw.println(" services")
        for (i in services.indices) {
            pw.print("  ")
            pw.print(i)
            pw.print(": ")
            pw.println(services[i])
        }
    }

    private fun dumpHelp(pw: PrintWriter) {
        pw.println("Let <invocation> be:")
        pw.println("$ adb shell dumpsys activity service com.android.systemui/.SystemUIService")
        pw.println()

        pw.println("Most common usage:")
        pw.println("$ <invocation> <targets>")
        pw.println("$ <invocation> NotifLog")
        pw.println("$ <invocation> StatusBar FalsingManager BootCompleteCacheImpl")
        pw.println("etc.")
        pw.println()

        pw.println("Special commands:")
        pw.println("$ <invocation> dumpables")
        pw.println("$ <invocation> buffers")
        pw.println("$ <invocation> bugreport-critical")
        pw.println("$ <invocation> bugreport-normal")
        pw.println()

        pw.println("Targets can be listed:")
        pw.println("$ <invocation> --list")
        pw.println("$ <invocation> dumpables --list")
        pw.println("$ <invocation> buffers --list")
        pw.println()

        pw.println("Show only the most recent N lines of buffers")
        pw.println("$ <invocation> NotifLog --tail 30")
    }

    private fun parseArgs(args: Array<String>): ParsedArgs {
        val mutArgs = args.toMutableList()
        val pArgs = ParsedArgs(args, mutArgs)

        val iterator = mutArgs.iterator()
        while (iterator.hasNext()) {
            val arg = iterator.next()
            if (arg.startsWith("-")) {
                iterator.remove()
                when (arg) {
                    PRIORITY_ARG -> {
                        pArgs.dumpPriority = readArgument(iterator, PRIORITY_ARG) {
                            if (PRIORITY_OPTIONS.contains(it)) {
                                it
                            } else {
                                throw IllegalArgumentException()
                            }
                        }
                    }
                    "-t", "--tail" -> {
                        pArgs.tailLength = readArgument(iterator, arg) {
                            it.toInt()
                        }
                    }
                    "-l", "--list" -> {
                        pArgs.listOnly = true
                    }
                    "-h", "--help" -> {
                        pArgs.command = "help"
                    }
                    else -> {
                        throw ArgParseException("Unknown flag: $arg")
                    }
                }
            }
        }

        if (pArgs.command == null && mutArgs.isNotEmpty() && COMMANDS.contains(mutArgs[0])) {
            pArgs.command = mutArgs.removeAt(0)
        }

        return pArgs
    }

    private fun <T> readArgument(
        iterator: MutableIterator<String>,
        flag: String,
        parser: (arg: String) -> T
    ): T {
        if (!iterator.hasNext()) {
            throw ArgParseException("Missing argument for $flag")
        }
        val value = iterator.next()

        return try {
            parser(value).also { iterator.remove() }
        } catch (e: Exception) {
            throw ArgParseException("Invalid argument '$value' for flag $flag")
        }
    }

    companion object {
        const val PRIORITY_ARG = "--dump-priority"
        const val PRIORITY_ARG_CRITICAL = "CRITICAL"
        const val PRIORITY_ARG_HIGH = "HIGH"
        const val PRIORITY_ARG_NORMAL = "NORMAL"
    }
}

private val PRIORITY_OPTIONS =
        arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_HIGH, PRIORITY_ARG_NORMAL)

private val COMMANDS = arrayOf("bugreport-critical", "bugreport-normal", "buffers", "dumpables")

private class ParsedArgs(
    val rawArgs: Array<String>,
    val nonFlagArgs: List<String>
) {
    var dumpPriority: String? = null
    var tailLength: Int = 0
    var command: String? = null
    var listOnly = false
}

class ArgParseException(message: String) : Exception(message)
+43 −234
Original line number Diff line number Diff line
@@ -16,15 +16,8 @@

package com.android.systemui.dump

import android.content.Context
import android.os.SystemClock
import android.os.Trace
import android.util.ArrayMap
import com.android.systemui.Dumpable
import com.android.systemui.R
import com.android.systemui.dump.DumpManager.Companion.PRIORITY_ARG_CRITICAL
import com.android.systemui.dump.DumpManager.Companion.PRIORITY_ARG_HIGH
import com.android.systemui.dump.DumpManager.Companion.PRIORITY_ARG_NORMAL
import com.android.systemui.log.LogBuffer
import java.io.FileDescriptor
import java.io.PrintWriter
@@ -32,58 +25,16 @@ import javax.inject.Inject
import javax.inject.Singleton

/**
 * Oversees SystemUI's output during bug reports (and dumpsys in general)
 * Maintains a registry of things that should be dumped when a bug report is taken
 *
 * When a bug report is taken, SystemUI dumps various diagnostic information that we hope will be
 * useful for the eventual readers of the bug report. Code that wishes to participate in this dump
 * should register itself here.
 *
 * Dump output is split into two sections, CRITICAL and NORMAL. All dumpables registered via
 * [registerDumpable] appear in the CRITICAL section, while all [LogBuffer]s appear in the NORMAL
 * section (due to their length).
 *
 * The CRITICAL and NORMAL sections can be found within a bug report by searching for
 * "SERVICE com.android.systemui/.SystemUIService" and
 * "SERVICE com.android.systemui/.dump.SystemUIAuxiliaryDumpService", respectively.
 *
 * Finally, some or all of the dump can be triggered on-demand via adb (see below).
 *
 * ```
 * # For the following, let <invocation> be:
 * $ adb shell dumpsys activity service com.android.systemui/.SystemUIService
 *
 * # To dump specific target(s), specify one or more registered names:
 * $ <invocation> NotifCollection
 * $ <invocation> StatusBar FalsingManager BootCompleteCacheImpl
 *
 * # Log buffers can be dumped in the same way (and can even be mixed in with other dump targets,
 * # although it's not clear why one would want such a thing):
 * $ <invocation> NotifLog
 * $ <invocation> StatusBar NotifLog BootCompleteCacheImpl
 *
 * # If passing -t or --tail, shows only the last N lines of any log buffers:
 * $ <invocation> NotifLog --tail 100
 *
 * # Dump targets are matched using String.endsWith(), so dumpables that register using their
 * # fully-qualified class name can still be dumped using their short name:
 * $ <invocation> com.android.keyguard.KeyguardUpdateMonitor
 * $ <invocation> keyguard.KeyguardUpdateMonitor
 * $ <invocation> KeyguardUpdateMonitor
 *
 * # To dump all dumpables or all buffers:
 * $ <invocation> dumpables
 * $ <invocation> buffers
 *
 * Finally, the following will simulate what we dump during the CRITICAL and NORMAL sections of a
 * bug report:
 * $ <invocation> bugreport-critical
 * $ <invocation> bugreport-normal
 * ```
 * See [DumpHandler] for more information on how and when this information is dumped.
 */
@Singleton
class DumpManager @Inject constructor(
    private val context: Context
) {
class DumpManager @Inject constructor() {
    private val dumpables: MutableMap<String, RegisteredDumpable<Dumpable>> = ArrayMap()
    private val buffers: MutableMap<String, RegisteredDumpable<LogBuffer>> = ArrayMap()

@@ -97,10 +48,6 @@ class DumpManager @Inject constructor(
     */
    @Synchronized
    fun registerDumpable(name: String, module: Dumpable) {
        if (RESERVED_NAMES.contains(name)) {
            throw IllegalArgumentException("'$name' is reserved")
        }

        if (!canAssignToNameLocked(name, module)) {
            throw IllegalArgumentException("'$name' is already registered")
        }
@@ -128,76 +75,16 @@ class DumpManager @Inject constructor(
    }

    /**
     * Dump the diagnostics! Behavior can be controlled via [args].
     * Dumps the first dumpable or buffer whose registered name ends with [target]
     */
    @Synchronized
    fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) {
        Trace.beginSection("DumpManager#dump()")
        val start = SystemClock.uptimeMillis()

        val parsedArgs = try {
            parseArgs(args)
        } catch (e: ArgParseException) {
            pw.println(e.message)
            return
        }

        when (parsedArgs.dumpPriority) {
            PRIORITY_ARG_CRITICAL -> dumpCriticalLocked(fd, pw, parsedArgs)
            PRIORITY_ARG_NORMAL -> dumpNormalLocked(pw, parsedArgs)
            else -> dumpParameterizedLocked(fd, pw, parsedArgs)
        }

        pw.println()
        pw.println("Dump took ${SystemClock.uptimeMillis() - start}ms")
        Trace.endSection()
    }

    private fun dumpCriticalLocked(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
        dumpDumpablesLocked(fd, pw, args)
        dumpConfig(pw)
    }

    private fun dumpNormalLocked(pw: PrintWriter, args: ParsedArgs) {
        dumpBuffersLocked(pw, args)
    }

    private fun dumpParameterizedLocked(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
        when (args.command) {
            "bugreport-critical" -> dumpCriticalLocked(fd, pw, args)
            "bugreport-normal" -> dumpNormalLocked(pw, args)
            "dumpables" -> dumpDumpablesLocked(fd, pw, args)
            "buffers" -> dumpBuffersLocked(pw, args)
            else -> dumpTargetsLocked(args.nonFlagArgs, fd, pw, args)
        }
    }

    private fun dumpTargetsLocked(
        targets: List<String>,
        fd: FileDescriptor,
        pw: PrintWriter,
        args: ParsedArgs
    ) {
        if (targets.isEmpty()) {
            pw.println("Nothing to dump :(")
        } else {
            for (target in targets) {
                dumpTarget(target, fd, pw, args)
            }
        }
    }

    private fun dumpTarget(
    fun dumpTarget(
        target: String,
        fd: FileDescriptor,
        pw: PrintWriter,
        args: ParsedArgs
        args: Array<String>,
        tailLength: Int
    ) {
        if (target == "config") {
            dumpConfig(pw)
            return
        }

        for (dumpable in dumpables.values) {
            if (dumpable.name.endsWith(target)) {
                dumpDumpable(dumpable, fd, pw, args)
@@ -207,21 +94,49 @@ class DumpManager @Inject constructor(

        for (buffer in buffers.values) {
            if (buffer.name.endsWith(target)) {
                dumpBuffer(buffer, pw, args)
                dumpBuffer(buffer, pw, tailLength)
                return
            }
        }
    }

    private fun dumpDumpablesLocked(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
    /**
     * Dumps all registered dumpables to [pw]
     */
    @Synchronized
    fun dumpDumpables(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) {
        for (module in dumpables.values) {
            dumpDumpable(module, fd, pw, args)
        }
    }

    private fun dumpBuffersLocked(pw: PrintWriter, args: ParsedArgs) {
    /**
     * 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) {
            dumpBuffer(buffer, pw, args)
            pw.println(buffer.name)
        }
    }

@@ -229,139 +144,33 @@ class DumpManager @Inject constructor(
        dumpable: RegisteredDumpable<Dumpable>,
        fd: FileDescriptor,
        pw: PrintWriter,
        args: ParsedArgs
        args: Array<String>
    ) {
        pw.println()
        pw.println("${dumpable.name}:")
        pw.println("----------------------------------------------------------------------------")
        dumpable.dumpable.dump(fd, pw, args.rawArgs)
        dumpable.dumpable.dump(fd, pw, args)
    }

    private fun dumpBuffer(
        buffer: RegisteredDumpable<LogBuffer>,
        pw: PrintWriter,
        args: ParsedArgs
        tailLength: Int
    ) {
        pw.println()
        pw.println()
        pw.println("BUFFER ${buffer.name}:")
        pw.println("============================================================================")
        buffer.dumpable.dump(pw, args.tailLength)
    }

    private fun dumpConfig(pw: PrintWriter) {
        pw.println("SystemUiServiceComponents configuration:")
        pw.print("vendor component: ")
        pw.println(context.resources.getString(R.string.config_systemUIVendorServiceComponent))
        dumpServiceList(pw, "global", R.array.config_systemUIServiceComponents)
        dumpServiceList(pw, "per-user", R.array.config_systemUIServiceComponentsPerUser)
    }

    private fun dumpServiceList(pw: PrintWriter, type: String, resId: Int) {
        val services: Array<String>? = context.resources.getStringArray(resId)
        pw.print(type)
        pw.print(": ")
        if (services == null) {
            pw.println("N/A")
            return
        }
        pw.print(services.size)
        pw.println(" services")
        for (i in services.indices) {
            pw.print("  ")
            pw.print(i)
            pw.print(": ")
            pw.println(services[i])
        }
    }

    private fun parseArgs(args: Array<String>): ParsedArgs {
        val mutArgs = args.toMutableList()
        val pArgs = ParsedArgs(args, mutArgs)

        val iterator = mutArgs.iterator()
        while (iterator.hasNext()) {
            val arg = iterator.next()
            if (arg.startsWith("-")) {
                iterator.remove()
                when (arg) {
                    PRIORITY_ARG -> {
                        pArgs.dumpPriority = readArgument(iterator, PRIORITY_ARG) {
                            if (PRIORITY_OPTIONS.contains(it)) {
                                it
                            } else {
                                throw IllegalArgumentException()
                            }
                        }
                    }
                    "-t", "--tail" -> {
                        pArgs.tailLength = readArgument(iterator, "--tail") {
                            it.toInt()
                        }
                    }
                    else -> {
                        throw ArgParseException("Unknown flag: $arg")
                    }
                }
            }
        }

        if (mutArgs.isNotEmpty() && COMMANDS.contains(mutArgs[0])) {
            pArgs.command = mutArgs.removeAt(0)
        }

        return pArgs
    }

    private fun <T> readArgument(
        iterator: MutableIterator<String>,
        flag: String,
        parser: (arg: String) -> T
    ): T {
        if (!iterator.hasNext()) {
            throw ArgParseException("Missing argument for $flag")
        }
        val value = iterator.next()

        return try {
            parser(value).also { iterator.remove() }
        } catch (e: Exception) {
            throw ArgParseException("Invalid argument '$value' for flag $flag")
        }
        buffer.dumpable.dump(pw, tailLength)
    }

    private fun canAssignToNameLocked(name: String, newDumpable: Any): Boolean {
        val existingDumpable = dumpables[name]?.dumpable ?: buffers[name]?.dumpable
        return existingDumpable == null || newDumpable == existingDumpable
    }

    companion object {
        const val PRIORITY_ARG = "--dump-priority"
        const val PRIORITY_ARG_CRITICAL = "CRITICAL"
        const val PRIORITY_ARG_HIGH = "HIGH"
        const val PRIORITY_ARG_NORMAL = "NORMAL"
    }
}

private val PRIORITY_OPTIONS =
        arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_HIGH, PRIORITY_ARG_NORMAL)

private val COMMANDS = arrayOf("bugreport-critical", "bugreport-normal", "buffers", "dumpables")

private val RESERVED_NAMES = arrayOf("config", *COMMANDS)

private data class RegisteredDumpable<T>(
    val name: String,
    val dumpable: T
)

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

class ArgParseException(message: String) : Exception(message)
 No newline at end of file
+5 −5

File changed.

Preview size limit exceeded, changes collapsed.

+11 −9

File changed and moved.

Preview size limit exceeded, changes collapsed.