Connect minimal hosts to snapshots, target workers and shared Kit editing

This commit is contained in:
2026-11-12 11:07:45 +01:00
parent 28c1e47c95
commit 7db7218a6e
52 changed files with 4236 additions and 29 deletions
@@ -169,6 +169,10 @@ public final class TargetPreview implements AutoCloseable {
/** Raw, row-major, straight-alpha RGBA8 matching the wire contract. Must run on FX. */
public byte[] render(int width, int height) {
return renderNode("", width, height);
}
public byte[] renderNode(String nodeId, int width, int height) {
fx();
if (closed || !ready.isDone() || ready.isCompletedExceptionally())
throw new IllegalStateException("Preview not ready");
@@ -179,10 +183,18 @@ public final class TargetPreview implements AutoCloseable {
|| (long) width * height * 4 > 16 * 1024 * 1024)
throw new IllegalArgumentException("Image exceeds protocol limits");
layout(width, height);
var node = nodeId.isEmpty() ? view.getRoot() : view.getRoot().lookup("#" + nodeId);
if (node == null) throw new IllegalArgumentException("Unknown rendered node: " + nodeId);
if (node instanceof javafx.scene.layout.Region region) {
region.resize(width, height);
region.applyCss();
region.layout();
}
var parameters = new SnapshotParameters();
parameters.setFill(Color.TRANSPARENT);
parameters.setViewport(new Rectangle2D(0, 0, width, height));
var image = view.getRoot().snapshot(parameters, new WritableImage(width, height));
var bounds = node.getBoundsInParent();
parameters.setViewport(new Rectangle2D(bounds.getMinX(), bounds.getMinY(), width, height));
var image = node.snapshot(parameters, new WritableImage(width, height));
byte[] bytes = new byte[width * height * 4];
var pixels = image.getPixelReader();
for (int y = 0; y < height; y++)
@@ -0,0 +1,197 @@
package de.neitzel.fxdesigner.target;
import static de.neitzel.fxdesigner.protocol.WireProtocol.*;
import de.neitzel.fxdesigner.protocol.*;
import java.io.*;
import java.net.*;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import javafx.application.Platform;
/** Target-only entry point. Stdin is the reserved owner channel, never application input. */
public final class WorkerMain {
private static volatile TargetPreview preview;
private static volatile boolean fx;
private static synchronized void send(Socket socket, Message message) throws IOException {
FrameCodec.write(socket.getOutputStream(), message);
socket.getOutputStream().flush();
}
private static void stop() {
Thread.ofPlatform()
.daemon()
.start(
() -> {
try {
Thread.sleep(1500);
} catch (InterruptedException ignored) {
}
ProcessHandle.current().descendants().forEach(ProcessHandle::destroyForcibly);
Runtime.getRuntime().halt(0);
});
if (preview != null) preview.close();
if (fx) Platform.runLater(Platform::exit);
}
public static void main(String[] args) throws Exception {
var owner = new DataInputStream(System.in);
String token = owner.readUTF();
System.setIn(InputStream.nullInputStream());
Thread.ofPlatform()
.daemon()
.name("designer-owner-lifetime")
.start(
() -> {
try {
while (owner.read() != -1) {}
} catch (IOException ignored) {
}
stop();
});
try (var socket = new Socket(InetAddress.getLoopbackAddress(), Integer.parseInt(args[0]))) {
socket.setSoTimeout(15000);
String session = args[1];
send(
socket,
new Hello(
session,
ProcessHandle.current().pid(),
token,
new Version(1, 0),
new Version(1, 0),
new Version(1, 0),
Set.of("discover-v1", "render-rgba-v1"),
Set.of()));
var welcome = FrameCodec.read(socket.getInputStream());
if (!(welcome instanceof Welcome w) || !session.equals(w.session()) || w.wire().major() != 1)
throw new IOException("Invalid welcome");
socket.setSoTimeout(0);
boolean rendered = false;
String activeExample = null;
while (true) {
var message = FrameCodec.read(socket.getInputStream());
if (!(message instanceof Packet p) || !p.identity().session().equals(session))
throw new IOException("Invalid request");
if (p.kind() == Kind.CANCEL) {
stop();
return;
}
if (p.kind() != Kind.REQUEST || !w.operations().contains(p.operation()))
throw new IOException("Unnegotiated operation");
try {
var loader = WorkerMain.class.getClassLoader();
if (p.operation() == Operation.DISCOVER) {
if (p.payload().length != 0) throw new IOException("Discovery payload must be empty");
var entries = ExampleCatalog.discover(loader);
var data = new TreeMap<String, String>();
data.put("format", "1");
data.put("count", Integer.toString(entries.size()));
for (int i = 0; i < entries.size(); i++) {
var d = entries.get(i);
String prefix = "example." + i + ".";
data.put(prefix + "id", d.id());
data.put(prefix + "label", d.label());
data.put(prefix + "modelType", d.modelType());
data.put(prefix + "providerClass", d.providerClass());
data.put(prefix + "viewResource", d.viewResource());
data.put(prefix + "componentPackage", d.componentPackage());
}
send(
socket,
new Packet(Kind.RESULT, p.operation(), p.identity(), PreviewMessages.encode(data)));
} else {
var data = PreviewMessages.decode(p.payload());
if (!data.keySet().equals(Set.of("example", "node")))
throw new IOException("Invalid render metadata");
if (rendered && !data.get("example").equals(activeExample))
throw new IOException("Example changed; start a fresh worker");
if (!rendered) {
rendered = true;
activeExample = data.get("example");
var descriptor =
ExampleCatalog.discover(loader).stream()
.filter(d -> d.id().equals(data.get("example")))
.findFirst()
.orElseThrow();
var started = new CountDownLatch(1);
Platform.startup(
() -> {
Platform.setImplicitExit(false);
started.countDown();
});
fx = true;
if (!started.await(10, TimeUnit.SECONDS)) throw new IOException("FX startup timeout");
preview = TargetPreview.open(descriptor, loader, Duration.ofSeconds(20));
}
preview
.ready()
.whenComplete(
(ready, error) -> {
if (error != null) {
error(socket, p, error);
return;
}
Platform.runLater(
() -> {
try {
send(
socket,
new Packet(
Kind.RESULT,
p.operation(),
p.identity(),
ready.renderNode(
data.get("node"),
p.identity().width(),
p.identity().height())));
} catch (Throwable e) {
error(socket, p, e);
}
});
});
}
} catch (Throwable e) {
error(socket, p, e);
}
}
} catch (EOFException ignored) {
} finally {
stop();
}
}
private static void error(Socket socket, Packet p, Throwable error) {
try {
var description = new StringBuilder();
var visited =
java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<Throwable, Boolean>());
for (Throwable cause = error;
cause != null && visited.add(cause) && visited.size() <= 8;
cause = cause.getCause()) {
if (!description.isEmpty()) description.append("\nCaused by: ");
description
.append(
cause instanceof PreviewFailure failure
? failure.code()
: cause.getClass().getSimpleName())
.append(": ")
.append(cause.getMessage());
}
String detail = description.toString();
if (detail.length() > 8000) detail = detail.substring(0, 8000);
send(
socket,
new Packet(
Kind.ERROR,
p.operation(),
p.identity(),
("LOAD_FAILED\n" + detail).getBytes(java.nio.charset.StandardCharsets.UTF_8)));
} catch (IOException ignored) {
stop();
}
}
}