Connect minimal hosts to snapshots, target workers and shared Kit editing
This commit is contained in:
+14
-2
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user