diff --git a/README.md b/README.md index d5c557f..75c934d 100644 --- a/README.md +++ b/README.md @@ -1 +1,18 @@ -Experiment with java22 and Rust to monitor exceptions in a JVM \ No newline at end of file +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 +``` diff --git a/agent/pom.xml b/agent/pom.xml index deb8ba9..2ea49d2 100644 --- a/agent/pom.xml +++ b/agent/pom.xml @@ -18,6 +18,15 @@ 22 UTF-8 + + + + org.github.shautvast.exceptional + exceptional-lib + 1.0-SNAPSHOT + + + @@ -38,7 +47,9 @@ com.github.shautvast.exceptional.Agent true true - /Users/Shautvast/dev/exceptional/lib/target/exceptional-lib-1.0-SNAPSHOT.jar + + /Users/Shautvast/dev/exceptional/lib/target/exceptional-lib-1.0-SNAPSHOT.jar + diff --git a/agent/src/main/java/com/github/shautvast/exceptional/Agent.java b/agent/src/main/java/com/github/shautvast/exceptional/Agent.java index 86885fe..e06a45f 100644 --- a/agent/src/main/java/com/github/shautvast/exceptional/Agent.java +++ b/agent/src/main/java/com/github/shautvast/exceptional/Agent.java @@ -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("")) { - 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 + }); + })); } } \ No newline at end of file diff --git a/lib/dependency-reduced-pom.xml b/lib/dependency-reduced-pom.xml new file mode 100644 index 0000000..d4cf151 --- /dev/null +++ b/lib/dependency-reduced-pom.xml @@ -0,0 +1,63 @@ + + + + exceptional-parent + org.github.shautvast.exceptional + 1.0-SNAPSHOT + + 4.0.0 + exceptional-lib + 1.0-SNAPSHOT + + + + maven-compiler-plugin + + 22 + 22 + --enable-preview + + + + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + + + + + + org.junit.jupiter + junit-jupiter + RELEASE + test + + + junit-jupiter-api + org.junit.jupiter + + + junit-jupiter-params + org.junit.jupiter + + + junit-jupiter-engine + org.junit.jupiter + + + + + + 22 + 22 + UTF-8 + + diff --git a/lib/pom.xml b/lib/pom.xml index 31b0304..99ea5bb 100644 --- a/lib/pom.xml +++ b/lib/pom.xml @@ -18,6 +18,19 @@ 22 UTF-8 + + + com.fasterxml.jackson.core + jackson-databind + 2.16.0 + + + org.junit.jupiter + junit-jupiter + RELEASE + test + + @@ -30,8 +43,23 @@ --enable-preview - + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + + + \ No newline at end of file diff --git a/lib/src/main/java/com/github/shautvast/exceptional/ExceptionLogger.java b/lib/src/main/java/com/github/shautvast/exceptional/ExceptionLogger.java index b8df9ae..303a283 100644 --- a/lib/src/main/java/com/github/shautvast/exceptional/ExceptionLogger.java +++ b/lib/src/main/java/com/github/shautvast/exceptional/ExceptionLogger.java @@ -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); + } } } diff --git a/lib/src/test/java/com/github/shautvast/exceptional/ExceptionLoggerTest.java b/lib/src/test/java/com/github/shautvast/exceptional/ExceptionLoggerTest.java new file mode 100644 index 0000000..8ed57a2 --- /dev/null +++ b/lib/src/test/java/com/github/shautvast/exceptional/ExceptionLoggerTest.java @@ -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()); + } + +} \ No newline at end of file diff --git a/rustlib/Cargo.lock b/rustlib/Cargo.lock new file mode 100644 index 0000000..6236b71 --- /dev/null +++ b/rustlib/Cargo.lock @@ -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" diff --git a/rustlib/Cargo.toml b/rustlib/Cargo.toml new file mode 100644 index 0000000..f7a7650 --- /dev/null +++ b/rustlib/Cargo.toml @@ -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" \ No newline at end of file diff --git a/rustlib/src/Throwable.rs b/rustlib/src/Throwable.rs new file mode 100644 index 0000000..9c1de6e --- /dev/null +++ b/rustlib/src/Throwable.rs @@ -0,0 +1,32 @@ +use serde::Deserialize; + +#[derive(Deserialize, Debug)] +pub struct Throwable { + cause: Option>, + #[serde(rename (deserialize = "stackTrace"))] + stack_trace: Vec, + message: Option, + suppressed: Vec, + #[serde(rename (deserialize = "localizedMessage"))] + localized_message: Option, +} + +#[derive(Deserialize, Debug)] +pub struct Stacktrace{ + #[serde(rename (deserialize = "classLoaderName"))] + class_loader_name: Option, + #[serde(rename (deserialize = "moduleName"))] + module_name: Option, + #[serde(rename (deserialize = "moduleVersion"))] + module_version: Option, + #[serde(rename (deserialize = "methodName"))] + method_name: Option, + #[serde(rename (deserialize = "fileName"))] + file_name: Option, + #[serde(rename (deserialize = "lineNumber"))] + line_number: Option, + #[serde(rename (deserialize = "className"))] + class_name: Option, + #[serde(rename (deserialize = "nativeMethod"))] + native_method: Option, +} \ No newline at end of file diff --git a/rustlib/src/lib.rs b/rustlib/src/lib.rs new file mode 100644 index 0000000..a7f876d --- /dev/null +++ b/rustlib/src/lib.rs @@ -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 {}