This commit is contained in:
Christopher Schnick 2022-11-27 14:59:36 +01:00
parent 696dc036ac
commit de70b0d5b0
26 changed files with 344 additions and 327 deletions

View file

@ -1,6 +1,6 @@
package io.xpipe.api;
import io.xpipe.api.connector.XPipeConnection;
import io.xpipe.api.connector.XPipeApiConnection;
import io.xpipe.api.util.QuietDialogHandler;
import io.xpipe.beacon.exchange.cli.StoreAddExchange;
import io.xpipe.core.store.DataStore;
@ -10,7 +10,7 @@ import java.util.Map;
public class DataStores {
public static void addNamedStore(DataStore store, String name) {
XPipeConnection.execute(con -> {
XPipeApiConnection.execute(con -> {
var req = StoreAddExchange.Request.builder()
.storeInput(store)
.name(name)

View file

@ -1,23 +1,27 @@
package io.xpipe.api.connector;
import io.xpipe.beacon.*;
import io.xpipe.beacon.BeaconClient;
import io.xpipe.beacon.BeaconConnection;
import io.xpipe.beacon.BeaconException;
import io.xpipe.beacon.BeaconServer;
import io.xpipe.beacon.exchange.cli.DialogExchange;
import io.xpipe.core.dialog.DialogReference;
import io.xpipe.core.util.XPipeInstallation;
import java.util.Optional;
public final class XPipeConnection extends BeaconConnection {
public final class XPipeApiConnection extends BeaconConnection {
private XPipeConnection() {}
private XPipeApiConnection() {}
public static XPipeConnection open() {
var con = new XPipeConnection();
public static XPipeApiConnection open() {
var con = new XPipeApiConnection();
con.constructSocket();
return con;
}
public static void finishDialog(DialogReference reference) {
try (var con = new XPipeConnection()) {
try (var con = new XPipeApiConnection()) {
con.constructSocket();
var element = reference.getStart();
while (true) {
@ -41,7 +45,7 @@ public final class XPipeConnection extends BeaconConnection {
}
public static void execute(Handler handler) {
try (var con = new XPipeConnection()) {
try (var con = new XPipeApiConnection()) {
con.constructSocket();
handler.handle(con);
} catch (BeaconException e) {
@ -52,7 +56,7 @@ public final class XPipeConnection extends BeaconConnection {
}
public static <T> T execute(Mapper<T> mapper) {
try (var con = new XPipeConnection()) {
try (var con = new XPipeApiConnection()) {
con.constructSocket();
return mapper.handle(con);
} catch (BeaconException e) {
@ -73,7 +77,10 @@ public final class XPipeConnection extends BeaconConnection {
} catch (InterruptedException ignored) {
}
var s = BeaconClient.tryConnect(BeaconClient.ApiClientInformation.builder().version("?").language("Java").build());
var s = BeaconClient.tryConnect(BeaconClient.ApiClientInformation.builder()
.version("?")
.language("Java")
.build());
if (s.isPresent()) {
return s;
}
@ -114,17 +121,18 @@ public final class XPipeConnection extends BeaconConnection {
}
try {
beaconClient = BeaconClient.connect(BeaconClient.ApiClientInformation.builder().version("?").language("Java").build());
beaconClient = BeaconClient.connect(BeaconClient.ApiClientInformation.builder()
.version("?")
.language("Java")
.build());
} catch (Exception ex) {
throw new BeaconException("Unable to connect to running xpipe daemon", ex);
}
}
private void start() throws Exception {
if (BeaconServer.tryStart() == null) {
throw new UnsupportedOperationException("Unable to determine xpipe daemon launch command");
}
;
var installation = XPipeInstallation.getDefaultInstallationBasePath();
BeaconServer.start(installation);
}
@FunctionalInterface

View file

@ -2,7 +2,7 @@ package io.xpipe.api.impl;
import io.xpipe.api.DataSource;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.api.connector.XPipeConnection;
import io.xpipe.api.connector.XPipeApiConnection;
import io.xpipe.beacon.exchange.*;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceReference;
@ -25,7 +25,7 @@ public abstract class DataSourceImpl implements DataSource {
}
public static DataSource get(DataSourceReference ds) {
return XPipeConnection.execute(con -> {
return XPipeApiConnection.execute(con -> {
var req = QueryDataSourceExchange.Request.builder().ref(ds).build();
QueryDataSourceExchange.Response res = con.performSimpleExchange(req);
var config = new DataSourceConfig(res.getProvider(), res.getConfig());
@ -53,7 +53,7 @@ public abstract class DataSourceImpl implements DataSource {
public static DataSource create(DataSourceId id, io.xpipe.core.source.DataSource<?> source) {
var startReq =
AddSourceExchange.Request.builder().source(source).target(id).build();
var returnedId = XPipeConnection.execute(con -> {
var returnedId = XPipeApiConnection.execute(con -> {
AddSourceExchange.Response r = con.performSimpleExchange(startReq);
return r.getId();
});
@ -64,7 +64,7 @@ public abstract class DataSourceImpl implements DataSource {
public static DataSource create(DataSourceId id, String type, DataStore store) {
if (store instanceof StreamDataStore s && s.isContentExclusivelyAccessible()) {
var res = XPipeConnection.execute(con -> {
var res = XPipeApiConnection.execute(con -> {
var req = StoreStreamExchange.Request.builder().build();
StoreStreamExchange.Response r = con.performOutputExchange(req, out -> {
try (InputStream inputStream = s.openInput()) {
@ -83,20 +83,20 @@ public abstract class DataSourceImpl implements DataSource {
.target(id)
.configureAll(false)
.build();
var startRes = XPipeConnection.execute(con -> {
var startRes = XPipeApiConnection.execute(con -> {
ReadExchange.Response r = con.performSimpleExchange(startReq);
return r;
});
var configInstance = startRes.getConfig();
XPipeConnection.finishDialog(configInstance);
XPipeApiConnection.finishDialog(configInstance);
var ref = id != null ? DataSourceReference.id(id) : DataSourceReference.latest();
return get(ref);
}
public static DataSource create(DataSourceId id, String type, InputStream in) {
var res = XPipeConnection.execute(con -> {
var res = XPipeApiConnection.execute(con -> {
var req = StoreStreamExchange.Request.builder().build();
StoreStreamExchange.Response r = con.performOutputExchange(req, out -> in.transferTo(out));
return r;
@ -110,13 +110,13 @@ public abstract class DataSourceImpl implements DataSource {
.target(id)
.configureAll(false)
.build();
var startRes = XPipeConnection.execute(con -> {
var startRes = XPipeApiConnection.execute(con -> {
ReadExchange.Response r = con.performSimpleExchange(startReq);
return r;
});
var configInstance = startRes.getConfig();
XPipeConnection.finishDialog(configInstance);
XPipeApiConnection.finishDialog(configInstance);
var ref = id != null ? DataSourceReference.id(id) : DataSourceReference.latest();
return get(ref);
@ -124,7 +124,7 @@ public abstract class DataSourceImpl implements DataSource {
@Override
public void forwardTo(DataSource target) {
XPipeConnection.execute(con -> {
XPipeApiConnection.execute(con -> {
var req = ForwardExchange.Request.builder()
.source(DataSourceReference.id(sourceId))
.target(DataSourceReference.id(target.getId()))
@ -135,7 +135,7 @@ public abstract class DataSourceImpl implements DataSource {
@Override
public void appendTo(DataSource target) {
XPipeConnection.execute(con -> {
XPipeApiConnection.execute(con -> {
var req = ForwardExchange.Request.builder()
.source(DataSourceReference.id(sourceId))
.target(DataSourceReference.id(target.getId()))

View file

@ -3,7 +3,7 @@ package io.xpipe.api.impl;
import io.xpipe.api.DataSource;
import io.xpipe.api.DataTable;
import io.xpipe.api.DataTableAccumulator;
import io.xpipe.api.connector.XPipeConnection;
import io.xpipe.api.connector.XPipeApiConnection;
import io.xpipe.api.util.TypeDescriptor;
import io.xpipe.beacon.BeaconException;
import io.xpipe.beacon.exchange.ReadExchange;
@ -22,7 +22,7 @@ import java.nio.charset.StandardCharsets;
public class DataTableAccumulatorImpl implements DataTableAccumulator {
private final XPipeConnection connection;
private final XPipeApiConnection connection;
private final TupleType type;
private int rows;
private TupleType writtenDescriptor;
@ -30,7 +30,7 @@ public class DataTableAccumulatorImpl implements DataTableAccumulator {
public DataTableAccumulatorImpl(TupleType type) {
this.type = type;
connection = XPipeConnection.open();
connection = XPipeApiConnection.open();
connection.sendRequest(StoreStreamExchange.Request.builder().build());
bodyOutput = connection.sendBody();
}
@ -52,12 +52,12 @@ public class DataTableAccumulatorImpl implements DataTableAccumulator {
.provider("xpbt")
.configureAll(false)
.build();
ReadExchange.Response response = XPipeConnection.execute(con -> {
ReadExchange.Response response = XPipeApiConnection.execute(con -> {
return con.performSimpleExchange(req);
});
var configInstance = response.getConfig();
XPipeConnection.finishDialog(configInstance);
XPipeApiConnection.finishDialog(configInstance);
return DataSource.get(DataSourceReference.id(id)).asTable();
}

View file

@ -2,7 +2,7 @@ package io.xpipe.api.impl;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.api.DataTable;
import io.xpipe.api.connector.XPipeConnection;
import io.xpipe.api.connector.XPipeApiConnection;
import io.xpipe.beacon.BeaconConnection;
import io.xpipe.beacon.BeaconException;
import io.xpipe.beacon.exchange.api.QueryTableDataExchange;
@ -55,7 +55,7 @@ public class DataTableImpl extends DataSourceImpl implements DataTable {
@Override
public ArrayNode read(int maxRows) {
List<DataStructureNode> nodes = new ArrayList<>();
XPipeConnection.execute(con -> {
XPipeApiConnection.execute(con -> {
var req = QueryTableDataExchange.Request.builder()
.ref(DataSourceReference.id(getId()))
.maxRows(maxRows)
@ -83,7 +83,7 @@ public class DataTableImpl extends DataSourceImpl implements DataTable {
private TupleNode node;
{
connection = XPipeConnection.open();
connection = XPipeApiConnection.open();
var req = QueryTableDataExchange.Request.builder()
.ref(DataSourceReference.id(getId()))
.maxRows(Integer.MAX_VALUE)

View file

@ -2,7 +2,7 @@ package io.xpipe.api.impl;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.api.DataText;
import io.xpipe.api.connector.XPipeConnection;
import io.xpipe.api.connector.XPipeApiConnection;
import io.xpipe.beacon.BeaconConnection;
import io.xpipe.beacon.BeaconException;
import io.xpipe.beacon.exchange.api.QueryTextDataExchange;
@ -62,7 +62,7 @@ public class DataTextImpl extends DataSourceImpl implements DataText {
private String nextValue;
{
connection = XPipeConnection.open();
connection = XPipeApiConnection.open();
var req = QueryTextDataExchange.Request.builder()
.ref(DataSourceReference.id(getId()))
.maxLines(-1)

View file

@ -1,46 +0,0 @@
package io.xpipe.api.util;
import io.xpipe.api.connector.XPipeConnection;
import io.xpipe.beacon.BeaconClient;
import io.xpipe.beacon.BeaconServer;
public class XPipeDaemonController {
private static boolean alreadyStarted;
public static void start() throws Exception {
if (BeaconServer.isRunning()) {
alreadyStarted = true;
return;
}
Process process = null;
if ((process = BeaconServer.tryStartCustom()) != null) {
} else {
if ((process = BeaconServer.tryStart()) == null) {
throw new AssertionError();
}
}
XPipeConnection.waitForStartup(process).orElseThrow();
if (!BeaconServer.isRunning()) {
throw new AssertionError();
}
}
public static void stop() throws Exception {
if (alreadyStarted) {
return;
}
if (!BeaconServer.isRunning()) {
return;
}
var client = BeaconClient.connect(BeaconClient.ApiClientInformation.builder().version("?").language("Java API Test").build());
if (!BeaconServer.tryStop(client)) {
throw new AssertionError();
}
XPipeConnection.waitForShutdown();
}
}

View file

@ -1,6 +1,6 @@
package io.xpipe.api.test;
import io.xpipe.api.util.XPipeDaemonController;
import io.xpipe.beacon.BeaconDaemonController;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
@ -8,11 +8,11 @@ public class ApiTest {
@BeforeAll
public static void setup() throws Exception {
XPipeDaemonController.start();
BeaconDaemonController.start();
}
@AfterAll
public static void teardown() throws Exception {
XPipeDaemonController.stop();
BeaconDaemonController.stop();
}
}

View file

@ -12,6 +12,7 @@ import io.xpipe.beacon.exchange.MessageExchanges;
import io.xpipe.beacon.exchange.data.ClientErrorMessage;
import io.xpipe.beacon.exchange.data.ServerErrorMessage;
import io.xpipe.core.process.CommandProcessControl;
import io.xpipe.core.store.ShellStore;
import io.xpipe.core.util.JacksonMapper;
import lombok.Builder;
import lombok.EqualsAndHashCode;
@ -47,12 +48,11 @@ public class BeaconClient implements AutoCloseable {
@EqualsAndHashCode(callSuper = false)
public static class CliClientInformation extends ClientInformation {
String version;
int consoleWidth;
@Override
public String toDisplayString() {
return "X-Pipe CLI " + version;
return "X-Pipe CLI";
}
}
@ -104,7 +104,16 @@ public class BeaconClient implements AutoCloseable {
return client;
}
public static BeaconClient connectGateway(CommandProcessControl control, GatewayClientInformation information) throws Exception {
public static BeaconClient connectProxy(ShellStore proxy) throws Exception {
var control = proxy.create().start();
var command = control.command("xpipe beacon").start();
return BeaconClient.connectGateway(
command,
BeaconClient.GatewayClientInformation.builder()
.build());
}
private static BeaconClient connectGateway(CommandProcessControl control, GatewayClientInformation information) throws Exception {
var client = new BeaconClient(() -> {}, control.getStdout(), control.getStdin());
client.sendObject(JacksonMapper.newMapper().valueToTree(information));
return client;

View file

@ -0,0 +1,88 @@
package io.xpipe.beacon;
import io.xpipe.core.util.XPipeInstallation;
import java.io.IOException;
public class BeaconDaemonController {
private static boolean alreadyStarted;
public static void start() throws Exception {
if (BeaconServer.isRunning()) {
alreadyStarted = true;
return;
}
var custom = false;
Process process;
if ((process = BeaconServer.tryStartCustom()) != null) {
custom = true;
} else {
var defaultBase = XPipeInstallation.getDefaultInstallationBasePath();
process = BeaconServer.start(defaultBase);
}
waitForStartup(process, custom);
if (!BeaconServer.isRunning()) {
throw new AssertionError();
}
}
public static void stop() throws Exception {
if (alreadyStarted) {
return;
}
if (!BeaconServer.isRunning()) {
return;
}
var client = BeaconClient.connect(BeaconClient.ApiClientInformation.builder().version("?").language("Java API Test").build());
if (!BeaconServer.tryStop(client)) {
throw new AssertionError();
}
waitForShutdown();
}
private static void waitForStartup(Process process, boolean custom) throws IOException {
for (int i = 0; i < 160; i++) {
if (process != null && !custom && !process.isAlive()) {
throw new IOException("Daemon start failed");
}
if (process != null && custom && !process.isAlive() && process.exitValue() != 0) {
throw new IOException("Custom launch command failed");
}
try {
Thread.sleep(500);
} catch (InterruptedException ignored) {
}
var s = BeaconClient.tryConnect(BeaconClient.ApiClientInformation.builder()
.version("?")
.language("Java")
.build());
if (s.isPresent()) {
return;
}
}
throw new IOException("Wait for daemon start up timed out");
}
private static void waitForShutdown() {
for (int i = 0; i < 40; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException ignored) {
}
var r = BeaconServer.isRunning();
if (!r) {
return;
}
}
}
}

View file

@ -1,14 +1,15 @@
package io.xpipe.beacon;
import io.xpipe.beacon.exchange.StopExchange;
import io.xpipe.core.process.OsType;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.process.ShellProcessControl;
import io.xpipe.core.util.XPipeInstallation;
import lombok.experimental.UtilityClass;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
/**
* Contains basic functionality to start, communicate, and stop a remote beacon server.
@ -16,16 +17,8 @@ import java.util.Optional;
@UtilityClass
public class BeaconServer {
public static void main(String[] args) throws Exception {
if (tryStartCustom() == null) {
if (tryStart() == null) {
System.exit(1);
}
}
}
public static boolean isRunning() {
try (var socket = BeaconClient.connect(null)) {
try (var ignored = BeaconClient.connect(null)) {
return true;
} catch (Exception e) {
return false;
@ -44,18 +37,14 @@ public class BeaconServer {
return null;
}
public static Process tryStart() throws Exception {
var daemonExecutable = getDaemonExecutable();
if (daemonExecutable.isPresent()) {
var command = "\"" + daemonExecutable.get() + "\" --external "
+ (BeaconConfig.getDaemonArguments() != null ? BeaconConfig.getDaemonArguments() : "");
// Tell daemon that we launched from an external tool
Process process = Runtime.getRuntime().exec(command);
printDaemonOutput(process, command);
return process;
}
return null;
public static Process start(String installationBase) throws Exception {
var daemonExecutable = getDaemonExecutable(installationBase);
// Tell daemon that we launched from an external tool
var command = "\"" + daemonExecutable + "\" --external "
+ (BeaconConfig.getDaemonArguments() != null ? BeaconConfig.getDaemonArguments() : "");
Process process = Runtime.getRuntime().exec(command);
printDaemonOutput(process, command);
return process;
}
private static void printDaemonOutput(Process proc, String command) {
@ -111,65 +100,21 @@ public class BeaconServer {
return res.isSuccess();
}
private static Optional<Path> getDaemonBasePath(OsType type) {
Path base = null;
// Prepare for invalid XPIPE_HOME path value
try {
var environmentVariable = System.getenv("XPIPE_HOME");
base = environmentVariable != null ? Path.of(environmentVariable) : null;
} catch (Exception ex) {
}
if (base == null) {
if (type.equals(OsType.WINDOWS)) {
base = Path.of(System.getenv("LOCALAPPDATA"), "X-Pipe");
public static Path getDaemonExecutable(String installationBase) throws Exception {
try (ShellProcessControl pc = new LocalStore().create().start()) {
var debug = BeaconConfig.launchDaemonInDebugMode();
if (!debug) {
return Path.of(
FileNames.join(installationBase, XPipeInstallation.getDaemonExecutablePath(pc.getOsType())));
} else {
base = Path.of("/opt/xpipe/");
if (BeaconConfig.attachDebuggerToDaemon()) {
return Path.of(FileNames.join(
installationBase, XPipeInstallation.getDaemonDebugAttachScriptPath(pc.getOsType())));
} else {
return Path.of(FileNames.join(
installationBase, XPipeInstallation.getDaemonDebugScriptPath(pc.getOsType())));
}
}
if (!Files.exists(base)) {
base = null;
}
}
return Optional.ofNullable(base);
}
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()) {
scriptName = "xpiped_debug_attach";
} else {
scriptName = "xpiped_debug";
}
if (System.getProperty("os.name").startsWith("Windows")) {
scriptName = scriptName + ".bat";
} else {
scriptName = scriptName + ".sh";
}
executable = Path.of("app", "scripts", scriptName);
}
Path file = base.resolve(executable);
if (Files.exists(file)) {
return Optional.of(file);
} else {
return Optional.empty();
}
}
}

View file

@ -0,0 +1,37 @@
package io.xpipe.beacon;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.xpipe.beacon.exchange.NamedFunctionExchange;
import io.xpipe.core.util.Proxyable;
import lombok.Getter;
import lombok.SneakyThrows;
import java.util.Arrays;
@Getter
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public abstract class NamedFunction<T> {
@SneakyThrows
public T call() {
var proxyStore = Proxyable.getProxy(getProxyBase());
if (proxyStore != null) {
var client = BeaconClient.connectProxy(proxyStore);
client.sendRequest(
NamedFunctionExchange.Request.builder().function(this).build());
NamedFunctionExchange.Response response = client.receiveResponse();
return (T) response.getReturnValue();
} else {
return callLocal();
}
}
@SneakyThrows
protected Object getProxyBase() {
var first = Arrays.stream(getClass().getDeclaredFields()).findFirst().orElseThrow();
first.setAccessible(true);
return first.get(this);
}
public abstract T callLocal();
}

View file

@ -1,10 +1,9 @@
package io.xpipe.beacon.exchange;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.xpipe.beacon.NamedFunction;
import io.xpipe.beacon.RequestMessage;
import io.xpipe.beacon.ResponseMessage;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
@ -19,11 +18,7 @@ public class NamedFunctionExchange implements MessageExchange {
@Builder
@Value
public static class Request implements RequestMessage {
@NonNull
String id;
@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, property="type")
@NonNull Object[] arguments;
NamedFunction<?> function;
}
@Jacksonized

View file

@ -25,6 +25,7 @@ module io.xpipe.beacon {
requires static lombok;
uses MessageExchange;
uses io.xpipe.beacon.NamedFunction;
provides Module with BeaconJacksonModule;
provides io.xpipe.beacon.exchange.MessageExchange with

View file

@ -18,6 +18,14 @@ public class FileNames {
return normalize(joined);
}
public static String getParent(String file) {
return file.substring(0, file.length() - getFileName(file).length() - 1);
}
public static boolean startsWith(String file, String start) {
return normalize(file).startsWith(normalize(start));
}
public static String normalize(String file) {
var backslash = file.contains("\\");
return backslash ? toWindows(file) : toUnix(file);

View file

@ -17,6 +17,12 @@ public interface ShellProcessControl extends ProcessControl {
}
}
default boolean executeBooleanSimpleCommand(String command) throws Exception {
try (CommandProcessControl c = command(command).start()) {
return c.discardAndCheckExit();
}
}
default String executeSimpleCommand(ShellType type, String command) throws Exception {
return executeSimpleCommand(type.switchTo(command));
}

View file

@ -49,7 +49,7 @@ public interface ShellType {
String createFileWriteCommand(String file);
List<String> createFileExistsCommand(String file);
String createFileExistsCommand(String file);
Charset determineCharset(ShellProcessControl control) throws Exception;

View file

@ -37,7 +37,7 @@ public interface MachineStore extends FileSystemStore, ShellStore {
@Override
public default boolean exists(String file) throws Exception {
try (var pc = create().commandListFunction(proc -> proc.getShellType().createFileExistsCommand(proc.getOsType().normalizeFileName(file)))
try (var pc = create().commandFunction(proc -> proc.getShellType().createFileExistsCommand(proc.getOsType().normalizeFileName(file)))
.start()) {
return pc.discardAndCheckExit();
}

View file

@ -0,0 +1,21 @@
package io.xpipe.core.util;
import io.xpipe.core.store.ShellStore;
public interface Proxyable {
public static ShellStore getProxy(Object base) {
var proxy = base instanceof Proxyable p ? p.getProxy() : null;
return ShellStore.isLocal(proxy) ? null : proxy;
}
public static boolean isRemote(Object base) {
if (base == null) {
throw new IllegalArgumentException("Proxy base is null");
}
return base instanceof Proxyable p && !ShellStore.isLocal(p.getProxy());
}
ShellStore getProxy();
}

View file

@ -1,43 +1,56 @@
package io.xpipe.core.util;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.process.CommandProcessControl;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellProcessControl;
import java.util.Optional;
import java.io.IOException;
public class XPipeInstallation {
public static Optional<String> queryInstallationVersion(ShellProcessControl p) throws Exception {
var executable = getInstallationExecutable(p);
if (executable.isEmpty()) {
return Optional.empty();
public static String getInstallationBasePathForCLI(ShellProcessControl p, String cliExecutable) throws Exception {
var defaultInstallation = getDefaultInstallationBasePath(p);
if (p.getOsType().equals(OsType.LINUX) && cliExecutable.equals("/usr/bin/xpipe")) {
return defaultInstallation;
}
try (CommandProcessControl c = p.command(executable.get() + " version").start()) {
return Optional.ofNullable(c.readOrThrow());
if (FileNames.startsWith(cliExecutable, defaultInstallation)) {
return defaultInstallation;
}
return FileNames.getParent(FileNames.getParent(cliExecutable));
}
public static String queryInstallationVersion(ShellProcessControl p, String exec) throws Exception {
try (CommandProcessControl c = p.command(exec + " version").start()) {
return c.readOrThrow();
}
}
public static boolean containsCompatibleInstallation(ShellProcessControl p, String version) throws Exception {
var executable = getInstallationExecutable(p);
public static boolean containsCompatibleDefaultInstallation(ShellProcessControl p, String version) throws Exception {
var defaultBase = getDefaultInstallationBasePath(p);
var executable = getInstallationExecutable(p, defaultBase);
if (executable.isEmpty()) {
return false;
}
try (CommandProcessControl c = p.command(executable.get() + " version").start()) {
try (CommandProcessControl c = p.command(executable + " version").start()) {
return c.readOrThrow().equals(version);
}
}
public static Optional<String> getInstallationExecutable(ShellProcessControl p) throws Exception {
var installation = getDefaultInstallationBasePath(p);
var executable = getDaemonExecutableInInstallationDirectory(p.getOsType());
public static String getInstallationExecutable(ShellProcessControl p, String installation) throws Exception {
var executable = getDaemonExecutablePath(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();
if (!c.startAndCheckExit()) {
throw new IOException("File not found: " + file);
}
return file;
}
}
@ -50,20 +63,57 @@ public class XPipeInstallation {
}
}
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 getDefaultInstallationBasePath() throws Exception {
try (ShellProcessControl pc = new LocalStore().create().start()) {
return getDefaultInstallationBasePath(pc);
}
}
public static String getDaemonExecutableInInstallationDirectory(OsType type) {
if (type.equals(OsType.WINDOWS)) {
return FileNames.join("app", "runtime", "bin", "xpiped.bat");
public static String getDefaultInstallationBasePath(ShellProcessControl p) throws Exception {
var customHome = p.executeSimpleCommand(p.getShellType().getPrintVariableCommand("XPIPE_HOME"));
if (!customHome.isEmpty()) {
return customHome;
}
String path = null;
if (p.getOsType().equals(OsType.WINDOWS)) {
var base = p.executeSimpleCommand(p.getShellType().getPrintVariableCommand("LOCALAPPDATA"));
path = FileNames.join(base, "X-Pipe");
} else {
return FileNames.join("app/bin/xpiped");
path = "/opt/xpipe";
}
try (CommandProcessControl c =
p.command(p.getShellType().createFileExistsCommand(path)).start()) {
if (!c.discardAndCheckExit()) {
throw new IOException("Installation not found in " + path);
}
}
return path;
}
public static String getDaemonDebugScriptPath(OsType type) {
if (type.equals(OsType.WINDOWS)) {
return FileNames.join("app", "scripts", "xpiped_debug.bat");
} else {
return FileNames.join("app", "scripts", "xpiped_debug.sh");
}
}
public static String getDaemonDebugAttachScriptPath(OsType type) {
if (type.equals(OsType.WINDOWS)) {
return FileNames.join("app", "scripts", "xpiped_debug_attach.bat");
} else {
return FileNames.join("app", "scripts", "xpiped_debug_attach.sh");
}
}
public static String getDaemonExecutablePath(OsType type) {
if (type.equals(OsType.WINDOWS)) {
return FileNames.join("app", "xpiped.exe");
} else {
return FileNames.join("app", "bin", "xpiped");
}
}
}

View file

@ -1,83 +0,0 @@
package io.xpipe.extension;
import io.xpipe.beacon.exchange.NamedFunctionExchange;
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);
}
@SneakyThrows
public static <T> T callRemote(String id, Object... args) {
var proxy = XPipeProxy.getProxy(args[0]);
var client = XPipeProxy.connect(proxy);
client.sendRequest(
NamedFunctionExchange.Request.builder().id(id).arguments(args).build());
NamedFunctionExchange.Response response = client.receiveResponse();
return (T) response.getReturnValue();
}
@SneakyThrows
public static <T> T call(Class<? extends NamedFunction> clazz, Object... args) {
var base = args[0];
var proxy = XPipeProxy.getProxy(base);
if (proxy != null) {
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

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

View file

@ -2,9 +2,9 @@ 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.BeaconClient;
import io.xpipe.api.connector.XPipeApiConnection;
import io.xpipe.beacon.exchange.ProxyReadConnectionExchange;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.impl.InputStreamStore;
import io.xpipe.core.process.ShellProcessControl;
import io.xpipe.core.source.DataSource;
@ -21,18 +21,8 @@ import java.util.function.Function;
public class XPipeProxy {
public static BeaconClient connect(ShellStore proxy) throws Exception {
var control = proxy.create().start();
var command = control.command("xpipe beacon").start();
return BeaconClient.connectGateway(
command,
BeaconClient.GatewayClientInformation.builder()
.version(XPipeDaemon.getInstance().getVersion())
.build());
}
@SneakyThrows
private static DataSource<?> downstreamTransform(DataSource<?> input, ShellStore proxy) {
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());
@ -40,8 +30,7 @@ public class XPipeProxy {
return JacksonMapper.newMapper().treeToValue(inputNode, DataSource.class);
}
private static JsonNode replace(
JsonNode node, Function<JsonNode, Optional<JsonNode>> function) {
private static JsonNode replace(JsonNode node, Function<JsonNode, Optional<JsonNode>> function) {
var value = function.apply(node);
if (value.isPresent()) {
return value.get();
@ -63,8 +52,10 @@ public class XPipeProxy {
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());
return (T) XPipeApiConnection.execute(con -> {
con.sendRequest(ProxyReadConnectionExchange.Request.builder()
.source(downstream)
.build());
con.receiveResponse();
var inputSource = DataSource.createInternalDataSource(
source.determineInfo().getType(), new InputStreamStore(con.receiveBody()));
@ -72,23 +63,18 @@ public class XPipeProxy {
});
}
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()) {
var defaultInstallationExecutable = FileNames.join(
XPipeInstallation.getDefaultInstallationBasePath(s),
XPipeInstallation.getDaemonExecutablePath(s.getOsType()));
if (!s.executeBooleanSimpleCommand(s.getShellType().createFileExistsCommand(defaultInstallationExecutable))) {
throw new IOException(I18n.get("noInstallationFound"));
}
if (!version.equals(installationVersion.get())) {
var installationVersion = XPipeInstallation.queryInstallationVersion(s, defaultInstallationExecutable);
if (!version.equals(installationVersion)) {
throw new IOException(I18n.get("versionMismatch", version, installationVersion));
}
}

View file

@ -35,7 +35,6 @@ public class XPipeServiceProviders {
SupportedApplicationProviders.loadAll(layer);
PrefsProviders.init(layer);
NamedFunction.init(layer);
TrackEvent.info("Finished loading extension providers");
}
}

View file

@ -1,7 +1,7 @@
package io.xpipe.extension.util;
import io.xpipe.api.DataSource;
import io.xpipe.api.util.XPipeDaemonController;
import io.xpipe.beacon.BeaconDaemonController;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.JacksonMapper;
import io.xpipe.extension.XPipeServiceProviders;
@ -26,11 +26,11 @@ public class DaemonExtensionTest extends ExtensionTest {
public static void setup() throws Exception {
JacksonMapper.initModularized(ModuleLayer.boot());
XPipeServiceProviders.load(ModuleLayer.boot());
XPipeDaemonController.start();
BeaconDaemonController.start();
}
@AfterAll
public static void teardown() throws Exception {
XPipeDaemonController.stop();
BeaconDaemonController.stop();
}
}

View file

@ -1,3 +1,4 @@
import io.xpipe.beacon.NamedFunction;
import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.DataStoreActionProvider;
import io.xpipe.extension.SupportedApplicationProvider;
@ -40,5 +41,5 @@ open module io.xpipe.extension {
uses XPipeDaemon;
uses io.xpipe.extension.Cache;
uses io.xpipe.extension.DataSourceActionProvider;
uses io.xpipe.extension.NamedFunction;
uses NamedFunction;
}