Skip to content

Commit 6758d68

Browse files
committed
Richer console output and richer exception output
1 parent 15114a1 commit 6758d68

File tree

9 files changed

+193
-44
lines changed

9 files changed

+193
-44
lines changed

app/src/processing/app/gradle/ActionGradleJob.kt

Lines changed: 0 additions & 3 deletions
This file was deleted.

app/src/processing/app/gradle/BackgroundGradleJob.kt

Lines changed: 0 additions & 3 deletions
This file was deleted.

app/src/processing/app/gradle/GradleJob.kt

Lines changed: 172 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ package processing.app.gradle
22

33
import androidx.compose.runtime.mutableStateListOf
44
import androidx.compose.runtime.mutableStateOf
5-
import com.sun.jdi.Bootstrap
6-
import com.sun.jdi.VirtualMachine
5+
import com.sun.jdi.*
76
import com.sun.jdi.connect.AttachingConnector
7+
import com.sun.jdi.event.ExceptionEvent
8+
import com.sun.jdi.request.EventRequest
89
import kotlinx.coroutines.CoroutineScope
910
import kotlinx.coroutines.Dispatchers
1011
import kotlinx.coroutines.delay
@@ -13,14 +14,24 @@ import org.gradle.tooling.BuildLauncher
1314
import org.gradle.tooling.GradleConnector
1415
import org.gradle.tooling.events.ProgressListener
1516
import org.gradle.tooling.events.problems.ProblemEvent
17+
import org.gradle.tooling.events.problems.Severity
18+
import org.gradle.tooling.events.problems.internal.DefaultFileLocation
19+
import org.gradle.tooling.events.problems.internal.DefaultSingleProblemEvent
1620
import org.gradle.tooling.events.task.TaskFinishEvent
1721
import org.gradle.tooling.events.task.TaskStartEvent
22+
import processing.app.Base
1823
import processing.app.Messages
24+
import java.io.InputStreamReader
25+
import java.io.PipedInputStream
26+
import java.io.PipedOutputStream
1927
import kotlin.time.Duration.Companion.seconds
2028
import kotlin.time.TimeSource
2129

22-
// TODO: Capture and filter gradle output
23-
open abstract class GradleJob{
30+
31+
// TODO: Refactor, reduce its scope
32+
// TODO: Move the error reporting to its own file
33+
// TODO: Move the output filtering to its own file
34+
abstract class GradleJob{
2435
enum class State{
2536
NONE,
2637
BUILDING,
@@ -38,6 +49,9 @@ open abstract class GradleJob{
3849
private val scope = CoroutineScope(Dispatchers.IO)
3950
private val cancel = GradleConnector.newCancellationTokenSource()
4051

52+
private val outputStream = PipedOutputStream()
53+
private val errorStream = PipedOutputStream()
54+
4155
fun start() {
4256
service?.jobs?.add(this)
4357
val connection = service?.connection ?: return
@@ -48,32 +62,77 @@ open abstract class GradleJob{
4862
connection.newBuild()
4963
.apply {
5064
configure()
65+
withCancellationToken(cancel.token())
66+
addStateListener()
67+
addDebugging()
68+
if(Base.DEBUG) {
69+
setStandardOutput(System.out)
70+
setStandardError(System.err)
71+
} else {
72+
setStandardOutput(outputStream)
73+
setStandardError(errorStream)
74+
}
75+
run()
5176
}
52-
.withCancellationToken(cancel.token())
53-
.addStateListener()
54-
.addDebugging()
55-
.run()
77+
5678

5779
}catch (e: Exception){
58-
if(state.value == State.RUNNING){
59-
Messages.log("Error while running: ${e.message}")
60-
}else{
61-
throw e
62-
}
80+
Messages.log("Error while running: ${e.message}")
6381
}finally {
6482
state.value = State.DONE
6583
vm.value = null
6684
}
67-
6885
}
86+
scope.launch {
87+
try {
88+
InputStreamReader(PipedInputStream(outputStream)).buffered().use { reader ->
89+
reader.lineSequence()
90+
.forEach { line ->
91+
if (cancel.token().isCancellationRequested) {
92+
return@launch
93+
}
94+
if (state.value != State.RUNNING) {
95+
return@forEach
96+
}
97+
service?.editor?.console?.out?.println(line)
98+
}
99+
}
100+
}catch (e: Exception){
101+
Messages.log("Error while reading output: ${e.message}")
102+
}
103+
}
104+
scope.launch {
105+
try {
106+
InputStreamReader(PipedInputStream(errorStream)).buffered().use { reader ->
107+
reader.lineSequence()
108+
.forEach { line ->
109+
if (cancel.token().isCancellationRequested) {
110+
return@launch
111+
}
112+
if (state.value != State.RUNNING) {
113+
return@forEach
114+
}
115+
when{
116+
line.contains("+[IMKClient subclass]: chose IMKClient_Modern") -> return@forEach
117+
line.contains("+[IMKInputSession subclass]: chose IMKInputSession_Modern") -> return@forEach
118+
line.startsWith("__MOVE__") -> return@forEach
119+
else -> service?.editor?.console?.err?.println(line)
120+
}
121+
}
122+
}
123+
}catch (e: Exception){
124+
Messages.log("Error while reading error: ${e.message}")
125+
}
126+
}
127+
69128
}
70129

71130
fun cancel(){
72131
cancel.cancel()
73132
}
74133

75-
private fun BuildLauncher.addStateListener(): BuildLauncher{
76-
this.addProgressListener(ProgressListener { event ->
134+
private fun BuildLauncher.addStateListener(){
135+
addProgressListener(ProgressListener { event ->
77136
if(event is TaskStartEvent) {
78137
when(event.descriptor.name) {
79138
":run" -> {
@@ -94,15 +153,105 @@ open abstract class GradleJob{
94153
}
95154
}
96155
}
97-
if(event is ProblemEvent) {
156+
if(event is DefaultSingleProblemEvent) {
157+
if(event.definition.severity == Severity.ADVICE) return@ProgressListener
98158
problems.add(event)
159+
160+
val path = (event.locations.firstOrNull() as DefaultFileLocation?)?.path
161+
// Not Trimming indent as the content might contain newlines
162+
service?.editor?.console?.err?.println(
163+
"""
164+
${event.definition.id.displayName}:
165+
${event.contextualLabel.contextualLabel}
166+
167+
${event.details.details?.replace(path ?: "", "")}
168+
${event.solutions.joinToString("\n") { it.solution }}
169+
"""
170+
)
99171
}
100172
})
101-
return this
102173
}
103174

104-
private fun BuildLauncher.addDebugging(): BuildLauncher{
105-
this.addProgressListener(ProgressListener { event ->
175+
private fun listenForExceptions(virtualMachine: VirtualMachine){
176+
scope.launch {
177+
try {
178+
val manager = virtualMachine.eventRequestManager()
179+
180+
val request = manager.createExceptionRequest(null, false, true)
181+
request.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD)
182+
request.enable()
183+
184+
val queue = virtualMachine.eventQueue()
185+
while (true) {
186+
val eventSet = queue.remove()
187+
for(event in eventSet) {
188+
if (event is ExceptionEvent) {
189+
printExceptionDetails(event)
190+
event.thread().resume()
191+
}
192+
}
193+
eventSet.resume()
194+
}
195+
} catch (e: Exception) {
196+
Messages.log("Error while listening for exceptions: ${e.message}")
197+
}
198+
199+
}
200+
}
201+
fun printExceptionDetails(event: ExceptionEvent) {
202+
val exception = event.exception()
203+
val thread = event.thread()
204+
val location = event.location()
205+
val stackFrames = thread.frames()
206+
207+
println("\n🚨 Exception Caught 🚨")
208+
println("Type : ${exception.referenceType().name()}")
209+
// println("Message : ${getExceptionMessage(exception)}")
210+
println("Thread : ${thread.name()}")
211+
println("Location : ${location.sourcePath()}:${location.lineNumber()}\n")
212+
213+
// Separate stack frames
214+
val userFrames = mutableListOf<StackFrame>()
215+
val processingFrames = mutableListOf<StackFrame>()
216+
217+
stackFrames.forEach { frame ->
218+
val className = frame.location().declaringType().name()
219+
if (className.startsWith("processing.")) {
220+
processingFrames.add(frame)
221+
} else {
222+
userFrames.add(frame)
223+
}
224+
}
225+
226+
// Print user frames first
227+
println("🔍 Stacktrace (Your Code First):")
228+
userFrames.forEachIndexed { index, frame -> printStackFrame(index, frame) }
229+
230+
// Print Processing frames second
231+
if (processingFrames.isNotEmpty()) {
232+
println("\n🔧 Processing Stacktrace (Hidden Initially):")
233+
processingFrames.forEachIndexed { index, frame -> printStackFrame(index, frame) }
234+
}
235+
236+
println("──────────────────────────────────\n")
237+
}
238+
239+
fun printStackFrame(index: Int, frame: StackFrame) {
240+
val location = frame.location()
241+
val method = location.method()
242+
println(" #$index ${location.sourcePath()}:${location.lineNumber()} -> ${method.declaringType().name()}.${method.name()}()")
243+
}
244+
245+
// Extracts the exception's message
246+
fun getExceptionMessage(exception: ObjectReference): String {
247+
val messageMethod = exception.referenceType().methodsByName("getMessage").firstOrNull() ?: return "Unknown"
248+
val messageValue = exception.invokeMethod(null, messageMethod, emptyList(), ObjectReference.INVOKE_SINGLE_THREADED)
249+
return (messageValue as? StringReference)?.value() ?: "Unknown"
250+
}
251+
252+
253+
private fun BuildLauncher.addDebugging(){
254+
addProgressListener(ProgressListener { event ->
106255
if (event !is TaskStartEvent) return@ProgressListener
107256
if (event.descriptor.name != ":run") return@ProgressListener
108257

@@ -123,19 +272,19 @@ open abstract class GradleJob{
123272
try {
124273
val sketch = connector.attach(args)
125274
vm.value = sketch
126-
break
275+
sketch.resume()
127276
Messages.log("Attached to VM: ${sketch.name()}")
277+
listenForExceptions(sketch)
278+
break
128279
} catch (e: Exception) {
129280
Messages.log("Error while attaching to VM: ${e.message}... Retrying")
130281
}
131282
delay(250)
132283
}
133-
if(vm.value == null) throw IllegalStateException("Failed to attach to VM after 10 seconds")
134284
}
135285
} catch (e: Exception) {
136286
Messages.log("Error while attaching to VM: ${e.message}")
137287
}
138288
})
139-
return this
140289
}
141290
}

app/src/processing/app/gradle/GradleService.kt

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import org.gradle.tooling.ProjectConnection
1111
import processing.app.Base
1212
import processing.app.Messages
1313
import processing.app.Platform
14+
import processing.app.gradle.helpers.ActionGradleJob
15+
import processing.app.gradle.helpers.BackgroundGradleJob
1416
import processing.app.ui.Editor
1517
import java.io.*
1618
import javax.swing.SwingUtilities
@@ -20,10 +22,9 @@ import kotlin.io.path.writeText
2022

2123
// TODO: Remove dependency on editor (editor is not mockable, or move editor away from JFrame)
2224
// TODO: Improve progress tracking
23-
// TODO: Improve error reporting
2425
// TODO: PoC new debugger/tweak mode
25-
26-
26+
// TODO: Allow for plugins to skip gradle entirely
27+
// TODO: Improve background building
2728
// The gradle service runs the gradle tasks and manages the gradle connection
2829
// It will create the necessary build files for gradle to run
2930
class GradleService(val editor: Editor) {
@@ -57,7 +58,7 @@ class GradleService(val editor: Editor) {
5758
startBuilding()
5859
}
5960

60-
// TODO: Improve background building
61+
6162
fun startBuilding(){
6263
scope.launch {
6364
// TODO: Improve the experience with unsaved
@@ -67,10 +68,6 @@ class GradleService(val editor: Editor) {
6768
setup()
6869
forTasks("jar")
6970
addArguments("--continuous")
70-
if (Base.DEBUG){
71-
setStandardError(editor.console.err)
72-
setStandardOutput(editor.console.out)
73-
}
7471
}
7572
job.start()
7673
}
@@ -100,28 +97,26 @@ class GradleService(val editor: Editor) {
10097
}
10198
fun run(){
10299
stopActions()
100+
editor.console.clear()
103101

104102
val job = ActionGradleJob()
105103
job.service = this
106104
job.configure = {
107105
setup()
108106
forTasks("run")
109-
setStandardError(editor.console.err)
110-
if (Base.DEBUG) setStandardOutput(editor.console.out)
111107
}
112108
job.start()
113109
}
114110

115111
fun export(){
116112
stopActions()
113+
editor.console.clear()
117114

118115
val job = ActionGradleJob()
119116
job.service = this
120117
job.configure = {
121118
setup()
122119
forTasks("runDistributable")
123-
setStandardError(editor.console.err)
124-
if (Base.DEBUG) setStandardOutput(editor.console.out)
125120
}
126121
job.start()
127122
}

app/src/processing/app/gradle/ScreenshotService.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import java.nio.file.Path
1111
import kotlin.time.Duration.Companion.seconds
1212
import kotlin.time.TimeSource
1313

14-
//TODO: Move to java mode
14+
// TODO: Move to java mode
15+
// TODO: Add more feedback when things go wrong
1516
class ScreenshotService {
1617
companion object{
1718
fun takeScreenshot(vm: VirtualMachine, onComplete: (Path) -> Unit) {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package processing.app.gradle.helpers
2+
3+
import processing.app.gradle.GradleJob
4+
5+
class ActionGradleJob : GradleJob()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package processing.app.gradle.helpers
2+
3+
import processing.app.gradle.GradleJob
4+
5+
class BackgroundGradleJob : GradleJob()

app/src/processing/app/gradle/ui/Toolbar.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import androidx.compose.ui.unit.dp
3434
import androidx.compose.ui.window.Window
3535
import androidx.compose.ui.window.WindowPosition
3636
import androidx.compose.ui.window.rememberWindowState
37-
import processing.app.gradle.ActionGradleJob
37+
import processing.app.gradle.helpers.ActionGradleJob
3838
import processing.app.gradle.GradleJob
3939
import processing.app.gradle.ScreenshotService
4040
import processing.app.ui.Editor

java/gradle/src/main/kotlin/ProcessingPlugin.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact
115115
application.mainClass = sketchName
116116
application.nativeDistributions.modules("java.management")
117117
if(debugPort != null) {
118-
application.jvmArgs("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort")
118+
application.jvmArgs("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=$debugPort")
119119
}
120120
}
121121
}

0 commit comments

Comments
 (0)