1 /*
<lambda>null2 * Copyright (C) 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.protolog.tool
18
19 import com.android.internal.protolog.common.LogLevel
20 import com.android.internal.protolog.common.ProtoLog
21 import com.android.internal.protolog.common.ProtoLogToolInjected
22 import com.android.protolog.tool.CommandOptions.Companion.USAGE
23 import com.github.javaparser.ParseProblemException
24 import com.github.javaparser.ParserConfiguration
25 import com.github.javaparser.StaticJavaParser
26 import com.github.javaparser.ast.CompilationUnit
27 import com.github.javaparser.ast.Modifier
28 import com.github.javaparser.ast.NodeList
29 import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
30 import com.github.javaparser.ast.body.InitializerDeclaration
31 import com.github.javaparser.ast.expr.ArrayAccessExpr
32 import com.github.javaparser.ast.expr.ArrayCreationExpr
33 import com.github.javaparser.ast.expr.ArrayInitializerExpr
34 import com.github.javaparser.ast.expr.AssignExpr
35 import com.github.javaparser.ast.expr.BooleanLiteralExpr
36 import com.github.javaparser.ast.expr.Expression
37 import com.github.javaparser.ast.expr.FieldAccessExpr
38 import com.github.javaparser.ast.expr.IntegerLiteralExpr
39 import com.github.javaparser.ast.expr.MethodCallExpr
40 import com.github.javaparser.ast.expr.MethodReferenceExpr
41 import com.github.javaparser.ast.expr.NameExpr
42 import com.github.javaparser.ast.expr.NullLiteralExpr
43 import com.github.javaparser.ast.expr.ObjectCreationExpr
44 import com.github.javaparser.ast.expr.SimpleName
45 import com.github.javaparser.ast.expr.StringLiteralExpr
46 import com.github.javaparser.ast.stmt.BlockStmt
47 import java.io.File
48 import java.io.FileInputStream
49 import java.io.FileNotFoundException
50 import java.io.FileOutputStream
51 import java.io.OutputStream
52 import java.time.LocalDateTime
53 import java.util.concurrent.ExecutorService
54 import java.util.concurrent.Executors
55 import java.util.jar.JarOutputStream
56 import java.util.zip.ZipEntry
57 import kotlin.math.absoluteValue
58 import kotlin.system.exitProcess
59
60 object ProtoLogTool {
61 const val PROTOLOG_IMPL_SRC_PATH =
62 "frameworks/base/core/java/com/android/internal/protolog/ProtoLogImpl.java"
63
64 data class LogCall(
65 val messageString: String,
66 val logLevel: LogLevel,
67 val logGroup: LogGroup,
68 val position: String
69 )
70
71 private fun showHelpAndExit() {
72 println(USAGE)
73 exitProcess(-1)
74 }
75
76 private fun containsProtoLogText(source: String, protoLogClassName: String): Boolean {
77 val protoLogSimpleClassName = protoLogClassName.substringAfterLast('.')
78 return source.contains(protoLogSimpleClassName)
79 }
80
81 private fun zipEntry(path: String): ZipEntry {
82 val entry = ZipEntry(path)
83 // Use a constant time to improve the cachability of build actions.
84 entry.timeLocal = LocalDateTime.of(2008, 1, 1, 0, 0, 0)
85 return entry
86 }
87
88 private fun processClasses(command: CommandOptions) {
89 // A deterministic hash based on the group jar path and the source files we are processing.
90 // The hash is required to make sure different ProtoLogImpls don't conflict.
91 val generationHash = (command.javaSourceArgs.toTypedArray() + command.protoLogGroupsJarArg)
92 .contentHashCode().absoluteValue
93
94 // Need to generate a new impl class to inject static constants into the class.
95 val generatedProtoLogImplClass =
96 "com.android.internal.protolog.ProtoLogImpl_$generationHash"
97
98 val groups = injector.readLogGroups(
99 command.protoLogGroupsJarArg,
100 command.protoLogGroupsClassNameArg)
101 val out = injector.fileOutputStream(command.outputSourceJarArg)
102 val outJar = JarOutputStream(out)
103 val processor = ProtoLogCallProcessorImpl(
104 command.protoLogClassNameArg,
105 command.protoLogGroupsClassNameArg,
106 groups)
107
108 val protologImplName = generatedProtoLogImplClass.split(".").last()
109 val protologImplPath = "gen/${generatedProtoLogImplClass.split(".")
110 .joinToString("/")}.java"
111 outJar.putNextEntry(zipEntry(protologImplPath))
112
113 outJar.write(generateProtoLogImpl(protologImplName, command.viewerConfigFilePathArg,
114 command.legacyViewerConfigFilePathArg, command.legacyOutputFilePath,
115 groups, command.protoLogGroupsClassNameArg).toByteArray())
116
117 val executor = newThreadPool()
118
119 try {
120 command.javaSourceArgs.map { path ->
121 executor.submitCallable {
122 val transformer = SourceTransformer(generatedProtoLogImplClass, processor)
123 val file = File(path)
124 val text = injector.readText(file)
125 val outSrc = try {
126 val code = tryParse(text, path)
127 if (containsProtoLogText(text, ProtoLog::class.java.simpleName)) {
128 transformer.processClass(text, path, packagePath(file, code), code)
129 } else {
130 text
131 }
132 } catch (ex: ParsingException) {
133 // If we cannot parse this file, skip it (and log why). Compilation will
134 // fail in a subsequent build step.
135 injector.reportParseError(ex)
136 text
137 }
138 path to outSrc
139 }
140 }.map { future ->
141 val (path, outSrc) = future.get()
142 outJar.putNextEntry(zipEntry(path))
143 outJar.write(outSrc.toByteArray())
144 outJar.closeEntry()
145 }
146 } finally {
147 executor.shutdown()
148 }
149
150 outJar.close()
151 out.close()
152 }
153
154 private fun generateProtoLogImpl(
155 protoLogImplGenName: String,
156 viewerConfigFilePath: String,
157 legacyViewerConfigFilePath: String?,
158 legacyOutputFilePath: String?,
159 groups: Map<String, LogGroup>,
160 protoLogGroupsClassName: String,
161 ): String {
162 val file = File(PROTOLOG_IMPL_SRC_PATH)
163
164 val text = try {
165 injector.readText(file)
166 } catch (e: FileNotFoundException) {
167 throw RuntimeException("Expected to find '$PROTOLOG_IMPL_SRC_PATH' but file was not " +
168 "included in source for the ProtoLog Tool to process.")
169 }
170
171 val code = tryParse(text, PROTOLOG_IMPL_SRC_PATH)
172
173 val classDeclarations = code.findAll(ClassOrInterfaceDeclaration::class.java)
174 require(classDeclarations.size == 1) { "Expected exactly one class declaration" }
175 val classDeclaration = classDeclarations[0]
176
177 val classNameNode = classDeclaration.findFirst(SimpleName::class.java).get()
178 classNameNode.setId(protoLogImplGenName)
179
180 injectCacheClass(classDeclaration, groups, protoLogGroupsClassName)
181
182 injectConstants(classDeclaration,
183 viewerConfigFilePath, legacyViewerConfigFilePath, legacyOutputFilePath, groups,
184 protoLogGroupsClassName)
185
186 return code.toString()
187 }
188
189 private fun injectConstants(
190 classDeclaration: ClassOrInterfaceDeclaration,
191 viewerConfigFilePath: String,
192 legacyViewerConfigFilePath: String?,
193 legacyOutputFilePath: String?,
194 groups: Map<String, LogGroup>,
195 protoLogGroupsClassName: String
196 ) {
197 classDeclaration.fields.forEach { field ->
198 field.getAnnotationByClass(ProtoLogToolInjected::class.java)
199 .ifPresent { annotationExpr ->
200 if (annotationExpr.isSingleMemberAnnotationExpr) {
201 val valueName = annotationExpr.asSingleMemberAnnotationExpr()
202 .memberValue.asNameExpr().name.asString()
203 when (valueName) {
204 ProtoLogToolInjected.Value.VIEWER_CONFIG_PATH.name -> {
205 field.setFinal(true)
206 field.variables.first()
207 .setInitializer(StringLiteralExpr(viewerConfigFilePath))
208 }
209 ProtoLogToolInjected.Value.LEGACY_OUTPUT_FILE_PATH.name -> {
210 field.setFinal(true)
211 field.variables.first()
212 .setInitializer(legacyOutputFilePath?.let {
213 StringLiteralExpr(it)
214 } ?: NullLiteralExpr())
215 }
216 ProtoLogToolInjected.Value.LEGACY_VIEWER_CONFIG_PATH.name -> {
217 field.setFinal(true)
218 field.variables.first()
219 .setInitializer(legacyViewerConfigFilePath?.let {
220 StringLiteralExpr(it)
221 } ?: NullLiteralExpr())
222 }
223 ProtoLogToolInjected.Value.LOG_GROUPS.name -> {
224 val initializerBlockStmt = BlockStmt()
225 for (group in groups) {
226 initializerBlockStmt.addStatement(
227 MethodCallExpr()
228 .setName("put")
229 .setArguments(
230 NodeList(StringLiteralExpr(group.key),
231 FieldAccessExpr()
232 .setScope(
233 NameExpr(
234 protoLogGroupsClassName
235 ))
236 .setName(group.value.name)))
237 )
238 group.key
239 }
240
241 val treeMapCreation = ObjectCreationExpr()
242 .setType("TreeMap<String, IProtoLogGroup>")
243 .setAnonymousClassBody(NodeList(
244 InitializerDeclaration().setBody(
245 initializerBlockStmt
246 )
247 ))
248
249 field.setFinal(true)
250 field.variables.first().setInitializer(treeMapCreation)
251 }
252 ProtoLogToolInjected.Value.CACHE_UPDATER.name -> {
253 field.setFinal(true)
254 field.variables.first().setInitializer(MethodReferenceExpr()
255 .setScope(NameExpr("Cache"))
256 .setIdentifier("update"))
257 }
258 else -> error("Unhandled ProtoLogToolInjected value: $valueName.")
259 }
260 }
261 }
262 }
263 }
264
265 private fun injectCacheClass(
266 classDeclaration: ClassOrInterfaceDeclaration,
267 groups: Map<String, LogGroup>,
268 protoLogGroupsClassName: String,
269 ) {
270 val cacheClass = ClassOrInterfaceDeclaration()
271 .setName("Cache")
272 .setPublic(true)
273 .setStatic(true)
274 for (group in groups) {
275 val nodeList = NodeList<Expression>()
276 val defaultVal = BooleanLiteralExpr(group.value.textEnabled || group.value.enabled)
277 repeat(LogLevel.entries.size) { nodeList.add(defaultVal) }
278 cacheClass.addFieldWithInitializer(
279 "boolean[]",
280 "${group.key}_enabled",
281 ArrayCreationExpr().setElementType("boolean[]").setInitializer(
282 ArrayInitializerExpr().setValues(nodeList)
283 ),
284 Modifier.Keyword.PUBLIC,
285 Modifier.Keyword.STATIC
286 )
287 }
288
289 val updateBlockStmt = BlockStmt()
290 for (group in groups) {
291 for (level in LogLevel.entries) {
292 updateBlockStmt.addStatement(
293 AssignExpr()
294 .setTarget(
295 ArrayAccessExpr()
296 .setName(NameExpr("${group.key}_enabled"))
297 .setIndex(IntegerLiteralExpr(level.ordinal))
298 ).setValue(
299 MethodCallExpr()
300 .setName("isEnabled")
301 .setArguments(NodeList(
302 FieldAccessExpr()
303 .setScope(NameExpr(protoLogGroupsClassName))
304 .setName(group.value.name),
305 FieldAccessExpr()
306 .setScope(NameExpr("LogLevel"))
307 .setName(level.toString()),
308 ))
309 )
310 )
311 }
312 }
313
314 cacheClass.addMethod("update").setPrivate(true).setStatic(true)
315 .setBody(updateBlockStmt)
316
317 classDeclaration.addMember(cacheClass)
318 }
319
320 private fun tryParse(code: String, fileName: String): CompilationUnit {
321 try {
322 return StaticJavaParser.parse(code)
323 } catch (ex: ParseProblemException) {
324 val problem = ex.problems.first()
325 throw ParsingException("Java parsing error: ${problem.verboseMessage}",
326 ParsingContext(fileName, problem.location.orElse(null)
327 ?.begin?.range?.orElse(null)?.begin?.line
328 ?: 0))
329 }
330 }
331
332 class LogCallRegistry {
333 private val statements = mutableMapOf<LogCall, Long>()
334
335 fun addLogCalls(calls: List<LogCall>) {
336 calls.forEach { logCall ->
337 if (logCall.logGroup.enabled) {
338 statements.putIfAbsent(logCall,
339 CodeUtils.hash(logCall.position, logCall.messageString,
340 logCall.logLevel, logCall.logGroup))
341 }
342 }
343 }
344
345 fun getStatements(): Map<LogCall, Long> {
346 return statements
347 }
348 }
349
350 interface ProtologViewerConfigBuilder {
351 fun build(statements: Map<LogCall, Long>): ByteArray
352 }
353
354 private fun viewerConf(command: CommandOptions) {
355 val groups = injector.readLogGroups(
356 command.protoLogGroupsJarArg,
357 command.protoLogGroupsClassNameArg)
358 val processor = ProtoLogCallProcessorImpl(command.protoLogClassNameArg,
359 command.protoLogGroupsClassNameArg, groups)
360 val outputType = command.viewerConfigTypeArg
361
362 val configBuilder: ProtologViewerConfigBuilder = when (outputType.lowercase()) {
363 "json" -> ViewerConfigJsonBuilder()
364 "proto" -> ViewerConfigProtoBuilder()
365 else -> error("Invalid output type provide. Provided '$outputType'.")
366 }
367
368 val executor = newThreadPool()
369
370 val logCallRegistry = LogCallRegistry()
371
372 try {
373 command.javaSourceArgs.map { path ->
374 executor.submitCallable {
375 val file = File(path)
376 val text = injector.readText(file)
377 if (containsProtoLogText(text, command.protoLogClassNameArg)) {
378 try {
379 val code = tryParse(text, path)
380 findLogCalls(code, path, packagePath(file, code), processor)
381 } catch (ex: ParsingException) {
382 // If we cannot parse this file, skip it (and log why). Compilation will
383 // fail in a subsequent build step.
384 injector.reportParseError(ex)
385 null
386 }
387 } else {
388 null
389 }
390 }
391 }.forEach { future ->
392 logCallRegistry.addLogCalls(future.get() ?: return@forEach)
393 }
394 } finally {
395 executor.shutdown()
396 }
397
398 val outFile = injector.fileOutputStream(command.viewerConfigFileNameArg)
399 outFile.write(configBuilder.build(logCallRegistry.getStatements()))
400 outFile.close()
401 }
402
403 private fun findLogCalls(
404 unit: CompilationUnit,
405 path: String,
406 packagePath: String,
407 processor: ProtoLogCallProcessorImpl
408 ): List<LogCall> {
409 val calls = mutableListOf<LogCall>()
410 val logCallVisitor = object : ProtoLogCallVisitor {
411 override fun processCall(
412 call: MethodCallExpr,
413 messageString: String,
414 level: LogLevel,
415 group: LogGroup
416 ) {
417 val logCall = LogCall(messageString, level, group, packagePath)
418 calls.add(logCall)
419 }
420 }
421 processor.process(unit, logCallVisitor, path)
422
423 return calls
424 }
425
426 private fun packagePath(file: File, code: CompilationUnit): String {
427 val pack = if (code.packageDeclaration.isPresent) code.packageDeclaration
428 .get().nameAsString else ""
429 val packagePath = pack.replace('.', '/') + '/' + file.name
430 return packagePath
431 }
432
433 private fun read(command: CommandOptions) {
434 LogParser(ViewerConfigParser())
435 .parse(FileInputStream(command.logProtofileArg),
436 FileInputStream(command.viewerConfigFileNameArg), System.out)
437 }
438
439 @JvmStatic
440 fun main(args: Array<String>) {
441 try {
442 val command = CommandOptions(args)
443 invoke(command)
444 } catch (ex: InvalidCommandException) {
445 println("\n${ex.message}\n")
446 showHelpAndExit()
447 } catch (ex: CodeProcessingException) {
448 println("\n${ex.message}\n")
449 exitProcess(1)
450 }
451 }
452
453 fun invoke(command: CommandOptions) {
454 StaticJavaParser.setConfiguration(ParserConfiguration().apply {
455 setLanguageLevel(ParserConfiguration.LanguageLevel.RAW)
456 setAttributeComments(false)
457 })
458
459 when (command.command) {
460 CommandOptions.TRANSFORM_CALLS_CMD -> processClasses(command)
461 CommandOptions.GENERATE_CONFIG_CMD -> viewerConf(command)
462 CommandOptions.READ_LOG_CMD -> read(command)
463 }
464 }
465
466 var injector = object : Injector {
467 override fun fileOutputStream(file: String) = FileOutputStream(file)
468 override fun readText(file: File) = file.readText()
469 override fun readLogGroups(jarPath: String, className: String) =
470 ProtoLogGroupReader().loadFromJar(jarPath, className)
471 override fun reportParseError(ex: ParsingException) {
472 println("\n${ex.message}\n")
473 }
474 }
475
476 interface Injector {
477 fun fileOutputStream(file: String): OutputStream
478 fun readText(file: File): String
479 fun readLogGroups(jarPath: String, className: String): Map<String, LogGroup>
480 fun reportParseError(ex: ParsingException)
481 }
482 }
483
submitCallablenull484 private fun <T> ExecutorService.submitCallable(f: () -> T) = submit(f)
485
486 private fun newThreadPool() = Executors.newFixedThreadPool(
487 Runtime.getRuntime().availableProcessors())
488