Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions buildSrc/src/main/kotlin/TraceAgentTasks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ fun Project.registerTraceAgentTasks(fatJarName: String, fatJarTaskName: String,
val packagesToShade = listOf(
"org.objectweb.asm",
"net.bytebuddy",
"kotlinx.serialization",
)

packagesToShade.forEach { packageName ->
Expand Down
3 changes: 3 additions & 0 deletions jvm-agent/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("maven-publish")
id("org.jetbrains.dokka")
kotlin("plugin.serialization")
}

repositories {
Expand All @@ -23,6 +24,7 @@ sourceSets {
// main
val asmVersion: String by project
val byteBuddyVersion: String by project
val kotlinxSerializationVersion: String by project

compileOnly(project(":bootstrap"))
implementation(project(":common"))
Expand All @@ -32,6 +34,7 @@ sourceSets {
api("org.ow2.asm:asm-util:${asmVersion}")
api("net.bytebuddy:byte-buddy:${byteBuddyVersion}")
api("net.bytebuddy:byte-buddy-agent:${byteBuddyVersion}")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:${kotlinxSerializationVersion}")

val junitVersion: String by project

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,21 @@

package org.jetbrains.lincheck.jvm.agent

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.jetbrains.lincheck.util.Logger
import java.io.File
import java.util.*

data class LiveDebuggerSettings(
val lineBreakPoints: MutableList<SnapshotBreakpoint>
) {
fun addBreakpoints(list: List<String>): List<SnapshotBreakpoint> {
val breakpoints = list.map { SnapshotBreakpoint.read(it) }
return addBreakpointsFromList(breakpoints)
}

fun addBreakpointsFromList(breakpoints: List<SnapshotBreakpoint>): List<SnapshotBreakpoint> {
val addedBreakpoints = mutableListOf<SnapshotBreakpoint>()
for (breakpoint in breakpoints) {
if (!lineBreakPoints.contains(breakpoint)) {
Expand All @@ -38,7 +46,7 @@ data class LiveDebuggerSettings(
}
return removedBreakpoints
}

companion object {
const val MAX_ARRAY_ELEMENTS = 10
}
Expand Down Expand Up @@ -100,4 +108,136 @@ data class SnapshotBreakpoint(
result = 31 * result + fileName.hashCode()
return result
}
}
}

/**
* JSON representation of a breakpoint condition.
*/
@Serializable
data class BreakpointConditionJson(
val className: String,
val factoryMethodName: String,
val capturedVariables: List<String>,
val bytecode: String // Base64-encoded bytecode
)

/**
* JSON representation of a breakpoint definition.
*/
@Serializable
data class BreakpointJson(
val className: String,
val filePath: String,
val line: Int,
val condition: BreakpointConditionJson?
)

/**
* Parser for breakpoints JSON file passed via the `breakpointsFile` agent argument.
*/
object BreakpointsFileParser {
private val json = Json { ignoreUnknownKeys = true }

/**
* Parses breakpoints from a JSON file.
*
* @param filePath Path to the JSON file containing breakpoint definitions
* @return List of parsed [SnapshotBreakpoint] objects
* @throws BreakpointsFileException if the file cannot be read or parsed
*/
fun parseBreakpointsFile(filePath: String): List<SnapshotBreakpoint> {
val file = File(filePath)
if (!file.exists()) {
throw BreakpointsFileException("Breakpoints file not found: $filePath")
}
if (!file.canRead()) {
throw BreakpointsFileException("Cannot read breakpoints file: $filePath")
}

val jsonContent = try {
file.readText()
} catch (e: Exception) {
throw BreakpointsFileException("Failed to read breakpoints file: $filePath", e)
}

return parseBreakpointsJson(jsonContent)
}

/**
* Parses breakpoints from a JSON string.
*
* @param jsonContent JSON string containing breakpoint definitions
* @return List of parsed [SnapshotBreakpoint] objects
* @throws BreakpointsFileException if the JSON is malformed or contains invalid data
*/
fun parseBreakpointsJson(jsonContent: String): List<SnapshotBreakpoint> {
val breakpointsJson = try {
json.decodeFromString<List<BreakpointJson>>(jsonContent)
} catch (e: Exception) {
throw BreakpointsFileException("Failed to parse breakpoints JSON: ${e.message}", e)
}

return breakpointsJson.mapIndexed { index, bp ->
try {
convertToSnapshotBreakpoint(bp)
} catch (e: Exception) {
throw BreakpointsFileException(
"Failed to process breakpoint at index $index (${bp.className}:${bp.line}): ${e.message}",
e
)
}
}
}

private fun convertToSnapshotBreakpoint(bp: BreakpointJson): SnapshotBreakpoint {
require(bp.className.isNotBlank()) { "Class name is blank" }
require(bp.filePath.isNotBlank()) { "File path is blank" }
require(bp.line > 0) { "Line number must be positive" }

val condition = bp.condition
if (condition != null) {
require(condition.className.isNotBlank()) { "Condition class name is blank" }
require(condition.factoryMethodName.isNotBlank()) { "Condition factory method name is blank" }
}
val conditionBytecode = condition?.let {
try {
Base64.getDecoder().decode(it.bytecode)
} catch (e: IllegalArgumentException) {
throw BreakpointsFileException(
"Invalid Base64 bytecode for condition class '${it.className}'",
e
)
}
}

return SnapshotBreakpoint(
className = bp.className,
fileName = bp.filePath,
lineNumber = bp.line,
conditionClassName = condition?.className,
conditionFactoryMethodName = condition?.factoryMethodName?.ifBlank { null },
conditionCapturedVars = condition?.capturedVariables,
conditionCodeFragment = conditionBytecode
)
}

/**
* Loads and registers breakpoints from a file into the given [LiveDebuggerSettings].
*
* @param filePath Path to the JSON file containing breakpoint definitions
* @param settings The [LiveDebuggerSettings] to register breakpoints with
* @return List of successfully added breakpoints
*/
fun loadAndRegisterBreakpoints(filePath: String, settings: LiveDebuggerSettings): List<SnapshotBreakpoint> {
Logger.info { "Loading breakpoints from file: $filePath" }
val breakpoints = parseBreakpointsFile(filePath)
val addedBreakpoints = settings.addBreakpointsFromList(breakpoints)
Logger.info { "Registered ${addedBreakpoints.size} new breakpoints from $filePath" }
return addedBreakpoints
}
}

/**
* Exception thrown when there's an error processing the breakpoints file.
*/
class BreakpointsFileException(message: String, cause: Throwable? = null) : Exception(message, cause)
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ import javax.management.remote.JMXServiceURL
* Example: `mode=traceRecorder`
* If not specified, falls back to system properties for backward compatibility.
*
* - class — fully qualified class name to run/transform (required).
* - class — fully qualified class name to run/transform (required for traceRecorder/traceDebugger;
* must be omitted for liveDebugger).
* Example: `class=org.example.MyTest`
*
* - method (required) — name of the public method in that class (required).
* - method — name of the public method in that class (required for traceRecorder/traceDebugger;
* must be omitted for liveDebugger).
* Example: `method=run`
*
* - output — path to a file for trace dump, if supported by the agent (optional).
Expand All @@ -62,6 +64,9 @@ import javax.management.remote.JMXServiceURL
* - jmxMBean — enables registration of JMX MBean for remote monitoring and management.
* Example: `jmxMBean=on` or `jmxMBean=off`, it is off by default.
*
* - breakpointsFile — path to a JSON file with live debugger breakpoints (optional, liveDebugger mode only).
* Example: `breakpointsFile="/tmp/breakpoints.json"`
*
* - format — output format for trace recorder dumps. Possible options are:
* * `binary` --- serialized binary format;
* * `text` --- text output;
Expand Down Expand Up @@ -114,6 +119,7 @@ object TraceAgentParameters {
const val ARGUMENT_INCLUDE = "include"
const val ARGUMENT_EXCLUDE = "exclude"
const val ARGUMENT_JMX_MBEAN = "jmxMBean"
const val ARGUMENT_BREAKPOINTS_FILE = "breakpointsFile"

const val DEFAULT_TRACE_PORT = 9997

Expand All @@ -133,22 +139,25 @@ object TraceAgentParameters {
val jmxMBeanEnabled: Boolean
get() = (getArg(ARGUMENT_JMX_MBEAN)?.lowercase() == "on")

@JvmStatic
val breakpointsFilePath: String?
get() = getArg(ARGUMENT_BREAKPOINTS_FILE)

@JvmStatic
private val namedArgs: MutableMap<String, String?> = mutableMapOf()

@JvmStatic
fun parseArgs(args: String?, validAdditionalArgs: List<String>) {
if (args == null) {
error("Please provide class and method names as arguments")
}
namedArgs.clear()
val actualArgs = args ?: ""
// Store for metainformation
rawArgs = args
rawArgs = actualArgs

// Try to parse key=value format
val kvArguments = parseKVArgs(args)
val kvArguments = parseKVArgs(actualArgs)
if (kvArguments == null) {
Logger.warn { "Looks like old-style arguments found, consider migrate to key-value arguments" }
val actualArguments = splitArgs(args)
val actualArguments = splitArgs(actualArgs)

classUnderTraceDebugging = actualArguments.getOrNull(0) ?: ""
methodUnderTraceDebugging = actualArguments.getOrNull(1) ?: ""
Expand Down Expand Up @@ -201,6 +210,13 @@ object TraceAgentParameters {
}
}

@JvmStatic
fun validateClassAndMethodArgumentsAreNotProvidedInLiveDebuggerMode() {
if (classUnderTraceDebugging.isNotBlank() || methodUnderTraceDebugging.isNotBlank()) {
error("Class and method arguments are not allowed in live debugger mode")
}
}

@JvmStatic
private fun setupMode() {
// Parse mode from agent arguments,
Expand Down
Loading