• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package shark
2 
3 import com.github.ajalt.clikt.core.CliktCommand
4 import com.github.ajalt.clikt.core.CliktError
5 import com.github.ajalt.clikt.core.Context
6 import com.github.ajalt.clikt.core.UsageError
7 import com.github.ajalt.clikt.output.TermUi
8 import com.github.ajalt.clikt.parameters.groups.OptionGroup
9 import com.github.ajalt.clikt.parameters.groups.cooccurring
10 import com.github.ajalt.clikt.parameters.options.flag
11 import com.github.ajalt.clikt.parameters.options.option
12 import com.github.ajalt.clikt.parameters.options.required
13 import com.github.ajalt.clikt.parameters.options.versionOption
14 import com.github.ajalt.clikt.parameters.types.file
15 import shark.DumpProcessCommand.Companion.dumpHeap
16 import shark.SharkCliCommand.HeapDumpSource.HprofFileSource
17 import shark.SharkCliCommand.HeapDumpSource.ProcessSource
18 import shark.SharkLog.Logger
19 import java.io.File
20 import java.io.PrintWriter
21 import java.io.StringWriter
22 import java.util.Properties
23 import java.util.concurrent.TimeUnit.SECONDS
24 
25 class SharkCliCommand : CliktCommand(
26   name = "shark-cli",
27   // This ASCII art is a remix of a shark from -David "TAZ" Baltazar- and chick from jgs.
28   help = """
29     |Version: $versionName
30     |
31     |```
32     |$S                ^`.                 .=""=.
33     |$S^_              \  \               / _  _ \
34     |$S\ \             {   \             |  d  b  |
35     |$S{  \           /     `~~~--__     \   /\   /
36     |$S{   \___----~~'              `~~-_/'-=\/=-'\,
37     |$S \                         /// a  `~.      \ \
38     |$S / /~~~~-, ,__.    ,      ///  __,,,,)      \ |
39     |$S \/      \/    `~~~;   ,---~~-_`/ \        / \/
40     |$S                  /   /            '.    .'
41     |$S                 '._.'             _|`~~`|_
42     |$S                                   /|\  /|\
43     |```
44     """.trimMargin()
45 ) {
46 
47   private class ProcessOptions : OptionGroup() {
48     val processName by option(
49       "--process", "-p",
50       help = "Full or partial name of a process, e.g. \"example\" would match \"com.example.app\""
51     ).required()
52 
53     val device by option(
54       "-d", "--device", metavar = "ID", help = "device/emulator id"
55     )
56   }
57 
58   private val processOptions by ProcessOptions().cooccurring()
59 
60   private val obfuscationMappingPath by option(
61     "-m", "--obfuscation-mapping", help = "path to obfuscation mapping file"
62   ).file()
63 
64   private val verbose by option(
65     help = "provide additional details as to what shark-cli is doing"
66   ).flag("--no-verbose")
67 
68   private val heapDumpFile by option("--hprof", "-h", help = "path to a .hprof file").file(
69     exists = true,
70     folderOkay = false,
71     readable = true
72   )
73 
74   init {
75     versionOption(versionName)
76   }
77 
78   class CommandParams(
79     val source: HeapDumpSource,
80     val obfuscationMappingPath: File?
81   )
82 
83   sealed class HeapDumpSource {
84     class HprofFileSource(val file: File) : HeapDumpSource()
85     class ProcessSource(
86       val processName: String,
87       val deviceId: String?
88     ) : HeapDumpSource()
89   }
90 
runnull91   override fun run() {
92     if (verbose) {
93       setupVerboseLogger()
94     }
95     if (processOptions != null && heapDumpFile != null) {
96       throw UsageError("Option --process cannot be used with --hprof")
97     } else if (processOptions != null) {
98       context.sharkCliParams = CommandParams(
99         source = ProcessSource(processOptions!!.processName, processOptions!!.device),
100         obfuscationMappingPath = obfuscationMappingPath
101       )
102     } else if (heapDumpFile != null) {
103       context.sharkCliParams = CommandParams(
104         source = HprofFileSource(heapDumpFile!!),
105         obfuscationMappingPath = obfuscationMappingPath
106       )
107     } else {
108       throw UsageError("Must provide one of --process, --hprof")
109     }
110   }
111 
setupVerboseLoggernull112   private fun setupVerboseLogger() {
113     class CLILogger : Logger {
114 
115       override fun d(message: String) {
116         echo(message)
117       }
118 
119       override fun d(
120         throwable: Throwable,
121         message: String
122       ) {
123         d("$message\n${getStackTraceString(throwable)}")
124       }
125 
126       private fun getStackTraceString(throwable: Throwable): String {
127         val stringWriter = StringWriter()
128         val printWriter = PrintWriter(stringWriter, false)
129         throwable.printStackTrace(printWriter)
130         printWriter.flush()
131         return stringWriter.toString()
132       }
133     }
134 
135     SharkLog.logger = CLILogger()
136   }
137 
138   companion object {
139     /** Zero width space */
140     private const val S = '\u200b'
141 
142     var Context.sharkCliParams: CommandParams
143       get() {
144         var ctx: Context? = this
145         while (ctx != null) {
146           if (ctx.obj is CommandParams) return ctx.obj as CommandParams
147           ctx = ctx.parent
148         }
149         throw IllegalStateException("CommandParams not found in Context.obj")
150       }
151       set(value) {
152         obj = value
153       }
154 
CliktCommandnull155     fun CliktCommand.retrieveHeapDumpFile(params: CommandParams): File {
156       return when (val source = params.source) {
157         is HprofFileSource -> source.file
158         is ProcessSource -> dumpHeap(source.processName, source.deviceId)
159       }
160     }
161 
echoNewlinenull162     fun CliktCommand.echoNewline() {
163       echo("")
164     }
165 
166     /**
167      * Copy of [CliktCommand.echo] to make it publicly visible and therefore accessible
168      * from [CliktCommand] extension functions
169      */
CliktCommandnull170     fun CliktCommand.echo(
171       message: Any?,
172       trailingNewline: Boolean = true,
173       err: Boolean = false,
174       lineSeparator: String = context.console.lineSeparator
175     ) {
176       TermUi.echo(message, trailingNewline, err, context.console, lineSeparator)
177     }
178 
runCommandnull179     fun runCommand(
180       directory: File,
181       vararg arguments: String
182     ): String {
183       val process = ProcessBuilder(*arguments)
184         .directory(directory)
185         .start()
186         .also { it.waitFor(10, SECONDS) }
187 
188       // See https://github.com/square/leakcanary/issues/1711
189       // On Windows, the process doesn't always exit; calling to readText() makes it finish, so
190       // we're reading the output before checking for the exit value
191       val output = process.inputStream.bufferedReader().readText()
192       if (process.exitValue() != 0) {
193         val command = arguments.joinToString(" ")
194         val errorOutput = process.errorStream.bufferedReader()
195           .readText()
196         throw CliktError(
197           "Failed command: '$command', error output:\n---\n$errorOutput---"
198         )
199       }
200       return output
201     }
202 
<lambda>null203     private val versionName = run {
204       val properties = Properties()
205       properties.load(
206         SharkCliCommand::class.java.getResourceAsStream("/version.properties")
207           ?: throw IllegalStateException("version.properties missing")
208       )
209       properties.getProperty("version_name") ?: throw IllegalStateException(
210         "version_name property missing"
211       )
212     }
213   }
214 }