Finally working it is!

This commit is contained in:
Shautvast 2024-06-25 18:05:19 +02:00
parent 84374f5d0d
commit 5d368dd063
11 changed files with 371 additions and 53 deletions

View file

@ -1 +1,18 @@
Experiment with java22 and Rust to monitor exceptions in a JVM
Running:
* Update the path to the rust lib (temp fix) in ExceptionLogger for your setup
* mvn clean install
* cd rustlib; cargo build
* create a minimal class in a separate project
```java
public class Main {
public static void main(String[] args) throws Throwable {
throw new Throwable();
}
}
```
* run it with (adjust paths):
``` bash
java22 -javaagent:$EXCEPTIONAL_PROJECT/exceptional/agent/target/exceptional-agent-1.0-SNAPSHOT.jar --enable-preview -classpath $YOUR_CLASSPATH Main
```

View file

@ -18,6 +18,15 @@
<maven.compiler.target>22</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.github.shautvast.exceptional</groupId>
<artifactId>exceptional-lib</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
@ -38,7 +47,9 @@
<Premain-Class>com.github.shautvast.exceptional.Agent</Premain-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Boot-Class-Path>/Users/Shautvast/dev/exceptional/lib/target/exceptional-lib-1.0-SNAPSHOT.jar</Boot-Class-Path>
<Boot-Class-Path>
/Users/Shautvast/dev/exceptional/lib/target/exceptional-lib-1.0-SNAPSHOT.jar
</Boot-Class-Path>
</manifestEntries>
</archive>
</configuration>

View file

@ -1,80 +1,80 @@
package com.github.shautvast.exceptional;
import java.lang.classfile.*;
import java.lang.classfile.instruction.ReturnInstruction;
import java.lang.classfile.ClassBuilder;
import java.lang.classfile.ClassElement;
import java.lang.classfile.ClassFile;
import java.lang.classfile.MethodModel;
import java.lang.classfile.instruction.ThrowInstruction;
import java.lang.constant.ClassDesc;
import java.lang.constant.MethodTypeDesc;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.nio.file.Files;
import java.nio.file.Path;
import java.lang.reflect.AccessFlag;
import java.security.ProtectionDomain;
public class Agent {
@SuppressWarnings("DataFlowIssue")
private static final String EXCEPTIONLOGGER = ExceptionLogger.class.getName();
public static void premain(String agentArgs, Instrumentation instrumentation) throws Exception {
System.err.println("--->Exceptional agent active");
// add transformer
instrumentation.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
return instrumentThrowable(className, classfileBuffer);
return instrumentExceptionLoggerAtThrow(className, classfileBuffer);
}
@Override
public byte[] transform(Module module, ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
return instrumentThrowable(className, classfileBuffer);
return instrumentExceptionLoggerAtThrow(className, classfileBuffer);
}
}, true);
// we only want to redefine Throwable
byte[] bytecode = Files.readAllBytes(
Path.of(Agent.class.getResource("/java/lang/Throwable.class").toURI()));
instrumentation.redefineClasses(new ClassDefinition(Throwable.class, bytecode));
}
private static byte[] instrumentThrowable(String className, byte[] bytecode) {
// we only want to instrument Throwable
// or rather,,, is this the right way? This way we also intercept new any Exception that is not thrown
// But,,, who does that?? (famous last words)
if (className.equals("java/lang/Throwable")) {
ClassFile classFile = ClassFile.of();
ClassModel classModel = classFile.parse(bytecode);
return instrumentConstructors(classFile, classModel);
} else {
return bytecode;
}
}
private static byte[] instrumentConstructors(ClassFile classFile, ClassModel classModel) {
/*
* Every throw opcode will be preceded by a call to our ExceptionLogger
*/
private static byte[] instrumentExceptionLoggerAtThrow(String className, byte[] classfileBuffer) {
var classFile = ClassFile.of();
var classModel = classFile.parse(classfileBuffer);
return classFile.build(classModel.thisClass().asSymbol(), classBuilder -> {
for (ClassElement ce : classModel) {
if (ce instanceof MethodModel mm && mm.methodName().toString().equals("<init>")) {
instrumentMethodWithMyExceptionLogger(classBuilder, mm);
continue;
// not all methods, TODO include synthetic?
if (ce instanceof MethodModel methodModel && !methodModel.flags().has(AccessFlag.ABSTRACT)
&& !methodModel.flags().has(AccessFlag.NATIVE)
&& !methodModel.flags().has(AccessFlag.BRIDGE)) {
transform(classBuilder, methodModel);
} else {
// keep all other class elements
classBuilder.with(ce);
}
classBuilder.with(ce);
}
});
}
private static void instrumentMethodWithMyExceptionLogger(ClassBuilder classBuilder, MethodModel methodModel) {
methodModel.code().ifPresent(code ->
classBuilder.withMethod(methodModel.methodName(), methodModel.methodType(), methodModel.flags().flagsMask(),
methodBuilder ->
methodBuilder.transformCode(code, ((builder, element) -> {
if (element instanceof ReturnInstruction) {
builder.aload(0); // load `this` on the stack, ie the Throwable instance
builder.invokestatic( // call my code
ClassDesc.of("com.github.shautvast.exceptional.ExceptionLogger"), "log",
MethodTypeDesc.ofDescriptor("(Ljava/lang/Throwable;)V"));
builder.return_();
} else {
builder.with(element); // leave any other bytecode in place
}
})
)));
private static void transform(ClassBuilder classBuilder, MethodModel methodModel) {
methodModel.code().ifPresent(code -> classBuilder.withMethod(
methodModel.methodName(), // copy name, type and modifiers from the original
methodModel.methodType(),
methodModel.flags().flagsMask(),
methodBuilder -> {
// keep the existing annotations (and other method elements) in place
methodModel.forEachElement(methodBuilder::with);
// change the code
methodBuilder.transformCode(code, (builder, element) -> {
// this way of instrumenting may miss the already loaded classes, java.lang.String for example.
// May need to circumvent that
if (element instanceof ThrowInstruction) {
builder.dup(); // on top of the stack is the current exception instance
// duplicate it to make sure the `athrow` op has something to throw
// after the invoke to ExceptionLogger has popped one off
builder.invokestatic( // call my code with the exception as argument
ClassDesc.of(EXCEPTIONLOGGER), "log",
MethodTypeDesc.ofDescriptor("(Ljava/lang/Throwable;)V"));
}
builder.with(element); // leave any other element in place
});
}));
}
}

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>exceptional-parent</artifactId>
<groupId>org.github.shautvast.exceptional</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>exceptional-lib</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>22</source>
<target>22</target>
<compilerArgs>--enable-preview</compilerArgs>
</configuration>
</plugin>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration />
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>junit-jupiter-api</artifactId>
<groupId>org.junit.jupiter</groupId>
</exclusion>
<exclusion>
<artifactId>junit-jupiter-params</artifactId>
<groupId>org.junit.jupiter</groupId>
</exclusion>
<exclusion>
<artifactId>junit-jupiter-engine</artifactId>
<groupId>org.junit.jupiter</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<properties>
<maven.compiler.target>22</maven.compiler.target>
<maven.compiler.source>22</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View file

@ -18,6 +18,19 @@
<maven.compiler.target>22</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
@ -30,8 +43,23 @@
<compilerArgs>--enable-preview</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

@ -1,11 +1,38 @@
package com.github.shautvast.exceptional;
import java.util.Arrays;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
@SuppressWarnings("unused") // this code is called from the instrumented code
public class ExceptionLogger {
private static final Arena arena = Arena.ofConfined();
private static final Linker linker = Linker.nativeLinker();
// //TODO relative path, or configurable
private static final SymbolLookup rustlib = SymbolLookup.libraryLookup("/Users/Shautvast/dev/exceptional/rustlib/target/debug/librustlib.dylib", arena);
private final static MethodHandle logNative;
private final static ObjectMapper objectMapper = new ObjectMapper();
static {
MemorySegment logFunction = rustlib.find("log_java_exception").orElseThrow();
logNative = linker.downcallHandle(logFunction, FunctionDescriptor.ofVoid(
ValueLayout.ADDRESS
));
}
// how does this behave in a multithreaded context??
// probably need a ringbuffer with fixed memory to make this work efficiently
public static void log(Throwable throwable) {
System.out.print("Logging exception:");
Arrays.stream(throwable.getStackTrace()).forEach(System.out::println);
try {
// use json for now because of ease of integration
if (throwable != null) {
String json = objectMapper.writeValueAsString(throwable);
var data = arena.allocateFrom(json); // reuse instead of reallocating?
logNative.invoke(data); // invoke the rust function
}
} catch (Throwable e) {
e.printStackTrace(System.err);
}
}
}

View file

@ -0,0 +1,14 @@
package com.github.shautvast.exceptional;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ExceptionLoggerTest {
@Test
void test(){
ExceptionLogger.log(new Throwable());
}
}

96
rustlib/Cargo.lock generated Normal file
View file

@ -0,0 +1,96 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anyhow"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustlib"
version = "0.1.0"
dependencies = [
"anyhow",
"serde",
"serde_json",
]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff8655ed1d86f3af4ee3fd3263786bc14245ad17c4c7e85ba7187fb3ae028c90"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"

13
rustlib/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "rustlib"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
bench = false
[dependencies]
serde_json = { version = "1.0", features = [] }
serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0"

32
rustlib/src/Throwable.rs Normal file
View file

@ -0,0 +1,32 @@
use serde::Deserialize;
#[derive(Deserialize, Debug)]
pub struct Throwable {
cause: Option<Box<Throwable>>,
#[serde(rename (deserialize = "stackTrace"))]
stack_trace: Vec<Stacktrace>,
message: Option<String>,
suppressed: Vec<String>,
#[serde(rename (deserialize = "localizedMessage"))]
localized_message: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct Stacktrace{
#[serde(rename (deserialize = "classLoaderName"))]
class_loader_name: Option<String>,
#[serde(rename (deserialize = "moduleName"))]
module_name: Option<String>,
#[serde(rename (deserialize = "moduleVersion"))]
module_version: Option<String>,
#[serde(rename (deserialize = "methodName"))]
method_name: Option<String>,
#[serde(rename (deserialize = "fileName"))]
file_name: Option<String>,
#[serde(rename (deserialize = "lineNumber"))]
line_number: Option<u32>,
#[serde(rename (deserialize = "className"))]
class_name: Option<String>,
#[serde(rename (deserialize = "nativeMethod"))]
native_method: Option<bool>,
}

17
rustlib/src/lib.rs Normal file
View file

@ -0,0 +1,17 @@
mod throwable;
use std::ffi::{c_char, CStr};
#[no_mangle]
pub extern "C" fn log_java_exception(raw_string: *const c_char) {
let c_str = unsafe { CStr::from_ptr(raw_string) };
let string = c_str.to_str();
if let Ok(json) = string {
println!("receiving {}", json);
let error: throwable::Throwable = serde_json::from_str(json).unwrap();
println!("{:?}", error);
}
}
#[cfg(test)]
mod tests {}