mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-25 00:50:31 +00:00
Basic work for proxies
This commit is contained in:
parent
02a2502fa5
commit
097d23f306
22 changed files with 607 additions and 58 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
39
core/src/main/java/io/xpipe/core/impl/FileNames.java
Normal file
39
core/src/main/java/io/xpipe/core/impl/FileNames.java
Normal 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));
|
||||
}
|
||||
}
|
28
core/src/main/java/io/xpipe/core/impl/InputStreamStore.java
Normal file
28
core/src/main/java/io/xpipe/core/impl/InputStreamStore.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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("[\\\\/]+"));
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
69
core/src/main/java/io/xpipe/core/util/XPipeInstallation.java
Normal file
69
core/src/main/java/io/xpipe/core/util/XPipeInstallation.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package io.xpipe.extension;
|
||||
|
||||
import io.xpipe.core.store.ShellStore;
|
||||
|
||||
public interface Proxyable {
|
||||
|
||||
ShellStore getProxy();
|
||||
}
|
85
extension/src/main/java/io/xpipe/extension/XPipeProxy.java
Normal file
85
extension/src/main/java/io/xpipe/extension/XPipeProxy.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue