• 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.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