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