Updated error reporting

This commit is contained in:
Stef Tervelde
2025-07-01 17:02:03 +02:00
parent 0d1fcc45a9
commit 4cca647724
4 changed files with 90 additions and 134 deletions
+80 -85
View File
@@ -1,103 +1,98 @@
package processing.app.gradle
import com.sun.jdi.ObjectReference
import com.sun.jdi.Location
import com.sun.jdi.StackFrame
import com.sun.jdi.StringReference
import com.sun.jdi.VirtualMachine
import com.sun.jdi.event.ExceptionEvent
import com.sun.jdi.request.EventRequest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import processing.app.Messages
import processing.app.SketchException
import processing.app.ui.Editor
// TODO: Consider adding a panel to the footer
class Exceptions {
companion object {
suspend fun listen(vm: VirtualMachine) {
try {
val manager = vm.eventRequestManager()
class Exceptions (val vm: VirtualMachine, val editor: Editor?) {
suspend fun listen() {
try {
val manager = vm.eventRequestManager()
val request = manager.createExceptionRequest(null, false, true)
request.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD)
request.enable()
val request = manager.createExceptionRequest(null, false, true)
request.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD)
request.enable()
val queue = vm.eventQueue()
while (true) {
val eventSet = queue.remove()
for (event in eventSet) {
if (event is ExceptionEvent) {
printExceptionDetails(event)
event.thread().resume()
}
val queue = vm.eventQueue()
while (true) {
val eventSet = queue.remove()
for (event in eventSet) {
if (event is ExceptionEvent) {
printExceptionDetails(event)
event.thread().resume()
}
eventSet.resume()
delay(10)
}
} catch (e: Exception) {
Messages.log("Error while listening for exceptions: ${e.message}")
eventSet.resume()
delay(10)
}
}
fun printExceptionDetails(event: ExceptionEvent) {
val exception = event.exception()
val thread = event.thread()
val location = event.location()
val stackFrames = thread.frames()
println("\n🚨 Exception Caught 🚨")
println("Type : ${exception.referenceType().name()}")
// TODO: Fix exception message retrieval
// println("Message : ${getExceptionMessage(exception)}")
println("Thread : ${thread.name()}")
println("Location : ${location.sourcePath()}:${location.lineNumber()}\n")
// TODO: Map to .pde file again
// TODO: Communicate back to Editor
// Separate stack frames
val userFrames = mutableListOf<StackFrame>()
val processingFrames = mutableListOf<StackFrame>()
stackFrames.forEach { frame ->
val className = frame.location().declaringType().name()
if (className.startsWith("processing.")) {
processingFrames.add(frame)
} else {
userFrames.add(frame)
}
}
// Print user frames first
println("🔍 Stacktrace (Your Code First):")
userFrames.forEachIndexed { index, frame -> printStackFrame(index, frame) }
// Print Processing frames second
if (processingFrames.isNotEmpty()) {
println("\n🔧 Processing Stacktrace (Hidden Initially):")
processingFrames.forEachIndexed { index, frame -> printStackFrame(index, frame) }
}
println("──────────────────────────────────\n")
}
fun printStackFrame(index: Int, frame: StackFrame) {
val location = frame.location()
val method = location.method()
println(
" #$index ${location.sourcePath()}:${location.lineNumber()} -> ${
method.declaringType().name()
}.${method.name()}()"
)
}
// Extracts the exception's message
fun getExceptionMessage(exception: ObjectReference): String {
val messageMethod = exception.referenceType().methodsByName("getMessage").firstOrNull() ?: return "Unknown"
val messageValue =
exception.invokeMethod(null, messageMethod, emptyList(), ObjectReference.INVOKE_SINGLE_THREADED)
return (messageValue as? StringReference)?.value() ?: "Unknown"
} catch (e: Exception) {
Messages.log("Error while listening for exceptions: ${e.message}")
}
}
fun printExceptionDetails(event: ExceptionEvent) {
val exception = event.exception()
val thread = event.thread()
val location = event.location().mapToPdeFile()
val stackFrames = thread.frames()
val (processingFrames, userFrames) = stackFrames
.map{
val location = it.location().mapToPdeFile()
val method = location.method()
it to "${method.declaringType().name()}.${method.name()}() @ ${location.sourcePath()}:${location.lineNumber()}"
}
.partition {
it.first.location().declaringType().name().startsWith("processing.")
}
/*
We have 6 lines by default within the editor to display more information about the exception.
*/
val message = """
In Processing code:
#processingFrames
In your code:
#userFrames
"""
.trimIndent()
.replace("#processingFrames", processingFrames.joinToString("\n ") { it.second })
.replace("#userFrames", userFrames.joinToString("\n ") { it.second })
val error = """
Exception: ${exception.referenceType().name()} @ ${location.sourcePath()}:${location.lineNumber()}
""".trimIndent()
println(message)
System.err.println(error)
editor?.statusError(exception.referenceType().name())
}
fun Location.mapToPdeFile(): Location {
if(editor == null) return this
// Check if the source is a .java file
val sketch = editor.sketch
sketch.code.forEach { code ->
if(code.extension != "java") return@forEach
if(sourceName() != code.fileName) return@forEach
return@mapToPdeFile this
}
// TODO: Map to .pde file again, @see JavaBuild.placeException
// BLOCKED: Because we don't run the JavaBuild code.prepocOffset is empty
return this
}
}
+6 -49
View File
@@ -42,9 +42,6 @@ class GradleJob{
private val scope = CoroutineScope(Dispatchers.IO)
private val cancel = GradleConnector.newCancellationTokenSource()
private val outputStream = PipedOutputStream()
private val errorStream = PipedOutputStream()
fun start() {
val folder = service?.sketch?.folder ?: throw IllegalStateException("Sketch folder is not set")
scope.launch {
@@ -60,8 +57,10 @@ class GradleJob{
withCancellationToken(cancel.token())
addStateListener()
addDebugging()
setStandardOutput(outputStream)
setStandardError(errorStream)
if(Base.DEBUG) {
setStandardOutput(System.out)
setStandardError(System.err)
}
run()
}
}catch (e: Exception){
@@ -71,49 +70,6 @@ class GradleJob{
vm.value = null
}
}
// TODO: I'm sure this can be done better
scope.launch {
try {
InputStreamReader(PipedInputStream(outputStream)).buffered().use { reader ->
reader.lineSequence()
.forEach { line ->
if (cancel.token().isCancellationRequested) {
return@launch
}
if (state.value != State.RUNNING) {
return@forEach
}
service?.out?.println(line)
}
}
}catch (e: Exception){
Messages.log("Error while reading output: ${e.message}")
}
}
scope.launch {
try {
InputStreamReader(PipedInputStream(errorStream)).buffered().use { reader ->
reader.lineSequence()
.forEach { line ->
if (cancel.token().isCancellationRequested) {
return@launch
}
if (state.value != State.RUNNING) {
return@forEach
}
when{
line.contains("+[IMKClient subclass]: chose IMKClient_Modern") -> return@forEach
line.contains("+[IMKInputSession subclass]: chose IMKInputSession_Modern") -> return@forEach
line.startsWith("__MOVE__") -> return@forEach
else -> service?.err?.println(line)
}
}
}
}catch (e: Exception){
Messages.log("Error while reading error: ${e.message}")
}
}
}
fun cancel(){
@@ -169,7 +125,8 @@ class GradleJob{
scope.launch {
val debugger = Debugger.connect(service?.debugPort) ?: return@launch
vm.value = debugger
Exceptions.listen(debugger)
val exceptions = Exceptions(debugger, service?.editor)
exceptions.listen()
}
})
@@ -20,6 +20,7 @@ import kotlin.io.path.writeText
// TODO: Test running examples
// TODO: Report failures to the console
// TODO: Highlight errors in the editor
// TODO: Stop running sketches if modern build system is turned off
// TODO: ---- FUTURE ----
// TODO: Improve progress tracking
@@ -114,6 +115,7 @@ class GradleService(
"fullscreen" to false, // TODO: Implement
"display" to 1, // TODO: Implement
"external" to true,
"location" to null, // TODO: Implement
"editor.location" to editor?.location?.let { "${it.x},${it.y}" },
//"awt.disable" to false,
//"window.color" to "0xFF000000", // TODO: Implement
@@ -52,6 +52,8 @@ abstract class DependenciesTask: DefaultTask() {
}
project.dependencies.add("implementation", project.files(dependencies) )
// TODO: Mutating the dependencies of configuration ':implementation' after it has been resolved or consumed. This
// TODO: Add only if user is compiling for P2D or P3D
// Add JOGL and Gluegen dependencies
project.dependencies.add("runtimeOnly", "org.jogamp.jogl:jogl-all-main:2.5.0")