Basic work for proxies

This commit is contained in:
Christopher Schnick 2022-11-26 12:32:09 +01:00
parent 02a2502fa5
commit 097d23f306
22 changed files with 607 additions and 58 deletions

View file

@ -130,12 +130,12 @@ public final class XPipeConnection extends BeaconConnection {
@FunctionalInterface
public static interface Handler {
void handle(BeaconConnection con) throws ClientException, ServerException, ConnectorException;
void handle(BeaconConnection con) throws Exception;
}
@FunctionalInterface
public static interface Mapper<T> {
T handle(BeaconConnection con) throws ClientException, ServerException, ConnectorException;
T handle(BeaconConnection con) throws Exception;
}
}

View file

@ -1,6 +1,7 @@
package io.xpipe.beacon;
import io.xpipe.beacon.exchange.StopExchange;
import io.xpipe.core.process.OsType;
import lombok.experimental.UtilityClass;
import java.io.BufferedReader;
@ -64,42 +65,42 @@ public class BeaconServer {
}
var out = new Thread(
null,
() -> {
try {
InputStreamReader isr = new InputStreamReader(proc.getInputStream());
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
if (print) {
System.out.println("[xpiped] " + line);
}
}
} catch (Exception ioe) {
ioe.printStackTrace();
null,
() -> {
try {
InputStreamReader isr = new InputStreamReader(proc.getInputStream());
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
if (print) {
System.out.println("[xpiped] " + line);
}
},
"daemon sysout");
}
} catch (Exception ioe) {
ioe.printStackTrace();
}
},
"daemon sysout");
out.setDaemon(true);
out.start();
var err = new Thread(
null,
() -> {
try {
InputStreamReader isr = new InputStreamReader(proc.getErrorStream());
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
if (print) {
System.err.println("[xpiped] " + line);
}
}
} catch (Exception ioe) {
ioe.printStackTrace();
null,
() -> {
try {
InputStreamReader isr = new InputStreamReader(proc.getErrorStream());
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
if (print) {
System.err.println("[xpiped] " + line);
}
},
"daemon syserr");
}
} catch (Exception ioe) {
ioe.printStackTrace();
}
},
"daemon syserr");
err.setDaemon(true);
err.start();
}
@ -110,7 +111,7 @@ public class BeaconServer {
return res.isSuccess();
}
private static Optional<Path> getDaemonBasePath() {
private static Optional<Path> getDaemonBasePath(OsType type) {
Path base = null;
// Prepare for invalid XPIPE_HOME path value
try {
@ -120,7 +121,7 @@ public class BeaconServer {
}
if (base == null) {
if (System.getProperty("os.name").startsWith("Windows")) {
if (type.equals(OsType.WINDOWS)) {
base = Path.of(System.getenv("LOCALAPPDATA"), "X-Pipe");
} else {
base = Path.of("/opt/xpipe/");
@ -133,17 +134,20 @@ public class BeaconServer {
return Optional.ofNullable(base);
}
public static Optional<Path> getDaemonExecutable() {
var base = getDaemonBasePath().orElseThrow();
var debug = BeaconConfig.launchDaemonInDebugMode();
Path executable = null;
if (!debug) {
if (System.getProperty("os.name").startsWith("Windows")) {
executable = Path.of("app", "runtime", "bin", "xpiped.bat");
} else {
executable = Path.of("app/bin/xpiped");
}
public static Path getDaemonExecutableInBaseDirectory(OsType type) {
if (type.equals(OsType.WINDOWS)) {
return Path.of("app", "runtime", "bin", "xpiped.bat");
} else {
return Path.of("app/bin/xpiped");
}
}
public static Optional<Path> getDaemonExecutable() {
var base = getDaemonBasePath(OsType.getLocal()).orElseThrow();
var debug = BeaconConfig.launchDaemonInDebugMode();
Path executable;
if (!debug) {
executable = getDaemonExecutableInBaseDirectory(OsType.getLocal());
} else {
String scriptName = null;
if (BeaconConfig.attachDebuggerToDaemon()) {

View file

@ -0,0 +1,36 @@
package io.xpipe.beacon.exchange;
import io.xpipe.beacon.RequestMessage;
import io.xpipe.beacon.ResponseMessage;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
public class NamedFunctionExchange implements MessageExchange {
@Override
public String getId() {
return "namedFunction";
}
@Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
@NonNull
String id;
@NonNull List<Object> arguments;
}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
Object returnValue;
}
}

View file

@ -0,0 +1,30 @@
package io.xpipe.beacon.exchange;
import io.xpipe.beacon.RequestMessage;
import io.xpipe.beacon.ResponseMessage;
import io.xpipe.core.source.DataSource;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
public class ProxyReadConnectionExchange implements MessageExchange {
@Override
public String getId() {
return "proxyReadConnection";
}
@Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
@NonNull DataSource<?> source;
}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
}
}

View file

@ -36,6 +36,7 @@ module io.xpipe.beacon {
ListCollectionsExchange,
ListEntriesExchange,
ModeExchange,
NamedFunctionExchange,
StatusExchange,
StopExchange,
RenameStoreExchange,
@ -43,6 +44,7 @@ module io.xpipe.beacon {
StoreAddExchange,
ReadDrainExchange,
WritePreparationExchange,
ProxyReadConnectionExchange,
WriteExecuteExchange,
SelectExchange,
ReadExchange,

View file

@ -0,0 +1,39 @@
package io.xpipe.core.impl;
import java.util.Arrays;
import java.util.List;
public class FileNames {
public static String getFileName(String file) {
var split = file.split("[\\\\/]");
if (split.length == 0) {
return "";
}
return split[split.length - 1];
}
public static String join(String... parts) {
var joined = String.join("/", parts);
return normalize(joined);
}
public static String normalize(String file) {
var backslash = file.contains("\\");
return backslash ? toWindows(file) : toUnix(file);
}
private static List<String> split(String file) {
var split = file.split("[\\\\/]");
return Arrays.stream(split).filter(s -> !s.isEmpty()).toList();
}
public static String toUnix(String file) {
var joined = String.join("/", split(file));
return file.startsWith("/") ? "/" + joined : joined;
}
public static String toWindows(String file) {
return String.join("\\", split(file));
}
}

View file

@ -0,0 +1,28 @@
package io.xpipe.core.impl;
import io.xpipe.core.store.StreamDataStore;
import java.io.InputStream;
/**
* A data store that is only represented by an InputStream.
* This can be useful for development.
*/
public class InputStreamStore implements StreamDataStore {
private final InputStream in;
public InputStreamStore(InputStream in) {
this.in = in;
}
@Override
public InputStream openInput() throws Exception {
return in;
}
@Override
public boolean canOpen() {
return true;
}
}

View file

@ -13,6 +13,8 @@ public interface OsType {
String getName();
String getTempDirectory(ShellProcessControl pc) throws Exception;
String normalizeFileName(String file);
Map<String, String> getProperties(ShellProcessControl pc) throws Exception;
@ -43,6 +45,11 @@ public interface OsType {
return "Windows";
}
@Override
public String getTempDirectory(ShellProcessControl pc) throws Exception {
return pc.executeSimpleCommand(pc.getShellType().getPrintVariableCommand("TEMP"));
}
@Override
public String normalizeFileName(String file) {
return String.join("\\", file.split("[\\\\/]+"));
@ -81,6 +88,11 @@ public interface OsType {
static class Linux implements OsType {
@Override
public String getTempDirectory(ShellProcessControl pc) throws Exception {
return "/tmp/";
}
@Override
public String normalizeFileName(String file) {
return String.join("/", file.split("[\\\\/]+"));
@ -146,6 +158,11 @@ public interface OsType {
static class Mac implements OsType {
@Override
public String getTempDirectory(ShellProcessControl pc) throws Exception {
return pc.executeSimpleCommand(pc.getShellType().getPrintVariableCommand("TEMP"));
}
@Override
public String normalizeFileName(String file) {
return String.join("/", file.split("[\\\\/]+"));

View file

@ -21,6 +21,13 @@ public interface ShellProcessControl extends ProcessControl {
return executeSimpleCommand(type.switchTo(command));
}
default void restart() throws Exception {
exitAndWait();
start();
}
boolean isLocal();
int getProcessId();
OsType getOsType();
@ -34,7 +41,7 @@ public interface ShellProcessControl extends ProcessControl {
SecretValue getElevationPassword();
default ShellProcessControl shell(@NonNull ShellType type) {
return shell(type.openCommand());
return shell(type.openCommand()).elevation(getElevationPassword());
}
default CommandProcessControl command(@NonNull ShellType type, String command) {

View file

@ -13,6 +13,8 @@ public interface ShellType {
return String.join(getConcatenationOperator(), s);
}
String escape(String input);
void elevate(ShellProcessControl control, String command, String displayCommand) throws Exception;
default String getExitCommand() {
@ -35,6 +37,8 @@ public interface ShellType {
String getSetVariableCommand(String variableName, String value);
String getPrintVariableCommand(String name);
List<String> openCommand();
String switchTo(String cmd);

View file

@ -0,0 +1,69 @@
package io.xpipe.core.util;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.process.CommandProcessControl;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellProcessControl;
import java.util.Optional;
public class XPipeInstallation {
public static Optional<String> queryInstallationVersion(ShellProcessControl p) throws Exception {
var executable = getInstallationExecutable(p);
if (executable.isEmpty()) {
return Optional.empty();
}
try (CommandProcessControl c = p.command(executable.get() + " version").start()) {
return Optional.ofNullable(c.readOrThrow());
}
}
public static boolean containsCompatibleInstallation(ShellProcessControl p, String version) throws Exception {
var executable = getInstallationExecutable(p);
if (executable.isEmpty()) {
return false;
}
try (CommandProcessControl c = p.command(executable.get() + " version").start()) {
return c.readOrThrow().equals(version);
}
}
public static Optional<String> getInstallationExecutable(ShellProcessControl p) throws Exception {
var installation = getDefaultInstallationBasePath(p);
var executable = FileNames.join(installation, getDaemonExecutableInInstallationDirectory(p.getOsType()));
var file = FileNames.join(installation, executable);
try (CommandProcessControl c =
p.command(p.getShellType().createFileExistsCommand(file)).start()) {
return c.startAndCheckExit() ? Optional.of(file) : Optional.empty();
}
}
public static String getDataBasePath(ShellProcessControl p) throws Exception {
if (p.getOsType().equals(OsType.WINDOWS)) {
var base = p.executeSimpleCommand(p.getShellType().getPrintVariableCommand("userprofile"));
return FileNames.join(base, "X-Pipe");
} else {
return FileNames.join("~", "xpipe");
}
}
public static String getDefaultInstallationBasePath(ShellProcessControl p) throws Exception {
if (p.getOsType().equals(OsType.WINDOWS)) {
var base = p.executeSimpleCommand(p.getShellType().getPrintVariableCommand("LOCALAPPDATA"));
return FileNames.join(base, "X-Pipe");
} else {
return "/opt/xpipe";
}
}
public static String getDaemonExecutableInInstallationDirectory(OsType type) {
if (type.equals(OsType.WINDOWS)) {
return FileNames.join("app", "runtime", "bin", "xpiped.bat");
} else {
return FileNames.join("app/bin/xpiped");
}
}
}

View file

@ -0,0 +1,79 @@
package io.xpipe.extension;
import io.xpipe.api.connector.XPipeConnection;
import io.xpipe.extension.event.ErrorEvent;
import lombok.Getter;
import lombok.SneakyThrows;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ServiceLoader;
@Getter
public class NamedFunction {
public static final List<NamedFunction> ALL = new ArrayList<>();
public static void init(ModuleLayer layer) {
if (ALL.size() == 0) {
ALL.addAll(ServiceLoader.load(layer, NamedFunction.class).stream()
.map(p -> p.get())
.toList());
}
}
public static NamedFunction get(String id) {
return ALL.stream()
.filter(namedFunction -> namedFunction.id.equalsIgnoreCase(id))
.findFirst()
.orElseThrow();
}
public static <T> T callLocal(String id, Object... args) {
return get(id).callLocal(args);
}
public static <T> T callRemote(String id, Object... args) {
XPipeConnection.execute(con -> {
con.sendRequest(null);
});
return get(id).callLocal(args);
}
@SneakyThrows
public static <T> T call(Class<? extends NamedFunction> clazz, Object... args) {
var base = args[0];
if (base instanceof Proxyable) {
return callRemote(clazz.getDeclaredConstructor().newInstance().getId(), args);
} else {
return callLocal(clazz.getDeclaredConstructor().newInstance().getId(), args);
}
}
private final String id;
private final Method method;
public NamedFunction(String id, Method method) {
this.id = id;
this.method = method;
}
public NamedFunction(String id, Class<?> clazz) {
this.id = id;
this.method = Arrays.stream(clazz.getDeclaredMethods())
.filter(method1 -> method1.getName().equals(id))
.findFirst()
.orElseThrow();
}
public <T> T callLocal(Object... args) {
try {
return (T) method.invoke(null, args);
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).handle();
return null;
}
}
}

View file

@ -0,0 +1,8 @@
package io.xpipe.extension;
import io.xpipe.core.store.ShellStore;
public interface Proxyable {
ShellStore getProxy();
}

View file

@ -0,0 +1,85 @@
package io.xpipe.extension;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import io.xpipe.api.connector.XPipeConnection;
import io.xpipe.beacon.exchange.ProxyReadConnectionExchange;
import io.xpipe.core.impl.InputStreamStore;
import io.xpipe.core.process.ShellProcessControl;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.source.DataSourceReadConnection;
import io.xpipe.core.store.ShellStore;
import io.xpipe.core.util.JacksonMapper;
import io.xpipe.core.util.XPipeInstallation;
import io.xpipe.extension.util.XPipeDaemon;
import lombok.SneakyThrows;
import java.io.IOException;
import java.util.Optional;
import java.util.function.Function;
public class XPipeProxy {
@SneakyThrows
private static DataSource<?> downstreamTransform(DataSource<?> input, ShellStore proxy) {
var proxyNode = JacksonMapper.newMapper().valueToTree(proxy);
var inputNode = JacksonMapper.newMapper().valueToTree(input);
var localNode = JacksonMapper.newMapper().valueToTree(ShellStore.local());
replace(inputNode, node -> node.equals(proxyNode) ? Optional.of(localNode) : Optional.empty());
return JacksonMapper.newMapper().treeToValue(inputNode, DataSource.class);
}
private static JsonNode replace(
JsonNode node, Function<JsonNode, Optional<JsonNode>> function) {
var value = function.apply(node);
if (value.isPresent()) {
return value.get();
}
if (!node.isObject()) {
return node;
}
var replacement = JsonNodeFactory.instance.objectNode();
var iterator = node.fields();
while (iterator.hasNext()) {
var stringJsonNodeEntry = iterator.next();
var resolved = function.apply(stringJsonNodeEntry.getValue()).orElse(stringJsonNodeEntry.getValue());
replacement.set(stringJsonNodeEntry.getKey(), resolved);
}
return replacement;
}
public static <T extends DataSourceReadConnection> T remoteReadConnection(DataSource<?> source, ShellStore proxy) {
var downstream = downstreamTransform(source, proxy);
return (T) XPipeConnection.execute(con -> {
con.sendRequest(ProxyReadConnectionExchange.Request.builder().source(downstream).build());
con.receiveResponse();
var inputSource = DataSource.createInternalDataSource(
source.determineInfo().getType(), new InputStreamStore(con.receiveBody()));
return inputSource.openReadConnection();
});
}
public static ShellStore getProxy(Object base) {
return base instanceof Proxyable p ? p.getProxy() : null;
}
public static boolean isRemote(Object base) {
return base instanceof Proxyable p && !ShellStore.isLocal(p.getProxy());
}
public static void checkSupport(ShellStore store) throws Exception {
var version = XPipeDaemon.getInstance().getVersion();
try (ShellProcessControl s = store.create().start()) {
var installationVersion = XPipeInstallation.queryInstallationVersion(s);
if (installationVersion.isEmpty()) {
throw new IOException(I18n.get("noInstallationFound"));
}
if (!version.equals(installationVersion.get())) {
throw new IOException(I18n.get("versionMismatch", version, installationVersion));
}
}
}
}

View file

@ -0,0 +1,35 @@
package io.xpipe.extension.comp;
import io.xpipe.core.store.ShellStore;
import io.xpipe.extension.XPipeProxy;
import io.xpipe.extension.util.SimpleValidator;
import io.xpipe.extension.util.Validatable;
import io.xpipe.extension.util.Validator;
import io.xpipe.fxcomps.SimpleComp;
import javafx.beans.property.Property;
import javafx.scene.layout.Region;
import net.synedra.validatorfx.Check;
public class ProxyChoiceComp extends SimpleComp implements Validatable {
private final Property<ShellStore> selected;
private final Validator validator = new SimpleValidator();
private final Check check;
public ProxyChoiceComp(Property<ShellStore> selected) {
this.selected = selected;
check = Validator.exceptionWrapper(validator, selected, () -> XPipeProxy.checkSupport(selected.getValue()));
}
@Override
protected Region createSimple() {
var choice = new ShellStoreChoiceComp<>(selected, ShellStore.class, shellStore -> true, shellStore -> true);
choice.apply(struc -> check.decorates(struc.get()));
return choice.createRegion();
}
@Override
public Validator getValidator() {
return validator;
}
}

View file

@ -0,0 +1,91 @@
package io.xpipe.extension.comp;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.store.ShellStore;
import io.xpipe.extension.DataStoreProviders;
import io.xpipe.extension.I18n;
import io.xpipe.extension.event.ErrorEvent;
import io.xpipe.extension.util.CustomComboBoxBuilder;
import io.xpipe.extension.util.XPipeDaemon;
import io.xpipe.fxcomps.SimpleComp;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import lombok.AllArgsConstructor;
import java.util.function.Predicate;
import java.util.stream.Stream;
/*
TODO: Integrate store validation more into this comp.
*/
@AllArgsConstructor
public class ShellStoreChoiceComp<T extends ShellStore> extends SimpleComp {
private final Property<T> selected;
private final Class<T> storeClass;
private final Predicate<T> applicableCheck;
private final Predicate<T> supportCheck;
private Region createGraphic(T s) {
var provider = DataStoreProviders.byStore(s);
var imgView =
new PrettyImageComp(new SimpleStringProperty(provider.getDisplayIconFileName()), 16, 16).createRegion();
var name = XPipeDaemon.getInstance().getNamedStores().stream()
.filter(e -> e.equals(s))
.findAny()
.flatMap(store -> XPipeDaemon.getInstance().getStoreName(store))
.orElse(I18n.get("localMachine"));
return new Label(name, imgView);
}
@Override
@SuppressWarnings("unchecked")
protected Region createSimple() {
var comboBox = new CustomComboBoxBuilder<T>(selected, this::createGraphic, null, n -> {
if (n != null) {
try {
n.checkComplete();
// n.test();
} catch (Exception ex) {
var name = XPipeDaemon.getInstance().getNamedStores().stream()
.filter(e -> e.equals(n))
.findAny()
.flatMap(store -> XPipeDaemon.getInstance().getStoreName(store))
.orElse(I18n.get("localMachine"));
ErrorEvent.fromMessage(I18n.get("extension.namedHostNotActive", name))
.reportable(false)
.handle();
return false;
}
}
// if (n != null && !supportCheck.test(n)) {
// var name = XPipeDaemon.getInstance().getNamedStores().stream()
// .filter(e -> e.equals(n)).findAny()
// .flatMap(store ->
// XPipeDaemon.getInstance().getStoreName(store)).orElse(I18n.get("localMachine"));
// ErrorEvent.fromMessage(I18n.get("extension.namedHostFeatureUnsupported",
// name)).reportable(false).handle();
// return false;
// }
return true;
});
var available = Stream.concat(
Stream.of(new LocalStore()),
XPipeDaemon.getInstance().getNamedStores().stream()
.filter(s -> storeClass.isAssignableFrom(s.getClass()) && applicableCheck.test((T) s))
.map(s -> (ShellStore) s))
.toList();
available.forEach(s -> comboBox.add((T) s));
ComboBox<Node> cb = comboBox.build();
cb.getStyleClass().add("choice-comp");
return cb;
}
}

View file

@ -5,7 +5,6 @@ import io.xpipe.extension.I18n;
import io.xpipe.extension.util.SimpleValidator;
import io.xpipe.extension.util.Validatable;
import io.xpipe.extension.util.Validator;
import io.xpipe.extension.util.Validators;
import io.xpipe.fxcomps.SimpleComp;
import io.xpipe.fxcomps.util.PlatformThread;
import javafx.beans.property.Property;
@ -36,7 +35,7 @@ public class WriteModeChoiceComp extends SimpleComp implements Validatable {
if (available.size() == 1) {
selected.setValue(available.get(0));
}
check = Validators.nonNull(validator, I18n.observable("mode"), selected);
check = Validator.nonNull(validator, I18n.observable("mode"), selected);
}
@Override

View file

@ -60,7 +60,7 @@ public class DynamicOptionsBuilder {
public DynamicOptionsBuilder nonNull(Validator v) {
var e = entries.get(entries.size() - 1);
var p = props.get(props.size() - 1);
return decorate(Validators.nonNull(v, e.name(), p));
return decorate(Validator.nonNull(v, e.name(), p));
}
public DynamicOptionsBuilder addNewLine(Property<NewLine> prop) {

View file

@ -1,13 +1,36 @@
package io.xpipe.extension.util;
import io.xpipe.extension.I18n;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.value.ObservableValue;
import net.synedra.validatorfx.Check;
import net.synedra.validatorfx.ValidationResult;
import org.apache.commons.lang3.function.FailableRunnable;
public interface Validator {
static Check nonNull(Validator v, ObservableValue<String> name, ObservableValue<?> s) {
return v.createCheck().dependsOn("val", s).withMethod(c -> {
if (c.get("val") == null) {
c.error(I18n.get("extension.mustNotBeEmpty", name.getValue()));
}
});
}
static Check exceptionWrapper(Validator v, ObservableValue<?> s, FailableRunnable<Exception> ex) {
return v.createCheck().dependsOn("val", s).withMethod(c -> {
if (c.get("val") == null) {
try {
ex.run();
} catch (Exception e) {
c.error(e.getMessage());
}
}
});
}
public Check createCheck();
/** Add another check to the checker. Changes in the check's validationResultProperty will be reflected in the checker.

View file

@ -4,21 +4,11 @@ import io.xpipe.core.store.DataStore;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.store.ShellStore;
import io.xpipe.extension.I18n;
import javafx.beans.value.ObservableValue;
import net.synedra.validatorfx.Check;
import java.util.function.Predicate;
public class Validators {
public static Check nonNull(Validator v, ObservableValue<String> name, ObservableValue<?> s) {
return v.createCheck().dependsOn("val", s).withMethod(c -> {
if (c.get("val") == null) {
c.error(I18n.get("extension.mustNotBeEmpty", name.getValue()));
}
});
}
public static void nonNull(Object object, String name) {
if (object == null) {
throw new IllegalArgumentException(I18n.get("extension.null", name));

View file

@ -27,6 +27,8 @@ public interface XPipeDaemon {
void withResource(String module, String file, Charsetter.FailableConsumer<Path, IOException> con);
List<DataStore> getNamedStores();
String getVersion();
Image image(String file);
String svgImage(String file);

View file

@ -40,4 +40,5 @@ open module io.xpipe.extension {
uses XPipeDaemon;
uses io.xpipe.extension.Cache;
uses io.xpipe.extension.DataSourceActionProvider;
uses io.xpipe.extension.NamedFunction;
}