Keep Glass attached through target-worker shutdown

Early Platform.exit detached JavaFX while queued macOS activation callbacks still called Java. Close the owned example before terminating its JVM, make shutdown idempotent, and reject late preview work. Add real startup-cancellation regressions and native crash-report checks.
This commit is contained in:
2026-11-12 15:03:51 +01:00
parent 886ce4a96f
commit 7b1d754320
10 changed files with 245 additions and 14 deletions
@@ -14,26 +14,55 @@ import javafx.application.Platform;
public final class WorkerMain {
private static volatile TargetPreview preview;
private static volatile boolean fx;
private static final Object lifecycle = new Object();
private static final java.util.concurrent.atomic.AtomicBoolean stopping =
new java.util.concurrent.atomic.AtomicBoolean();
private static synchronized void send(Socket socket, Message message) throws IOException {
FrameCodec.write(socket.getOutputStream(), message);
socket.getOutputStream().flush();
}
private static void terminate() {
try {
ProcessHandle.current().descendants().forEach(ProcessHandle::destroyForcibly);
} finally {
Runtime.getRuntime().halt(0);
}
}
private static void stop() {
if (!stopping.compareAndSet(false, true)) return;
// Independent of FX responsiveness and of arbitrary application cleanup callbacks.
Thread.ofPlatform()
.daemon()
.name("designer-shutdown-deadline")
.start(
() -> {
try {
Thread.sleep(1500);
} catch (InterruptedException ignored) {
}
ProcessHandle.current().descendants().forEach(ProcessHandle::destroyForcibly);
Runtime.getRuntime().halt(0);
terminate();
});
if (preview != null) preview.close();
if (fx) Platform.runLater(Platform::exit);
TargetPreview owned;
synchronized (lifecycle) {
owned = preview;
}
if (!fx) {
terminate();
return;
}
Platform.runLater(
() -> {
try {
if (owned != null) owned.close();
} finally {
// Do not call Platform.exit(): macOS can deliver an activation callback after
// Glass detaches its Java thread. Keep Glass attached until this owned JVM exits.
terminate();
}
});
}
public static void main(String[] args) throws Exception {
@@ -132,22 +161,27 @@ public final class WorkerMain {
});
fx = true;
if (!started.await(10, TimeUnit.SECONDS)) throw new IOException("FX startup timeout");
preview =
TargetPreview.open(
descriptor,
loader,
Duration.ofSeconds(args.length > 2 ? Integer.parseInt(args[2]) : 20));
synchronized (lifecycle) {
if (stopping.get()) return;
preview =
TargetPreview.open(
descriptor,
loader,
Duration.ofSeconds(args.length > 2 ? Integer.parseInt(args[2]) : 20));
}
}
preview
.ready()
.whenComplete(
(ready, error) -> {
if (stopping.get()) return;
if (error != null) {
error(socket, p, error);
return;
}
Platform.runLater(
() -> {
if (stopping.get()) return;
try {
send(
socket,