diff --git a/idea/idea-android/idea-android.iml b/idea/idea-android/idea-android.iml index 2a5b3985a11..d7e839afa2d 100644 --- a/idea/idea-android/idea-android.iml +++ b/idea/idea-android/idea-android.iml @@ -24,5 +24,6 @@ + \ No newline at end of file diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/debugger/AndroidDexWrapper.kt b/idea/idea-android/src/org/jetbrains/kotlin/android/debugger/AndroidDexWrapper.kt new file mode 100644 index 00000000000..9c1bff78ce7 --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/debugger/AndroidDexWrapper.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * 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 org.jetbrains.kotlin.android.debugger + +import com.android.dx.cf.direct.DirectClassFile +import com.android.dx.cf.direct.StdAttributeFactory +import com.android.dx.command.dexer.Main +import com.android.dx.dex.cf.CfTranslator +import com.android.dx.dex.file.DexFile +import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassToLoad + +class AndroidDexWrapper { + @Suppress("unused") // Used in AndroidOClassLoadingAdapter#dex + fun dex(classes: Collection): ByteArray? { + val dexArguments = Main.Arguments().apply { parse(arrayOf("testArgs")) } + + val dexFile = DexFile(dexArguments.dexOptions) + + for ((_, relativeFileName, bytes) in classes) { + val cf = DirectClassFile(bytes, relativeFileName, true) + cf.setAttributeFactory(StdAttributeFactory.THE_ONE) + val classDef = CfTranslator.translate( + cf, + bytes, + dexArguments.cfOptions, + dexArguments.dexOptions, + dexFile + ) + + dexFile.add(classDef) + } + + return dexFile.toDex(null, false) + } +} \ No newline at end of file diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/debugger/AndroidDexerImpl.kt b/idea/idea-android/src/org/jetbrains/kotlin/android/debugger/AndroidDexerImpl.kt new file mode 100644 index 00000000000..0cafa358089 --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/debugger/AndroidDexerImpl.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * 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 org.jetbrains.kotlin.android.debugger + +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ProjectRootModificationTracker +import com.intellij.psi.util.CachedValueProvider +import com.intellij.psi.util.CachedValuesManager +import org.jetbrains.android.facet.AndroidFacet +import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.AndroidDexer +import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassToLoad +import java.io.File +import java.net.URLClassLoader +import java.security.ProtectionDomain + +class AndroidDexerImpl(val project: Project) : AndroidDexer { + private val cachedDexWrapper = CachedValuesManager.getManager(project).createCachedValue({ + val dexWrapper = doGetAndroidDexFile()?.let { dexJarFile -> + val androidDexWrapperName = AndroidDexWrapper::class.java.canonicalName + val classBytes = this.javaClass.classLoader.getResource( + androidDexWrapperName.replace('.', '/') + ".class").readBytes() + + val dexClassLoader = object : URLClassLoader(arrayOf(dexJarFile.toURI().toURL()), this::class.java.classLoader) { + init { + defineClass(androidDexWrapperName, classBytes, 0, classBytes.size, null as ProtectionDomain?) + } + } + + Class.forName(androidDexWrapperName, true, dexClassLoader).newInstance() + } + + CachedValueProvider.Result.createSingleDependency(dexWrapper, ProjectRootModificationTracker.getInstance(project)) + }, /* trackValue = */ false) + + override fun dex(classes: Collection): ByteArray? { + val dexWrapper = cachedDexWrapper.value + val dexMethod = dexWrapper::class.java.methods.firstOrNull { it.name == "dex" } ?: return null + return dexMethod.invoke(dexWrapper, classes) as? ByteArray ?: return null + } + + private fun doGetAndroidDexFile(): File? { + for (module in ModuleManager.getInstance(project).modules) { + val androidFacet = AndroidFacet.getInstance(module) ?: continue + val sdkData = androidFacet.sdkData ?: continue + val latestBuildTool = sdkData.getLatestBuildTool(/* allowPreview = */ false) + ?: sdkData.getLatestBuildTool(/* allowPreview = */ true) + ?: continue + + val dxJar = File(latestBuildTool.location, "lib/dx.jar") + if (dxJar.exists()) { + return dxJar + } + } + + return null + } +} \ No newline at end of file diff --git a/idea/src/META-INF/android.xml b/idea/src/META-INF/android.xml index 77ae4f2fa91..1b5e34fe10a 100644 --- a/idea/src/META-INF/android.xml +++ b/idea/src/META-INF/android.xml @@ -84,6 +84,8 @@ + + diff --git a/idea/src/META-INF/extensions/ide.xml b/idea/src/META-INF/extensions/ide.xml index 907aca56000..d83cc27d7ce 100644 --- a/idea/src/META-INF/extensions/ide.xml +++ b/idea/src/META-INF/extensions/ide.xml @@ -6,6 +6,10 @@ + + diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinDebuggerCaches.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinDebuggerCaches.kt index d01efd09c47..f4fc6fac609 100644 --- a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinDebuggerCaches.kt +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinDebuggerCaches.kt @@ -42,6 +42,7 @@ import org.jetbrains.kotlin.idea.caches.resolve.analyzeFullyAndGetResult import org.jetbrains.kotlin.idea.debugger.BinaryCacheKey import org.jetbrains.kotlin.idea.debugger.BytecodeDebugInfo import org.jetbrains.kotlin.idea.debugger.WeakBytecodeDebugInfoStorage +import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassToLoad import org.jetbrains.kotlin.idea.runInReadActionWithWriteActionPriority import org.jetbrains.kotlin.idea.util.application.runReadAction import org.jetbrains.kotlin.psi.KtCodeFragment @@ -217,8 +218,7 @@ class KotlinDebuggerCaches(project: Project) { } data class CompiledDataDescriptor( - val bytecodes: ByteArray, - val additionalClasses: List>, + val classes: List, val sourcePosition: SourcePosition, val parameters: ParametersDescriptor ) diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluationBuilder.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluationBuilder.kt index c1ea26f6290..bd96a5df65f 100644 --- a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluationBuilder.kt +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluationBuilder.kt @@ -61,7 +61,9 @@ import org.jetbrains.kotlin.idea.core.quoteSegmentsIfNeeded import org.jetbrains.kotlin.idea.debugger.DebuggerUtils import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches.CompiledDataDescriptor import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches.ParametersDescriptor +import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassToLoad import org.jetbrains.kotlin.idea.debugger.evaluate.compilingEvaluator.loadClasses +import org.jetbrains.kotlin.idea.debugger.evaluate.compilingEvaluator.loadClassesSafely import org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.ExtractionResult import org.jetbrains.kotlin.idea.runInReadActionWithWriteActionPriority import org.jetbrains.kotlin.idea.util.application.runReadAction @@ -78,6 +80,7 @@ import org.jetbrains.kotlin.resolve.jvm.JvmClassName import org.jetbrains.org.objectweb.asm.* import org.jetbrains.org.objectweb.asm.Opcodes.ASM5 import org.jetbrains.org.objectweb.asm.Type +import org.jetbrains.org.objectweb.asm.tree.ClassNode import org.jetbrains.org.objectweb.asm.tree.MethodNode import java.util.* @@ -134,14 +137,37 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour isCompiledDataFromCache = false extractAndCompile(fragment, position, context) } - val result = runEval4j(context, compiledData) + + val classLoaderHandler = loadClassesSafely(context, compiledData.classes) + + val result = if (classLoaderHandler != null) { + try { + evaluateWithCompilation(context, compiledData) ?: runEval4j(context, compiledData) + } finally { + classLoaderHandler.dispose() + } + } + else { + runEval4j(context, compiledData) + } // If bytecode was taken from cache and exception was thrown - recompile bytecode and run eval4j again if (isCompiledDataFromCache && result is ExceptionThrown && result.kind == ExceptionThrown.ExceptionKind.BROKEN_CODE) { - return runEval4j(context, extractAndCompile(codeFragment, sourcePosition, context)).toJdiValue(context) + // We need only lambda classes here cause we using only eval4j evaluation method + val classLoaderHandler = loadClasses(context, compiledData.classes.drop(1)) + + try { + return runEval4j(context, extractAndCompile(codeFragment, sourcePosition, context)).toJdiValue(context) + } finally { + classLoaderHandler.dispose() + } } - return result.toJdiValue(context) + return if (result is InterpreterResult) { + result.toJdiValue(context) + } else { + result + } } catch(e: EvaluateException) { throw e @@ -217,10 +243,9 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour } } - val additionalFiles = outputFiles.drop(1).map { getClassName(it.relativePath) to it.asByteArray() } + val additionalFiles = outputFiles.map { ClassToLoad(getClassName(it.relativePath), it.relativePath, it.asByteArray()) } return CompiledDataDescriptor( - outputFiles.first().asByteArray(), additionalFiles, sourcePosition, parametersDescriptor) @@ -230,15 +255,66 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour return fileName.substringBeforeLast(".class").replace("/", ".") } + private fun evaluateWithCompilation(context: EvaluationContextImpl, compiledData: CompiledDataDescriptor): Any? { + val vm = context.debugProcess.virtualMachineProxy.virtualMachine + val classLoader = context.classLoader ?: return null + val mainClassBytecode = compiledData.classes[0].bytes + + try { + val mainClassAsmNode = ClassNode().apply { ClassReader(mainClassBytecode).accept(this, ClassReader.EXPAND_FRAMES) } + val mainClassJdiName = mainClassAsmNode.name.replace('/', '.') + assert(mainClassAsmNode.methods.size == 1) + + val methodToInvoke = mainClassAsmNode.methods[0] + assert(methodToInvoke.parameters == null || methodToInvoke.parameters.isEmpty()) + + val mainClass = classByName(context, mainClassJdiName, classLoader).reflectedType() as ClassType + + val thread = context.suspendContext.thread?.threadReference!! + val invokePolicy = context.suspendContext.getInvokePolicy() + val eval = JDIEval(vm, classLoader, thread, invokePolicy) + + return vm.executeWithBreakpointsDisabled { + // Prepare the main class + eval.loadClass(Type.getObjectType(mainClassAsmNode.name), classLoader) + + val argumentTypes = Type.getArgumentTypes(methodToInvoke.desc) + val args = context.getArgumentsForEval4j(compiledData.parameters, argumentTypes) + .zip(argumentTypes) + .map { (value, type) -> + // Make argument type classes prepared for sure + eval.loadClass(type, classLoader) + boxOrUnboxArgumentIfNeeded(eval, value, type).asJdiValue(vm, type) + } + + + mainClass.invokeMethod(thread, mainClass.methods().single(), args, invokePolicy) + } + } catch (e: Throwable) { + LOG.debug("Unable to evaluate expression with compilation", e) + return null + } + } + + private fun classByName(context: EvaluationContextImpl, className: String, classLoader: ClassLoaderReference): ClassObjectReference { + val process = context.debugProcess + val vm = process.virtualMachineProxy + val classClass = process.findClass(context, Class::class.java.canonicalName, classLoader) as ClassType + val forNameMethod = classClass.concreteMethodByName( + "forName", "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;") ?: error("'forName' method not found") + return process.invokeMethod(context, classClass, forNameMethod, listOf( + vm.mirrorOf(className), + vm.mirrorOf(true), + classLoader)) as ClassObjectReference + } + private fun runEval4j(context: EvaluationContextImpl, compiledData: CompiledDataDescriptor): InterpreterResult { val virtualMachine = context.debugProcess.virtualMachineProxy.virtualMachine - - if (compiledData.additionalClasses.isNotEmpty()) { - loadClasses(context, compiledData.additionalClasses) - } - var resultValue: InterpreterResult? = null - ClassReader(compiledData.bytecodes).accept(object : ClassVisitor(ASM5) { + + val mainClassBytecode = compiledData.classes[0].bytes + + ClassReader(mainClassBytecode).accept(object : ClassVisitor(ASM5) { override fun visitMethod(access: Int, name: String, desc: String, signature: String?, exceptions: Array?): MethodVisitor? { if (name == GENERATED_FUNCTION_NAME) { val argumentTypes = Type.getArgumentTypes(desc) @@ -246,22 +322,18 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour return object : MethodNode(Opcodes.ASM5, access, name, desc, signature, exceptions) { override fun visitEnd() { - val allRequests = virtualMachine.eventRequestManager().breakpointRequests() + - virtualMachine.eventRequestManager().classPrepareRequests() - allRequests.forEach { it.disable() } + virtualMachine.executeWithBreakpointsDisabled { + val eval = JDIEval(virtualMachine, + context.classLoader, + context.suspendContext.thread?.threadReference!!, + context.suspendContext.getInvokePolicy()) - val eval = JDIEval(virtualMachine, - context.classLoader, - context.suspendContext.thread?.threadReference!!, - context.suspendContext.getInvokePolicy()) - - resultValue = interpreterLoop( - this, - makeInitialFrame(this, args.zip(argumentTypes).map { boxOrUnboxArgumentIfNeeded(eval, it.first, it.second) }), - eval - ) - - allRequests.forEach { it.enable() } + resultValue = interpreterLoop( + this, + makeInitialFrame(this, args.zip(argumentTypes).map { boxOrUnboxArgumentIfNeeded(eval, it.first, it.second) }), + eval + ) + } } } } @@ -273,6 +345,17 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour return resultValue ?: throw IllegalStateException("resultValue is null: cannot find method " + GENERATED_FUNCTION_NAME) } + private inline fun VirtualMachine.executeWithBreakpointsDisabled(block: () -> T): T { + val allRequests = eventRequestManager().breakpointRequests() + eventRequestManager().classPrepareRequests() + + try { + allRequests.forEach { it.disable() } + return block() + } finally { + allRequests.forEach { it.enable() } + } + } + private fun boxOrUnboxArgumentIfNeeded(eval: JDIEval, argumentValue: Value, parameterType: Type): Value { val argumentType = argumentValue.asmType @@ -541,7 +624,7 @@ private fun PsiElement.createKtFile(fileName: String, fileText: String): KtFile return jetFile } -private fun SuspendContext.getInvokePolicy(): Int { +internal fun SuspendContext.getInvokePolicy(): Int { return if (suspendPolicy == EventRequest.SUSPEND_EVENT_THREAD) ObjectReference.INVOKE_SINGLE_THREADED else 0 } diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AbstractAndroidClassLoadingAdapter.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AbstractAndroidClassLoadingAdapter.kt new file mode 100644 index 00000000000..7d648a386ff --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AbstractAndroidClassLoadingAdapter.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * 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 org.jetbrains.kotlin.idea.debugger.evaluate.classLoading + +import com.intellij.debugger.engine.DebugProcessImpl +import com.intellij.debugger.engine.evaluation.EvaluationContext +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.debugger.impl.DebuggerUtilsEx +import com.sun.jdi.* + +abstract class AbstractAndroidClassLoadingAdapter : ClassLoadingAdapter { + protected fun dex(context: EvaluationContextImpl, classes: Collection): ByteArray? { + return AndroidDexer.getInstances(context.project).single().dex(classes) + } + + protected fun mirrorOfByteArray(bytes: ByteArray, context: EvaluationContextImpl, process: DebugProcessImpl): ArrayReference { + val arrayClass = process.findClass(context, "byte[]", context.classLoader) as ArrayType + val reference = process.newInstance(arrayClass, bytes.size) + DebuggerUtilsEx.keep(reference, context) + + for (i in 0..bytes.lastIndex) { + reference.setValue(i, process.virtualMachineProxy.mirrorOf(bytes[i])) + } + + return reference + } + + protected fun wrapToByteBuffer(bytes: ArrayReference, context: EvaluationContext, process: DebugProcessImpl): ObjectReference { + val byteBufferClass = process.findClass(context, "java.nio.ByteBuffer", context.classLoader) as ClassType + val wrapMethod = byteBufferClass.concreteMethodByName("wrap", "([B)Ljava/nio/ByteBuffer;") + ?: error("'wrap' method not found") + + return process.invokeMethod(context, byteBufferClass, wrapMethod, listOf(bytes)) as ObjectReference + } + + protected fun DebugProcessImpl.tryLoadClass( + context: EvaluationContextImpl, + fqName: String, + classLoader: ClassLoaderReference? + ): ReferenceType? { + return try { + loadClass(context, fqName, classLoader) + } catch (e: Throwable) { + null + } + } +} + +internal val ClassLoaderReference.isDalvikClassLoader: Boolean + get() = referenceType()?.name()?.startsWith("dalvik.system.") ?: false \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AndroidDexer.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AndroidDexer.kt new file mode 100644 index 00000000000..032726ac12a --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AndroidDexer.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * 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 org.jetbrains.kotlin.idea.debugger.evaluate.classLoading + +import org.jetbrains.kotlin.extensions.ProjectExtensionDescriptor + +interface AndroidDexer { + companion object : ProjectExtensionDescriptor( + "org.jetbrains.kotlin.androidDexer", AndroidDexer::class.java) + + fun dex(classes: Collection): ByteArray? +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AndroidOClassLoadingAdapter.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AndroidOClassLoadingAdapter.kt new file mode 100644 index 00000000000..a06fae047d9 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/AndroidOClassLoadingAdapter.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * 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 org.jetbrains.kotlin.idea.debugger.evaluate.classLoading + +import com.intellij.debugger.engine.JVMNameUtil +import com.intellij.debugger.engine.evaluation.EvaluateException +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.debugger.impl.DebuggerUtilsEx +import com.sun.jdi.* + +class AndroidOClassLoadingAdapter : AbstractAndroidClassLoadingAdapter() { + override fun isApplicable(context: EvaluationContextImpl, classes: Collection): Boolean { + if (classes.size <= 1) { + // Dex takes significant amount of time so we load classes only if we have some non-inline lambdas + return false + } + + return context.classLoader?.isDalvikClassLoader ?: false + } + + private fun resolveClassLoaderClass(context: EvaluationContextImpl): ClassType? { + try { + return context.debugProcess.tryLoadClass( + context, "dalvik.system.InMemoryDexClassLoader", context.classLoader) as? ClassType + } catch (e: EvaluateException) { + return null + } + } + + override fun loadClasses(context: EvaluationContextImpl, classes: Collection): ClassLoaderHandler { + val process = context.debugProcess + val inMemoryClassLoaderClass = resolveClassLoaderClass(context) ?: error("InMemoryDexClassLoader class not found") + val constructorMethod = inMemoryClassLoaderClass.concreteMethodByName( + JVMNameUtil.CONSTRUCTOR_NAME, "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V") ?: error("Constructor method not found") + + val dexBytes = dex(context, classes) ?: error("Can't dex classes") + val dexBytesMirror = mirrorOfByteArray(dexBytes, context, process) + val dexByteBuffer = wrapToByteBuffer(dexBytesMirror, context, process) + + val newClassLoader = process.newInstance(context, inMemoryClassLoaderClass, constructorMethod, + listOf(dexByteBuffer, context.classLoader)) + + DebuggerUtilsEx.keep(newClassLoader, context) + + return ClassLoaderHandler(newClassLoader as ClassLoaderReference) + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassLoaderHandler.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassLoaderHandler.kt new file mode 100644 index 00000000000..94f4eb18de4 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassLoaderHandler.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * 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 org.jetbrains.kotlin.idea.debugger.evaluate.classLoading + +import com.intellij.openapi.Disposable +import com.sun.jdi.ClassLoaderReference + +open class ClassLoaderHandler(val reference: ClassLoaderReference?) : Disposable { + override fun dispose() {} +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassLoadingAdapter.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassLoadingAdapter.kt new file mode 100644 index 00000000000..578d42eef75 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassLoadingAdapter.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * 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 org.jetbrains.kotlin.idea.debugger.evaluate.classLoading + +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.psi.PsiFile +import com.sun.jdi.ClassLoaderReference + +interface ClassLoadingAdapter { + companion object { + private val ADAPTERS = listOf( + AndroidOClassLoadingAdapter(), + OrdinaryClassLoadingAdapter()) + + fun loadClasses(context: EvaluationContextImpl, classes: Collection): ClassLoaderHandler { + for (adapter in ADAPTERS) { + if (adapter.isApplicable(context, classes)) { + return adapter.loadClasses(context, classes) + } + } + + // Should never happen because OrdinaryClassLoadingAdapter is always applicable + throw IllegalStateException("Class loading adapter not found for $context") + } + } + + fun isApplicable(context: EvaluationContextImpl, classes: Collection): Boolean + + fun loadClasses(context: EvaluationContextImpl, classes: Collection): ClassLoaderHandler +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassToLoad.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassToLoad.kt new file mode 100644 index 00000000000..571d242ffd3 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/ClassToLoad.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * 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 org.jetbrains.kotlin.idea.debugger.evaluate.classLoading + +@Suppress("ArrayInDataClass") +data class ClassToLoad(val className: String, val relativeFileName: String, val bytes: ByteArray) \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/OrdinaryClassLoadingAdapter.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/OrdinaryClassLoadingAdapter.kt new file mode 100644 index 00000000000..35aa320040b --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/OrdinaryClassLoadingAdapter.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * 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 org.jetbrains.kotlin.idea.debugger.evaluate.classLoading + +import com.intellij.debugger.engine.DebugProcess +import com.intellij.debugger.engine.evaluation.EvaluateException +import com.intellij.debugger.engine.evaluation.EvaluationContext +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.debugger.impl.ClassLoadingUtils +import com.intellij.openapi.projectRoots.JdkVersionUtil +import com.intellij.openapi.util.SystemInfo +import com.sun.jdi.ClassLoaderReference +import org.jetbrains.kotlin.idea.debugger.evaluate.CompilingEvaluatorUtils + +class OrdinaryClassLoadingAdapter : ClassLoadingAdapter { + private companion object { + // This list should contain all superclasses of lambda classes. + // The order is relevant here: if we load Lambda first instead, during the definition of Lambda the class loader will try + // to load its superclass. It will succeed, probably with the help of some parent class loader, and the subsequent attempt to define + // the patched version of that superclass will fail with LinkageError (cannot redefine class) + private val LAMBDA_SUPERCLASSES = listOf(ClassBytes("kotlin.jvm.internal.Lambda")) + } + + override fun isApplicable(context: EvaluationContextImpl, classes: Collection): Boolean { + val classLoader = context.classLoader + return classLoader != null && !classLoader.isDalvikClassLoader + } + + override fun loadClasses(context: EvaluationContextImpl, classes: Collection): ClassLoaderHandler { + val process = context.debugProcess + + val classLoader = try { + ClassLoadingUtils.getClassLoader(context, process) + } + catch (e: Exception) { + throw EvaluateException("Error creating evaluation class loader: " + e, e) + } + + val version = process.virtualMachineProxy.version() + val sdkVersion = JdkVersionUtil.getVersion(version) + + if (!SystemInfo.isJavaVersionAtLeast(sdkVersion.description)) { + throw EvaluateException( + "Unable to compile for target level ${sdkVersion.description}. " + + "Need to run IDEA on java version at least ${sdkVersion.description}, " + + "currently running on ${SystemInfo.JAVA_RUNTIME_VERSION}") + } + + try { + defineClasses(classes, context, process, classLoader) + } + catch (e: Exception) { + throw EvaluateException("Error during classes definition " + e, e) + } + + return ClassLoaderHandler(classLoader) + } + + private fun defineClasses( + classes: Collection, + context: EvaluationContext, + process: DebugProcess, + classLoader: ClassLoaderReference + ) { + val classesToLoad = if (classes.size == 1) { + // No need in loading lambda superclass if there're no lambdas + classes + } + else { + val lambdaSuperclasses = LAMBDA_SUPERCLASSES.map { + ClassToLoad(it.name, it.name.replace('.', '/') + ".class", it.bytes) + } + lambdaSuperclasses + classes + } + + for ((className, _, bytes) in classesToLoad) { + val patchedBytes = CompilingEvaluatorUtils.changeSuperToMagicAccessor(bytes) + ClassLoadingUtils.defineClass(className, patchedBytes, context, process, classLoader) + } + } + + private class ClassBytes(val name: String) { + val bytes: ByteArray by lazy { + val inputStream = this::class.java.classLoader.getResourceAsStream(name.replace('.', '/') + ".class") + ?: throw EvaluateException("Couldn't find $name class in current class loader") + + inputStream.use { + it.readBytes() + } + } + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/compilingEvaluator.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/compilingEvaluator.kt index 6ae3644140e..e0064beffe8 100644 --- a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/compilingEvaluator.kt +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/compilingEvaluator.kt @@ -16,72 +16,23 @@ package org.jetbrains.kotlin.idea.debugger.evaluate.compilingEvaluator -import com.intellij.debugger.engine.DebugProcess -import com.intellij.debugger.engine.evaluation.EvaluateException -import com.intellij.debugger.engine.evaluation.EvaluationContext import com.intellij.debugger.engine.evaluation.EvaluationContextImpl -import com.intellij.debugger.impl.ClassLoadingUtils -import com.intellij.openapi.projectRoots.JdkVersionUtil -import com.intellij.openapi.util.SystemInfo -import com.sun.jdi.ClassLoaderReference -import org.jetbrains.kotlin.idea.debugger.evaluate.CompilingEvaluatorUtils +import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassLoaderHandler +import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassLoadingAdapter +import org.jetbrains.kotlin.idea.debugger.evaluate.classLoading.ClassToLoad -fun loadClasses(evaluationContext: EvaluationContextImpl, classes: Collection>) { - val process = evaluationContext.debugProcess - - val classLoader: ClassLoaderReference +fun loadClassesSafely(evaluationContext: EvaluationContextImpl, classes: Collection): ClassLoaderHandler? { try { - classLoader = ClassLoadingUtils.getClassLoader(evaluationContext, process) - } - catch (e: Exception) { - throw EvaluateException("Error creating evaluation class loader: " + e, e) - } - - val version = (process.virtualMachineProxy).version() - val sdkVersion = JdkVersionUtil.getVersion(version) - - if (!SystemInfo.isJavaVersionAtLeast(sdkVersion.description)) { - throw EvaluateException("Unable to compile for target level " + sdkVersion.description + ". Need to run IDEA on java version at least " + sdkVersion.description + ", currently running on " + SystemInfo.JAVA_RUNTIME_VERSION) - } - - try { - defineClasses(classes, evaluationContext, process, classLoader) - } - catch (e: Exception) { - throw EvaluateException("Error during classes definition " + e, e) - } - - evaluationContext.classLoader = classLoader -} - -private fun defineClasses( - classes: Collection>, - context: EvaluationContext, - process: DebugProcess, - classLoader: ClassLoaderReference -) { - val lambdaSuperclasses = LAMBDA_SUPERCLASSES.map { it.name to it.bytes } - for ((className, bytes) in lambdaSuperclasses + classes) { - val patchedBytes = CompilingEvaluatorUtils.changeSuperToMagicAccessor(bytes) - ClassLoadingUtils.defineClass(className, patchedBytes, context, process, classLoader) + return loadClasses(evaluationContext, classes) + } catch (e: Throwable) { + return null } } -// This list should contain all superclasses of lambda classes. -// The order is relevant here: if we load Lambda first instead, during the definition of Lambda the class loader will try -// to load its superclass. It will succeed, probably with the help of some parent class loader, and the subsequent attempt to define -// the patched version of that superclass will fail with LinkageError (cannot redefine class) -private val LAMBDA_SUPERCLASSES = listOf( - ClassBytes("kotlin.jvm.internal.Lambda") -) +fun loadClasses(evaluationContext: EvaluationContextImpl, classes: Collection): ClassLoaderHandler { + if (classes.isEmpty()) return ClassLoaderHandler(evaluationContext.classLoader) -private class ClassBytes(val name: String) { - val bytes: ByteArray by lazy { - val inputStream = this::class.java.classLoader.getResourceAsStream(name.replace('.', '/') + ".class") - ?: throw EvaluateException("Couldn't find $name class in current class loader") - - inputStream.use { - it.readBytes() - } + return ClassLoadingAdapter.loadClasses(evaluationContext, classes).apply { + evaluationContext.classLoader = this.reference } -} +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/AbstractKotlinEvaluateExpressionTest.kt b/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/AbstractKotlinEvaluateExpressionTest.kt index dba5591b0f3..18319ad4b78 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/AbstractKotlinEvaluateExpressionTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/AbstractKotlinEvaluateExpressionTest.kt @@ -23,6 +23,7 @@ import com.intellij.debugger.engine.evaluation.EvaluateException import com.intellij.debugger.engine.evaluation.TextWithImports import com.intellij.debugger.engine.evaluation.TextWithImportsImpl import com.intellij.debugger.engine.evaluation.expression.EvaluatorBuilderImpl +import com.intellij.debugger.engine.events.SuspendContextCommandImpl import com.intellij.debugger.settings.NodeRendererSettings import com.intellij.debugger.ui.impl.watch.* import com.intellij.debugger.ui.tree.* @@ -454,26 +455,39 @@ abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestBase() { contextElement.putCopyableUserData(KotlinCodeFragmentFactory.DEBUG_CONTEXT_FOR_TESTS, this@AbstractKotlinEvaluateExpressionTest.debuggerContext) - try { - val evaluator = - EvaluatorBuilderImpl.build(item, - contextElement, - sourcePosition, - project) + runActionInSuspendCommand { + try { + val evaluator = EvaluatorBuilderImpl.build(item, contextElement, sourcePosition, project) + ?: throw AssertionError("Cannot create an Evaluator for Evaluate Expression") - - if (evaluator == null) throw AssertionError("Cannot create an Evaluator for Evaluate Expression") - - val value = evaluator.evaluate(this@AbstractKotlinEvaluateExpressionTest.evaluationContext) - val actualResult = value.asValue().asString() - if (expectedResult != null) { - Assert.assertTrue("Evaluate expression returns wrong result for ${item.text}:\nexpected = $expectedResult\nactual = $actualResult\n", expectedResult == actualResult) + val value = evaluator.evaluate(this@AbstractKotlinEvaluateExpressionTest.evaluationContext) + val actualResult = value.asValue().asString() + if (expectedResult != null) { + Assert.assertTrue("Evaluate expression returns wrong result for ${item.text}:" + + "\nexpected = $expectedResult\nactual = $actualResult\n", expectedResult == actualResult) + } + } + catch (e: EvaluateException) { + Assert.assertTrue("Evaluate expression throws wrong exception for ${item.text}:" + + "\nexpected = $expectedResult\nactual = ${e.message}\n", expectedResult == e.message?.replaceFirst(ID_PART_REGEX, "id=ID")) } } - catch (e: EvaluateException) { - Assert.assertTrue("Evaluate expression throws wrong exception for ${item.text}:\nexpected = $expectedResult\nactual = ${e.message}\n", expectedResult == e.message?.replaceFirst(ID_PART_REGEX, "id=ID")) + } + } + + private fun SuspendContextImpl.runActionInSuspendCommand(action: SuspendContextImpl.() -> Unit) { + if (myInProgress) { + action() + } else { + val command = object : SuspendContextCommandImpl(this) { + override fun contextAction(suspendContext: SuspendContextImpl) { + action(suspendContext) + } } + + // Try to execute the action inside a command if we aren't already inside it. + debuggerContext.debugProcess?.managerThread?.invoke(command) ?: command.contextAction(this) } }