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

Commit 0bc1b83c authored by Ned Burns's avatar Ned Burns Committed by Automerger Merge Worker
Browse files

Merge changes I22ff91e8,I6dcef977 into rvc-dev am: 52c8bac7 am: 38e10c82

Change-Id: I86cf1ca709205f7f05d1349fb69df71112677915
parents 5340c666 38e10c82
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);
    }
}
+311 −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,
    private val logBufferEulogizer: LogBufferEulogizer
) {
    /**
     * 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)
        logBufferEulogizer.readEulogyIfPresent(pw)
    }

    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
+150 −0

File added.

Preview size limit exceeded, changes collapsed.

+5 −5

File changed.

Preview size limit exceeded, changes collapsed.

Loading