diff --git a/platform/jvm/capture-plugin/build.gradle.kts b/platform/jvm/capture-plugin/build.gradle.kts new file mode 100644 index 00000000..fe05bf20 --- /dev/null +++ b/platform/jvm/capture-plugin/build.gradle.kts @@ -0,0 +1,38 @@ + plugins { + alias(libs.plugins.kotlin) + alias(libs.plugins.maven.publish) + id("dependency-license-config") + id("java-gradle-plugin") +} + +dependencies { + compileOnly("com.android.tools.build:gradle:7.4.0") + compileOnly("org.ow2.asm:asm-commons:9.4") + compileOnly("org.ow2.asm:asm-util:9.4") + + testImplementation(gradleTestKit()) + testImplementation(kotlin("test")) + testImplementation("com.android.tools.build:gradle:7.4.0") + testImplementation("junit:junit:4.13.2") + testImplementation("org.ow2.asm:asm-commons:9.4") + testImplementation("org.ow2.asm:asm-util:9.4") + testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") +} + +gradlePlugin { + plugins { + create("capturePlugin") { + id = "io.bitdrift.capture.capture-plugin" + implementationClass = "io.bitdrift.capture.CapturePlugin" + } + } +} + +publishing { + repositories { + mavenLocal() + } +} + +group = "io.bitdrift.capture.capture-plugin" +version = "0.1.0" \ No newline at end of file diff --git a/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/AndroidComponentsConfig.kt b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/AndroidComponentsConfig.kt new file mode 100644 index 00000000..e41e67a0 --- /dev/null +++ b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/AndroidComponentsConfig.kt @@ -0,0 +1,82 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/** + * Adapted from https://github.com/getsentry/sentry-android-gradle-plugin/tree/4.14.1 + * + * MIT License + * + * Copyright (c) 2020 Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bitdrift.capture + +import com.android.build.api.instrumentation.AsmClassVisitorFactory +import com.android.build.api.instrumentation.FramesComputationMode +import com.android.build.api.instrumentation.InstrumentationParameters +import com.android.build.api.instrumentation.InstrumentationScope +import com.android.build.api.variant.AndroidComponentsExtension +import com.android.build.api.variant.Variant +import io.bitdrift.capture.CapturePlugin.Companion.sep +import io.bitdrift.capture.extension.BitdriftPluginExtension +import io.bitdrift.capture.instrumentation.SpanAddingClassVisitorFactory +import org.gradle.api.Project +import java.io.File + +fun AndroidComponentsExtension<*, *, *>.configure( + project: Project, + extension: BitdriftPluginExtension, +) { + // Temp folder for outputting debug logs + val tmpDir = File("${project.layout.buildDirectory}${sep}tmp${sep}bitdrift") + tmpDir.mkdirs() + + onVariants { variant -> + if (extension.instrumentation.enabled.get()) { + variant.configureInstrumentation( + SpanAddingClassVisitorFactory::class.java, + InstrumentationScope.ALL, + FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS, + ) { params -> + params.tmpDir.set(tmpDir) + params.debug.set(false) + } + } + } +} + +private fun Variant.configureInstrumentation( + classVisitorFactoryImplClass: Class>, + scope: InstrumentationScope, + mode: FramesComputationMode, + instrumentationParamsConfig: (T) -> Unit, +) { + instrumentation.transformClassesWith( + classVisitorFactoryImplClass, + scope, + instrumentationParamsConfig + ) + instrumentation.setAsmFramesComputationMode(mode) +} \ No newline at end of file diff --git a/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/CapturePlugin.kt b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/CapturePlugin.kt new file mode 100644 index 00000000..6b3e7e15 --- /dev/null +++ b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/CapturePlugin.kt @@ -0,0 +1,40 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +package io.bitdrift.capture + +import com.android.build.api.variant.AndroidComponentsExtension +import io.bitdrift.capture.extension.BitdriftPluginExtension +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.slf4j.LoggerFactory +import java.io.File +import javax.inject.Inject + +abstract class CapturePlugin @Inject constructor() : Plugin { + override fun apply(target: Project) { + val extension = target.extensions.create("bitdrift", BitdriftPluginExtension::class.java, target) + + target.pluginManager.withPlugin("com.android.application") { + val androidComponentsExt = + target.extensions.getByType(AndroidComponentsExtension::class.java) + + androidComponentsExt.configure( + target, + extension, + ) + } + } + + companion object { + internal val sep = File.separator + + internal val logger by lazy { + LoggerFactory.getLogger(CapturePlugin::class.java) + } + } +} \ No newline at end of file diff --git a/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/extension/BitdriftPluginExtension.kt b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/extension/BitdriftPluginExtension.kt new file mode 100644 index 00000000..e61610db --- /dev/null +++ b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/extension/BitdriftPluginExtension.kt @@ -0,0 +1,17 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +package io.bitdrift.capture.extension + +import org.gradle.api.Project +import javax.inject.Inject + +abstract class BitdriftPluginExtension @Inject constructor(project: Project) { + private val objects = project.objects + + val instrumentation: InstrumentationExtension = objects.newInstance(InstrumentationExtension::class.java) +} \ No newline at end of file diff --git a/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/extension/InstrumentationExtension.kt b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/extension/InstrumentationExtension.kt new file mode 100644 index 00000000..3e4a848f --- /dev/null +++ b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/extension/InstrumentationExtension.kt @@ -0,0 +1,22 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +package io.bitdrift.capture.extension + +import javax.inject.Inject +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property + +open class InstrumentationExtension @Inject constructor(objects: ObjectFactory) { + + val enabled: Property = objects.property(Boolean::class.java) + .convention(true) + + val debug: Property = objects.property(Boolean::class.java).convention( + false + ) +} \ No newline at end of file diff --git a/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/ChainedInstrumentable.kt b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/ChainedInstrumentable.kt new file mode 100644 index 00000000..7b47d792 --- /dev/null +++ b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/ChainedInstrumentable.kt @@ -0,0 +1,81 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/** + * Adapted from https://github.com/getsentry/sentry-android-gradle-plugin/tree/4.14.1 + * + * MIT License + * + * Copyright (c) 2020 Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +@file:Suppress("UnstableApiUsage") + +package io.bitdrift.capture.instrumentation + +import com.android.build.api.instrumentation.ClassContext +import java.util.LinkedList +import org.objectweb.asm.ClassVisitor + +class ChainedInstrumentable( + private val instrumentables: List = emptyList() +) : ClassInstrumentable { + + override fun getVisitor( + instrumentableContext: ClassContext, + apiVersion: Int, + originalVisitor: ClassVisitor, + parameters: SpanAddingClassVisitorFactory.SpanAddingParameters + ): ClassVisitor { + // build a chain of visitors in order they are provided + val queue = LinkedList(instrumentables) + var prevVisitor = originalVisitor + var visitor: ClassVisitor? = null + while (queue.isNotEmpty()) { + val instrumentable = queue.poll() + + visitor = if (instrumentable.isInstrumentable(instrumentableContext)) { + instrumentable.getVisitor( + instrumentableContext, + apiVersion, + prevVisitor, + parameters + ) + } else { + prevVisitor + } + prevVisitor = visitor + } + return visitor ?: originalVisitor + } + + override fun isInstrumentable(data: ClassContext): Boolean = + instrumentables.any { it.isInstrumentable(data) } + + override fun toString(): String { + return "ChainedInstrumentable(instrumentables=" + + "${instrumentables.joinToString(", ") { it.javaClass.simpleName }})" + } +} \ No newline at end of file diff --git a/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/CommonClassVisitor.kt b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/CommonClassVisitor.kt new file mode 100644 index 00000000..17273310 --- /dev/null +++ b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/CommonClassVisitor.kt @@ -0,0 +1,102 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/** + * Adapted from https://github.com/getsentry/sentry-android-gradle-plugin/tree/4.14.1 + * + * MIT License + * + * Copyright (c) 2020 Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bitdrift.capture.instrumentation + +import io.bitdrift.capture.instrumentation.util.CatchingMethodVisitor +import io.bitdrift.capture.instrumentation.util.ExceptionHandler +import io.bitdrift.capture.instrumentation.util.FileLogTextifier +import java.io.File +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.util.TraceMethodVisitor + +@Suppress("UnstableApiUsage") +class CommonClassVisitor( + apiVersion: Int, + classVisitor: ClassVisitor, + private val className: String, + private val methodInstrumentables: List, + private val parameters: SpanAddingClassVisitorFactory.SpanAddingParameters +) : ClassVisitor(apiVersion, classVisitor) { + + private lateinit var log: File + + init { + // to avoid file creation in case the debug mode is not set + if (parameters.debug.get()) { + + // create log dir. + val logDir = parameters.tmpDir.get() + logDir.mkdirs() + + // delete and recreate file + log = File(parameters.tmpDir.get(), "$className-instrumentation.log") + if (log.exists()) { + log.delete() + } + log.createNewFile() + } + } + + override fun visitMethod( + access: Int, + name: String?, + descriptor: String?, + signature: String?, + exceptions: Array? + ): MethodVisitor { + var mv = super.visitMethod(access, name, descriptor, signature, exceptions) + val methodContext = MethodContext(access, name, descriptor, signature, exceptions?.toList()) + val instrumentable = methodInstrumentables.find { it.isInstrumentable(methodContext) } + + var textifier: ExceptionHandler? = null + if (parameters.debug.get() && instrumentable != null) { + textifier = FileLogTextifier(api, log, name, descriptor) + mv = TraceMethodVisitor(mv, textifier) + } + + val instrumentableVisitor = instrumentable?.getVisitor(methodContext, api, mv, parameters) + return if (instrumentableVisitor != null) { + CatchingMethodVisitor( + api, + instrumentableVisitor, + className, + methodContext, + textifier + ) + } else { + mv + } + } +} \ No newline at end of file diff --git a/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/Instrumentable.kt b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/Instrumentable.kt new file mode 100644 index 00000000..00b21239 --- /dev/null +++ b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/Instrumentable.kt @@ -0,0 +1,80 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/** + * Adapted from https://github.com/getsentry/sentry-android-gradle-plugin/tree/4.14.1 + * + * MIT License + * + * Copyright (c) 2020 Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bitdrift.capture.instrumentation + +import com.android.build.api.instrumentation.ClassContext +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor +import java.io.Serializable + +interface Instrumentable : Serializable { + + /** + * Fully-qualified name of the instrumentable. Examples: + * Class: androidx.sqlite.db.framework.FrameworkSQLiteDatabase + * Method: query + */ + val fqName: String get() = "" + + /** + * Provides a visitor for this instrumentable. A visitor can be one of the visitors defined + * in [ASM](https://asm.ow2.io/javadoc/org/objectweb/asm/package-summary.html) + * + * @param instrumentableContext A context of the instrumentable. + * @param apiVersion Defines the ASM api version, usually provided from the parent + * @param originalVisitor The original visitor that ASM provides us with before visiting code + * @param parameters Parameters that are configured by users and passed via the Sentry gradle plugin + */ + fun getVisitor( + instrumentableContext: InstrumentableContext, + apiVersion: Int, + originalVisitor: Visitor, + parameters: SpanAddingClassVisitorFactory.SpanAddingParameters + ): Visitor + + /** + * Defines whether this object is instrumentable or not based on [data] + */ + fun isInstrumentable(data: InstrumentableContext): Boolean +} +interface ClassInstrumentable : Instrumentable { + + override fun isInstrumentable(data: ClassContext): Boolean = + fqName == data.currentClassData.className +} + +interface MethodInstrumentable : Instrumentable { + + override fun isInstrumentable(data: MethodContext): Boolean = fqName == data.name +} diff --git a/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/InstrumentableContext.kt b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/InstrumentableContext.kt new file mode 100644 index 00000000..432b0aa7 --- /dev/null +++ b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/InstrumentableContext.kt @@ -0,0 +1,50 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/** + * Adapted from https://github.com/getsentry/sentry-android-gradle-plugin/tree/4.14.1 + * + * MIT License + * + * Copyright (c) 2020 Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bitdrift.capture.instrumentation + +import com.android.build.api.instrumentation.ClassData +import com.android.build.gradle.internal.instrumentation.ClassContextImpl +import com.android.build.gradle.internal.instrumentation.ClassesDataCache +import com.android.build.gradle.internal.instrumentation.ClassesHierarchyResolver + +data class MethodContext( + val access: Int, + val name: String?, + val descriptor: String?, + val signature: String?, + val exceptions: List? +) + +fun ClassData.toClassContext() = + ClassContextImpl(this, ClassesHierarchyResolver.Builder(ClassesDataCache()).build()) \ No newline at end of file diff --git a/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/SpanAddingClassVisitorFactory.kt b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/SpanAddingClassVisitorFactory.kt new file mode 100644 index 00000000..c6ff448b --- /dev/null +++ b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/SpanAddingClassVisitorFactory.kt @@ -0,0 +1,109 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/** + * Adapted from https://github.com/getsentry/sentry-android-gradle-plugin/tree/4.14.1 + * + * MIT License + * + * Copyright (c) 2020 Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bitdrift.capture.instrumentation + +import com.android.build.api.instrumentation.AsmClassVisitorFactory +import com.android.build.api.instrumentation.ClassContext +import com.android.build.api.instrumentation.ClassData +import com.android.build.api.instrumentation.InstrumentationParameters +import io.bitdrift.capture.CapturePlugin +import io.bitdrift.capture.instrumentation.okhttp.OkHttpEventListener +import io.bitdrift.capture.instrumentation.util.findClassReader +import io.bitdrift.capture.instrumentation.util.findClassWriter +import io.bitdrift.capture.instrumentation.util.isMinifiedClass +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.objectweb.asm.ClassVisitor +import java.io.File + +abstract class SpanAddingClassVisitorFactory : AsmClassVisitorFactory { + + interface SpanAddingParameters : InstrumentationParameters { + @get:Input + val debug: Property + + @get:Internal + val tmpDir: Property + + @get:Internal + var _instrumentable: ClassInstrumentable? + } + + private val instrumentable: ClassInstrumentable + get() { + val memoized = parameters.get()._instrumentable + if (memoized != null) { + return memoized + } + + val instrumentable = ChainedInstrumentable( + listOfNotNull( + OkHttpEventListener() + ) + ) + CapturePlugin.logger.info( + "Instrumentable: $instrumentable" + ) + parameters.get()._instrumentable = instrumentable + return instrumentable + } + + + override fun createClassVisitor( + classContext: ClassContext, + nextClassVisitor: ClassVisitor + ): ClassVisitor { + val className = classContext.currentClassData.className + + val classReader = nextClassVisitor.findClassWriter()?.findClassReader() + val isMinifiedClass = classReader?.isMinifiedClass() ?: false + if (isMinifiedClass) { + CapturePlugin.logger.info( + "$className skipped from instrumentation because it's a minified class." + ) + return nextClassVisitor + } + + return instrumentable.getVisitor( + classContext, + instrumentationContext.apiVersion.get(), + nextClassVisitor, + parameters = parameters.get() + ) + } + + override fun isInstrumentable(classData: ClassData): Boolean = + instrumentable.isInstrumentable(classData.toClassContext()) +} diff --git a/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/okhttp/OkHttpEventListener.kt b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/okhttp/OkHttpEventListener.kt new file mode 100644 index 00000000..64ca3410 --- /dev/null +++ b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/okhttp/OkHttpEventListener.kt @@ -0,0 +1,85 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/** + * Adapted from https://github.com/getsentry/sentry-android-gradle-plugin/tree/4.14.1 + * + * MIT License + * + * Copyright (c) 2020 Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bitdrift.capture.instrumentation.okhttp + +import com.android.build.api.instrumentation.ClassContext +import io.bitdrift.capture.instrumentation.SpanAddingClassVisitorFactory +import io.bitdrift.capture.instrumentation.ClassInstrumentable +import io.bitdrift.capture.instrumentation.CommonClassVisitor +import io.bitdrift.capture.instrumentation.MethodContext +import io.bitdrift.capture.instrumentation.MethodInstrumentable +import io.bitdrift.capture.instrumentation.okhttp.visitor.OkHttpEventListenerMethodVisitor +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor + +class OkHttpEventListener( +) : ClassInstrumentable { + override val fqName: String get() = "okhttp3.OkHttpClient" + + override fun getVisitor( + instrumentableContext: ClassContext, + apiVersion: Int, + originalVisitor: ClassVisitor, + parameters: SpanAddingClassVisitorFactory.SpanAddingParameters + ): ClassVisitor = CommonClassVisitor( + apiVersion = apiVersion, + classVisitor = originalVisitor, + className = fqName.substringAfterLast('.'), + methodInstrumentables = listOf( + OkHttpEventListenerMethodInstrumentable( + ) + ), + parameters = parameters + ) +} + +class OkHttpEventListenerMethodInstrumentable( +) : MethodInstrumentable { + override val fqName: String get() = "" + + override fun getVisitor( + instrumentableContext: MethodContext, + apiVersion: Int, + originalVisitor: MethodVisitor, + parameters: SpanAddingClassVisitorFactory.SpanAddingParameters + ): MethodVisitor = OkHttpEventListenerMethodVisitor( + apiVersion = apiVersion, + originalVisitor = originalVisitor, + instrumentableContext = instrumentableContext, + ) + + override fun isInstrumentable(data: MethodContext): Boolean { + return data.name == fqName && data.descriptor == "(Lokhttp3/OkHttpClient\$Builder;)V" + } +} \ No newline at end of file diff --git a/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/okhttp/visitor/OkHttpEventListenerMethodVisitor.kt b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/okhttp/visitor/OkHttpEventListenerMethodVisitor.kt new file mode 100644 index 00000000..bec36f6b --- /dev/null +++ b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/okhttp/visitor/OkHttpEventListenerMethodVisitor.kt @@ -0,0 +1,103 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/** + * Adapted from https://github.com/getsentry/sentry-android-gradle-plugin/tree/4.14.1 + * + * MIT License + * + * Copyright (c) 2020 Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bitdrift.capture.instrumentation.okhttp.visitor + +import io.bitdrift.capture.instrumentation.MethodContext +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes +import org.objectweb.asm.commons.AdviceAdapter + +class OkHttpEventListenerMethodVisitor( + apiVersion: Int, + originalVisitor: MethodVisitor, + instrumentableContext: MethodContext, +) : AdviceAdapter( + apiVersion, + originalVisitor, + instrumentableContext.access, + instrumentableContext.name, + instrumentableContext.descriptor +) { + + private val captureOkHttpEventListenerFactory = + "io/bitdrift/capture/network/okhttp/CaptureOkHttpEventListenerFactory" + + override fun onMethodEnter() { + super.onMethodEnter() + + // Add the following call at the beginning of the constructor with the Builder parameter: + // builder.eventListener(new CaptureOkHttpEventListener(builder.eventListenerFactory)); + + // OkHttpClient.Builder is the parameter, retrieved here + visitVarInsn(Opcodes.ALOAD, 1) + + // Let's declare the CaptureOkHttpEventListenerFactory variable + visitTypeInsn(Opcodes.NEW, captureOkHttpEventListenerFactory) + + // The CaptureOkHttpEventListenerFactory constructor, which is called later, will consume the + // element without pushing anything back to the stack ( returns void). + // Dup will give a reference to the CaptureOkHttpEventListenerFactory after the constructor call + visitInsn(Opcodes.DUP) + + // Puts parameter OkHttpClient.Builder on top of the stack. + visitVarInsn(Opcodes.ALOAD, 1) + + // Read the "eventListenerFactory" field from OkHttpClient.Builder + visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + "okhttp3/OkHttpClient\$Builder", + "getEventListenerFactory\$okhttp", + "()Lokhttp3/EventListener\$Factory;", + false + ) + + // Call CaptureOkHttpEventListenerFactory constructor passing "eventListenerFactory" as parameter + visitMethodInsn( + Opcodes.INVOKESPECIAL, + captureOkHttpEventListenerFactory, + "", + "(Lokhttp3/EventListener\$Factory;)V", + false + ) + + // Call "eventListener" function of OkHttpClient.Builder passing CaptureOkHttpEventListenerFactory + visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + "okhttp3/OkHttpClient\$Builder", + "eventListenerFactory", + "(Lokhttp3/EventListener\$Factory;)Lokhttp3/OkHttpClient\$Builder;", + false + ) + } +} \ No newline at end of file diff --git a/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/util/CatchingMethodVisitor.kt b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/util/CatchingMethodVisitor.kt new file mode 100644 index 00000000..348632cc --- /dev/null +++ b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/util/CatchingMethodVisitor.kt @@ -0,0 +1,68 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/** + * Adapted from https://github.com/getsentry/sentry-android-gradle-plugin/tree/4.14.1 + * + * MIT License + * + * Copyright (c) 2020 Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bitdrift.capture.instrumentation.util + +import io.bitdrift.capture.CapturePlugin +import io.bitdrift.capture.instrumentation.MethodContext +import org.objectweb.asm.MethodVisitor +import org.slf4j.Logger + +interface ExceptionHandler { + fun handle(exception: Throwable) +} + +class CatchingMethodVisitor( + apiVersion: Int, + prevVisitor: MethodVisitor, + private val className: String, + private val methodContext: MethodContext, + private val exceptionHandler: ExceptionHandler? = null, + private val logger: Logger = CapturePlugin.logger +) : MethodVisitor(apiVersion, prevVisitor) { + + override fun visitMaxs(maxStack: Int, maxLocals: Int) { + try { + super.visitMaxs(maxStack, maxLocals) + } catch (e: Throwable) { + exceptionHandler?.handle(e) + logger.error( + """ + Error while instrumenting $className.${methodContext.name} ${methodContext.descriptor}. + Please report this issue at https://github.com/bitdriftlabs/capture-sdk/issues + """.trimIndent(), e + ) + throw e + } + } +} diff --git a/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/util/ConstantPoolHelpers.kt b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/util/ConstantPoolHelpers.kt new file mode 100644 index 00000000..9fc24a3c --- /dev/null +++ b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/util/ConstantPoolHelpers.kt @@ -0,0 +1,133 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/** + * Adapted from https://github.com/getsentry/sentry-android-gradle-plugin/tree/4.14.1 + * + * MIT License + * + * Copyright (c) 2020 Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bitdrift.capture.instrumentation.util + +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import java.lang.reflect.Field + +/** + * Looks up for the original [ClassWriter] up the visitor chain by looking at the private `cv` field + * of the [ClassVisitor]. + */ +internal fun ClassVisitor.findClassWriter(): ClassWriter? { + var classWriter: ClassVisitor = this + while (!ClassWriter::class.java.isAssignableFrom(classWriter::class.java)) { + val cvField: Field = try { + classWriter::class.java.allFields.find { it.name == "cv" } ?: return null + } catch (e: Throwable) { + return null + } + cvField.isAccessible = true + classWriter = (cvField.get(classWriter) as? ClassVisitor) ?: return null + } + return classWriter as ClassWriter +} + +/** + * Looks up for [ClassReader] of the [ClassWriter] through intermediate SymbolTable field. + */ +internal fun ClassWriter.findClassReader(): ClassReader? { + val clazz: Class = this::class.java + val symbolTableField: Field = try { + clazz.allFields.find { it.name == "symbolTable" } ?: return null + } catch (e: Throwable) { + return null + } + symbolTableField.isAccessible = true + val symbolTable = symbolTableField.get(this) + val classReaderField: Field = try { + symbolTable::class.java.getDeclaredField("sourceClassReader") + } catch (e: Throwable) { + return null + } + classReaderField.isAccessible = true + return (classReaderField.get(symbolTable) as? ClassReader) +} + +internal fun ClassReader.getSimpleClassName(): String { + return className.substringAfterLast("/") +} + +/** + * Looks at the constant pool entries and searches for R8 markers + */ +internal fun ClassReader.isMinifiedClass(): Boolean { + return isR8Minified(this) || classNameLooksMinified(this.getSimpleClassName(), this.className) +} + +private fun isR8Minified(classReader: ClassReader): Boolean { + val charBuffer = CharArray(classReader.maxStringLength) + // R8 marker is usually in the first 3-5 entries, so we limit it at 10 to speed it up + // (constant pool size can be huge otherwise) + val poolSize = minOf(10, classReader.itemCount) + for (i in 1 until poolSize) { + try { + val constantPoolEntry = classReader.readConst(i, charBuffer) + if (constantPoolEntry is String && "~~R8" in constantPoolEntry) { + // ~~R8 is a marker in the class' constant pool, which r8 itself is looking at when + // parsing a .class file. See here -> https://r8.googlesource.com/r8/+/refs/heads/main/src/main/java/com/android/tools/r8/dex/Marker.java#53 + return true + } + } catch (e: Throwable) { + // we ignore exceptions here, because some constant pool entries are nulls and the + // readConst method throws IllegalArgumentException when trying to read those + } + } + return false +} + +/** + * See https://github.com/getsentry/sentry-android-gradle-plugin/issues/360 + * and https://github.com/getsentry/sentry-android-gradle-plugin/issues/359#issuecomment-1193782500 + */ +/* ktlint-disable max-line-length */ +private val MINIFIED_CLASSNAME_REGEX = """^(((([a-zA-z])\4{1,}|[a-zA-Z]{1,2})([0-9]{1,})?(([a-zA-Z])\7{1,})?)|([a-zA-Z]([0-9])?))(${'\\'}${'$'}((((\w)\14{1,}|[a-zA-Z]{1,2})([0-9]{1,})?(([a-zA-Z])\17{1,})?)|(\w([0-9])?)))*${'$'}""".toRegex() + +/** + * See https://github.com/getsentry/sentry/blob/c943de2afc785083554e7fdfb10c67d0c0de0f98/static/app/components/events/eventEntries.tsx#L57-L58 + */ +private val MINIFIED_CLASSNAME_SENTRY_REGEX = + """^(([\w\${'$'}]\/[\w\${'$'}]{1,2})|([\w\${'$'}]{2}\/[\w\${'$'}]\/[\w\${'$'}]))(\/|${'$'})""".toRegex() +/* ktlint-enable max-line-length */ + +fun classNameLooksMinified(simpleClassName: String, fullClassName: String): Boolean { + return simpleClassName.isNotEmpty() && + simpleClassName[0].isLowerCase() && + ( + MINIFIED_CLASSNAME_REGEX.matches(simpleClassName) || + MINIFIED_CLASSNAME_SENTRY_REGEX.matches(fullClassName) + ) +} \ No newline at end of file diff --git a/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/util/FieldUtils.kt b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/util/FieldUtils.kt new file mode 100644 index 00000000..e44cd59f --- /dev/null +++ b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/util/FieldUtils.kt @@ -0,0 +1,44 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/* + * Adapted from https://github.com/apache/commons-lang/blob/ebcb39a62fc1e47251eceaf63a4b3d731c5227a0/src/main/java/org/apache/commons/lang3/reflect/FieldUtils.java + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 io.bitdrift.capture.instrumentation.util + +import java.lang.reflect.Field + +/** + * Gets all fields of the given class and its parents (if any). + */ +internal val Class<*>.allFields: List + get() { + val allFields = mutableListOf() + var currentClass: Class<*>? = this + while (currentClass != null) { + val declaredFields = currentClass.declaredFields + allFields += declaredFields + currentClass = currentClass.superclass + } + return allFields + } \ No newline at end of file diff --git a/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/util/FileLogTextifier.kt b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/util/FileLogTextifier.kt new file mode 100644 index 00000000..628e78d5 --- /dev/null +++ b/platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/util/FileLogTextifier.kt @@ -0,0 +1,75 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/** + * Adapted from https://github.com/getsentry/sentry-android-gradle-plugin/tree/4.14.1 + * + * MIT License + * + * Copyright (c) 2020 Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bitdrift.capture.instrumentation.util + +import java.io.File +import java.io.FileOutputStream +import java.io.PrintWriter +import org.objectweb.asm.util.Textifier + +class FileLogTextifier( + apiVersion: Int, + log: File, + methodName: String?, + methodDescriptor: String? +) : Textifier(apiVersion), ExceptionHandler { + + private var hasThrown = false + + private val fileOutputStream = FileOutputStream(log, true).apply { + write("function $methodName $methodDescriptor".toByteArray()) + write("\n".toByteArray()) + } + + override fun visitMethodEnd() { + if (!hasThrown) { + flushPrinter() + } + } + + override fun handle(exception: Throwable) { + hasThrown = true + flushPrinter() + } + + private fun flushPrinter() { + val printWriter = PrintWriter(fileOutputStream) + print(printWriter) + printWriter.flush() + // ASM textifier uses plain "\n" chars, so do we. As it's only for debug and dev purpose + // it doesn't matter to the end user + fileOutputStream.write("\n".toByteArray()) + fileOutputStream.close() + } +} \ No newline at end of file diff --git a/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/ChainedInstrumentableTest.kt b/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/ChainedInstrumentableTest.kt new file mode 100644 index 00000000..738e53fa --- /dev/null +++ b/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/ChainedInstrumentableTest.kt @@ -0,0 +1,142 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + + +/** + * Adapted from https://github.com/getsentry/sentry-android-gradle-plugin/tree/4.14.1 + * + * MIT License + * + * Copyright (c) 2020 Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bitdrift.capture.instrumentation + +import com.android.build.api.instrumentation.ClassContext +import io.bitdrift.capture.instrumentation.fakes.TestClassContext +import io.bitdrift.capture.instrumentation.fakes.TestClassData +import io.bitdrift.capture.instrumentation.fakes.TestSpanAddingParameters +import java.io.File +import kotlin.test.assertTrue +import org.junit.Test +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.Opcodes + +class ChainedInstrumentableTest { + class Fixture { + fun getSut( + originalVisitor: ClassVisitor, + instrumentables: List = emptyList() + ): ClassVisitor { + return ChainedInstrumentable(instrumentables).getVisitor( + TestClassContext(TestClassData("RandomClass")), + Opcodes.ASM7, + originalVisitor, + TestSpanAddingParameters(inMemoryDir = File("")) + ) + } + } + + private val fixture = Fixture() + + @Test + fun `when empty instrumentables list returns original visitor`() { + val sut = fixture.getSut(OriginalVisitor()) + + assertTrue { sut is OriginalVisitor } + } + + @Test + fun `when no isInstrumentable found returns original visitor`() { + val sut = fixture.getSut( + OriginalVisitor(), + listOf( + FirstInstrumentable(isInstrumentable = false), + SecondInstrumentable(isInstrumentable = false) + ) + ) + + assertTrue { sut is OriginalVisitor } + } + + @Test + fun `skip non-instrumentables in the chain`() { + val sut = fixture.getSut( + OriginalVisitor(), + listOf( + FirstInstrumentable(isInstrumentable = false), + SecondInstrumentable(isInstrumentable = true) + ) + ) + + assertTrue { + sut is SecondInstrumentable.SecondVisitor && sut.prevVisitor is OriginalVisitor + } + } + + @Test + fun `all instrumentables`() { + val sut = + fixture.getSut(OriginalVisitor(), listOf(FirstInstrumentable(), SecondInstrumentable())) + + assertTrue { + sut is SecondInstrumentable.SecondVisitor && + sut.prevVisitor is FirstInstrumentable.FirstVisitor && + (sut.prevVisitor as FirstInstrumentable.FirstVisitor).prevVisitor is OriginalVisitor + } + } +} + +class OriginalVisitor : ClassVisitor(Opcodes.ASM7) + +class FirstInstrumentable(val isInstrumentable: Boolean = true) : ClassInstrumentable { + class FirstVisitor(prevVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM7, prevVisitor) { + val prevVisitor get() = cv + } + + override fun getVisitor( + instrumentableContext: ClassContext, + apiVersion: Int, + originalVisitor: ClassVisitor, + parameters: SpanAddingClassVisitorFactory.SpanAddingParameters + ): ClassVisitor = FirstVisitor(originalVisitor) + + override fun isInstrumentable(data: ClassContext): Boolean = isInstrumentable +} + +class SecondInstrumentable(val isInstrumentable: Boolean = true) : ClassInstrumentable { + class SecondVisitor(prevVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM7, prevVisitor) { + val prevVisitor get() = cv + } + + override fun getVisitor( + instrumentableContext: ClassContext, + apiVersion: Int, + originalVisitor: ClassVisitor, + parameters: SpanAddingClassVisitorFactory.SpanAddingParameters + ): ClassVisitor = SecondVisitor(originalVisitor) + + override fun isInstrumentable(data: ClassContext): Boolean = isInstrumentable +} diff --git a/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/CommonClassVisitorTest.kt b/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/CommonClassVisitorTest.kt new file mode 100644 index 00000000..cacbebc9 --- /dev/null +++ b/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/CommonClassVisitorTest.kt @@ -0,0 +1,153 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/** + * Adapted from https://github.com/getsentry/sentry-android-gradle-plugin/tree/4.14.1 + * + * MIT License + * + * Copyright (c) 2020 Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bitdrift.capture.instrumentation + +import io.bitdrift.capture.instrumentation.util.CatchingMethodVisitor +import io.bitdrift.capture.instrumentation.fakes.TestSpanAddingParameters +import java.io.File +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +class CommonClassVisitorTest { + + class Fixture { + + fun getSut(tmpDir: File, debug: Boolean = false) = + CommonClassVisitor( + Opcodes.ASM7, + ParentClassVisitor(), + "SomeClass", + listOf(TestInstrumentable()), + TestSpanAddingParameters(debugOutput = debug, inMemoryDir = tmpDir) + ) + } + + @get:Rule + val tmpDir = TemporaryFolder() + + private val fixture = Fixture() + + @Test + fun `when debug - creates a file with class name on init`() { + fixture.getSut(tmpDir.root, true) + + val file = File(tmpDir.root, "SomeClass-instrumentation.log") + assertTrue { file.exists() } + } + + @Test + fun `when debug and is instrumentable - prepends with TraceMethodVisitor`() { + val mv = fixture.getSut(tmpDir.root, true) + .visitMethod(Opcodes.ACC_PUBLIC, "test", null, null, null) + + mv.visitVarInsn(Opcodes.ASTORE, 0) + mv.visitEnd() + + // we read the file and compare its content to ensure that TraceMethodVisitor was called and + // wrote the instructions into the file + val file = File(tmpDir.root, "SomeClass-instrumentation.log") + assertEquals( + file.readText(), + """ + |function test null + | ASTORE 0 + | + | + """.trimMargin() + ) + } + + @Test + fun `when no debug and is instrumentable - skips TraceMethodVisitor`() { + val mv = fixture.getSut(tmpDir.root, true) + .visitMethod(Opcodes.ACC_PUBLIC, "other", null, null, null) + + mv.visitVarInsn(Opcodes.ASTORE, 0) + mv.visitEnd() + + // we read the file and compare its content to ensure that TraceMethodVisitor was skipped + val file = File(tmpDir.root, "SomeClass-instrumentation.log") + assertTrue { file.readText().isEmpty() } + } + + @Test + fun `when matches method name returns instrumentable visitor wrapped into catching visitor`() { + val mv = + fixture.getSut(tmpDir.root).visitMethod(Opcodes.ACC_PUBLIC, "test", null, null, null) + + assertTrue { mv is CatchingMethodVisitor } + } + + @Test + fun `when doesn't match method name return original visitor`() { + val mv = + fixture.getSut(tmpDir.root).visitMethod(Opcodes.ACC_PUBLIC, "other", null, null, null) + + assertTrue { mv is ParentClassVisitor.ParentMethodVisitor } + } +} + +class ParentClassVisitor : ClassVisitor(Opcodes.ASM7) { + + inner class ParentMethodVisitor : MethodVisitor(Opcodes.ASM7) + + override fun visitMethod( + access: Int, + name: String?, + descriptor: String?, + signature: String?, + exceptions: Array? + ): MethodVisitor = ParentMethodVisitor() +} + +class TestInstrumentable : MethodInstrumentable { + + inner class TestVisitor(originalVisitor: MethodVisitor) : + MethodVisitor(Opcodes.ASM7, originalVisitor) + + override val fqName: String get() = "test" + + override fun getVisitor( + instrumentableContext: MethodContext, + apiVersion: Int, + originalVisitor: MethodVisitor, + parameters: SpanAddingClassVisitorFactory.SpanAddingParameters + ): MethodVisitor = TestVisitor(originalVisitor) +} diff --git a/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/fakes/BaseTestLogger.kt b/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/fakes/BaseTestLogger.kt new file mode 100644 index 00000000..24031189 --- /dev/null +++ b/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/fakes/BaseTestLogger.kt @@ -0,0 +1,159 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +package io.bitdrift.capture.instrumentation.fakes + +import org.gradle.api.logging.LogLevel +import org.gradle.api.logging.Logger +import org.slf4j.Marker + +abstract class BaseTestLogger : Logger { + + override fun isTraceEnabled(): Boolean = true + + override fun isTraceEnabled(marker: Marker): Boolean = true + + override fun trace(msg: String) = Unit + + override fun trace(msg: String, arg: Any) = Unit + + override fun trace(msg: String, arg: Any, arg2: Any) = Unit + + override fun trace(msg: String, vararg args: Any) = Unit + + override fun trace(msg: String, throwable: Throwable?) = Unit + + override fun trace(marker: Marker, msg: String) = Unit + + override fun trace(marker: Marker, msg: String, arg2: Any) = Unit + + override fun trace(marker: Marker, msg: String, arg2: Any, arg3: Any) = Unit + + override fun trace(marker: Marker, msg: String, vararg args: Any) = Unit + + override fun trace(marker: Marker, msg: String, throwable: Throwable?) = Unit + + override fun isDebugEnabled(): Boolean = true + + override fun isDebugEnabled(marker: Marker): Boolean = true + + override fun debug(msg: String) = Unit + + override fun debug(msg: String, arg: Any) = Unit + + override fun debug(msg: String, arg: Any, arg2: Any) = Unit + + override fun debug(msg: String, vararg args: Any) = Unit + + override fun debug(msg: String, throwable: Throwable?) = Unit + + override fun debug(marker: Marker, msg: String) = Unit + + override fun debug(marker: Marker, msg: String, arg2: Any) = Unit + + override fun debug(marker: Marker, msg: String, arg2: Any, arg3: Any) = Unit + + override fun debug(marker: Marker, msg: String, vararg args: Any) = Unit + + override fun debug(marker: Marker, msg: String, throwable: Throwable?) = Unit + + override fun isInfoEnabled(): Boolean = true + + override fun isInfoEnabled(marker: Marker): Boolean = true + + override fun info(msg: String) = Unit + + override fun info(msg: String, arg: Any) = Unit + + override fun info(msg: String, arg: Any, arg2: Any) = Unit + + override fun info(msg: String, vararg args: Any) = Unit + + override fun info(msg: String, throwable: Throwable?) = Unit + + override fun info(marker: Marker, msg: String) = Unit + + override fun info(marker: Marker, msg: String, arg2: Any) = Unit + + override fun info(marker: Marker, msg: String, arg2: Any, arg3: Any) = Unit + + override fun info(marker: Marker, msg: String, vararg args: Any) = Unit + + override fun info(marker: Marker, msg: String, throwable: Throwable?) = Unit + + override fun isWarnEnabled(): Boolean = true + + override fun isWarnEnabled(marker: Marker): Boolean = true + + override fun warn(msg: String) = Unit + + override fun warn(msg: String, arg: Any) = Unit + + override fun warn(msg: String, vararg args: Any) = Unit + + override fun warn(msg: String, arg: Any, arg2: Any) = Unit + + override fun warn(msg: String, throwable: Throwable?) = Unit + + override fun warn(marker: Marker, msg: String) = Unit + + override fun warn(marker: Marker, msg: String, arg2: Any) = Unit + + override fun warn(marker: Marker, msg: String, arg2: Any, arg3: Any) = Unit + + override fun warn(marker: Marker, msg: String, vararg args: Any) = Unit + + override fun warn(marker: Marker, msg: String, throwable: Throwable?) = Unit + + override fun isErrorEnabled(): Boolean = true + + override fun isErrorEnabled(marker: Marker): Boolean = true + + override fun error(msg: String) = Unit + + override fun error(msg: String, arg: Any) = Unit + + override fun error(msg: String, arg: Any, arg2: Any) = Unit + + override fun error(msg: String, vararg args: Any) = Unit + + override fun error(msg: String, throwable: Throwable?) = Unit + + override fun error(marker: Marker, msg: String) = Unit + + override fun error(marker: Marker, msg: String, arg2: Any) = Unit + + override fun error(marker: Marker, msg: String, arg2: Any, arg3: Any) = Unit + + override fun error(marker: Marker, msg: String, vararg args: Any) = Unit + + override fun error(marker: Marker, msg: String, throwable: Throwable?) = Unit + + override fun isLifecycleEnabled(): Boolean = true + + override fun lifecycle(message: String?) = Unit + + override fun lifecycle(message: String?, vararg objects: Any?) = Unit + + override fun lifecycle(message: String?, throwable: Throwable?) = Unit + + override fun isQuietEnabled(): Boolean = true + + override fun quiet(message: String?) = Unit + + override fun quiet(message: String?, vararg objects: Any?) = Unit + + override fun quiet(message: String?, throwable: Throwable?) = Unit + + override fun isEnabled(level: LogLevel?): Boolean = true + + override fun log(level: LogLevel?, message: String?) = Unit + + override fun log(level: LogLevel?, message: String?, vararg objects: Any?) = Unit + + override fun log(level: LogLevel?, message: String?, throwable: Throwable?) = Unit +} diff --git a/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/fakes/CapturingTestLogger.kt b/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/fakes/CapturingTestLogger.kt new file mode 100644 index 00000000..39eaf3a3 --- /dev/null +++ b/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/fakes/CapturingTestLogger.kt @@ -0,0 +1,65 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/** + * Adapted from https://github.com/getsentry/sentry-android-gradle-plugin/tree/4.14.1 + * + * MIT License + * + * Copyright (c) 2020 Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bitdrift.capture.instrumentation.fakes + +class CapturingTestLogger : BaseTestLogger() { + override fun getName(): String = "SentryPluginTest" + + var capturedMessage: String? = null + var capturedThrowable: Throwable? = null + + override fun error(msg: String, throwable: Throwable?) { + capturedMessage = msg + capturedThrowable = throwable + } + + override fun warn(msg: String, throwable: Throwable?) { + capturedMessage = msg + capturedThrowable = throwable + } + + override fun warn(msg: String) { + capturedMessage = msg + } + + override fun info(msg: String, throwable: Throwable?) { + capturedMessage = msg + capturedThrowable = throwable + } + + override fun debug(msg: String, throwable: Throwable?) { + capturedMessage = msg + capturedThrowable = throwable + } +} diff --git a/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/fakes/TestClassContext.kt b/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/fakes/TestClassContext.kt new file mode 100644 index 00000000..d3ad5cf7 --- /dev/null +++ b/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/fakes/TestClassContext.kt @@ -0,0 +1,33 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +@file:Suppress("UnstableApiUsage") + +package io.bitdrift.capture.instrumentation.fakes + +import com.android.build.api.instrumentation.ClassContext +import com.android.build.api.instrumentation.ClassData + +data class TestClassData( + override val className: String, + override val classAnnotations: List = emptyList(), + override val interfaces: List = emptyList(), + override val superClasses: List = emptyList() +) : ClassData + +data class TestClassContext( + override val currentClassData: ClassData, + private val classLoader: (String) -> ClassData? = { null } +) : ClassContext { + + constructor(className: String) : this(TestClassData(className)) + + constructor(className: String, classLoader: (String) -> ClassData?) : + this(TestClassData(className), classLoader) + + override fun loadClassData(className: String): ClassData? = classLoader(className) +} diff --git a/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/fakes/TestSpanAddingParameters.kt b/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/fakes/TestSpanAddingParameters.kt new file mode 100644 index 00000000..7cfaedb1 --- /dev/null +++ b/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/fakes/TestSpanAddingParameters.kt @@ -0,0 +1,56 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/** + * Adapted from https://github.com/getsentry/sentry-android-gradle-plugin/tree/4.14.1 + * + * MIT License + * + * Copyright (c) 2020 Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bitdrift.capture.instrumentation.fakes + +import io.bitdrift.capture.instrumentation.ClassInstrumentable +import io.bitdrift.capture.instrumentation.SpanAddingClassVisitorFactory +import java.io.File +import org.gradle.api.internal.provider.DefaultProperty +import org.gradle.api.internal.provider.PropertyHost +import org.gradle.api.provider.Property + +class TestSpanAddingParameters( + private val debugOutput: Boolean = true, + private val inMemoryDir: File +) : SpanAddingClassVisitorFactory.SpanAddingParameters { + override val debug: Property + get() = DefaultProperty(PropertyHost.NO_OP, Boolean::class.javaObjectType) + .convention(debugOutput) + + override val tmpDir: Property + get() = DefaultProperty(PropertyHost.NO_OP, File::class.java).convention(inMemoryDir) + + override var _instrumentable: ClassInstrumentable? = null + +} diff --git a/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/util/CatchingMethodVisitorTest.kt b/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/util/CatchingMethodVisitorTest.kt new file mode 100644 index 00000000..6890c652 --- /dev/null +++ b/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/util/CatchingMethodVisitorTest.kt @@ -0,0 +1,114 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/** + * Adapted from https://github.com/getsentry/sentry-android-gradle-plugin/tree/4.14.1 + * + * MIT License + * + * Copyright (c) 2020 Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bitdrift.capture.instrumentation.util + +import io.bitdrift.capture.instrumentation.MethodContext +import io.bitdrift.capture.instrumentation.fakes.CapturingTestLogger +import kotlin.test.assertEquals +import org.junit.Test +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +class CatchingMethodVisitorTest { + + class Fixture { + private val throwingVisitor = ThrowingMethodVisitor() + val handler = CapturingExceptionHandler() + val logger = CapturingTestLogger() + + private val methodContext = + MethodContext(Opcodes.ACC_PUBLIC, "someMethod", null, null, null) + val sut + get() = CatchingMethodVisitor( + Opcodes.ASM7, + throwingVisitor, + "SomeClass", + methodContext, + handler, + logger + ) + } + + private val fixture = Fixture() + + @Test + fun `forwards exception to ExceptionHandler`() { + try { + fixture.sut.visitMaxs(0, 0) + } catch (ignored: Throwable) { + } finally { + assertEquals(fixture.handler.capturedException!!.message, "This method throws!") + } + } + + @Test(expected = CustomException::class) + fun `rethrows exception`() { + fixture.sut.visitMaxs(0, 0) + } + + @Test + fun `prints message to log`() { + try { + fixture.sut.visitMaxs(0, 0) + } catch (ignored: Throwable) { + } finally { + assertEquals(fixture.logger.capturedThrowable!!.message, "This method throws!") + assertEquals( + fixture.logger.capturedMessage, + """ + Error while instrumenting SomeClass.someMethod null. + Please report this issue at https://github.com/bitdriftlabs/capture-sdk/issues + """.trimIndent() + ) + } + } +} + +class CustomException : RuntimeException("This method throws!") + +class ThrowingMethodVisitor : MethodVisitor(Opcodes.ASM7) { + + override fun visitMaxs(maxStack: Int, maxLocals: Int) { + throw CustomException() + } +} + +class CapturingExceptionHandler : ExceptionHandler { + + var capturedException: Throwable? = null + + override fun handle(exception: Throwable) { + capturedException = exception + } +} diff --git a/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/util/FileLogTextifierTest.kt b/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/util/FileLogTextifierTest.kt new file mode 100644 index 00000000..ba635ac3 --- /dev/null +++ b/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/util/FileLogTextifierTest.kt @@ -0,0 +1,138 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/** + * Adapted from https://github.com/getsentry/sentry-android-gradle-plugin/tree/4.14.1 + * + * MIT License + * + * Copyright (c) 2020 Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bitdrift.capture.instrumentation.util + +import java.io.File +import kotlin.test.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.objectweb.asm.Label +import org.objectweb.asm.Opcodes + +class FileLogTextifierTest { + + class Fixture { + + fun getSut(tmpFile: File) = + FileLogTextifier( + Opcodes.ASM7, + tmpFile, + "SomeMethod", + "(Ljava/lang/Throwable;)V" + ) + + fun visitMethodInstructions(sut: FileLogTextifier) { + sut.visitVarInsn(Opcodes.ASTORE, 0) + sut.visitLabel(Label()) + sut.visitLdcInsn("db") + } + } + + @get:Rule + val tmpDir = TemporaryFolder() + + private val fixture = Fixture() + + @Test + fun `prints methodName on ccreation`() { + fixture.getSut(tmpDir.newFile("instrumentation.log")) + + val file = File(tmpDir.root, "instrumentation.log") + assertEquals( + file.readText(), + "function SomeMethod (Ljava/lang/Throwable;)V\n" + ) + } + + @Test + fun `visitMethodEnd flushes output to file if hasn't thrown`() { + val sut = fixture.getSut(tmpDir.newFile("instrumentation.log")) + fixture.visitMethodInstructions(sut) + sut.visitMethodEnd() + + val file = File(tmpDir.root, "instrumentation.log") + assertEquals( + file.readText(), + """ + |function SomeMethod (Ljava/lang/Throwable;)V + | ASTORE 0 + | L0 + | LDC "db" + | + | + """.trimMargin() + ) + } + + @Test + fun `visitMethodEnd does nothing if has thrown`() { + val sut = fixture.getSut(tmpDir.newFile("instrumentation.log")) + sut.handle(RuntimeException()) + fixture.visitMethodInstructions(sut) + sut.visitMethodEnd() + + val file = File(tmpDir.root, "instrumentation.log") + // sut.handle will add one more newline to the end of file, but actual visited instructions + // will not be flushed to file + assertEquals( + file.readText(), + """ + |function SomeMethod (Ljava/lang/Throwable;)V + | + | + """.trimMargin() + ) + } + + @Test + fun `handle exception flushes output to file`() { + val sut = fixture.getSut(tmpDir.newFile("instrumentation.log")) + fixture.visitMethodInstructions(sut) + sut.handle(RuntimeException()) + + val file = File(tmpDir.root, "instrumentation.log") + assertEquals( + file.readText(), + """ + |function SomeMethod (Ljava/lang/Throwable;)V + | ASTORE 0 + | L0 + | LDC "db" + | + | + """.trimMargin() + ) + } +} diff --git a/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/util/MinifiedClassDetectionTest.kt b/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/util/MinifiedClassDetectionTest.kt new file mode 100644 index 00000000..55a0f143 --- /dev/null +++ b/platform/jvm/capture-plugin/src/test/kotlin/io/bitdrift/capture/instrumentation/util/MinifiedClassDetectionTest.kt @@ -0,0 +1,100 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/** + * Adapted from https://github.com/getsentry/sentry-android-gradle-plugin/tree/4.14.1 + * + * MIT License + * + * Copyright (c) 2020 Sentry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bitdrift.capture.instrumentation.util + +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.junit.Test + +class MinifiedClassDetectionTest { + + @Test + fun `detects minified class names`() { + val classNames = listOf( + "l0", + """a${'$'}a""", + "ccc017zz", + """ccc017zz${'$'}a""", + "aa", + "aa${'$'}a", + "ab", + "aa${'$'}ab", + "ab${'$'}a" + ) + + classNames.forEach { + assertTrue(classNameLooksMinified(it, "com/example/$it"), it) + } + } + + @Test + fun `detects minified class names with minified package name`() { + val classNames = listOf( + """a${'$'}""", + "aa" + ) + + classNames.forEach { + assertTrue(classNameLooksMinified(it, "a/$it"), it) + } + } + + @Test + fun `does not consider non minified classes as minified`() { + val classNames = listOf( + "ConstantPoolHelpers", + "FileUtil", + """FileUtil${"$"}Inner""" + ) + + classNames.forEach { + assertFalse(classNameLooksMinified(it, "com/example/$it"), it) + } + } + + @Test + fun `does not consider short class names as minified classes`() { + val classNames = listOf( + Pair("Call", "retrofit2/Call"), + Pair("Call", "okhttp3/Call"), + Pair("Fill", "androidx/compose/ui/graphics/drawscope/Fill"), + Pair("Px", "androidx/annotation/Px"), + Pair("Dp", "androidx/annotation/Dp") + ) + + classNames.forEach { (simpleName, fullName) -> + assertFalse(classNameLooksMinified(simpleName, fullName)) + } + } +} diff --git a/platform/jvm/gradle/libs.versions.toml b/platform/jvm/gradle/libs.versions.toml index e52840fe..aab5e344 100644 --- a/platform/jvm/gradle/libs.versions.toml +++ b/platform/jvm/gradle/libs.versions.toml @@ -15,12 +15,13 @@ kotlinResultJvm = "1.1.18" lifecycleCommon = "2.6.1" mavenPublishPlugin = "0.28.0" kotlinAndroidPlugin = "1.9.24" +kotlinPlugin = "1.9.24" mockitoCore = "4.9.0" mockitoKotlin = "2.2.0" mockitoKotlinVersion = "4.1.0" okhttp = "4.12.0" robolectric = "4.13" -rustAndroidPlugin = "0.9.3" +rustAndroidPlugin = "0.9.5" material3Android = "1.2.1" startupRuntime = "1.1.1" timber = "5.0.1" @@ -60,4 +61,5 @@ detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detektPlugin" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokkaPlugin" } maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublishPlugin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinAndroidPlugin" } +kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlinPlugin" } rust-android = { id = "org.mozilla.rust-android-gradle.rust-android", version.ref = "rustAndroidPlugin" } diff --git a/platform/jvm/gradle/wrapper/gradle-wrapper.properties b/platform/jvm/gradle/wrapper/gradle-wrapper.properties index a3638774..171d8761 100644 --- a/platform/jvm/gradle/wrapper/gradle-wrapper.properties +++ b/platform/jvm/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/platform/jvm/settings.gradle.kts b/platform/jvm/settings.gradle.kts index 36ff231f..ff56ebe1 100644 --- a/platform/jvm/settings.gradle.kts +++ b/platform/jvm/settings.gradle.kts @@ -1,22 +1,25 @@ rootProject.name = "capture-sdk" +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + google() + mavenCentral() + } +} + include(":capture") include(":capture-apollo3") include(":capture-timber") include(":common") include(":replay") +include(":capture-plugin") include(":gradle-test-app") include(":microbenchmark") -pluginManagement { - repositories { - gradlePluginPortal() - google() - mavenCentral() - } -} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)