main

square/leakcanary

Last updated at: 29/12/2023 09:39

SharkCliCommand.kt

TLDR

The SharkCliCommand.kt file is a part of the Shark CLI (Command Line Interface) project. It defines the main command for the Shark CLI tool, providing options for heap dump analysis. The file includes methods for setting up the command, running it, and retrieving the heap dump file.

Methods

run

This method is responsible for executing the Shark CLI command. It checks the command options and sets the corresponding parameters based on the user input.

setupVerboseLogger

This method sets up a verbose logger for the Shark CLI tool. It creates a custom logger class that outputs log messages to the console.

retrieveHeapDumpFile

This method retrieves the heap dump file based on the provided command parameters. It returns the heap dump file either from a specified file path or by dumping the heap of a running process.

echoNewline

This method echoes a newline character to the console.

echo

This method echoes the specified message to the console. It allows customizing whether to append a newline, whether to output to the error stream, and the line separator to use.

runCommand

This method runs a command in the specified directory with the provided arguments. It waits for the command to finish and returns the output. If the command fails, it throws a CliktError with the error output.

Classes

None

package shark

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.CliktError
import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.core.UsageError
import com.github.ajalt.clikt.output.TermUi
import com.github.ajalt.clikt.parameters.groups.OptionGroup
import com.github.ajalt.clikt.parameters.groups.cooccurring
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.github.ajalt.clikt.parameters.options.versionOption
import com.github.ajalt.clikt.parameters.types.file
import shark.DumpProcessCommand.Companion.dumpHeap
import shark.SharkCliCommand.HeapDumpSource.HprofFileSource
import shark.SharkCliCommand.HeapDumpSource.ProcessSource
import shark.SharkLog.Logger
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.util.Properties

class SharkCliCommand : CliktCommand(
  name = "shark-cli",
  // This ASCII art is a remix of a shark from -David "TAZ" Baltazar- and chick from jgs.
  help = """
    |Version: $versionName
    |
    |```
    |$S                ^`.                 .=""=.
    |$S^_              \  \               / _  _ \
    |$S\ \             {   \             |  d  b  |
    |$S{  \           /     `~~~--__     \   /\   /
    |$S{   \___----~~'              `~~-_/'-=\/=-'\,
    |$S \                         /// a  `~.      \ \
    |$S / /~~~~-, ,__.    ,      ///  __,,,,)      \ |
    |$S \/      \/    `~~~;   ,---~~-_`/ \        / \/
    |$S                  /   /            '.    .'
    |$S                 '._.'             _|`~~`|_
    |$S                                   /|\  /|\
    |```
    """.trimMargin()
) {

  private class ProcessOptions : OptionGroup() {
    val processName by option(
      "--process", "-p",
      help = "Full or partial name of a process, e.g. \"example\" would match \"com.example.app\""
    ).required()

    val device by option(
      "-d", "--device", metavar = "ID", help = "device/emulator id"
    )
  }

  private val processOptions by ProcessOptions().cooccurring()

  private val obfuscationMappingPath by option(
    "-m", "--obfuscation-mapping", help = "path to obfuscation mapping file"
  ).file()

  private val verbose by option(
    help = "provide additional details as to what shark-cli is doing"
  ).flag("--no-verbose")

  private val heapDumpFile by option("--hprof", "-h", help = "path to a .hprof file").file(
    exists = true,
    folderOkay = false,
    readable = true
  )

  init {
    versionOption(versionName)
  }

  class CommandParams(
    val source: HeapDumpSource,
    val obfuscationMappingPath: File?
  )

  sealed class HeapDumpSource {
    class HprofFileSource(val file: File) : HeapDumpSource()
    class ProcessSource(
      val processName: String,
      val deviceId: String?
    ) : HeapDumpSource()
  }

  override fun run() {
    if (verbose) {
      setupVerboseLogger()
    }
    if (processOptions != null && heapDumpFile != null) {
      throw UsageError("Option --process cannot be used with --hprof")
    } else if (processOptions != null) {
      context.sharkCliParams = CommandParams(
        source = ProcessSource(processOptions!!.processName, processOptions!!.device),
        obfuscationMappingPath = obfuscationMappingPath
      )
    } else if (heapDumpFile != null) {
      context.sharkCliParams = CommandParams(
        source = HprofFileSource(heapDumpFile!!),
        obfuscationMappingPath = obfuscationMappingPath
      )
    } else {
      throw UsageError("Must provide one of --process, --hprof")
    }
  }

  private fun setupVerboseLogger() {
    class CLILogger : Logger {

      override fun d(message: String) {
        echo(message)
      }

      override fun d(
        throwable: Throwable,
        message: String
      ) {
        d("$message\n${getStackTraceString(throwable)}")
      }

      private fun getStackTraceString(throwable: Throwable): String {
        val stringWriter = StringWriter()
        val printWriter = PrintWriter(stringWriter, false)
        throwable.printStackTrace(printWriter)
        printWriter.flush()
        return stringWriter.toString()
      }
    }

    SharkLog.logger = CLILogger()
  }

  companion object {
    /** Zero width space */
    private const val S = '\u200b'

    var Context.sharkCliParams: CommandParams
      get() {
        var ctx: Context? = this
        while (ctx != null) {
          if (ctx.obj is CommandParams) return ctx.obj as CommandParams
          ctx = ctx.parent
        }
        throw IllegalStateException("CommandParams not found in Context.obj")
      }
      set(value) {
        obj = value
      }

    fun CliktCommand.retrieveHeapDumpFile(params: CommandParams): File {
      return when (val source = params.source) {
        is HprofFileSource -> source.file
        is ProcessSource -> dumpHeap(source.processName, source.deviceId)
      }
    }

    fun CliktCommand.echoNewline() {
      echo("")
    }

    /**
     * Copy of [CliktCommand.echo] to make it publicly visible and therefore accessible
     * from [CliktCommand] extension functions
     */
    fun CliktCommand.echo(
      message: Any?,
      trailingNewline: Boolean = true,
      err: Boolean = false,
      lineSeparator: String = context.console.lineSeparator
    ) {
      TermUi.echo(message, trailingNewline, err, context.console, lineSeparator)
    }

    fun runCommand(
      directory: File,
      vararg arguments: String
    ): String {
      val process = ProcessBuilder(*arguments)
        .directory(directory)
        .start()
        .also { it.waitFor() }

      // See https://github.com/square/leakcanary/issues/1711
      // On Windows, the process doesn't always exit; calling to readText() makes it finish, so
      // we're reading the output before checking for the exit value
      val output = process.inputStream.bufferedReader().readText()
      if (process.exitValue() != 0) {
        val command = arguments.joinToString(" ")
        val errorOutput = process.errorStream.bufferedReader()
          .readText()
        throw CliktError(
          "Failed command: '$command', error output:\n---\n$errorOutput---"
        )
      }
      return output
    }

    private val versionName = run {
      val properties = Properties()
      properties.load(
        SharkCliCommand::class.java.getResourceAsStream("/version.properties")
          ?: throw IllegalStateException("version.properties missing")
      )
      properties.getProperty("version_name") ?: throw IllegalStateException(
        "version_name property missing"
      )
    }
  }
}