Gradle Service refactor

This commit is contained in:
Stef Tervelde
2025-07-07 16:21:59 +02:00
parent a11d2ffabe
commit b64505d476
4 changed files with 259 additions and 231 deletions
@@ -10,7 +10,6 @@ import kotlin.time.TimeSource
class Debugger {
companion object {
// TODO: Stop if build has failed
suspend fun connect(port: Int?): VirtualMachine? {
try {
Messages.log("Attaching to VM $port")
+223 -83
View File
@@ -17,14 +17,29 @@ import org.gradle.tooling.events.problems.internal.DefaultSingleProblemEvent
import org.gradle.tooling.events.task.TaskFinishEvent
import org.gradle.tooling.events.task.TaskStartEvent
import org.gradle.tooling.events.task.TaskSuccessResult
import processing.app.Base
import processing.app.Base.DEBUG
import processing.app.Base.getSketchbookFolder
import processing.app.Base.getVersionName
import processing.app.Language.text
import processing.app.Messages
import processing.app.Platform
import processing.app.Platform.getContentFile
import processing.app.Platform.getSettingsFolder
import processing.app.Sketch
import processing.app.gradle.Log.Companion.startLogServer
import processing.app.ui.Editor
import processing.app.ui.EditorStatus
import java.io.PrintStream
import java.net.ServerSocket
import java.nio.file.Path
import kotlin.io.path.deleteIfExists
import kotlin.io.path.writeText
class GradleJob{
// Starts a gradle job to run the sketch
class GradleJob(
vararg val tasks: String,
val workingDir: Path,
val sketch: Sketch,
val editor: Editor? = null,
){
enum class State{
NONE,
BUILDING,
@@ -33,8 +48,9 @@ class GradleJob{
DONE
}
var service: GradleService? = null
var configure: BuildLauncher.() -> Unit = {}
val debugPort = (30_000..60_000).random()
val logPort = debugPort + 1
val errPort = logPort + 1
val state = mutableStateOf(State.NONE)
val vm = mutableStateOf<VirtualMachine?>(null)
@@ -44,70 +60,177 @@ class GradleJob{
private val scope = CoroutineScope(Dispatchers.IO)
private val cancel = GradleConnector.newCancellationTokenSource()
fun start() {
val folder = service?.sketch?.folder ?: throw IllegalStateException("Sketch folder is not set")
launchJob {
try {
state.value = State.BUILDING
service?.editor?.statusMessage("Connecting to Gradle", EditorStatus.NOTICE)
// All the configuration for the gradle build
private fun BuildLauncher.setupGradle(extraArguments: List<String> = listOf()) {
val copy = sketch.isReadOnly || sketch.isUntitled
val sketchFolder = if(copy) workingDir.resolve("sketch").toFile() else sketch.folder
if(copy){
// If the sketch is read-only, we copy it to the working directory
// This allows us to run the sketch without modifying the original files
sketch.folder.copyRecursively(sketchFolder, overwrite = true)
}
// Save the unsaved code into the working directory for gradle to compile
val unsaved = sketch.code
.map { code ->
val file = workingDir.resolve("unsaved/${code.fileName}")
file.parent.toFile().mkdirs()
// If tab is marked modified save it to the working directory
// Otherwise delete the file so we don't compile with old code
if(code.isModified){
file.writeText(code.documentText)
}else{
file.deleteIfExists()
}
return@map code.fileName
}
// Collect the variables to pass to gradle
val variables = mapOf(
"group" to System.getProperty("processing.group", "org.processing"),
"version" to getVersionName(),
"sketchFolder" to sketchFolder,
"sketchbook" to getSketchbookFolder(),
"workingDir" to workingDir.toAbsolutePath().toString(),
"settings" to getSettingsFolder().absolutePath.toString(),
"unsaved" to unsaved.joinToString(","),
"debugPort" to debugPort.toString(),
"logPort" to logPort.toString(),
"errPort" to errPort.toString(),
"fullscreen" to System.getProperty("processing.fullscreen", "false").equals("true"),
"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
//"stop.color" to "0xFF000000", // TODO: Implement
"stop.hide" to false, // TODO: Implement
)
val repository = getContentFile("repository").absolutePath.replace("""\""", """\\""")
// Create the init.gradle.kts file in the working directory
// This allows us to run the gradle plugin that has been bundled with the editor
val initGradle = workingDir.resolve("init.gradle.kts").apply {
val content = """
beforeSettings{
pluginManagement {
repositories {
maven("$repository")
gradlePluginPortal()
}
}
}
allprojects{
repositories {
maven("$repository")
mavenCentral()
}
}
""".trimIndent()
writeText(content)
}
// Create the build.gradle.kts file in the sketch folder
val buildGradle = sketchFolder.resolve("build.gradle.kts")
val generate = buildGradle.let {
if(!it.exists()) return@let true
val contents = it.readText()
if(!contents.contains("@processing-auto-generated")) return@let false
val version = contents.substringAfter("version=").substringBefore("\n")
if(version != getVersionName()) return@let true
val modeTitle = contents.substringAfter("mode=").substringBefore(" ")
if(sketch.mode.title != modeTitle) return@let true
return@let DEBUG
}
if (generate) {
Messages.log("build.gradle.kts outdated or not found in ${sketch.folder}, creating one")
val header = """
// @processing-auto-generated mode=${sketch.mode.title} version=${getVersionName()}
//
""".trimIndent()
val instructions = text("gradle.instructions")
.split("\n")
.joinToString("\n") { "// $it" }
val configuration = """
plugins{
id("org.processing.java") version "${getVersionName()}"
}
""".trimIndent()
val content = "${header}\n${instructions}\n\n${configuration}"
buildGradle.writeText(content)
}
// Create and empty settings.gradle.kts file in the sketch folder
val settingsGradle = sketchFolder.resolve("settings.gradle.kts")
if (!settingsGradle.exists()) {
settingsGradle.createNewFile()
}
// Collect the arguments to pass to gradle
val arguments = mutableListOf("--init-script", initGradle.toAbsolutePath().toString())
// Hide Gradle output from the console if not in debug mode
if(!DEBUG) arguments += "--quiet"
if(copy) arguments += listOf("--project-dir", sketchFolder.absolutePath)
arguments += variables.entries
.filter { it.value != null }
.map { "-Pprocessing.${it.key}=${it.value}" }
arguments += extraArguments
withArguments(*arguments.toTypedArray())
forTasks(*tasks)
// TODO: Instead of shipping Processing with a build-in JDK we should download the JDK through Gradle
setJavaHome(Platform.getJavaHome())
withCancellationToken(cancel.token())
}
fun start() {
launchJob {
handleExceptions {
state.value = State.BUILDING
// Connect Gradle, configure the build and run it
GradleConnector.newConnector()
.forProjectDirectory(folder)
.forProjectDirectory(sketch.folder)
.apply {
editor?.statusMessage("Connecting to Gradle", EditorStatus.NOTICE)
// TODO: Remove when switched to classic confinement within Snap
if(System.getenv("SNAP_USER_COMMON") != null){
useGradleUserHomeDir(Platform.getSettingsFolder().resolve("gradle"))
if (System.getenv("SNAP_USER_COMMON") != null) {
useGradleUserHomeDir(getSettingsFolder().resolve("gradle"))
}
}
.connect()
.apply {
service?.editor?.statusMessage("Building sketch", EditorStatus.NOTICE)
editor?.statusMessage("Building sketch", EditorStatus.NOTICE)
}
.newBuild()
.apply {
configure()
withCancellationToken(cancel.token())
addStateListener()
addDebugging()
addLogserver()
if(Base.DEBUG) {
if (DEBUG) {
setStandardOutput(System.out)
setStandardError(System.err)
}
run()
setupGradle()
addStateListener()
addLogserver()
addDebugging()
}
}catch (e: Exception){
val causesList = mutableListOf<Throwable>()
var cause: Throwable? = e
while (cause != null && cause.cause != cause) {
causesList.add(cause)
cause = cause.cause
}
val errors = causesList.joinToString("\n") { it.message ?: "Unknown error" }
val skip = listOf(BuildCancelledException::class)
if (skip.any { it.isInstance(e) }) {
Messages.log("Gradle job error: $errors")
return@launchJob
}
if(state.value == State.RUNNING){
Messages.log("Gradle job error: $errors")
return@launchJob
}
// An error occurred during the build process
System.err.println(errors)
service?.editor?.statusError(causesList.last().message)
}finally {
state.value = State.DONE
vm.value = null
.run()
}
}
}
fun launchJob(block: suspend CoroutineScope.() -> Unit){
val job = scope.launch { block() }
jobs.add(job)
@@ -118,29 +241,66 @@ class GradleJob{
jobs.forEach(Job::cancel)
}
// Handle exception thrown by Gradle
private fun handleExceptions(action: () -> Unit){
try{
action()
}catch (e: Exception){
val causesList = mutableListOf<Throwable>()
var cause: Throwable? = e
while (cause != null && cause.cause != cause) {
causesList.add(cause)
cause = cause.cause
}
val errors = causesList.joinToString("\n") { it.message ?: "Unknown error" }
val skip = listOf(BuildCancelledException::class)
if (skip.any { it.isInstance(e) }) {
Messages.log("Gradle job error: $errors")
return
}
if(state.value == State.RUNNING){
Messages.log("Gradle job error: $errors")
return
}
// An error occurred during the build process
System.err.println(errors)
editor?.statusError(causesList.last().message)
}finally {
state.value = State.DONE
vm.value = null
}
}
private fun BuildLauncher.addStateListener(){
addProgressListener(ProgressListener { event ->
if(event is TaskStartEvent) {
service?.editor?.statusMessage("Running task: ${event.descriptor.name}", EditorStatus.NOTICE)
editor?.statusMessage("Running task: ${event.descriptor.name}", EditorStatus.NOTICE)
when(event.descriptor.name) {
":run" -> {
state.value = State.RUNNING
Messages.log("Start run")
service?.editor?.toolbar?.activateRun()
editor?.toolbar?.activateRun()
}
}
}
if(event is TaskFinishEvent) {
if(event.result is TaskSuccessResult){
service?.editor?.statusMessage("Finished task ${event.descriptor.name}", EditorStatus.NOTICE)
editor?.statusMessage("Finished task ${event.descriptor.name}", EditorStatus.NOTICE)
}
when(event.descriptor.name){
":run"->{
state.value = State.DONE
service?.editor?.toolbar?.deactivateRun()
service?.editor?.toolbar?.deactivateStop()
editor?.toolbar?.deactivateRun()
editor?.toolbar?.deactivateStop()
}
}
}
@@ -168,7 +328,7 @@ class GradleJob{
*/
val error = event.definition.id.displayName
service?.editor?.statusError(error)
editor?.statusError(error)
System.err.println("Problem: $error")
state.value = State.ERROR
@@ -183,32 +343,12 @@ class GradleJob{
})
}
fun addLogserver(){
fun BuildLauncher.addLogserver(){
launchJob {
startLogServer(service?.logPort ?: 5006, System.out)
startLogServer(logPort, System.out)
}
launchJob{
startLogServer(service?.errPort ?: 5007, System.err)
}
}
fun startLogServer(port: Int, target: PrintStream){
val server = ServerSocket(port)
Messages.log("Log server started on port $port")
val client = server.accept()
Messages.log("Log server client connected")
val reader = client.getInputStream().bufferedReader()
try {
reader.forEachLine { line ->
if (line.isNotBlank()) {
target.println(line)
}
}
} catch (e: Exception) {
Messages.log("Error while reading from log server: ${e.message}")
} finally {
client.close()
server.close()
startLogServer(errPort, System.err)
}
}
@@ -218,9 +358,9 @@ class GradleJob{
if (event.descriptor.name != ":run") return@ProgressListener
launchJob {
val debugger = Debugger.connect(service?.debugPort) ?: return@launchJob
val debugger = Debugger.connect(debugPort) ?: return@launchJob
vm.value = debugger
val exceptions = Exceptions(debugger, service?.editor)
val exceptions = Exceptions(debugger, editor)
exceptions.listen()
}
+6 -147
View File
@@ -2,22 +2,12 @@ package processing.app.gradle
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import org.gradle.tooling.BuildLauncher
import processing.app.Base.DEBUG
import processing.app.Base.getSketchbookFolder
import processing.app.Base.getVersionName
import processing.app.Language.text
import processing.app.Messages
import processing.app.Mode
import processing.app.Platform
import processing.app.Platform.getContentFile
import processing.app.Platform.getSettingsFolder
import processing.app.Preferences
import processing.app.Sketch
import processing.app.ui.Editor
import kotlin.io.path.createTempDirectory
import kotlin.io.path.deleteIfExists
import kotlin.io.path.writeText
// TODO: Test offline mode, gradle seems to be included as not needed to be downloaded.
// TODO: Test running examples
@@ -48,10 +38,6 @@ class GradleService(
val jobs = mutableStateListOf<GradleJob>()
val workingDir = createTempDirectory()
val debugPort = (30_000..60_000).random()
val logPort = debugPort + 1
val errPort = logPort + 1
fun run(){
startJob("run")
}
@@ -68,12 +54,12 @@ class GradleService(
if(!active.value) return
editor?.let { println(text("gradle.using_gradle")) }
val job = GradleJob()
job.service = this
job.configure = {
setupGradle()
forTasks(tasks.joinToString(" "))
}
val job = GradleJob(
tasks = tasks,
workingDir = workingDir,
sketch = sketch ?: throw IllegalStateException("Sketch is not set"),
editor = editor
)
jobs.add(job)
job.start()
}
@@ -82,133 +68,6 @@ class GradleService(
jobs.forEach(GradleJob::cancel)
}
private fun BuildLauncher.setupGradle(extraArguments: List<String> = listOf()) {
val sketch = sketch ?: throw IllegalStateException("Sketch is not set")
val copy = sketch.isReadOnly || sketch.isUntitled
val sketchFolder = if(copy) workingDir.resolve("sketch").toFile() else sketch.folder
if(copy){
// If the sketch is read-only, we copy it to the working directory
// This allows us to run the sketch without modifying the original files
sketch.folder.copyRecursively(sketchFolder, overwrite = true)
}
// Save the unsaved code into the working directory for gradle to compile
val unsaved = sketch.code
.map { code ->
val file = workingDir.resolve("unsaved/${code.fileName}")
file.parent.toFile().mkdirs()
// If tab is marked modified save it to the working directory
// Otherwise delete the file so we don't compile with old code
if(code.isModified){
file.writeText(code.documentText)
}else{
file.deleteIfExists()
}
return@map code.fileName
}
// Collect the variables to pass to gradle
val variables = mapOf(
"group" to System.getProperty("processing.group", "org.processing"),
"version" to getVersionName(),
"sketchFolder" to sketchFolder,
"sketchbook" to getSketchbookFolder(),
"workingDir" to workingDir.toAbsolutePath().toString(),
"settings" to getSettingsFolder().absolutePath.toString(),
"unsaved" to unsaved.joinToString(","),
"debugPort" to debugPort.toString(),
"logPort" to logPort.toString(),
"errPort" to errPort.toString(),
"fullscreen" to System.getProperty("processing.fullscreen", "false").equals("true"),
"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
//"stop.color" to "0xFF000000", // TODO: Implement
"stop.hide" to false, // TODO: Implement
)
val repository = getContentFile("repository").absolutePath.replace("""\""", """\\""")
// Create the init.gradle.kts file in the working directory
val initGradle = workingDir.resolve("init.gradle.kts").apply {
val content = """
beforeSettings{
pluginManagement {
repositories {
maven { url = uri("$repository") }
gradlePluginPortal()
}
}
}
allprojects{
repositories {
maven { url = uri("$repository") }
mavenCentral()
}
}
""".trimIndent()
writeText(content)
}
// Create the build.gradle.kts file in the sketch folder
val buildGradle = sketchFolder.resolve("build.gradle.kts")
val generate = buildGradle.let {
if(!it.exists()) return@let true
val contents = it.readText()
if(!contents.contains("@processing-auto-generated")) return@let false
val version = contents.substringAfter("version=").substringBefore("\n")
if(version != getVersionName()) return@let true
val modeTitle = contents.substringAfter("mode=").substringBefore(" ")
if(mode.title != modeTitle) return@let true
return@let DEBUG
}
if (generate) {
Messages.log("build.gradle.kts outdated or not found in ${sketch.folder}, creating one")
val header = """
// @processing-auto-generated mode=${mode.title} version=${getVersionName()}
//
""".trimIndent()
val instructions = text("gradle.instructions")
.split("\n")
.joinToString("\n") { "// $it" }
val configuration = """
plugins{
id("org.processing.java") version "${getVersionName()}"
}
""".trimIndent()
val content = "${header}\n${instructions}\n${configuration}"
buildGradle.writeText(content)
}
// Create the settings.gradle.kts file in the sketch folder
val settingsGradle = sketchFolder.resolve("settings.gradle.kts")
if (!settingsGradle.exists()) {
settingsGradle.createNewFile()
}
// Collect the arguments to pass to gradle
val arguments = mutableListOf("--init-script", initGradle.toAbsolutePath().toString())
if (!DEBUG) arguments.add("--quiet")
if(copy){
arguments += listOf("--project-dir", sketchFolder.absolutePath)
}
arguments.addAll(variables.entries
.filter { it.value != null }
.map { "-Pprocessing.${it.key}=${it.value}" }
)
arguments.addAll(extraArguments)
withArguments(*arguments.toTypedArray())
// TODO: Instead of shipping Processing with a build-in JDK we should download the JDK through Gradle
setJavaHome(Platform.getJavaHome())
}
// Hooks for java to check if the Gradle service is running since mutableStateOf is not accessible in java
fun getEnabled(): Boolean {
return active.value
+30
View File
@@ -0,0 +1,30 @@
package processing.app.gradle
import processing.app.Messages
import java.io.PrintStream
import java.net.ServerSocket
class Log{
companion object{
fun startLogServer(port: Int, target: PrintStream){
val server = ServerSocket(port)
Messages.Companion.log("Log server started on port $port")
val client = server.accept()
Messages.Companion.log("Log server client connected")
val reader = client.getInputStream().bufferedReader()
try {
reader.forEachLine { line ->
if (line.isNotBlank()) {
target.println(line)
}
}
} catch (e: Exception) {
Messages.Companion.log("Error while reading from log server: ${e.message}")
} finally {
client.close()
server.close()
}
}
}
}