Skip to content
This repository has been archived by the owner on Mar 30, 2021. It is now read-only.

Commit

Permalink
App compatibility hack for Everest. Fix some GC roots that keep it pi…
Browse files Browse the repository at this point in the history
…nned.
  • Loading branch information
mikehearn committed Dec 29, 2018
1 parent 7fd61f3 commit ae1d55c
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 111 deletions.
15 changes: 15 additions & 0 deletions shell/src/main/java/app/graviton/ModuleHacks.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import com.sun.javafx.application.ParametersImpl;
import javafx.application.Application;

import java.lang.reflect.Field;
import java.util.Map;

/**
* Workaround for lack of ability to specify --add-opens equivalent in Kotlin. For Java 9+, starting a JavaFX Application after one has
* already been started requires access to an internal class, which requires us to override the module system. But that can't be done
Expand All @@ -12,4 +15,16 @@ public class ModuleHacks {
public static void setParams(Application application, String[] args) {
ParametersImpl.registerParameters(application, new ParametersImpl(args));
}

public static void removeParams(Application application) {
try {
Field params = ParametersImpl.class.getDeclaredField("params");
params.setAccessible(true);
@SuppressWarnings("unchecked")
Map<Application, Application.Parameters> map = (Map<Application, Application.Parameters>) params.get(null);
map.remove(application);
} catch (NoSuchFieldException | IllegalAccessException e) {
// Ignore, probably JavaFX version upgrade.
}
}
}
2 changes: 1 addition & 1 deletion shell/src/main/kotlin/app/graviton/shell/AppLaunchUI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class AppLaunchUI : View() {
companion object : Logging()

private val showcaseApps = linkedSetOf(
AppEntry("Everest", "A beautiful REST workbench.", "com.rohitawate:everest"),
AppEntry("Everest", "A beautiful, cross-platform REST client.", "com.github.rohitawate:Everest"),
AppEntry("Tic Tac Toe", "A reimplementation of the React.JS tutorial app, so complexity and lines of code can be directly compared.", "plan99.net:tictactoe"),
AppEntry("CalendarFXApp", "The parent project for the various CalendarFX modules.", "com.github.dlemmermann.calendarfx:application:v8.5.0"),
AppEntry("SpotBugs", "SpotBugs: Because it's easy!", "com.github.spotbugs")
Expand Down
80 changes: 50 additions & 30 deletions shell/src/main/kotlin/app/graviton/shell/AppLauncher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import app.graviton.api.v1.GravitonRunInShell
import app.graviton.codefetch.CodeFetcher
import app.graviton.codefetch.StartException
import app.graviton.shell.GravitonClassLoader.Companion.build
import com.sun.javafx.stage.StageHelper
import javafx.application.Application
import javafx.application.Platform
import javafx.beans.property.Property
Expand Down Expand Up @@ -204,7 +205,38 @@ class AppLauncher(private val options: GravitonCLI,
private fun startGravitonApp(loadResult: GravitonClassLoader, fetch: CodeFetcher.Result) {
val primaryStage = primaryStage!!
val appClass = loadResult.startClass.asSubclass(Application::class.java)

// TODO: Create a ThreadGroup and then use SecurityManager to ensure that newly started threads are in that group.
// Then we can interrupt them at termination time to enable more app code to be GCd out.

fun registerForWindowClose(stage: Stage, restore: () -> Unit) {
val oldOnCloseRequest = stage.onCloseRequest
val closeFilter = object : EventHandler<WindowEvent> {
/**
* Invoked when a specific event of the type for which this handler is
* registered happens.
*
* @param event the event which occurred
*/
override fun handle(event: WindowEvent) {
// Stop the window closing.
event.consume()
if (stage != primaryStage) {
stage.hide()
primaryStage.show()
}
// Let the app handle the event as if we had not intervened.
stage.onCloseRequest?.handle(event)
stage.onCloseRequest = oldOnCloseRequest
stage.removeEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, this)
info { "Inlined application quitting, back to the shell" }
restore()
events?.appFinished()
}
}
stage.addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, closeFilter)
}

thread(name = "App init thread: ${fetch.name}") {
Thread.currentThread().contextClassLoader = appClass.classLoader

Expand Down Expand Up @@ -236,41 +268,20 @@ class AppLauncher(private val options: GravitonCLI,
val newScene = (app as? GravitonRunInShell)?.createScene(Gateway())
val implementsAPI = newScene != null

fun restore() {
Thread.currentThread().contextClassLoader = AppLauncher::class.java.classLoader
primaryStage.titleProperty().unbind()
primaryStage.title = ""
primaryStage.scene = oldScene
Platform.setImplicitExit(true)
ModuleHacks.removeParams(app)
}

fun proceed() {
// Pick a default title. The app may of course override it in Application.start()
primaryStage.title = fetch.artifact.toString()
Platform.setImplicitExit(false)

fun restore() {
Thread.currentThread().contextClassLoader = AppLauncher::class.java.classLoader
primaryStage.titleProperty().unbind()
primaryStage.title = ""
primaryStage.scene = oldScene
Platform.setImplicitExit(true)
}

val oldOnCloseRequest = primaryStage.onCloseRequest
val closeFilter = object : EventHandler<WindowEvent> {
/**
* Invoked when a specific event of the type for which this handler is
* registered happens.
*
* @param event the event which occurred
*/
override fun handle(event: WindowEvent) {
// Stop the main window closing.
event.consume()
// Let the app handle the event as if we had not intervened.
primaryStage.onCloseRequest?.handle(event)
primaryStage.onCloseRequest = oldOnCloseRequest
primaryStage.removeEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, this)
info { "Inlined application quitting, back to the shell" }
restore()
events?.appFinished()
}
}
primaryStage.addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, closeFilter)

try {
// This messing around with minWidth/Height is to avoid an ugly window resize on macOS.
primaryStage.minWidth = widthBeforeStart
Expand All @@ -285,8 +296,17 @@ class AppLauncher(private val options: GravitonCLI,
// The app is expected to notice it's been called inside Graviton and thus, that the stage
// is visible. In that case start will do very little.
}

app.start(primaryStage)

// Compatibility hack: some apps ignore the primary stage and open up a fresh one, like Everest 1.4
// so we check for that here and register close handling on the new stage as well. INTERNAL API.
val stages = StageHelper.getStages()
if (stages[0] != primaryStage)
registerForWindowClose(stages[0], ::restore)
else
registerForWindowClose(primaryStage, ::restore)

if (primaryStage.minWidth == widthBeforeStart)
primaryStage.minWidth = 0.0
if (primaryStage.minHeight == heightBeforeStart)
Expand Down
80 changes: 0 additions & 80 deletions shell/src/main/kotlin/app/graviton/shell/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package app.graviton.shell
import javafx.application.Platform
import okhttp3.HttpUrl
import org.eclipse.aether.artifact.Artifact
import java.io.IOException
import java.io.InputStream
import java.io.PrintWriter
import java.io.StringWriter
import java.net.URL
Expand Down Expand Up @@ -193,82 +191,4 @@ fun Artifact.withNameAndDescription(name: String, description: String?): Artifac
props["model.name"] = name
props["model.description"] = description
return setProperties(props)
}

/**
* If instantiated, allows a block of code to be protected in a thread safe manner such that it can only be entered
* once. Any other attempt to enter the block will throw [IllegalStateException]. Instances can be used as functions:
*
* val once = Once()
*
* once {
* .. can't re-enter here ..
* }
*/
class Once {
private var consumed: Boolean = false

@Synchronized fun consume() {
check(!consumed) { "Operation can only be performed once" }
consumed = true
}

inline operator fun <T> invoke(block: () -> T): T {
consume()
return block()
}
}

/**
* An efficient stream searching class based on the Knuth-Morris-Pratt algorithm. Based on code from Twitter's
* Elephant-Bird library.
*/
class StreamSearcher(val pattern: ByteArray) {
private val borders = IntArray(pattern.size + 1)

init {
var i = 0
var j = -1
borders[i] = j
while (i < pattern.size) {
while (j >= 0 && pattern[i] != pattern[j])
j = borders[j]
borders[++i] = ++j
}
}

/**
* Searches for the next occurrence of the pattern in the stream, starting from the current stream position. Note
* that the position of the stream is changed. If a match is found, the stream points to the end of the match -- i.e. the
* byte AFTER the pattern. Else, the stream is entirely consumed. The latter is because InputStream semantics make it difficult to have
* another reasonable default, i.e. leave the stream unchanged.
*
* @return bytes consumed if found, -1 otherwise.
* @throws IOException
*/
fun search(stream: InputStream): Long {
var bytesRead: Long = 0

var b: Int
var j = 0

while (true) {
b = stream.read()
if (b == -1) break

bytesRead++

while (j >= 0 && b.toByte() != pattern[j])
j = borders[j]

// Move to the next character in the pattern.
++j

// If we've matched up to the full pattern length, we found it.
if (j == pattern.size)
return bytesRead
}

return -1
}
}

0 comments on commit ae1d55c

Please sign in to comment.