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
@@ -0,0 +1,94 @@
package de.neitzel.fxdesigner.standalone;
import de.neitzel.fxdesigner.build.SnapshotStore;
import de.neitzel.fxdesigner.protocol.*;
import de.neitzel.fxdesigner.workflow.*;
import java.nio.file.*;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
/** Real target processes: cancellation must not detach JavaFX beneath native callbacks. */
public final class WorkerLifecycleSmoke {
public static void main(String[] args) throws Exception {
Path root = Path.of(args[0]), fixture = Path.of(args[1]);
int count = args.length > 2 ? Integer.parseInt(args[2]) : 24;
var store = new SnapshotStore(fixture.resolve(".neitzel-fx-designer/snapshots"));
var tools =
List.of("target-tools", "contracts", "preview-api").stream()
.map(
n ->
root.resolve("designer-" + n + "/target/designer-" + n + "-0.1.0-SNAPSHOT.jar"))
.toList();
for (int i = 0; i < count; i++) {
var output = new StringBuffer();
var worker =
new PreviewWorker(
new DraftOverlay(store.lease(store.state("fixture").lastSuccessful()), List.of()));
CompletableFuture<Void> request = null;
Process process;
boolean readyClose = args.length > 3 || i >= count - 4;
int delay = readyClose ? 0 : new int[] {100, 150, 200, 250}[i % 4];
try {
worker.start(tools, output::append);
var exited = worker.exited().toCompletableFuture();
request =
CompletableFuture.runAsync(
() -> {
try {
worker.request(
WireProtocol.Operation.INTERACTIVE,
Map.of("lifecycle-fixture", 1L),
640,
480,
PreviewMessages.encode(Map.of("example", "full", "node", "")),
Duration.ofSeconds(20));
} catch (Exception error) {
throw new CompletionException(error);
}
});
if (readyClose) request.get(25, TimeUnit.SECONDS);
else Thread.sleep(delay);
worker.close();
process = exited.get(5, TimeUnit.SECONDS);
} finally {
worker.close();
}
if (request != null) {
try {
request.get(5, TimeUnit.SECONDS);
} catch (ExecutionException expectedCancellation) {
if (readyClose) throw expectedCancellation;
}
}
if (readyClose) {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
while (!output.toString().contains("Application cleanup: full")
&& System.nanoTime() < deadline) Thread.sleep(10);
if (!output.toString().contains("Application cleanup: full"))
throw new AssertionError("Ready worker skipped example cleanup: " + output);
}
if ((process.exitValue() != 0 && (readyClose || process.exitValue() != 137))
|| output.toString().contains("Java has been detached already")
|| output.toString().contains("A fatal error has been detected"))
throw new AssertionError(
"Native lifecycle failure: iteration="
+ i
+ " delay="
+ delay
+ " pid="
+ process.pid()
+ " exit="
+ process.exitValue()
+ "\n"
+ output);
System.out.println(
"PASS worker lifecycle "
+ i
+ " exit="
+ process.exitValue()
+ " "
+ (readyClose ? "ready close" : "startup cancel " + delay + "ms"));
}
}
}