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 }