Skip to content

Commit

Permalink
implement gradle plugin to facilitate okhttp instrumentation (#137)
Browse files Browse the repository at this point in the history
  • Loading branch information
snowp authored Dec 5, 2024
1 parent 34dae78 commit 19eeac8
Show file tree
Hide file tree
Showing 28 changed files with 2,103 additions and 9 deletions.
38 changes: 38 additions & 0 deletions platform/jvm/capture-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -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 <T : InstrumentationParameters> Variant.configureInstrumentation(
classVisitorFactoryImplClass: Class<out AsmClassVisitorFactory<T>>,
scope: InstrumentationScope,
mode: FramesComputationMode,
instrumentationParamsConfig: (T) -> Unit,
) {
instrumentation.transformClassesWith(
classVisitorFactoryImplClass,
scope,
instrumentationParamsConfig
)
instrumentation.setAsmFramesComputationMode(mode)
}
Original file line number Diff line number Diff line change
@@ -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<Project> {
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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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<Boolean> = objects.property(Boolean::class.java)
.convention(true)

val debug: Property<Boolean> = objects.property(Boolean::class.java).convention(
false
)
}
Original file line number Diff line number Diff line change
@@ -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<ClassInstrumentable> = 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 }})"
}
}
Original file line number Diff line number Diff line change
@@ -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<MethodInstrumentable>,
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<out String>?
): 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
}
}
}
Loading

0 comments on commit 19eeac8

Please sign in to comment.