@@ -2,9 +2,10 @@ package processing.app.gradle
22
33import androidx.compose.runtime.mutableStateListOf
44import androidx.compose.runtime.mutableStateOf
5- import com.sun.jdi.Bootstrap
6- import com.sun.jdi.VirtualMachine
5+ import com.sun.jdi.*
76import com.sun.jdi.connect.AttachingConnector
7+ import com.sun.jdi.event.ExceptionEvent
8+ import com.sun.jdi.request.EventRequest
89import kotlinx.coroutines.CoroutineScope
910import kotlinx.coroutines.Dispatchers
1011import kotlinx.coroutines.delay
@@ -13,14 +14,24 @@ import org.gradle.tooling.BuildLauncher
1314import org.gradle.tooling.GradleConnector
1415import org.gradle.tooling.events.ProgressListener
1516import 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
1620import org.gradle.tooling.events.task.TaskFinishEvent
1721import org.gradle.tooling.events.task.TaskStartEvent
22+ import processing.app.Base
1823import processing.app.Messages
24+ import java.io.InputStreamReader
25+ import java.io.PipedInputStream
26+ import java.io.PipedOutputStream
1927import kotlin.time.Duration.Companion.seconds
2028import 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}
0 commit comments