blob: 13d3529a11ad6bd674f9e9f6fba707bf1fd5f7e2 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.tools.debugger.test
import com.android.tools.idea.debug.AndroidFieldVisibilityProvider
import com.intellij.core.CoreApplicationEnvironment
import com.intellij.debugger.engine.FieldVisibilityProvider
import com.intellij.debugger.engine.RemoteStateState
import com.intellij.debugger.impl.DebuggerSession
import com.intellij.debugger.impl.RemoteConnectionBuilder
import com.intellij.debugger.settings.DebuggerSettings
import com.intellij.execution.configurations.JavaParameters
import com.intellij.execution.configurations.RemoteConnection
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.ComponentManager
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.observable.util.whenDisposed
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.ui.classFilter.ClassFilter
import com.intellij.util.io.Compressor
import com.intellij.util.io.delete
import org.jetbrains.kotlin.android.debugger.AndroidDexerImpl
import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.AndroidDexer
import org.jetbrains.kotlin.idea.debugger.test.KotlinDescriptorTestCase
import org.jetbrains.kotlin.idea.debugger.test.VmAttacher
import java.lang.ProcessBuilder.Redirect.PIPE
import java.lang.reflect.Method
import java.net.URL
import java.net.URLClassLoader
import java.nio.file.Files
import java.nio.file.Path
import java.security.MessageDigest
import kotlin.LazyThreadSafetyMode.NONE
import kotlin.io.path.createDirectories
import kotlin.io.path.exists
import kotlin.io.path.isDirectory
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.pathString
private const val STUDIO_ROOT_ENV = "INTELLIJ_DEBUGGER_TESTS_STUDIO_ROOT"
private const val STUDIO_ROOT_PROPERTY = "intellij.debugger.tests.studio.root"
private const val TIMEOUT_MILLIS_ENV = "INTELLIJ_DEBUGGER_TESTS_TIMEOUT_MILLIS"
private const val TIMEOUT_MILLIS_PROPERTY = "intellij.debugger.tests.timeout.millis"
private const val DEX_CACHE_ENV = "INTELLIJ_DEBUGGER_TESTS_DEX_CACHE"
private const val DEX_CACHE_PROPERTY = "intellij.debugger.tests.dex.cache"
private const val DEX_COMPILER = "prebuilts/r8/r8.jar"
private const val ART_ROOT = "prebuilts/tools/linux-x86_64/art"
private const val LIB_ART = "framework/core-libart-hostdex.jar"
private const val OJ = "framework/core-oj-hostdex.jar"
private const val ICU4J = "framework/core-icu4j-hostdex.jar"
private const val ART = "bin/art"
private const val JVMTI = "lib64/libopenjdkjvmti.so"
private const val JDWP = "lib64/libjdwp.so"
private val DEX_CACHE by lazy { getDexCache() }
private val ROOT by lazy(NONE) { getStudioRoot() }
private val D8_COMPILER by lazy(NONE) { loadD8Compiler() }
/** Private in [FieldVisibilityProvider] */
@Suppress("UnresolvedPluginConfigReference")
private val FIELD_VISIBILITY_PROVIDER_EP = ExtensionPointName.create<FieldVisibilityProvider>(
"com.intellij.debugger.fieldVisibilityProvider")
/** Attaches to an ART VM */
@Suppress("unused")
internal class ArtAttacher : VmAttacher {
private lateinit var steppingFilters: Array<ClassFilter>
private val disposable = Disposer.newDisposable("ArtAttacher")
override fun setUp() {
steppingFilters = DebuggerSettings.getInstance().steppingFilters
DebuggerSettings.getInstance().steppingFilters += arrayOf(
ClassFilter("android.*"),
ClassFilter("com.android.*"),
ClassFilter("androidx.*"),
ClassFilter("libcore.*"),
ClassFilter("dalvik.*"),
)
}
override fun tearDown() {
DebuggerSettings.getInstance().steppingFilters = steppingFilters
Disposer.dispose(disposable)
}
override fun attachVirtualMachine(
testCase: KotlinDescriptorTestCase,
javaParameters: JavaParameters,
environment: ExecutionEnvironment
): DebuggerSession {
val project = testCase.project
val application = ApplicationManager.getApplication()
// Register extensions
project.registerExtension(AndroidDexer.extensionPointName, AndroidDexerImpl(project), disposable)
application.registerExtension(FIELD_VISIBILITY_PROVIDER_EP, AndroidFieldVisibilityProvider(), disposable)
val remoteConnection = getRemoteConnection(testCase, javaParameters)
val remoteState = RemoteStateState(project, remoteConnection)
return testCase.attachVirtualMachine(remoteState, environment, remoteConnection, false)
}
private fun getRemoteConnection(testCase: KotlinDescriptorTestCase, javaParameters: JavaParameters): RemoteConnection {
println("Running on ART VM with DEX Cache")
val timeout = getTestTimeoutMillis()
if (timeout != null) {
testCase.setTimeout(timeout.toInt())
}
val mainClass = javaParameters.mainClass
val dexFiles = buildDexFiles(javaParameters.classPath.pathList)
if (DEX_CACHE == null) {
@Suppress("UnstableApiUsage")
testCase.testRootDisposable.whenDisposed {
dexFiles.forEach { it.delete() }
}
}
val command = buildCommandLine(dexFiles, mainClass)
val art = ProcessBuilder()
.command(command)
.redirectOutput(PIPE)
.start()
val port: String = art.inputStream.bufferedReader().use {
while (true) {
val line = it.readLine() ?: break
if (line.startsWith("Listening for transport")) {
val port = line.substringAfterLast(" ")
return@use port
}
}
throw IllegalStateException("Failed to read listening port from ART")
}
return RemoteConnectionBuilder(false, DebuggerSettings.SOCKET_TRANSPORT, port)
.checkValidity(true)
.asyncAgent(true)
.create(javaParameters)
}
/**
* Builds a DEX file from a list of dependencies
*/
private fun buildDexFiles(deps: List<String>): List<Path> {
return deps.mapNotNull {
val path = Path.of(it)
when (path.isDirectory()) {
true -> buildDexFromDir(path)
false -> buildDexFromJar(path)
}
}
}
private fun buildDexFromDir(dir: Path): Path? {
if (dir.listDirectoryEntries().isEmpty()) {
return null
}
val jarFile = Files.createTempFile(null, ".jar")
try {
Compressor.Jar(jarFile).use { jar ->
jar.addDirectory("", dir)
}
return buildDexFromJar(jarFile)
} finally {
jarFile.delete()
}
}
private fun buildDexFromJar(jar: Path): Path {
val fileName = "${jar.generateHash()}-dex.jar"
return synchronized(this) {
val cached = DEX_CACHE?.resolve(fileName)
val path = when {
cached == null -> Files.createTempFile(null, fileName)
cached.exists() -> {
return@synchronized cached
}
else -> cached
}
D8_COMPILER.invoke(null, arrayOf("--output", path.pathString, "--min-api", "30", jar.pathString))
path
}
}
/**
* Builds the command line to run the ART JVM
*/
private fun buildCommandLine(dexFiles: List<Path>, mainClass: String): List<String> {
val artDir = ROOT.resolve(ART_ROOT)
val bootClasspath = listOf(
artDir.resolve(LIB_ART),
artDir.resolve(OJ),
artDir.resolve(ICU4J),
).joinToString(":") { it.pathString }
val art = artDir.resolve(ART).pathString
val jvmti = artDir.resolve(JVMTI).pathString
val jdwp = artDir.resolve(JDWP).pathString
return listOf(
art,
"--64",
"-Xbootclasspath:$bootClasspath",
"-Xplugin:$jvmti",
"-agentpath:$jdwp=transport=dt_socket,server=y,suspend=y",
"-classpath",
dexFiles.joinToString(separator = ":") { it.pathString },
mainClass,
)
}
}
private fun getConfig(property: String, env: String): String? {
return System.getProperty(property) ?: System.getenv(env)
}
private fun getTestTimeoutMillis() =
getConfig(TIMEOUT_MILLIS_PROPERTY, TIMEOUT_MILLIS_ENV)?.toIntOrNull()
private fun getStudioRoot(): Path {
val path = getConfig(STUDIO_ROOT_PROPERTY, STUDIO_ROOT_ENV) ?: throw IllegalStateException("Studio Root was not provided")
val root = Path.of(path)
if (root.isDirectory()) {
return root
}
throw IllegalStateException("'$path' is not a directory")
}
private fun getDexCache(): Path? {
val path = getConfig(DEX_CACHE_PROPERTY, DEX_CACHE_ENV)?.toNioPathOrNull() ?: return null
path.createDirectories()
return path
}
private fun Path.generateHash(): String {
val digest = MessageDigest.getInstance("SHA-256")
val bytes = Files.readAllBytes(this)
val hashBytes = digest.digest(bytes)
return hashBytes.joinToString(separator = "") { String.format("%02x", it) }
}
private fun loadD8Compiler(): Method {
val classLoader = URLClassLoader(arrayOf(URL("file://${ROOT.resolve(DEX_COMPILER).pathString}")))
val d8 = classLoader.loadClass("com.android.tools.r8.D8")
return d8.getDeclaredMethod("main", Array<String>::class.java)
}
@Suppress("SameParameterValue")
private inline fun <reified T : Any> ComponentManager.registerExtension(ep: ExtensionPointName<T>, extension: T, disposable: Disposable) {
CoreApplicationEnvironment.registerExtensionPoint(extensionArea, ep, T::class.java)
extensionArea.getExtensionPoint(ep).registerExtension(extension, disposable)
}