Skip to content

Commit 2d5c1a7

Browse files
committed
Improve error handling for JavaFX initialization
Now, the JavaFX initialization may fail with an exception in case something went wrong. The driver for this change was that the initialization started hanging in headless environments with transition to JDK 11. Before, the initialization logic had a flaw. If a call to one API failed, another API would be attempted. However, this approach is problematic: if the first call failed with an exception for some reason, it would leave JavaFX in a broken state where a flag would imply that the system is being initialized. Subsequent calls would then proceed to wait forever for the initialization to complete. Now, exceptions are checked more carefully, ensuring that we only fall back to the internal API in case the public one is unavailable and not failed for some valid reason. This differentiation also allows to more boldly rethrow exceptions upwards, being more or less confident that these are relevant to the user.
1 parent 7c0b753 commit 2d5c1a7

File tree

1 file changed

+27
-25
lines changed

1 file changed

+27
-25
lines changed

ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt

+27-25
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import javafx.util.*
1111
import kotlinx.coroutines.*
1212
import kotlinx.coroutines.internal.*
1313
import kotlinx.coroutines.javafx.JavaFx.delay
14+
import java.lang.UnsupportedOperationException
1415
import java.lang.reflect.*
1516
import java.util.concurrent.*
1617
import kotlin.coroutines.*
@@ -115,41 +116,42 @@ private class PulseTimer : AnimationTimer() {
115116
}
116117
}
117118

119+
/** @return [true] if initialized successfully, and [false] if no display is detected */
118120
internal fun initPlatform(): Boolean = PlatformInitializer.success
119121

120122
// Lazily try to initialize JavaFx platform just once
121123
private object PlatformInitializer {
122124
val success = run {
123125
/*
124126
* Try to instantiate JavaFx platform in a way which works
125-
* both on Java 8 and Java 11 and does not produce "illegal reflective access":
126-
*
127-
* 1) Try to invoke javafx.application.Platform.startup if this class is
128-
* present in a classpath.
129-
* 2) If it is not successful and does not because it is already started,
130-
* fallback to PlatformImpl.
131-
*
132-
* Ignore exception anyway in case of unexpected changes in API, in that case
133-
* user will have to instantiate it manually.
127+
* both on Java 8 and Java 11 and does not produce "illegal reflective access".
134128
*/
135-
val runnable = Runnable {}
136-
runCatching {
137-
// Invoke public API if it is present
138-
Class.forName("javafx.application.Platform")
139-
.getMethod("startup", java.lang.Runnable::class.java)
140-
.invoke(null, runnable)
141-
}.recoverCatching { exception ->
142-
// Recover -> check re-initialization
143-
val cause = exception.cause
144-
if (exception is InvocationTargetException && cause is IllegalStateException
145-
&& "Toolkit already initialized" == cause.message) {
146-
// Toolkit is already initialized -> success, return
147-
Unit
148-
} else { // Fallback to Java 8 API
149-
Class.forName("com.sun.javafx.application.PlatformImpl")
129+
try {
130+
val runnable = Runnable {}
131+
// Invoke the public API if it is present.
132+
runCatching {
133+
Class.forName("javafx.application.Platform")
134+
.getMethod("startup", java.lang.Runnable::class.java)
135+
}.map { method ->
136+
method.invoke(null, runnable)
137+
return@run true
138+
}
139+
// If we are here, it means the public API is not present. Try the private API.
140+
Class.forName("com.sun.javafx.application.PlatformImpl")
150141
.getMethod("startup", java.lang.Runnable::class.java)
151142
.invoke(null, runnable)
143+
true
144+
} catch (exception: InvocationTargetException) {
145+
// Can only happen as a result of [Method.invoke].
146+
val cause = exception.cause!!
147+
when {
148+
// Maybe the problem is that JavaFX is already initialized? Everything is good then.
149+
cause is IllegalStateException && "Toolkit already initialized" == cause.message -> true
150+
// If the problem is the headless environment, it is okay.
151+
cause is UnsupportedOperationException && "Unable to open DISPLAY" == cause.message -> false
152+
// Otherwise, the exception demonstrates an anomaly.
153+
else -> throw cause
152154
}
153-
}.isSuccess
155+
}
154156
}
155157
}

0 commit comments

Comments
 (0)