Package and qualify standalone macOS designer with bundled runtime
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user