Skip to content

Commit 85790bd

Browse files
committed
Refactor process management and improve run workflow
Replaces single sketchProcess with a list to track multiple processes, updates process lifecycle management, and moves sketch running logic into a new runSketch() method. Refactors toolbar activation/deactivation based on process state, improves dependency installation checks, and cleans up code formatting. Updates mode name to 'experimental' in mode.properties.
1 parent 8de71fe commit 85790bd

File tree

3 files changed

+95
-86
lines changed

3 files changed

+95
-86
lines changed

p5js/library/mode.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name=p5.js Mode (Electron)
1+
name=p5.js Mode (experimental)
22
category=Unknown
33
authors=[The Processing Foundation](https://processingfoundation.org/)
44
url=https://github.com/processing/processing-p5.js-mode

p5js/src/main/kotlin/p5jsEditor.kt

Lines changed: 92 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import kotlinx.html.dom.createHTMLDocument
2323
import kotlinx.html.dom.serialize
2424
import kotlinx.serialization.json.*
2525
import processing.app.*
26-
import processing.app.Messages.Companion.log
2726
import processing.app.syntax.JEditTextArea
2827
import processing.app.syntax.PdeTextArea
2928
import processing.app.syntax.PdeTextAreaDefaults
@@ -40,11 +39,11 @@ import javax.swing.JMenu
4039
import javax.swing.JMenuItem
4140

4241

43-
class p5jsEditor(base: Base, path: String?, state: EditorState?, mode: Mode?): Editor(base, path, state, mode) {
42+
class p5jsEditor(base: Base, path: String?, state: EditorState?, mode: Mode?) : Editor(base, path, state, mode) {
4443

4544
val scope = CoroutineScope(Dispatchers.Default)
4645
val isWindows = System.getProperty("os.name").lowercase().contains("windows")
47-
var sketchProcess: Process? = null
46+
var processes: MutableList<Process> = mutableListOf()
4847

4948
init {
5049
scope.launch {
@@ -57,8 +56,7 @@ class p5jsEditor(base: Base, path: String?, state: EditorState?, mode: Mode?): E
5756
try {
5857
javascriptFolder?.resolve("package.json")?.copyTo(sketch.folder.resolve("package.json"))
5958
javascriptFolder?.resolve("pnpm-lock.yaml")?.copyTo(sketch.folder.resolve("pnpm-lock.yaml"))
60-
}
61-
catch (e: FileAlreadyExistsException) {
59+
} catch (e: FileAlreadyExistsException) {
6260
Messages.log("File already exists: ${e.message}")
6361
// TODO: How to differentiate example with own `package.json` and saved sketch?
6462
}
@@ -69,21 +67,17 @@ class p5jsEditor(base: Base, path: String?, state: EditorState?, mode: Mode?): E
6967
statusNotice("Looking for pnpm…")
7068
try {
7169
runCommand("pnpm -v")
72-
}
73-
catch (e: Exception) {
70+
} catch (e: Exception) {
7471
statusNotice("pnpm not found. Installing pnpm…")
7572
if (isWindows) {
7673
runCommand("powershell -command \"Invoke-WebRequest https://get.pnpm.io/install.ps1 -UseBasicParsing | Invoke-Expression\"")
77-
}
78-
else {
74+
} else {
7975
runCommand("chmod u+x ${mode?.folder}/install.sh")
8076
runCommand("${mode?.folder}/install.sh")
8177
}
8278

8379
statusNotice("Installing Node via pnpm…")
84-
runCommand("pnpm env use --global lts") {
85-
statusNotice("Installing Node dependencies…")
86-
}
80+
runCommand("pnpm env use --global lts")
8781
}
8882

8983
statusNotice("All done! Enjoy p5.js mode.")
@@ -126,22 +120,13 @@ class p5jsEditor(base: Base, path: String?, state: EditorState?, mode: Mode?): E
126120
runCommand("pnpm install --dangerously-allow-all-builds --force")
127121
}
128122

129-
runCommand("pnpm app:pack") {
123+
scope.launch {
124+
runCommand("pnpm app:pack")
130125
Platform.openFolder(sketch.folder)
131126
statusNotice(Language.text("export.notice.exporting.done"))
132127
}
133128
}
134129

135-
// override fun handleSaveAs(): Boolean {
136-
// val saved = super.handleSaveAs()
137-
// statusNotice("Rebuilding Node dependencies…")
138-
// TODO: Saving is async and might not be finished once the function returns
139-
// runCommand("pnpm install --force", onFinished = {
140-
// statusNotice("Rebuilding Node dependencies… Done.")
141-
// })
142-
// return saved
143-
// }
144-
145130
override fun buildSketchMenu(): JMenu {
146131
val runItem = Toolkit.newJMenuItem(Language.text("menu.sketch.run"), 'R'.code)
147132
runItem.addActionListener { e: ActionEvent? -> toolbar.handleRun(0) }
@@ -189,12 +174,11 @@ class p5jsEditor(base: Base, path: String?, state: EditorState?, mode: Mode?): E
189174
}
190175

191176
override fun internalCloseRunner() {
192-
sketchProcess?.destroy()
193177
}
194178

195179
override fun deactivateRun() {
196-
sketchProcess?.destroy()
197-
toolbar.deactivateRun()
180+
processes.forEach { it.destroy() }
181+
updateToolbar()
198182
}
199183

200184
override fun createFooter(): EditorFooter {
@@ -230,7 +214,8 @@ class p5jsEditor(base: Base, path: String?, state: EditorState?, mode: Mode?): E
230214
val npmResponseRaw = npmConn.getInputStream().readAllBytes().decodeToString()
231215
val npmResponse: JsonObject = Json.decodeFromString(npmResponseRaw)
232216
val npmPackages = npmResponse["objects"]!!.jsonArray
233-
packagesSearched = npmPackages.map { it.jsonObject["package"]!!.jsonObject["name"]!!.jsonPrimitive.content }
217+
packagesSearched =
218+
npmPackages.map { it.jsonObject["package"]!!.jsonObject["name"]!!.jsonPrimitive.content }
234219
packageToInstall = packagesSearched[0]
235220
}
236221
})
@@ -249,7 +234,11 @@ class p5jsEditor(base: Base, path: String?, state: EditorState?, mode: Mode?): E
249234
Spacer(Modifier.width(16.dp))
250235
LazyColumn {
251236
itemsIndexed(packagesSearched) { index, pkg ->
252-
Text(text = pkg, fontWeight = if (index == 0) FontWeight.Bold else FontWeight.Normal, modifier = Modifier.fillMaxWidth().padding(4.dp))
237+
Text(
238+
text = pkg,
239+
fontWeight = if (index == 0) FontWeight.Bold else FontWeight.Normal,
240+
modifier = Modifier.fillMaxWidth().padding(4.dp)
241+
)
253242
Divider()
254243
}
255244
}
@@ -286,59 +275,96 @@ class p5jsEditor(base: Base, path: String?, state: EditorState?, mode: Mode?): E
286275
sketch.folder.resolve("electron/index.html").writeText(htmlCode)
287276
}
288277

289-
fun runCommand(action: String, directory: File = sketch.folder, onFinished: () -> Unit = {}) {
290-
try {
291-
// TODO: Get rid of magic strings. Better way to distinguish “endless” processes and processes we wait for?
292-
if (action == "pnpm sketch:start") {
293-
sketchProcess?.destroy()
294-
}
295-
296-
val processBuilder = ProcessBuilder()
297-
298-
// Set the command based on the operating system
299-
val shell = System.getenv("SHELL")
300-
val command = if (isWindows) {
301-
listOf("cmd", "/c", action)
302-
} else {
303-
listOf(shell, "-ci", action)
304-
}
278+
fun updateToolbar() {
279+
if (processes.any { it.isAlive }) {
280+
toolbar.activateRun()
281+
} else {
282+
toolbar.deactivateRun()
283+
}
284+
}
305285

306-
processBuilder.command(command)
307-
processBuilder.directory(directory)
286+
fun runSketch(present: Boolean) {
287+
createIndexHtml()
288+
deactivateRun()
289+
statusNotice("Starting up sketch…")
308290

309-
val process = processBuilder.start()
310291

311-
if (action == "pnpm sketch:start") {
312-
sketchProcess = process;
313-
}
292+
scope.launch {
293+
try {
294+
val packageJson = sketch.folder.resolve("package.json")
295+
val hashFile = sketch.folder.resolve("electron/.package_json_hash")
296+
val newHash = packageJson.readText().hashCode()
297+
val oldHash = hashFile.let { if (it.exists()) it.readText().toInt() else null }
298+
if (newHash != oldHash) {
299+
statusNotice("Installing Node dependencies…")
300+
runCommand("pnpm install --dangerously-allow-all-builds")
301+
hashFile.writeText(newHash.toString())
302+
}
303+
statusNotice("Running sketch…")
304+
val builder = builder("PRESENT=$present pnpm sketch:start", sketch.folder)
305+
val process = builder.start()
306+
processes.add(process)
307+
updateToolbar()
308+
309+
// Handle output stream
310+
val reader = BufferedReader(InputStreamReader(process.inputStream))
311+
var line: String?
312+
313+
while (reader.readLine().also { line = it } != null) {
314+
// TODO: so much refactoring!
315+
// Only check for errors when running the sketch
316+
if (line?.startsWith("error") == true) {
317+
// TODO: more robust data exchange, double-check with @Stef
318+
// TODO: `statusError` does not do anything with column of a SketchException
319+
val (msgType, msgText, msgFile, msgLine, msgCol) = line.split("|")
320+
statusError(processing.utils.SketchException(msgText, 0, msgLine.toInt() - 1, msgCol.toInt()))
321+
continue
322+
}
314323

315-
// Handle output stream
316-
val reader = BufferedReader(InputStreamReader(process.inputStream))
317-
var line: String?
318-
319-
while (reader.readLine().also { line = it } != null) {
320-
// TODO: so much refactoring!
321-
// Only check for errors when running the sketch
322-
if (action == "pnpm sketch:start" && line?.startsWith("error") == true) {
323-
// TODO: more robust data exchange, double-check with @Stef
324-
// TODO: `statusError` does not do anything with column of a SketchException
325-
val ( msgType, msgText, msgFile, msgLine, msgCol ) = line.split("|")
326-
statusError(processing.utils.SketchException(msgText, 0, msgLine.toInt()-1, msgCol.toInt()))
327-
continue
324+
println(line)
328325
}
326+
process.waitFor()
327+
if (processes.lastOrNull() == process) {
328+
statusNotice("Sketch stopped.")
329+
}
330+
processes.remove(process)
331+
updateToolbar()
329332

330-
println(line)
333+
} catch (e: Exception) {
334+
statusError("Failed to run sketch: ${e.message}")
331335
}
336+
}
337+
}
338+
332339

340+
fun runCommand(action: String, directory: File = sketch.folder) {
341+
try {
342+
val processBuilder = builder(action, directory)
343+
val process = processBuilder.start()
344+
// suspend fun to wait for process to finish
333345
val exitCode = process.waitFor()
334346

335347
if (exitCode != 0) {
336348
throw RuntimeException("Command failed with non-zero exit code $exitCode.")
337349
}
338-
339-
onFinished()
340350
} catch (e: Exception) {
341351
statusError("Failed to run `$action`: ${e.message}")
342352
}
343353
}
354+
355+
private fun builder(action: String, directory: File): ProcessBuilder {
356+
val processBuilder = ProcessBuilder()
357+
358+
// Set the command based on the operating system
359+
val shell = System.getenv("SHELL")
360+
val command = if (isWindows) {
361+
listOf("cmd", "/c", action)
362+
} else {
363+
listOf(shell, "-ci", action)
364+
}
365+
366+
processBuilder.command(command)
367+
processBuilder.directory(directory)
368+
return processBuilder
369+
}
344370
}
Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package processing.p5js
22

3-
import kotlinx.coroutines.launch
43
import processing.app.ui.EditorToolbar
54
import java.awt.event.ActionEvent
65

@@ -10,27 +9,11 @@ class p5jsEditorToolbar(editor: p5jsEditor) : EditorToolbar(editor) {
109
editor.sketch.save()
1110

1211
val present = (modifiers and ActionEvent.SHIFT_MASK) != 0
13-
14-
// TODO: Re-create index.html here instead of on saveAs/rename/delete of code?
15-
editor.createIndexHtml()
16-
17-
activateRun()
18-
editor.statusNotice("Starting up sketch…")
19-
20-
editor.scope.launch {
21-
// TODO: Smarter way to only install deps when needed?
22-
// --dangerously-allow-all-builds allows electron in particular to install properly
23-
editor.runCommand("pnpm install --dangerously-allow-all-builds")
24-
editor.runCommand("PRESENT=$present pnpm sketch:start") {
25-
deactivateRun()
26-
editor.statusEmpty()
27-
}
28-
}
12+
editor.runSketch(present)
2913
}
3014

3115
override fun handleStop() {
3216
val editor = editor as p5jsEditor
33-
editor.sketchProcess?.destroy()
34-
deactivateRun()
17+
editor.deactivateRun()
3518
}
3619
}

0 commit comments

Comments
 (0)