Skip to content

Commit ff8b738

Browse files
committed
Reload worker threads in dev mode
1 parent ab47f73 commit ff8b738

File tree

7 files changed

+129
-61
lines changed

7 files changed

+129
-61
lines changed

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
# javascript-app-wrapper
22

3-
An executable JAR which can be run as a desktop app. App is implemented in JavaScript divided into:
4-
- UI code running in JavaFX WebView window (Webkit JS engine)
5-
- Worker code running in background threads for processor intensive work (Nashorn JS engine)
6-
- Small Kotlin main class linking everything
3+
An executable JAR which can be run as a desktop app
4+
- UI is JavaScript/HTML/CSS running in JavaFX WebView window (Webkit JS engine)
5+
- Javascript worker threads for processor intensive work and system calls (Nashorn JS engine)
6+
- Small Kotlin program linking UI and workers
7+
- Dev mode to watch JavaScript/HTML/CSS and reload app on changes
8+
- Firebug lite example for debugging UI
79

8-
### Execute
10+
### Build and run
911
```
1012
./gradlew clean shadowJar
1113
java -jar build/libs/javascript-app-wrapper-all.jar
1214
```
1315

1416
### Todo
15-
- Dev mode, reload worker scripts and thread pool?
1617
- Main window size/resize/transparency configuration and example?

build.gradle

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ plugins {
22
id 'application'
33
id 'org.jetbrains.kotlin.jvm' version '1.1.60'
44
id 'com.github.johnrengelman.shadow' version '2.0.1'
5-
id 'java'
65
}
76

87
repositories {
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
#Sat Nov 18 00:21:17 AEDT 2017
2-
distributionUrl=https\://services.gradle.org/distributions/gradle-4.3.1-bin.zip
1+
#Thu Nov 23 18:56:25 AEDT 2017
32
distributionBase=GRADLE_USER_HOME
43
distributionPath=wrapper/dists
5-
zipStorePath=wrapper/dists
64
zipStoreBase=GRADLE_USER_HOME
5+
zipStorePath=wrapper/dists
6+
distributionUrl=https\://services.gradle.org/distributions/gradle-4.3.1-all.zip

src/main/kotlin/com/github/sgdan/WebAppWrapper.kt

Lines changed: 41 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,29 @@
11
package com.github.sgdan
22

33
import javafx.application.Application
4+
import javafx.concurrent.Worker
45
import javafx.scene.web.WebView
56
import kotlinx.coroutines.experimental.launch
6-
import kotlinx.coroutines.experimental.newFixedThreadPoolContext
77
import netscape.javascript.JSObject
88
import tornadofx.*
99
import java.io.File
10+
import java.lang.Thread.sleep
1011
import java.net.URI
1112
import java.net.URL
1213
import java.nio.file.FileSystems
1314
import java.nio.file.Files
14-
import java.nio.file.Path
1515
import java.nio.file.Paths
1616
import java.nio.file.StandardWatchEventKinds.*
17-
import java.util.concurrent.LinkedBlockingQueue
1817
import javax.script.ScriptEngineManager
1918
import kotlinx.coroutines.experimental.javafx.JavaFx as UI
2019

2120
class WebAppWrapper : App(WebAppView::class)
2221

23-
/**
24-
* For worker threads to log to System.out, simulates console.log()
25-
*/
26-
class Console {
27-
fun log(vararg data: Any?) = println(data.joinToString())
28-
}
29-
3022
/** Task to be executed by worker thread */
3123
data class Task(val name: String, val args: Array<Any>)
3224

3325
class WebAppView : View() {
3426
private val nashorn = ScriptEngineManager().getEngineByName("nashorn")
35-
private val console = Console() // redirect messages from workers to console
36-
37-
/** Tasks to be processed by the workers */
38-
private val tasks = LinkedBlockingQueue<Task>()
3927

4028
private val web = WebView()
4129
override val root = web
@@ -44,13 +32,14 @@ class WebAppView : View() {
4432

4533
fun inDevMode() = devFolder().exists()
4634

47-
fun window() = web.engine.executeScript("window") as JSObject
48-
4935
/** For workers to send messages to the UI */
5036
private val ui = object {
5137
fun send(name: String, args: Array<Any>) {
5238
// call named method in JavaFX UI thread
53-
launch(UI) { window().call(name, args) }
39+
launch(UI) {
40+
val window = web.engine.executeScript("window") as JSObject
41+
window.call(name, args)
42+
}
5443
}
5544
}
5645

@@ -60,13 +49,6 @@ class WebAppView : View() {
6049
else emptyArray()
6150
}
6251

63-
/** For UI to create tasks */
64-
private val tasksHook = object {
65-
fun add(name: String, args: JSObject) {
66-
tasks.add(Task(name, toArray(args)))
67-
}
68-
}
69-
7052
/**
7153
* Check both classpath and current folder for a "web" folder resource
7254
*/
@@ -77,31 +59,44 @@ class WebAppView : View() {
7759
// from classpath (i.e. contained within executable jar
7860
else WebAppWrapper::class.java.classLoader.getResource("web/$name")
7961

62+
val uiHook: URL
63+
val workerHook: URL
64+
var workers: Workers? = null
65+
66+
/** For UI to create tasks */
67+
val tasks = object {
68+
fun add(name: String, args: JSObject) {
69+
workers?.add(name, args)
70+
}
71+
}
72+
73+
/**
74+
* Reload UI and reset thread pool & workers
75+
*/
76+
fun reset() {
77+
workers?.stop()
78+
workers = Workers(nashorn, workerHook, ui)
79+
launch(UI) {
80+
web.engine.reload()
81+
}
82+
}
83+
8084
init {
81-
val frontEndHook = checkNotNull(findResource("ui.html")) {
82-
"Must provide front end hook: web/ui.html"
85+
uiHook = checkNotNull(findResource("ui.html")) {
86+
"Must provide UI hook: web/ui.html"
8387
}
84-
val backEndHook = checkNotNull(findResource("worker.js")) {
85-
"Must provide back end hook: web/worker.js"
88+
workerHook = checkNotNull(findResource("worker.js")) {
89+
"Must provide worker hook: web/worker.js"
8690
}
91+
workers = Workers(nashorn, workerHook, ui)
8792

88-
// load worker threads, leave one core for the UI
89-
val nWorkers = Math.max(Runtime.getRuntime().availableProcessors() - 1, 1)
90-
val pool = newFixedThreadPoolContext(nWorkers, "worker-pool")
91-
(1..nWorkers).forEach {
92-
launch(pool) {
93-
// bindings aren't thread safe, so one for each thread
94-
val bindings = nashorn.createBindings()
95-
bindings.put("console", console) // support console.log for workers
96-
bindings.put("tasks", tasks)
97-
bindings.put("ui", ui)
98-
nashorn.eval(backEndHook.readText(), bindings)
93+
web.engine.loadWorker.stateProperty().addListener { _, _, newValue ->
94+
if (newValue == Worker.State.SUCCEEDED) {
95+
val window = web.engine.executeScript("window") as JSObject
96+
window.setMember("tasks", tasks)
9997
}
10098
}
101-
102-
// load front end
103-
window().setMember("tasks", tasksHook)
104-
web.engine.load(frontEndHook.toExternalForm())
99+
web.engine.load(uiHook.toExternalForm())
105100

106101
// watch folder for changes
107102
if (inDevMode()) {
@@ -111,10 +106,9 @@ class WebAppView : View() {
111106
launch {
112107
while (true) {
113108
val key = watcher.take()
114-
key.pollEvents().forEach {
115-
println("event: ${it.kind()}")
116-
}
117-
launch(UI) { web.engine.reload() }
109+
sleep(50) // wait a bit, in case there are multiple events
110+
key.pollEvents()
111+
reset()
118112
key.reset()
119113
}
120114
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.github.sgdan
2+
3+
import kotlinx.coroutines.experimental.launch
4+
import kotlinx.coroutines.experimental.newFixedThreadPoolContext
5+
import netscape.javascript.JSObject
6+
import java.net.URL
7+
import java.util.concurrent.LinkedBlockingQueue
8+
import java.util.concurrent.TimeUnit.SECONDS
9+
import javax.script.ScriptEngine
10+
import kotlinx.coroutines.experimental.javafx.JavaFx as UI
11+
12+
/**
13+
* For worker threads to log to System.out, simulates console.log()
14+
*/
15+
class Console {
16+
fun log(vararg data: Any?) = println(data.joinToString())
17+
}
18+
19+
/**
20+
* A pool of background worker threads to perform tasks asynchronously
21+
*/
22+
class Workers(engine: ScriptEngine, script: URL, ui: Any) {
23+
val nWorkers = Math.max(Runtime.getRuntime().availableProcessors() - 1, 1)
24+
val pool = newFixedThreadPoolContext(nWorkers, "worker-pool")
25+
private val console = Console() // redirect messages from workers to console
26+
27+
/** Tasks to be processed by the workers */
28+
private val tasks = LinkedBlockingQueue<Task>()
29+
private var running = true
30+
31+
init {
32+
(1..nWorkers).forEach {
33+
launch(pool) {
34+
// bindings aren't thread safe, so one for each thread
35+
val bindings = engine.createBindings()
36+
bindings.put("console", console) // support console.log for workers
37+
bindings.put("tasks", workerTake)
38+
bindings.put("ui", ui)
39+
engine.eval(script.readText(), bindings)
40+
}
41+
}
42+
}
43+
44+
fun stop() {
45+
running = false
46+
pool.close()
47+
tasks.clear()
48+
}
49+
50+
fun add(name: String, args: JSObject) {
51+
tasks.add(Task(name, toArray(args)))
52+
}
53+
54+
/**
55+
* Convert JSObject to java array
56+
*/
57+
private fun toArray(jso: JSObject): Array<Any> {
58+
val len = jso.getMember("length")
59+
return if (len is Int) Array(len) { i -> jso.getSlot(i) }
60+
else emptyArray()
61+
}
62+
63+
private val workerTake = object {
64+
fun take(): Task? {
65+
while (running) {
66+
val task = tasks.poll(5, SECONDS)
67+
if (task != null) return task
68+
}
69+
return null
70+
}
71+
}
72+
}

src/main/resources/web/ui.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
html,
22
body {
33
margin: 0;
4-
padding: 0;
5-
background-color: black;
4+
padding: 5;
5+
background-color: blue;
66
font-family: Arial, Helvetica, Verdana, Geneva, Tahoma, sans-serif;
77
color: #faa;
88
font-size: 12px;

src/main/resources/web/worker.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ var jobs = {
1313
}
1414
}
1515

16-
while (true) {
17-
var task = tasks.take()
16+
var task = tasks.take()
17+
while (task != null) {
1818
jobs[task.name](task.args)
19+
task = tasks.take()
1920
}
2021

22+
console.log("worker exiting")

0 commit comments

Comments
 (0)