220 lines
10 KiB
Java
220 lines
10 KiB
Java
package qualification;
|
|
|
|
import com.oracle.javafx.scenebuilder.kit.fxom.*;
|
|
import de.neitzel.fxdesigner.kit.*;
|
|
import java.lang.instrument.Instrumentation;
|
|
import java.nio.file.*;
|
|
import java.util.*;
|
|
import java.util.concurrent.*;
|
|
import javafx.application.Platform;
|
|
import javafx.scene.control.*;
|
|
import javafx.stage.*;
|
|
|
|
/** External qualification agent; never included in the distributable application. */
|
|
public final class PackageProbe {
|
|
static <T> T fx(Callable<T> action) throws Exception {
|
|
var result = new CompletableFuture<T>();
|
|
Platform.runLater(
|
|
() -> {
|
|
try {
|
|
result.complete(action.call());
|
|
} catch (Throwable e) {
|
|
result.completeExceptionally(e);
|
|
}
|
|
});
|
|
return result.get(20, TimeUnit.SECONDS);
|
|
}
|
|
|
|
static void await(Callable<Boolean> condition) throws Exception {
|
|
long until = System.nanoTime() + TimeUnit.SECONDS.toNanos(100);
|
|
while (System.nanoTime() < until) {
|
|
if (condition.call()) return;
|
|
Thread.sleep(100);
|
|
}
|
|
throw new AssertionError("Packaged workflow condition timed out");
|
|
}
|
|
|
|
public static void premain(String ignored, Instrumentation instrumentation) {
|
|
Thread.ofPlatform()
|
|
.daemon()
|
|
.start(
|
|
() -> {
|
|
Path evidence = Path.of(System.getProperty("package.evidence"));
|
|
try {
|
|
// Main launches JavaFX normally; the probe only observes an existing application.
|
|
for (int i = 0; i < 200; i++) {
|
|
try {
|
|
fx(() -> null);
|
|
break;
|
|
} catch (IllegalStateException e) {
|
|
Thread.sleep(100);
|
|
}
|
|
}
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
Window.getWindows().stream()
|
|
.anyMatch(w -> w instanceof Stage s && s.getScene() != null)));
|
|
String home = System.getProperty("java.home");
|
|
if (!home.contains("NeitzelFXDesigner.app/Contents/runtime"))
|
|
throw new AssertionError("External host JVM: " + home);
|
|
if (!System.getProperty("javafx.runtime.version").startsWith("26"))
|
|
throw new AssertionError("Wrong JavaFX");
|
|
String patch =
|
|
Class.forName(
|
|
"com.oracle.javafx.scenebuilder.kit.editor.panel.inspector.editors.I18nStringEditor")
|
|
.getProtectionDomain()
|
|
.getCodeSource()
|
|
.getLocation()
|
|
.toString();
|
|
try {
|
|
Class.forName("de.neitzel.fxdesigner.preview.PreviewSession");
|
|
throw new AssertionError("Target API leaked into the host classpath");
|
|
} catch (ClassNotFoundException expected) {
|
|
}
|
|
if (!patch.contains("designer-kit-ui-"))
|
|
throw new AssertionError("Unpatched Kit won classpath order: " + patch);
|
|
if (Boolean.getBoolean("package.sourceOnly")) {
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
Window.getWindows().stream()
|
|
.anyMatch(
|
|
w ->
|
|
w instanceof Stage s
|
|
&& s.getScene().getRoot()
|
|
instanceof SourceOnlyPane)));
|
|
var source =
|
|
fx(
|
|
() ->
|
|
(SourceOnlyPane)
|
|
Window.getWindows().stream()
|
|
.filter(
|
|
w ->
|
|
w instanceof Stage st
|
|
&& st.getScene().getRoot()
|
|
instanceof SourceOnlyPane)
|
|
.map(w -> ((Stage) w).getScene().getRoot())
|
|
.findFirst()
|
|
.orElseThrow());
|
|
await(() -> fx(() -> ((TextArea) source.getCenter()).isEditable()));
|
|
String original =
|
|
Files.readString(Path.of(System.getProperty("package.document")));
|
|
fx(
|
|
() -> {
|
|
((TextArea) source.getCenter())
|
|
.appendText("<!-- packaged fallback edit -->");
|
|
return null;
|
|
});
|
|
fx(source::save).toCompletableFuture().get(10, TimeUnit.SECONDS);
|
|
if (!Files.readString(Path.of(System.getProperty("package.document")))
|
|
.equals(original + "<!-- packaged fallback edit -->"))
|
|
throw new AssertionError("Fallback did not save exact source");
|
|
} else {
|
|
var pane =
|
|
fx(
|
|
() ->
|
|
(DesignerPane)
|
|
Window.getWindows().stream()
|
|
.filter(
|
|
w ->
|
|
w instanceof Stage s
|
|
&& s.getScene().getRoot() instanceof DesignerPane)
|
|
.map(w -> ((Stage) w).getScene().getRoot())
|
|
.findFirst()
|
|
.orElseThrow());
|
|
if (!fx(
|
|
() -> ((Stage) pane.getScene().getWindow()).getTitle().contains("[fixture]")))
|
|
throw new AssertionError("Active application context is not displayed");
|
|
await(() -> fx(() -> pane.kit().editor().getFxomDocument() != null));
|
|
fx(
|
|
() -> {
|
|
pane.lookupAll(".button").stream()
|
|
.filter(
|
|
n ->
|
|
n instanceof Button b
|
|
&& b.getText().equals("Build and Refresh"))
|
|
.map(n -> (Button) n)
|
|
.findFirst()
|
|
.orElseThrow()
|
|
.fire();
|
|
return null;
|
|
});
|
|
if (Boolean.getBoolean("package.missingRuntime")) {
|
|
var member = DesignerPane.class.getDeclaredField("buildRun");
|
|
member.setAccessible(true);
|
|
var run =
|
|
fx(() -> (de.neitzel.fxdesigner.build.BuildRefresh.Run) member.get(pane));
|
|
var result = run.completion().toCompletableFuture().get(30, TimeUnit.SECONDS);
|
|
if (result.status() != de.neitzel.fxdesigner.build.BuildRefresh.Status.FAILED
|
|
|| !result.message().contains("Invalid JDK installation"))
|
|
throw new AssertionError("Missing runtime not diagnosed: " + result);
|
|
var source = fx(() -> (TextArea) pane.lookup("#fxml-source"));
|
|
await(() -> fx(source::isEditable));
|
|
if (source == null) throw new AssertionError("Source editing unavailable");
|
|
} else {
|
|
await(
|
|
() ->
|
|
fx(
|
|
() -> {
|
|
var object =
|
|
new FXOMFxIdIndex(pane.kit().editor().getFxomDocument())
|
|
.lookup("card");
|
|
return object instanceof FXOMInstance i
|
|
&& i.getSceneGraphObject() instanceof SnapshotRegion r
|
|
&& r.image() != null
|
|
&& r.statusText().equals("Target runtime");
|
|
}));
|
|
try (var doc =
|
|
pane.kit()
|
|
.host()
|
|
.documents()
|
|
.open(Path.of(System.getProperty("package.document")).toUri())
|
|
.toCompletableFuture()
|
|
.get()) {
|
|
ProjectionHostChecks.run(pane, doc);
|
|
}
|
|
}
|
|
}
|
|
Files.writeString(
|
|
evidence.resolve("runtime.txt"),
|
|
home + "\n" + patch + "\n" + System.getProperty("javafx.runtime.version"));
|
|
Files.writeString(
|
|
evidence.resolve("workers.txt"),
|
|
String.join(
|
|
"\n",
|
|
ProcessHandle.current()
|
|
.descendants()
|
|
.map(p -> Long.toString(p.pid()))
|
|
.toList()));
|
|
fx(
|
|
() -> {
|
|
for (var window : List.copyOf(Window.getWindows()))
|
|
if (window instanceof Stage stage) {
|
|
// All checks save their drafts; exercise real onHidden cleanup without a
|
|
// confirmation dialog.
|
|
stage.setOnCloseRequest(null);
|
|
stage.close();
|
|
}
|
|
return null;
|
|
});
|
|
Files.writeString(
|
|
evidence.resolve("SUCCESS.txt"),
|
|
"PASS packaged launcher, bundled runtime/Kit, workflow and window cleanup\n");
|
|
Platform.exit();
|
|
} catch (Throwable failure) {
|
|
try (var out =
|
|
new java.io.PrintWriter(
|
|
Files.newBufferedWriter(evidence.resolve("FAILURE.txt")))) {
|
|
failure.printStackTrace(out);
|
|
} catch (Exception ignoredError) {
|
|
}
|
|
failure.printStackTrace();
|
|
System.exit(1);
|
|
}
|
|
});
|
|
}
|
|
}
|