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