diff --git a/api/src/main/java/io/xpipe/api/connector/XPipeConnection.java b/api/src/main/java/io/xpipe/api/connector/XPipeConnection.java index c24dddbf7..8a8d67a87 100644 --- a/api/src/main/java/io/xpipe/api/connector/XPipeConnection.java +++ b/api/src/main/java/io/xpipe/api/connector/XPipeConnection.java @@ -73,7 +73,7 @@ public final class XPipeConnection extends BeaconConnection { } catch (InterruptedException ignored) { } - var s = BeaconClient.tryConnect(); + var s = BeaconClient.tryConnect(BeaconClient.ApiClientInformation.builder().version("?").language("Java").build()); if (s.isPresent()) { return s; } @@ -114,7 +114,7 @@ public final class XPipeConnection extends BeaconConnection { } try { - beaconClient = new BeaconClient(); + beaconClient = BeaconClient.connect(BeaconClient.ApiClientInformation.builder().version("?").language("Java").build()); } catch (Exception ex) { throw new BeaconException("Unable to connect to running xpipe daemon", ex); } diff --git a/api/src/main/java/io/xpipe/api/util/XPipeDaemonController.java b/api/src/main/java/io/xpipe/api/util/XPipeDaemonController.java index 1875b7cf4..59325f1b5 100644 --- a/api/src/main/java/io/xpipe/api/util/XPipeDaemonController.java +++ b/api/src/main/java/io/xpipe/api/util/XPipeDaemonController.java @@ -37,7 +37,7 @@ public class XPipeDaemonController { return; } - var client = new BeaconClient(); + var client = BeaconClient.connect(BeaconClient.ApiClientInformation.builder().version("?").language("Java API Test").build()); if (!BeaconServer.tryStop(client)) { throw new AssertionError(); } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java index db4b1c04a..2e8165ade 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java @@ -1,5 +1,7 @@ package io.xpipe.beacon; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.databind.JsonNode; @@ -9,7 +11,12 @@ import com.fasterxml.jackson.databind.node.TextNode; import io.xpipe.beacon.exchange.MessageExchanges; import io.xpipe.beacon.exchange.data.ClientErrorMessage; import io.xpipe.beacon.exchange.data.ServerErrorMessage; +import io.xpipe.core.store.ProcessControl; import io.xpipe.core.util.JacksonMapper; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; import java.io.*; import java.net.InetAddress; @@ -23,27 +30,90 @@ import static io.xpipe.beacon.BeaconConfig.BODY_SEPARATOR; public class BeaconClient implements AutoCloseable { + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") + public static abstract class ClientInformation { + + public final CliClientInformation cli() { + return (CliClientInformation) this; + } + + public abstract String toDisplayString(); + } + + @JsonTypeName("cli") + @Value + @Builder + @Jacksonized + @EqualsAndHashCode(callSuper = false) + public static class CliClientInformation extends ClientInformation { + + String version; + int consoleWidth; + + @Override + public String toDisplayString() { + return "X-Pipe CLI " + version; + } + } + + @JsonTypeName("gateway") + @Value + @Builder + @Jacksonized + @EqualsAndHashCode(callSuper = false) + public static class GatewayClientInformation extends ClientInformation { + + String version; + + @Override + public String toDisplayString() { + return "X-Pipe Gateway " + version; + } + } + + @JsonTypeName("api") + @Value + @Builder + @Jacksonized + @EqualsAndHashCode(callSuper = false) + public static class ApiClientInformation extends ClientInformation { + + String version; + String language; + + @Override + public String toDisplayString() { + return String.format("X-Pipe %s API v%s", language, version); + } + } + private final Closeable closeable; private final InputStream in; private final OutputStream out; - public BeaconClient() throws IOException { - var socket = new Socket(InetAddress.getLoopbackAddress(), BeaconConfig.getUsedPort()); - closeable = socket; - in = socket.getInputStream(); - out = socket.getOutputStream(); - } - - public BeaconClient(Closeable closeable, InputStream in, OutputStream out) { + private BeaconClient(Closeable closeable, InputStream in, OutputStream out) { this.closeable = closeable; this.in = in; this.out = out; } - public static Optional tryConnect() { + public static BeaconClient connect(ClientInformation information) throws Exception { + var socket = new Socket(InetAddress.getLoopbackAddress(), BeaconConfig.getUsedPort()); + var client = new BeaconClient(socket, socket.getInputStream(), socket.getOutputStream()); + client.sendObject(JacksonMapper.newMapper().valueToTree(information)); + return client; + } + + public static BeaconClient connectGateway(ProcessControl control, GatewayClientInformation information) throws Exception { + var client = new BeaconClient(() -> {}, control.getStdout(), control.getStdin()); + client.sendObject(JacksonMapper.newMapper().valueToTree(information)); + return client; + } + + public static Optional tryConnect(ClientInformation information) { try { - return Optional.of(new BeaconClient()); - } catch (IOException ex) { + return Optional.of(connect(information)); + } catch (Exception ex) { return Optional.empty(); } } @@ -95,10 +165,14 @@ public class BeaconClient implements AutoCloseable { "Sending request to server of type " + req.getClass().getName()); } + sendObject(msg); + } + + public void sendObject(JsonNode node) throws ConnectorException { var writer = new StringWriter(); var mapper = JacksonMapper.newMapper(); try (JsonGenerator g = mapper.createGenerator(writer).setPrettyPrinter(new DefaultPrettyPrinter())) { - g.writeTree(msg); + g.writeTree(node); } catch (IOException ex) { throw new ConnectorException("Couldn't serialize request", ex); } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconJacksonModule.java b/beacon/src/main/java/io/xpipe/beacon/BeaconJacksonModule.java new file mode 100644 index 000000000..cc3a6dfa0 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconJacksonModule.java @@ -0,0 +1,15 @@ +package io.xpipe.beacon; + +import com.fasterxml.jackson.databind.jsontype.NamedType; +import com.fasterxml.jackson.databind.module.SimpleModule; + +public class BeaconJacksonModule extends SimpleModule { + + @Override + public void setupModule(SetupContext context) { + context.registerSubtypes( + new NamedType(BeaconClient.ApiClientInformation.class), + new NamedType(BeaconClient.CliClientInformation.class), + new NamedType(BeaconClient.GatewayClientInformation.class)); + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java index c4398d323..d1cd89aea 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java @@ -24,7 +24,7 @@ public class BeaconServer { } public static boolean isRunning() { - try (var socket = new BeaconClient()) { + try (var socket = BeaconClient.connect(null)) { return true; } catch (Exception e) { return false; diff --git a/beacon/src/main/java/module-info.java b/beacon/src/main/java/module-info.java index 395dfb9ee..5ec300281 100644 --- a/beacon/src/main/java/module-info.java +++ b/beacon/src/main/java/module-info.java @@ -1,3 +1,5 @@ +import com.fasterxml.jackson.databind.Module; +import io.xpipe.beacon.BeaconJacksonModule; import io.xpipe.beacon.exchange.*; import io.xpipe.beacon.exchange.api.QueryRawDataExchange; import io.xpipe.beacon.exchange.api.QueryTableDataExchange; @@ -24,6 +26,7 @@ module io.xpipe.beacon { uses MessageExchange; + provides Module with BeaconJacksonModule; provides io.xpipe.beacon.exchange.MessageExchange with ForwardExchange, InstanceExchange, diff --git a/core/src/main/java/io/xpipe/core/store/ProcessControl.java b/core/src/main/java/io/xpipe/core/store/ProcessControl.java index 6ce897ed0..fc686b59c 100644 --- a/core/src/main/java/io/xpipe/core/store/ProcessControl.java +++ b/core/src/main/java/io/xpipe/core/store/ProcessControl.java @@ -16,6 +16,7 @@ public abstract class ProcessControl { pc.discardErr(); var bytes = pc.getStdout().readAllBytes(); var string = new String(bytes, pc.getCharset()); + pc.waitFor(); return string; } @@ -27,6 +28,15 @@ public abstract class ProcessControl { pc.waitFor(); } + public boolean executeAndCheckStatus() { + try { + executeOrThrow(); + return true; + } catch (Exception ex) { + return false; + } + } + public Optional executeAndReadStderrIfPresent() throws Exception { var pc = this; pc.start(); diff --git a/extension/src/main/java/io/xpipe/extension/DataStoreActionProvider.java b/extension/src/main/java/io/xpipe/extension/DataStoreActionProvider.java new file mode 100644 index 000000000..2cf866a17 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/DataStoreActionProvider.java @@ -0,0 +1,48 @@ +package io.xpipe.extension; + +import io.xpipe.core.store.DataStore; +import io.xpipe.extension.event.ErrorEvent; +import javafx.scene.layout.Region; + +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; + +public interface DataStoreActionProvider { + + static List> ALL = new ArrayList<>(); + + public static void init(ModuleLayer layer) { + if (ALL.size() == 0) { + ALL.addAll(ServiceLoader.load(layer, DataStoreActionProvider.class).stream() + .map(p -> (DataStoreActionProvider) p.get()) + .filter(provider -> { + try { + return provider.isActive(); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).handle(); + return false; + } + }) + .toList()); + } + } + + Class getApplicableClass(); + + default boolean isActive() throws Exception { + return true; + } + + default boolean isApplicable(T o) throws Exception { + return true; + } + + default void applyToRegion(T store, Region region) {} + + String getName(T store); + + String getIcon(T store); + + default void execute(T store) throws Exception {} +} diff --git a/extension/src/main/java/io/xpipe/extension/comp/FancyTooltipAugment.java b/extension/src/main/java/io/xpipe/extension/comp/FancyTooltipAugment.java index 3ca11eec7..21f46b838 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/FancyTooltipAugment.java +++ b/extension/src/main/java/io/xpipe/extension/comp/FancyTooltipAugment.java @@ -6,17 +6,204 @@ import io.xpipe.fxcomps.CompStructure; import io.xpipe.fxcomps.augment.Augment; import io.xpipe.fxcomps.util.PlatformThread; import io.xpipe.fxcomps.util.Shortcuts; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; import javafx.beans.value.ObservableValue; +import javafx.event.EventHandler; +import javafx.event.WeakEventHandler; +import javafx.geometry.NodeOrientation; import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.Region; import javafx.stage.Window; import javafx.util.Duration; public class FancyTooltipAugment> implements Augment { - static { - JFXTooltip.setHoverDelay(Duration.millis(400)); - JFXTooltip.setVisibleDuration(Duration.INDEFINITE); + private static final TooltipBehavior BEHAVIOR = new TooltipBehavior(Duration.millis(400), Duration.INDEFINITE, Duration.millis(100)); + + private static class TooltipBehavior { + + private static String TOOLTIP_PROP = "jfoenix-tooltip"; + private Timeline hoverTimer = new Timeline(); + private Timeline visibleTimer = new Timeline(); + private Timeline leftTimer = new Timeline(); + /** + * the currently hovered node + */ + private Node hoveredNode; + /** + * the next tooltip to be shown + */ + private JFXTooltip nextTooltip; + /** + * the current showing tooltip + */ + private JFXTooltip currentTooltip; + + private TooltipBehavior(Duration hoverDelay, Duration visibleDuration, Duration leftDelay) { + setHoverDelay(hoverDelay); + hoverTimer.setOnFinished(event -> { + ensureHoveredNodeIsVisible(() -> { + // set tooltip orientation + NodeOrientation nodeOrientation = hoveredNode.getEffectiveNodeOrientation(); + nextTooltip.getScene().setNodeOrientation(nodeOrientation); + //show tooltip + showTooltip(nextTooltip); + currentTooltip = nextTooltip; + hoveredNode = null; + // start visible timer + visibleTimer.playFromStart(); + }); + // clear next tooltip + nextTooltip = null; + }); + setVisibleDuration(visibleDuration); + visibleTimer.setOnFinished(event -> hideCurrentTooltip()); + setLeftDelay(leftDelay); + leftTimer.setOnFinished(event -> hideCurrentTooltip()); + } + + private void setHoverDelay(Duration duration) { + hoverTimer.getKeyFrames().setAll(new KeyFrame(duration)); + } + + private void setVisibleDuration(Duration duration) { + visibleTimer.getKeyFrames().setAll(new KeyFrame(duration)); + } + + private void setLeftDelay(Duration duration) { + leftTimer.getKeyFrames().setAll(new KeyFrame(duration)); + } + + private void hideCurrentTooltip() { + currentTooltip.hide(); + currentTooltip = null; + hoveredNode = null; + } + + private void showTooltip(JFXTooltip tooltip) { + // anchors are computed differently for each tooltip + tooltip.show(hoveredNode, -1, -1); + } + + private EventHandler moveHandler = (MouseEvent event) -> { + // if tool tip is already showing, do nothing + if (visibleTimer.getStatus() == Timeline.Status.RUNNING) { + return; + } + hoveredNode = (Node) event.getSource(); + Object property = hoveredNode.getProperties().get(TOOLTIP_PROP); + if (property instanceof JFXTooltip) { + JFXTooltip tooltip = (JFXTooltip) property; + ensureHoveredNodeIsVisible(() -> { + // if a tooltip is already showing then show this tooltip immediately + if (leftTimer.getStatus() == Timeline.Status.RUNNING) { + if (currentTooltip != null) { + currentTooltip.hide(); + } + currentTooltip = tooltip; + // show the tooltip + showTooltip(tooltip); + // stop left timer and start the visible timer to hide the tooltip + // once finished + leftTimer.stop(); + visibleTimer.playFromStart(); + } else { + // else mark the tooltip as the next tooltip to be shown once the hover + // timer is finished (restart the timer) + // t.setActivated(true); + nextTooltip = tooltip; + hoverTimer.stop(); + hoverTimer.playFromStart(); + } + }); + } else { + uninstall(hoveredNode); + } + }; + private WeakEventHandler weakMoveHandler = new WeakEventHandler<>(moveHandler); + + private EventHandler exitHandler = (MouseEvent event) -> { + // stop running hover timer as the mouse exited the node + if (hoverTimer.getStatus() == Timeline.Status.RUNNING) { + hoverTimer.stop(); + } else if (visibleTimer.getStatus() == Timeline.Status.RUNNING) { + // if tool tip was already showing, stop the visible timer + // and start the left timer to hide the current tooltip + visibleTimer.stop(); + leftTimer.playFromStart(); + } + hoveredNode = null; + nextTooltip = null; + }; + private WeakEventHandler weakExitHandler = new WeakEventHandler<>(exitHandler); + + // if mouse is pressed then stop all timers / clear all fields + private EventHandler pressedHandler = (MouseEvent event) -> { + // stop timers + hoverTimer.stop(); + visibleTimer.stop(); + leftTimer.stop(); + // hide current tooltip + if (currentTooltip != null) { + currentTooltip.hide(); + } + // clear fields + hoveredNode = null; + currentTooltip = null; + nextTooltip = null; + }; + private WeakEventHandler weakPressedHandler = new WeakEventHandler<>(pressedHandler); + + private void install(Node node, JFXTooltip tooltip) { + if (node == null) { + return; + } + if (tooltip == null) { + uninstall(node); + return; + } + node.removeEventHandler(MouseEvent.MOUSE_MOVED, weakMoveHandler); + node.removeEventHandler(MouseEvent.MOUSE_EXITED, weakExitHandler); + node.removeEventHandler(MouseEvent.MOUSE_PRESSED, weakPressedHandler); + node.addEventHandler(MouseEvent.MOUSE_MOVED, weakMoveHandler); + node.addEventHandler(MouseEvent.MOUSE_EXITED, weakExitHandler); + node.addEventHandler(MouseEvent.MOUSE_PRESSED, weakPressedHandler); + node.getProperties().put(TOOLTIP_PROP, tooltip); + } + + private void uninstall(Node node) { + if (node == null) { + return; + } + node.removeEventHandler(MouseEvent.MOUSE_MOVED, weakMoveHandler); + node.removeEventHandler(MouseEvent.MOUSE_EXITED, weakExitHandler); + node.removeEventHandler(MouseEvent.MOUSE_PRESSED, weakPressedHandler); + Object tooltip = node.getProperties().get(TOOLTIP_PROP); + if (tooltip != null) { + node.getProperties().remove(TOOLTIP_PROP); + if (tooltip.equals(currentTooltip) || tooltip.equals(nextTooltip)) { + weakPressedHandler.handle(null); + } + } + } + + private void ensureHoveredNodeIsVisible(Runnable visibleRunnable) { + final Window owner = getWindow(hoveredNode); + if (owner != null && owner.isShowing()) { + final boolean treeVisible = true; // NodeHelper.isTreeVisible(hoveredNode); + if (treeVisible && owner.isFocused()) { + visibleRunnable.run(); + } + } + } + + private Window getWindow(final Node node) { + final Scene scene = node == null ? null : node.getScene(); + return scene == null ? null : scene.getWindow(); + } } private final ObservableValue text; @@ -31,21 +218,11 @@ public class FancyTooltipAugment> implements Augment< @Override public void augment(S struc) { - var tt = new FocusTooltip(); - var toDisplay = text.getValue(); - if (Shortcuts.getShortcut(struc.get()) != null) { - toDisplay = toDisplay + " (" + Shortcuts.getShortcut(struc.get()).getDisplayText() + ")"; - } - tt.textProperty().setValue(toDisplay); - tt.setStyle("-fx-font-size: 11pt;"); - JFXTooltip.install(struc.get(), tt); - tt.setWrapText(true); - tt.setMaxWidth(400); - tt.getStyleClass().add("fancy-tooltip"); + augment(struc.get()); } public void augment(Node region) { - var tt = new FocusTooltip(); + var tt = new JFXTooltip(); var toDisplay = text.getValue(); if (Shortcuts.getShortcut((Region) region) != null) { toDisplay = @@ -53,57 +230,10 @@ public class FancyTooltipAugment> implements Augment< } tt.textProperty().setValue(toDisplay); tt.setStyle("-fx-font-size: 11pt;"); - JFXTooltip.install(region, tt); tt.setWrapText(true); tt.setMaxWidth(400); tt.getStyleClass().add("fancy-tooltip"); - } - private static class FocusTooltip extends JFXTooltip { - - public FocusTooltip() {} - - public FocusTooltip(String string) { - super(string); - } - - @Override - protected void show() { - Window owner = getOwnerWindow(); - if (owner == null || owner.isFocused()) { - super.show(); - } - } - - @Override - public void show(Node ownerNode, double anchorX, double anchorY) { - Window owner = getOwnerWindow(); - if (owner == null || owner.isFocused()) { - super.show(ownerNode, anchorX, anchorY); - } - } - - @Override - public void showOnAnchors(Node ownerNode, double anchorX, double anchorY) { - Window owner = getOwnerWindow(); - if (owner == null || owner.isFocused()) { - super.showOnAnchors(ownerNode, anchorX, anchorY); - } - } - - @Override - public void show(Window owner) { - if (owner == null || owner.isFocused()) { - super.show(owner); - } - } - - @Override - public void show(Window ownerWindow, double anchorX, double anchorY) { - Window owner = getOwnerWindow(); - if (owner == null || owner.isFocused()) { - super.show(ownerWindow, anchorX, anchorY); - } - } + BEHAVIOR.install(region, tt); } } diff --git a/extension/src/main/java/io/xpipe/extension/util/OsHelper.java b/extension/src/main/java/io/xpipe/extension/util/OsHelper.java new file mode 100644 index 000000000..98f15fc52 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/util/OsHelper.java @@ -0,0 +1,62 @@ +package io.xpipe.extension.util; + +import io.xpipe.extension.event.ErrorEvent; +import org.apache.commons.lang3.SystemUtils; + +import java.awt.*; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class OsHelper { + + public static String getFileSystemCompatibleName(String name) { + return name.replaceAll("[\\\\/:*?\"<>|]", "_"); + } + + public static Path getUserDocumentsPath() { + if (SystemUtils.IS_OS_WINDOWS) { + return Paths.get(System.getProperty("user.home")); + } else { + return Paths.get(System.getProperty("user.home"), ".local", "share"); + } + } + + public static void browseFile(Path file) { + if (!Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) { + return; + } + + ThreadHelper.run(() -> { + try { + Desktop.getDesktop().open(file.toFile()); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).omit().handle(); + } + }); + } + + public static void browseFileInDirectory(Path file) { + if (!Desktop.getDesktop().isSupported(Desktop.Action.BROWSE_FILE_DIR)) { + if (!Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) { + return; + } + + ThreadHelper.run(() -> { + try { + Desktop.getDesktop().open(file.getParent().toFile()); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).omit().handle(); + } + }); + return; + } + + ThreadHelper.run(() -> { + try { + Desktop.getDesktop().browseFileDirectory(file.toFile()); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).omit().handle(); + } + }); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/util/ThreadHelper.java b/extension/src/main/java/io/xpipe/extension/util/ThreadHelper.java new file mode 100644 index 000000000..7d71b7aeb --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/util/ThreadHelper.java @@ -0,0 +1,42 @@ +package io.xpipe.extension.util; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +public class ThreadHelper { + + public static Thread run(Runnable r) { + var t = new Thread(r); + t.setDaemon(true); + t.start(); + return t; + } + + public static T run(Supplier r) { + AtomicReference ret = new AtomicReference<>(); + var t = new Thread(() -> ret.set(r.get())); + t.setDaemon(true); + t.start(); + try { + t.join(); + } catch (InterruptedException e) { + return null; + } + return ret.get(); + } + + public static Thread create(String name, boolean daemon, Runnable r) { + var t = new Thread(r); + t.setDaemon(daemon); + t.setName(name); + return t; + } + + public static void sleep(long ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/extension/src/main/java/module-info.java b/extension/src/main/java/module-info.java index 4a28d52ff..8088b0f70 100644 --- a/extension/src/main/java/module-info.java +++ b/extension/src/main/java/module-info.java @@ -1,4 +1,5 @@ import io.xpipe.extension.DataSourceProvider; +import io.xpipe.extension.DataStoreActionProvider; import io.xpipe.extension.SupportedApplicationProvider; import io.xpipe.extension.util.XPipeDaemon; @@ -31,6 +32,7 @@ open module io.xpipe.extension { uses DataSourceProvider; uses SupportedApplicationProvider; + uses DataStoreActionProvider; uses io.xpipe.extension.I18n; uses io.xpipe.extension.event.EventHandler; uses io.xpipe.extension.prefs.PrefsProvider; diff --git a/extension/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module b/extension/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module new file mode 100644 index 000000000..d4caa9b68 --- /dev/null +++ b/extension/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module @@ -0,0 +1 @@ +io.xpipe.beacon.BeaconJacksonModule \ No newline at end of file