Skip to content

Commit 667a44f

Browse files
Marcono1234urielsalis
authored andcommitted
Improve JVM crash report parsing
1 parent f235936 commit 667a44f

16 files changed

+877
-33
lines changed

src/main/kotlin/com/urielsalis/mccrashlib/Crash.kt

+41-2
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,48 @@ sealed class Crash {
1515
) : Crash()
1616

1717
data class Jvm(
18-
val modded: Boolean,
19-
val code: String
18+
/** Exception / OS signal causing the crash, e.g. `EXCEPTION_ACCESS_VIOLATION (0xc0000005) at ...`*/
19+
val exception: String?,
20+
/** Frame reported as 'problematic frame', causing the crash */
21+
val problematicFrame: JvmFrame?,
22+
/** Frames of native code stack trace, in the order in which they appear in the crash report (top to bottom) */
23+
val nativeFrames: List<JvmFrame>?,
24+
/** Frames of Java stack trace, in the order in which they appear in the crash report (top to bottom) */
25+
val javaFrames: List<JvmFrame>?,
26+
/** Whether the crash report indicates that Minecraft is modded */
27+
val isModded: Boolean
2028
) : Crash()
2129

30+
// See also https://github.com/openjdk/jdk/blob/829dea45c9ab90518f03a66aad7e681cd4fda8b3/src/hotspot/share/runtime/frame.cpp#L569-L574
31+
sealed class JvmFrame {
32+
/** Frame in compiled Java code (type `J`) */
33+
data class JavaFrameCompiled(val location: String) : JvmFrame()
34+
/** Frame in interpreted Java code (type `j`) */
35+
data class JavaFrameInterpreted(val location: String) : JvmFrame()
36+
/** Frame in VM code (type `V`) */
37+
data class VmFrame(val location: String) : JvmFrame()
38+
/** Frame in VM generated code (type `v`) */
39+
data class VmGeneratedFrame(val location: String) : JvmFrame()
40+
/** Frame in C/C++ code (type `C`) */
41+
data class CFrame(
42+
/** The complete location information of the frame */
43+
val location: String,
44+
/** Name of the library, e.g. `ig75icd64.dll` */
45+
val libraryName: String?,
46+
/** Offset in the library, e.g. `0x1c82` */
47+
val libraryOffset: String?,
48+
/** Name of the function, e.g. `DrvSetLayerPaletteEntries` */
49+
val functionName: String?,
50+
/** Offset in the function, e.g. `0x96885` */
51+
val functionOffset: String?
52+
) : JvmFrame()
53+
54+
/** Frame type which could not be parsed as one of the other types */
55+
data class OtherFrame(
56+
/** The complete value of the frame, including the prefix char, if any */
57+
val value: String
58+
) : JvmFrame()
59+
}
60+
2261
object LauncherLog : Crash()
2362
}

src/main/kotlin/com/urielsalis/mccrashlib/CrashReader.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
package com.urielsalis.mccrashlib
22

33
import arrow.core.Either
4+
import com.urielsalis.mccrashlib.parser.JVM_CRASH_HEADER
45
import com.urielsalis.mccrashlib.parser.JvmCrashParser
56
import com.urielsalis.mccrashlib.parser.MinecraftCrashParser
67
import com.urielsalis.mccrashlib.parser.ParserError
78
import java.io.File
89

9-
const val MINECRAFT_CRASH_HEADER = "---- Minecraft Crash Report ----"
10-
const val JVM_CRASH_HEADER = "# EXCEPTION_ACCESS_VIOLATION"
11-
const val SERVER_CONSOLE_LOG = "Client> "
12-
const val LAUNCHER_LOG_CONTENT = "Launcher/launcher (main) Info"
10+
private const val MINECRAFT_CRASH_HEADER = "---- Minecraft Crash Report ----"
11+
private const val SERVER_CONSOLE_LOG = "Client> "
12+
private const val LAUNCHER_LOG_CONTENT = "Launcher/launcher (main) Info"
1313

1414
class CrashReader {
1515
private val minecraftCrashParser = MinecraftCrashParser()

src/main/kotlin/com/urielsalis/mccrashlib/parser/JvmCrashParser.kt

+124-17
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,136 @@
11
package com.urielsalis.mccrashlib.parser
22

33
import arrow.core.Either
4-
import arrow.core.firstOrNone
54
import com.urielsalis.mccrashlib.Crash
65
import java.io.File
76

8-
/*
9-
All the lines of the crash start with #
10-
The line we care about is
11-
# C [xxxxxxxx+0x1c82]
12-
where we want to extract the xxxxxx
13-
*/
7+
// https://github.com/openjdk/jdk/blob/829dea45c9ab90518f03a66aad7e681cd4fda8b3/src/hotspot/share/utilities/vmError.cpp#L537
8+
internal const val JVM_CRASH_HEADER = "# A fatal error has been detected by the Java Runtime Environment:"
9+
10+
// https://github.com/openjdk/jdk/blob/829dea45c9ab90518f03a66aad7e681cd4fda8b3/src/hotspot/share/utilities/vmError.cpp#L700
11+
private const val PROBLEMATIC_FRAME_MARKER = "# Problematic frame:"
12+
// https://github.com/openjdk/jdk/blob/829dea45c9ab90518f03a66aad7e681cd4fda8b3/src/hotspot/share/utilities/vmError.cpp#L236
13+
private const val JAVA_STACK_TRACE_MARKER = "Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)"
14+
15+
private val NATIVE_STACK_TRACE_MARKERS = listOf(
16+
// https://github.com/openjdk/jdk/blob/829dea45c9ab90518f03a66aad7e681cd4fda8b3/src/hotspot/share/utilities/vmError.cpp#L350
17+
"Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)",
18+
// Before JDK-8264805
19+
// https://github.com/openjdk/jdk/blob/15d4787724ad8723d36e771a9709db51933df2c1/src/hotspot/share/utilities/vmError.cpp#L245
20+
"Native frames: (J=compiled Java code, A=aot compiled Java code, j=interpreted, Vv=VM code, C=native code)"
21+
)
22+
1423
class JvmCrashParser : CrashParser {
15-
object IncompleteJvmCrash : ParserError
24+
/** If present removes the prefix, otherwise returns `null` */
25+
private fun String.removeRequiredPrefix(prefix: String): String? {
26+
return if (startsWith(prefix)) substring(prefix.length) else null
27+
}
28+
29+
private fun parseFrame(frameLine: String): Crash.JvmFrame {
30+
if (frameLine.length >= 2 && frameLine[1] == ' ') {
31+
val frameTypeChar = frameLine[0]
32+
// Use `trimStart()` because some frames have two spaces between type char and location
33+
val frameLocation = frameLine.substring(2).trimStart()
34+
35+
// https://github.com/openjdk/jdk/blob/829dea45c9ab90518f03a66aad7e681cd4fda8b3/src/hotspot/share/runtime/frame.cpp#L569-L574
36+
return when (frameTypeChar) {
37+
// Not parsing most of these because they are not relevant for Arisa
38+
'J' -> Crash.JvmFrame.JavaFrameCompiled(frameLocation)
39+
'j' -> Crash.JvmFrame.JavaFrameInterpreted(frameLocation)
40+
'V' -> Crash.JvmFrame.VmFrame(frameLocation)
41+
'v' -> Crash.JvmFrame.VmGeneratedFrame(frameLocation)
42+
'C' -> parseCFrame(frameLocation)
43+
else -> Crash.JvmFrame.OtherFrame(frameLine)
44+
}
45+
}
46+
return Crash.JvmFrame.OtherFrame(frameLine)
47+
}
48+
49+
// https://github.com/openjdk/jdk/blob/829dea45c9ab90518f03a66aad7e681cd4fda8b3/src/hotspot/share/runtime/frame.cpp#L536
50+
/**
51+
* Groups:
52+
* 1. library name
53+
* 2. library offset
54+
* 3. function name (optional)
55+
* 4. function offset (optional)
56+
*
57+
* Only tries to parse function name and offset when library location was parsed successfully;
58+
* otherwise it might be ambiguous.
59+
*/
60+
private val C_FRAME_REGEX = Regex("""\[(.+)\+(.+)\](?: (.+)\+(.+))?""")
61+
private fun parseCFrame(frameLocation: String): Crash.JvmFrame.CFrame {
62+
var libraryName: String? = null
63+
var libraryOffset: String? = null
64+
var functionName: String? = null
65+
var functionOffset: String? = null
66+
67+
C_FRAME_REGEX.matchEntire(frameLocation)?.groups?.apply {
68+
libraryName = get(1)?.value
69+
libraryOffset = get(2)?.value
70+
functionName = get(3)?.value
71+
functionOffset = get(4)?.value
72+
}
73+
74+
return Crash.JvmFrame.CFrame(frameLocation, libraryName, libraryOffset, functionName, functionOffset)
75+
}
76+
77+
private fun parseStackTrace(lines: List<String>, marker: String): List<Crash.JvmFrame>? {
78+
val markerIndex = lines.indexOf(marker)
79+
if (markerIndex == -1) {
80+
return null
81+
}
82+
83+
val stackTrace = mutableListOf<Crash.JvmFrame>()
84+
for (i in markerIndex + 1 until lines.size) {
85+
val line = lines[i]
86+
if (line.isBlank()) {
87+
break
88+
}
89+
90+
stackTrace.add(parseFrame(line))
91+
}
92+
return stackTrace
93+
}
94+
95+
private fun <E> List<E>.indexOf(e: E, startIndex: Int): Int {
96+
val i = subList(startIndex, size).indexOf(e)
97+
return if (i == -1) i else startIndex + i
98+
}
1699

17100
override fun parse(lines: List<String>, mappingsDirectory: File): Either<ParserError, Crash> {
18-
val linesWithMarker = lines.map(String::trim).filter { it.startsWith("#") }
19-
val importantLine = linesWithMarker.firstOrNone { it.startsWith("# C") && "[" in it && "+" in it }
20-
return importantLine.fold(
21-
{ Either.left(IncompleteJvmCrash) },
22-
{ Either.right(Crash.Jvm(
23-
isModded(lines),
24-
it.substring(it.indexOf('[') + 1, it.indexOf('+')))
25-
) }
26-
)
101+
val trimmedLines = lines.map(String::trim)
102+
103+
val crashHeaderIndex = trimmedLines.indexOf(JVM_CRASH_HEADER)
104+
if (crashHeaderIndex == -1) {
105+
throw IllegalArgumentException("Not a JVM crash")
106+
}
107+
108+
// Find next line containing only '#' followed by line containing exception
109+
// https://github.com/openjdk/jdk/blob/829dea45c9ab90518f03a66aad7e681cd4fda8b3/src/hotspot/share/utilities/vmError.cpp#L643-L644
110+
var exception: String? = null
111+
val nextEmptyIndex = lines.indexOf("#", crashHeaderIndex)
112+
if (nextEmptyIndex != -1) {
113+
exception = lines.getOrNull(nextEmptyIndex + 1)?.removeRequiredPrefix("# ")
114+
}
115+
116+
val frameMarkerIndex = trimmedLines.indexOf(PROBLEMATIC_FRAME_MARKER)
117+
var frame: Crash.JvmFrame? = null
118+
119+
if (frameMarkerIndex != -1) {
120+
val frameLine = trimmedLines.getOrNull(frameMarkerIndex + 1)?.removeRequiredPrefix("# ")
121+
frame = frameLine?.let(::parseFrame)
122+
}
123+
124+
return Either.right(Crash.Jvm(
125+
exception,
126+
frame,
127+
// Find first matching marker
128+
NATIVE_STACK_TRACE_MARKERS.asSequence()
129+
.mapNotNull { marker -> parseStackTrace(trimmedLines, marker) }
130+
.firstOrNull(),
131+
parseStackTrace(trimmedLines, JAVA_STACK_TRACE_MARKER),
132+
isModded(lines)
133+
))
27134
}
28135

29136
private fun isModded(lines: List<String>): Boolean {

src/test/kotlin/CrashReaderIntegrationTest.kt

+13-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import arrow.core.Either
2+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
23
import com.urielsalis.mccrashlib.Crash
34
import com.urielsalis.mccrashlib.CrashReader
45
import org.junit.jupiter.api.Assertions.assertEquals
@@ -26,8 +27,8 @@ class CrashReaderIntegrationTest {
2627
private lateinit var directory: String
2728
private lateinit var ignoredNameSuffixes: Array<String>
2829

29-
override fun accept(t: ResourceFilesSource?) {
30-
directory = t!!.directory
30+
override fun accept(t: ResourceFilesSource) {
31+
directory = t.directory
3132
ignoredNameSuffixes = t.ignoredNameSuffixes
3233
}
3334

@@ -67,19 +68,21 @@ class CrashReaderIntegrationTest {
6768
}
6869

6970
@ParameterizedTest
70-
@ResourceFilesSource("crashes/jvm")
71+
@ResourceFilesSource("crashes/jvm", ignoredNameSuffixes = ["-parsed"])
7172
fun shouldRunAllJvmCrashes(crashFile: File) {
72-
val name = crashFile.name
73-
val parts = name.split("-")
74-
val isModded = parts[0] == "true"
75-
val expectedCode = parts[1]
73+
val expectedParsedCrashString = File(crashFile.parent, crashFile.nameWithoutExtension + "-parsed.txt")
74+
.takeIf(File::isFile)?.readText()
7675

7776
val crashEither = crashReader.processCrash(crashFile.readLines(), tempDir)
7877
assertTrue(crashEither is Either.Right)
7978
val crash = (crashEither as Either.Right).b
8079
assertTrue(crash is Crash.Jvm)
81-
assertEquals(isModded, (crash as Crash.Jvm).modded)
82-
assertEquals(expectedCode, crash.code)
80+
81+
val mapper = jacksonObjectMapper()
82+
// For simplicity compare the JSON string representation of the parsed crash
83+
val actualParsedCrashString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(crash)
84+
85+
assertEquals(expectedParsedCrashString, actualParsedCrashString)
8386
}
8487

8588
@ParameterizedTest
@@ -95,7 +98,7 @@ class CrashReaderIntegrationTest {
9598
private fun String.dropLastLineTerminator() = removeSuffix("\n").removeSuffix("\r")
9699

97100
@ParameterizedTest
98-
@ResourceFilesSource("crashes/deobfuscator", ignoredNameSuffixes = ["deobf"])
101+
@ResourceFilesSource("crashes/deobfuscator", ignoredNameSuffixes = ["-deobf"])
99102
fun shouldProcessDeobfuscation(crashFile: File) {
100103
val either = crashReader.processCrash(crashFile.readLines(), tempDir)
101104
val deobfContent = File(crashFile.parent, crashFile.nameWithoutExtension + "-deobf.txt")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
{
2+
"exception" : "EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00007ffef24cbad2, pid=1968, tid=8448",
3+
"problematicFrame" : {
4+
"location" : "[atio6axx.dll+0xbbbad2]",
5+
"libraryName" : "atio6axx.dll",
6+
"libraryOffset" : "0xbbbad2",
7+
"functionName" : null,
8+
"functionOffset" : null
9+
},
10+
"nativeFrames" : [ {
11+
"location" : "[atio6axx.dll+0xbbbad2]",
12+
"libraryName" : "atio6axx.dll",
13+
"libraryOffset" : "0xbbbad2",
14+
"functionName" : null,
15+
"functionOffset" : null
16+
}, {
17+
"location" : "[atio6axx.dll+0xbb930f]",
18+
"libraryName" : "atio6axx.dll",
19+
"libraryOffset" : "0xbb930f",
20+
"functionName" : null,
21+
"functionOffset" : null
22+
}, {
23+
"location" : "[atio6axx.dll+0xdcaee7]",
24+
"libraryName" : "atio6axx.dll",
25+
"libraryOffset" : "0xdcaee7",
26+
"functionName" : null,
27+
"functionOffset" : null
28+
}, {
29+
"location" : "0x0000000003215894",
30+
"libraryName" : null,
31+
"libraryOffset" : null,
32+
"functionName" : null,
33+
"functionOffset" : null
34+
} ],
35+
"javaFrames" : [ {
36+
"location" : "org.lwjgl.opengl.ARBShaderObjects.nglShaderSourceARB(IIJJ)V+0"
37+
}, {
38+
"location" : "org.lwjgl.opengl.ARBShaderObjects.glShaderSourceARB(ILjava/lang/CharSequence;)V+38"
39+
}, {
40+
"location" : "net.optifine.shaders.Shaders.createFragShader(Lnet/optifine/shaders/Program;Ljava/lang/String;)I+2097"
41+
}, {
42+
"location" : "net.optifine.shaders.Shaders.setupProgram(Lnet/optifine/shaders/Program;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V+52"
43+
}, {
44+
"location" : "net.optifine.shaders.Shaders.init()V+710"
45+
}, {
46+
"location" : "19468 C1 net.optifine.shaders.Shaders.beginRender(Lcyc;Lcxq;FJ)V (935 bytes) @ 0x000000000658491c [0x0000000006584720+0x1fc]"
47+
}, {
48+
"location" : "19467 C1 dnc.a(FJ)V (105 bytes) @ 0x0000000006577a0c [0x0000000006577740+0x2cc]"
49+
}, {
50+
"location" : "7699 C1 dnc.a(FJZ)V (1040 bytes) @ 0x0000000004662ce4 [0x0000000004661ac0+0x1224]"
51+
}, {
52+
"location" : "18177 C2 cyc.e(Z)V (916 bytes) @ 0x00000000060781bc [0x0000000006077000+0x11bc]"
53+
}, {
54+
"location" : "cyc.b()V+85"
55+
}, {
56+
"location" : "net.minecraft.client.main.Main.main([Ljava/lang/String;)V+1174"
57+
}, {
58+
"location" : "~StubRoutines::call_stub"
59+
}, {
60+
"location" : "sun.reflect.NativeMethodAccessorImpl.invoke0(Ljava/lang/reflect/Method;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;+0"
61+
}, {
62+
"location" : "sun.reflect.NativeMethodAccessorImpl.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;+100"
63+
}, {
64+
"location" : "sun.reflect.DelegatingMethodAccessorImpl.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;+6"
65+
}, {
66+
"location" : "java.lang.reflect.Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;+56"
67+
}, {
68+
"location" : "net.minecraft.launchwrapper.Launch.launch([Ljava/lang/String;)V+661"
69+
}, {
70+
"location" : "net.minecraft.launchwrapper.Launch.main([Ljava/lang/String;)V+8"
71+
}, {
72+
"location" : "~StubRoutines::call_stub"
73+
} ],
74+
"isModded" : true
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"exception" : "EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x0000000000000000, pid=10652, tid=4420",
3+
"problematicFrame" : {
4+
"location" : "0x0000000000000000",
5+
"libraryName" : null,
6+
"libraryOffset" : null,
7+
"functionName" : null,
8+
"functionOffset" : null
9+
},
10+
"nativeFrames" : [ ],
11+
"javaFrames" : [ {
12+
"location" : "org.lwjgl.system.JNI.invokePPPP(IIJJJJ)J+0"
13+
}, {
14+
"location" : "org.lwjgl.glfw.GLFW.nglfwCreateWindow(IIJJJ)J+14"
15+
}, {
16+
"location" : "org.lwjgl.glfw.GLFW.glfwCreateWindow(IILjava/lang/CharSequence;JJ)J+37"
17+
}, {
18+
"location" : "dpr.<init>(Ldps;Ldpo;Ldpd;Ljava/lang/String;Ljava/lang/String;)V+286"
19+
}, {
20+
"location" : "enx.a(Ldpd;Ljava/lang/String;Ljava/lang/String;)Ldpr;+15"
21+
}, {
22+
"location" : "dvp.<init>(Leey;)V+712"
23+
}, {
24+
"location" : "net.minecraft.client.main.Main.main([Ljava/lang/String;)V+1211"
25+
}, {
26+
"location" : "~StubRoutines::call_stub"
27+
} ],
28+
"isModded" : false
29+
}

0 commit comments

Comments
 (0)