mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-09-28 19:35:41 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
package io.xpipe.beacon;
|
||||
|
||||
public class ClientException extends Exception {
|
||||
|
||||
public ClientException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package io.xpipe.beacon;
|
||||
|
||||
public class ConnectorException extends Exception {
|
||||
|
||||
public ConnectorException() {
|
||||
}
|
||||
|
||||
public ConnectorException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ConnectorException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public ConnectorException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public ConnectorException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.xpipe.beacon;
|
||||
|
||||
public class ServerException extends Exception {
|
||||
|
||||
public ServerException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package io.xpipe.beacon;
|
||||
|
||||
import io.xpipe.beacon.message.RequestMessage;
|
||||
import io.xpipe.beacon.message.ResponseMessage;
|
||||
import io.xpipe.beacon.socket.SocketClient;
|
||||
import org.apache.commons.lang3.function.FailableBiConsumer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public abstract class XPipeConnector {
|
||||
|
||||
protected abstract void waitForStartup();
|
||||
|
||||
protected SocketClient constructSocket() throws ConnectorException {
|
||||
if (!XPipeDaemon.isDaemonRunning()) {
|
||||
try {
|
||||
XPipeDaemon.startDaemon();
|
||||
waitForStartup();
|
||||
if (!XPipeDaemon.isDaemonRunning()) {
|
||||
throw new ConnectorException("Unable to start xpipe daemon");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new ConnectorException("Unable to start xpipe daemon: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return new SocketClient();
|
||||
} catch (Exception ex) {
|
||||
throw new ConnectorException("Unable to connect to running xpipe daemon: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected <REQ extends RequestMessage, RES extends ResponseMessage> void performExchange(
|
||||
SocketClient socket,
|
||||
REQ req,
|
||||
FailableBiConsumer<RES, InputStream, IOException> responseConsumer,
|
||||
boolean keepOpen) throws ServerException, ConnectorException, ClientException {
|
||||
performExchange(socket, req, null, responseConsumer, keepOpen);
|
||||
}
|
||||
|
||||
protected <REQ extends RequestMessage, RES extends ResponseMessage> void performExchange(
|
||||
SocketClient socket,
|
||||
REQ req,
|
||||
Consumer<OutputStream> output,
|
||||
FailableBiConsumer<RES, InputStream, IOException> responseConsumer,
|
||||
boolean keepOpen) throws ServerException, ConnectorException, ClientException {
|
||||
socket.exchange(req, output, responseConsumer, keepOpen);
|
||||
}
|
||||
|
||||
protected <REQ extends RequestMessage, RES extends ResponseMessage> RES performSimpleExchange(
|
||||
SocketClient socket,
|
||||
REQ req) throws ServerException, ConnectorException, ClientException {
|
||||
return socket.simpleExchange(req);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package io.xpipe.beacon;
|
||||
|
||||
import io.xpipe.app.Main;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.beacon.socket.SocketServer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.ServerSocket;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
public class XPipeDaemon {
|
||||
|
||||
private static final String IN_PROCESS_PROP = "io.xpipe.beacon.startInProcess";
|
||||
|
||||
public static Path getUserDir() {
|
||||
return Path.of(System.getProperty("user.home"), ".xpipe");
|
||||
}
|
||||
|
||||
private static boolean isPortAvailable(int port) {
|
||||
try (var ss = new ServerSocket(port); var ds = new DatagramSocket(port)) {
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isDaemonRunning() {
|
||||
var port = SocketServer.determineUsedPort();
|
||||
return !isPortAvailable(port);
|
||||
}
|
||||
|
||||
public static void startDaemon() throws Exception {
|
||||
if (Optional.ofNullable(System.getProperty("io.xpipe.beacon.startInProcess"))
|
||||
.map(Boolean::parseBoolean).orElse(false)) {
|
||||
startInProcess();
|
||||
return;
|
||||
}
|
||||
|
||||
// if (System.getenv().containsKey(EXEC_PROPERTY)) {
|
||||
// Runtime.getRuntime().exec(System.getenv(EXEC_PROPERTY));
|
||||
// return;
|
||||
// }
|
||||
|
||||
var file = getUserDir().resolve("run");
|
||||
if (Files.exists(file)) {
|
||||
Runtime.getRuntime().exec(Files.readString(file));
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unable to find xpipe daemon installation");
|
||||
}
|
||||
|
||||
private static void startInProcess() {
|
||||
ThreadHelper.create("XPipe daemon", false, () -> Main.main(new String[0])).start();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package io.xpipe.beacon.message;
|
||||
|
||||
import io.xpipe.beacon.ClientException;
|
||||
|
||||
public record ClientErrorMessage(String message) {
|
||||
|
||||
public ClientException throwException() {
|
||||
return new ClientException(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.xpipe.beacon.message;
|
||||
|
||||
import io.xpipe.beacon.socket.SocketServer;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
public interface MessageProvider<RQ extends RequestMessage, RP extends ResponseMessage> {
|
||||
|
||||
String getId();
|
||||
|
||||
Class<RQ> getRequestClass();
|
||||
|
||||
Class<RP> getResponseClass();
|
||||
|
||||
default void handleRequest(SocketServer server, RQ msg, InputStream body, Socket clientSocket) throws Exception {}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package io.xpipe.beacon.message;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MessageProviders {
|
||||
|
||||
private static Set<MessageProvider> ALL;
|
||||
|
||||
private static void loadAll() {
|
||||
if (ALL == null) {
|
||||
ALL = ServiceLoader.load(MessageProvider.class).stream()
|
||||
.map(ServiceLoader.Provider::get).collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
|
||||
public static <RQ extends RequestMessage, RP extends ResponseMessage> Optional<MessageProvider<RQ, RP>> byId(String name) {
|
||||
loadAll();
|
||||
var r = ALL.stream().filter(d -> d.getId().equals(name)).findAny();
|
||||
return Optional.ofNullable(r.orElse(null));
|
||||
}
|
||||
|
||||
|
||||
public static <RQ extends RequestMessage, RP extends ResponseMessage> Optional<MessageProvider<RQ, RP>> byRequest(RQ req) {
|
||||
loadAll();
|
||||
var r = ALL.stream().filter(d -> d.getRequestClass().equals(req.getClass())).findAny();
|
||||
return Optional.ofNullable(r.orElse(null));
|
||||
}
|
||||
|
||||
public static <RQ extends RequestMessage, RP extends ResponseMessage> Optional<MessageProvider<RQ, RP>> byResponse(RP rep) {
|
||||
loadAll();
|
||||
var r = ALL.stream().filter(d -> d.getResponseClass().equals(rep.getClass())).findAny();
|
||||
return Optional.ofNullable(r.orElse(null));
|
||||
}
|
||||
|
||||
public static Set<MessageProvider> getAll() {
|
||||
loadAll();
|
||||
return ALL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package io.xpipe.beacon.message;
|
||||
|
||||
public interface RequestMessage {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package io.xpipe.beacon.message;
|
||||
|
||||
public interface ResponseMessage {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.xpipe.beacon.message;
|
||||
|
||||
import io.xpipe.beacon.ServerException;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record ServerErrorMessage(UUID requestId, Throwable error) {
|
||||
|
||||
public void throwError() throws ServerException {
|
||||
throw new ServerException(error.getMessage(), error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package io.xpipe.beacon.message.impl;
|
||||
|
||||
import io.xpipe.beacon.socket.SocketServer;
|
||||
import io.xpipe.beacon.message.MessageProvider;
|
||||
import io.xpipe.beacon.message.RequestMessage;
|
||||
import io.xpipe.beacon.message.ResponseMessage;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.List;
|
||||
|
||||
public class ListCollectionsExchange implements MessageProvider<ListCollectionsExchange.Request, ListCollectionsExchange.Response> {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "listCollections";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Request> getRequestClass() {
|
||||
return Request.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Response> getResponseClass() {
|
||||
return Response.class;
|
||||
}
|
||||
|
||||
public static record Request() implements RequestMessage {
|
||||
|
||||
}
|
||||
|
||||
public static record Entry(String name, int count) {
|
||||
|
||||
}
|
||||
|
||||
public static record Response(List<Entry> entries) implements ResponseMessage {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package io.xpipe.beacon.message.impl;
|
||||
|
||||
import io.xpipe.beacon.socket.SocketServer;
|
||||
import io.xpipe.beacon.message.MessageProvider;
|
||||
import io.xpipe.beacon.message.RequestMessage;
|
||||
import io.xpipe.beacon.message.ResponseMessage;
|
||||
import io.xpipe.storage.DataSourceStorage;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.List;
|
||||
|
||||
public class ListEntriesExchange implements MessageProvider<ListEntriesExchange.Request, ListEntriesExchange.Response> {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "listEntries";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Request> getRequestClass() {
|
||||
return Request.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Response> getResponseClass() {
|
||||
return Response.class;
|
||||
}
|
||||
|
||||
public static record Request(String collection) implements RequestMessage {
|
||||
|
||||
}
|
||||
|
||||
private static record Entry(String name, String type, String description, String date, String size) {
|
||||
|
||||
}
|
||||
|
||||
public static record Response(List<Entry> entries) implements ResponseMessage {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package io.xpipe.beacon.message.impl;
|
||||
|
||||
import io.xpipe.app.core.OperationMode;
|
||||
import io.xpipe.beacon.socket.SocketServer;
|
||||
import io.xpipe.beacon.message.MessageProvider;
|
||||
import io.xpipe.beacon.message.RequestMessage;
|
||||
import io.xpipe.beacon.message.ResponseMessage;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ModeExchange implements MessageProvider<ModeExchange.Request, ModeExchange.Response> {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "mode";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<ModeExchange.Request> getRequestClass() {
|
||||
return ModeExchange.Request.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<ModeExchange.Response> getResponseClass() {
|
||||
return ModeExchange.Response.class;
|
||||
}
|
||||
|
||||
public static record Request(String modeId) implements RequestMessage {
|
||||
|
||||
}
|
||||
|
||||
public static record Response() implements ResponseMessage {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package io.xpipe.beacon.message.impl;
|
||||
|
||||
import io.xpipe.beacon.socket.SocketServer;
|
||||
import io.xpipe.beacon.message.MessageProvider;
|
||||
import io.xpipe.beacon.message.RequestMessage;
|
||||
import io.xpipe.beacon.message.ResponseMessage;
|
||||
import io.xpipe.core.source.DataSourceId;
|
||||
import io.xpipe.storage.DataSourceStorage;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
public class ReadStructureExchange implements MessageProvider<ReadStructureExchange.Request, ReadStructureExchange.Response> {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "readStructure";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Request> getRequestClass() {
|
||||
return Request.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Response> getResponseClass() {
|
||||
return Response.class;
|
||||
}
|
||||
|
||||
public static record Request(DataSourceId id) implements RequestMessage {
|
||||
|
||||
}
|
||||
|
||||
public static record Response() implements ResponseMessage {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package io.xpipe.beacon.message.impl;
|
||||
|
||||
import io.xpipe.beacon.socket.SocketServer;
|
||||
import io.xpipe.beacon.message.MessageProvider;
|
||||
import io.xpipe.beacon.message.RequestMessage;
|
||||
import io.xpipe.beacon.message.ResponseMessage;
|
||||
import io.xpipe.core.source.DataSourceId;
|
||||
import io.xpipe.storage.DataSourceStorage;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
public class ReadTableDataExchange implements MessageProvider<ReadTableDataExchange.Request, ReadTableDataExchange.Response> {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "readTable";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<ReadTableDataExchange.Request> getRequestClass() {
|
||||
return ReadTableDataExchange.Request.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<ReadTableDataExchange.Response> getResponseClass() {
|
||||
return ReadTableDataExchange.Response.class;
|
||||
}
|
||||
|
||||
public static record Request(DataSourceId sourceId, int maxLines) implements RequestMessage {
|
||||
|
||||
}
|
||||
|
||||
public static record Response() implements ResponseMessage {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package io.xpipe.beacon.message.impl;
|
||||
|
||||
import io.xpipe.beacon.socket.SocketServer;
|
||||
import io.xpipe.beacon.message.MessageProvider;
|
||||
import io.xpipe.beacon.message.RequestMessage;
|
||||
import io.xpipe.beacon.message.ResponseMessage;
|
||||
import io.xpipe.core.data.type.DataType;
|
||||
import io.xpipe.core.source.DataSourceId;
|
||||
import io.xpipe.storage.DataSourceStorage;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
public class ReadTableInfoExchange implements MessageProvider<ReadTableInfoExchange.Request, ReadTableInfoExchange.Response> {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "readTableInfo";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<ReadTableInfoExchange.Request> getRequestClass() {
|
||||
return ReadTableInfoExchange.Request.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<ReadTableInfoExchange.Response> getResponseClass() {
|
||||
return ReadTableInfoExchange.Response.class;
|
||||
}
|
||||
|
||||
public static record Request(DataSourceId sourceId) implements RequestMessage {
|
||||
|
||||
}
|
||||
|
||||
public static record Response(DataSourceId sourceId, DataType dataType, int rowCount) implements ResponseMessage {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package io.xpipe.beacon.message.impl;
|
||||
|
||||
import io.xpipe.app.core.OperationMode;
|
||||
import io.xpipe.beacon.socket.SocketServer;
|
||||
import io.xpipe.beacon.message.MessageProvider;
|
||||
import io.xpipe.beacon.message.RequestMessage;
|
||||
import io.xpipe.beacon.message.ResponseMessage;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
public class StatusExchange implements MessageProvider<StatusExchange.Request, StatusExchange.Response> {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "status";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Request> getRequestClass() {
|
||||
return Request.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Response> getResponseClass() {
|
||||
return Response.class;
|
||||
}
|
||||
|
||||
public static record Request() implements RequestMessage {
|
||||
|
||||
}
|
||||
|
||||
public static record Response(String mode) implements ResponseMessage {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package io.xpipe.beacon.message.impl;
|
||||
|
||||
import io.xpipe.app.core.AppInstallation;
|
||||
import io.xpipe.beacon.socket.SocketServer;
|
||||
import io.xpipe.beacon.message.MessageProvider;
|
||||
import io.xpipe.beacon.message.RequestMessage;
|
||||
import io.xpipe.beacon.message.ResponseMessage;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
public class VersionExchange implements MessageProvider<VersionExchange.Request, VersionExchange.Response> {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "version";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<VersionExchange.Request> getRequestClass() {
|
||||
return VersionExchange.Request.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<VersionExchange.Response> getResponseClass() {
|
||||
return VersionExchange.Response.class;
|
||||
}
|
||||
|
||||
public static record Request() implements RequestMessage {
|
||||
|
||||
}
|
||||
|
||||
public static class Response implements ResponseMessage {
|
||||
|
||||
public final String version;
|
||||
public final String jvmVersion;
|
||||
|
||||
public Response(String version, String jvmVersion) {
|
||||
this.version = version;
|
||||
this.jvmVersion = jvmVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package io.xpipe.beacon.socket;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.databind.node.TextNode;
|
||||
import io.xpipe.beacon.ClientException;
|
||||
import io.xpipe.beacon.ConnectorException;
|
||||
import io.xpipe.beacon.ServerException;
|
||||
import io.xpipe.beacon.message.*;
|
||||
import io.xpipe.core.util.JacksonHelper;
|
||||
import org.apache.commons.lang3.function.FailableBiConsumer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static io.xpipe.beacon.socket.Sockets.BODY_SEPARATOR;
|
||||
|
||||
public class SocketClient {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SocketClient.class);
|
||||
|
||||
private final Socket socket;
|
||||
private final InputStream in;
|
||||
private final OutputStream out;
|
||||
|
||||
public SocketClient() throws IOException {
|
||||
socket = new Socket(InetAddress.getLoopbackAddress(), SocketServer.determineUsedPort());
|
||||
in = socket.getInputStream();
|
||||
out = socket.getOutputStream();
|
||||
}
|
||||
|
||||
public void close() throws ConnectorException {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException ex) {
|
||||
throw new ConnectorException("Couldn't close socket", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public <REQ extends RequestMessage, RES extends ResponseMessage> void exchange(
|
||||
REQ req,
|
||||
Consumer<OutputStream> output,
|
||||
FailableBiConsumer<RES, InputStream, IOException> responseConsumer,
|
||||
boolean keepOpen) throws ConnectorException, ClientException, ServerException {
|
||||
try {
|
||||
sendRequest(req);
|
||||
if (output != null) {
|
||||
out.write(BODY_SEPARATOR);
|
||||
output.accept(out);
|
||||
}
|
||||
|
||||
var res = this.<RES>receiveResponse();
|
||||
var sep = in.readNBytes(BODY_SEPARATOR.length);
|
||||
if (!Arrays.equals(BODY_SEPARATOR, sep)) {
|
||||
throw new ConnectorException("Invalid body separator");
|
||||
}
|
||||
|
||||
responseConsumer.accept(res, in);
|
||||
} catch (IOException ex) {
|
||||
throw new ConnectorException("Couldn't communicate with socket", ex);
|
||||
} finally {
|
||||
if (!keepOpen) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public <REQ extends RequestMessage, RES extends ResponseMessage> RES simpleExchange(REQ req)
|
||||
throws ServerException, ConnectorException, ClientException {
|
||||
try {
|
||||
sendRequest(req);
|
||||
return this.receiveResponse();
|
||||
} finally {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends RequestMessage> void sendRequest(T req) throws ClientException, ConnectorException {
|
||||
ObjectNode json = JacksonHelper.newMapper().valueToTree(req);
|
||||
var prov = MessageProviders.byRequest(req);
|
||||
if (prov.isEmpty()) {
|
||||
throw new ClientException("Unknown request class " + req.getClass());
|
||||
}
|
||||
|
||||
json.set("type", new TextNode(prov.get().getId()));
|
||||
json.set("phase", new TextNode("request"));
|
||||
//json.set("id", new TextNode(UUID.randomUUID().toString()));
|
||||
var msg = JsonNodeFactory.instance.objectNode();
|
||||
msg.set("xPipeMessage", json);
|
||||
|
||||
|
||||
try {
|
||||
var mapper = JacksonHelper.newMapper().disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
|
||||
var gen = mapper.createGenerator(socket.getOutputStream());
|
||||
gen.writeTree(msg);
|
||||
} catch (IOException ex) {
|
||||
throw new ConnectorException("Couldn't write to socket", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends ResponseMessage> T receiveResponse() throws ConnectorException, ClientException, ServerException {
|
||||
JsonNode read;
|
||||
try {
|
||||
var in = socket.getInputStream();
|
||||
read = JacksonHelper.newMapper().disable(JsonParser.Feature.AUTO_CLOSE_SOURCE).readTree(in);
|
||||
} catch (IOException ex) {
|
||||
throw new ConnectorException("Couldn't read from socket", ex);
|
||||
}
|
||||
|
||||
if (Sockets.debugEnabled()) {
|
||||
System.out.println("Recieved response:");
|
||||
System.out.println(read.toPrettyString());
|
||||
}
|
||||
|
||||
var se = parseServerError(read);
|
||||
if (se.isPresent()) {
|
||||
se.get().throwError();
|
||||
}
|
||||
|
||||
var ce = parseClientError(read);
|
||||
if (ce.isPresent()) {
|
||||
throw ce.get().throwException();
|
||||
}
|
||||
|
||||
return parseResponse(read);
|
||||
}
|
||||
|
||||
private Optional<ClientErrorMessage> parseClientError(JsonNode node) throws ConnectorException {
|
||||
ObjectNode content = (ObjectNode) node.get("xPipeClientError");
|
||||
if (content == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
var reader = JacksonHelper.newMapper().readerFor(ClientErrorMessage.class);
|
||||
return Optional.of(reader.readValue(content));
|
||||
} catch (IOException ex) {
|
||||
throw new ConnectorException("Couldn't parse client error message", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<ServerErrorMessage> parseServerError(JsonNode node) throws ConnectorException {
|
||||
ObjectNode content = (ObjectNode) node.get("xPipeServerError");
|
||||
if (content == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
var reader = JacksonHelper.newMapper().readerFor(ServerErrorMessage.class);
|
||||
return Optional.of(reader.readValue(content));
|
||||
} catch (IOException ex) {
|
||||
throw new ConnectorException("Couldn't parse server error message", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends ResponseMessage> T parseResponse(JsonNode header) throws ConnectorException {
|
||||
ObjectNode content = (ObjectNode) header.required("xPipeMessage");
|
||||
|
||||
var type = content.required("type").textValue();
|
||||
var phase = content.required("phase").textValue();
|
||||
//var requestId = UUID.fromString(content.required("id").textValue());
|
||||
if (!phase.equals("response")) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
content.remove("type");
|
||||
content.remove("phase");
|
||||
//content.remove("id");
|
||||
|
||||
var prov = MessageProviders.byId(type);
|
||||
if (prov.isEmpty()) {
|
||||
throw new IllegalArgumentException("Unknown response id " + type);
|
||||
}
|
||||
|
||||
try {
|
||||
var reader = JacksonHelper.newMapper().readerFor(prov.get().getResponseClass());
|
||||
return reader.readValue(content);
|
||||
} catch (IOException ex) {
|
||||
throw new ConnectorException("Couldn't parse response", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream getInputStream() {
|
||||
return in;
|
||||
}
|
||||
|
||||
public OutputStream getOutputStream() {
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package io.xpipe.beacon.socket;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.databind.node.TextNode;
|
||||
import io.xpipe.beacon.message.*;
|
||||
import io.xpipe.core.util.JacksonHelper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
|
||||
public class SocketServer {
|
||||
|
||||
private static final String BEACON_PORT_PROP = "io.xpipe.beacon.port";
|
||||
private static final Logger logger = LoggerFactory.getLogger(SocketServer.class);
|
||||
|
||||
private static final int DEFAULT_PORT = 21721;
|
||||
private static SocketServer INSTANCE;
|
||||
private final int port;
|
||||
private ServerSocket socket;
|
||||
private boolean running;
|
||||
private int connectionCounter;
|
||||
|
||||
private SocketServer(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public static Path getUserDir() {
|
||||
return Path.of(System.getProperty("user.home"), ".xpipe");
|
||||
}
|
||||
|
||||
public static int determineUsedPort() {
|
||||
if (System.getProperty(BEACON_PORT_PROP) != null) {
|
||||
return Integer.parseInt(System.getProperty(BEACON_PORT_PROP));
|
||||
}
|
||||
|
||||
var file = getUserDir().resolve("port");
|
||||
if (Files.exists(file)) {
|
||||
try {
|
||||
return Integer.parseInt(Files.readString(file));
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return DEFAULT_PORT;
|
||||
}
|
||||
|
||||
public static void init() throws IOException {
|
||||
var port = determineUsedPort();
|
||||
INSTANCE = new SocketServer(port);
|
||||
INSTANCE.createSocket();
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
INSTANCE.stop();
|
||||
INSTANCE = null;
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
|
||||
}
|
||||
|
||||
private void createSocket() throws IOException {
|
||||
socket = new ServerSocket(port, 1000, InetAddress.getLoopbackAddress());
|
||||
running = true;
|
||||
var t = new Thread(() -> {
|
||||
while (running) {
|
||||
try {
|
||||
var clientSocket = socket.accept();
|
||||
handleClientConnection(clientSocket);
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
connectionCounter++;
|
||||
}
|
||||
}, "socket server");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private void handleClientConnection(Socket clientSocket) {
|
||||
var t = new Thread(() -> {
|
||||
try {
|
||||
var in = clientSocket.getInputStream();
|
||||
var read = JacksonHelper.newMapper().disable(JsonParser.Feature.AUTO_CLOSE_SOURCE).readTree(in);
|
||||
logger.debug("Received request: \n" + read.toPrettyString());
|
||||
|
||||
var req = parseRequest(read);
|
||||
var prov = MessageProviders.byRequest(req).get();
|
||||
prov.onRequestReceived(this, req, in, clientSocket);
|
||||
} catch (SocketException ex) {
|
||||
try {
|
||||
ex.printStackTrace();
|
||||
} catch (Exception ioex) {
|
||||
ioex.printStackTrace();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
try {
|
||||
ex.printStackTrace();
|
||||
sendServerErrorResponse(clientSocket, ex);
|
||||
} catch (Exception ioex) {
|
||||
ioex.printStackTrace();
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
clientSocket.close();
|
||||
} catch (Exception ioex) {
|
||||
ioex.printStackTrace();
|
||||
}
|
||||
}
|
||||
}, "socket connection #" + connectionCounter);
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
public void prepareBody(Socket outSocket) throws IOException {
|
||||
outSocket.getOutputStream().write(Sockets.BODY_SEPARATOR);
|
||||
}
|
||||
|
||||
public <T extends ResponseMessage> void sendResponse(Socket outSocket, T obj) throws Exception {
|
||||
ObjectNode json = JacksonHelper.newMapper().valueToTree(obj);
|
||||
var prov = MessageProviders.byResponse(obj).get();
|
||||
json.set("type", new TextNode(prov.getId()));
|
||||
json.set("phase", new TextNode("response"));
|
||||
var msg = JsonNodeFactory.instance.objectNode();
|
||||
msg.set("xPipeMessage", json);
|
||||
|
||||
var mapper = JacksonHelper.newMapper().disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
|
||||
var gen = mapper.createGenerator(outSocket.getOutputStream());
|
||||
gen.writeTree(msg);
|
||||
}
|
||||
|
||||
public void sendClientErrorResponse(Socket outSocket, String message) throws Exception {
|
||||
var err = new ClientErrorMessage(message);
|
||||
ObjectNode json = JacksonHelper.newMapper().valueToTree(err);
|
||||
var msg = JsonNodeFactory.instance.objectNode();
|
||||
msg.set("xPipeClientError", json);
|
||||
|
||||
var mapper = JacksonHelper.newMapper().disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
|
||||
var gen = mapper.createGenerator(outSocket.getOutputStream());
|
||||
gen.writeTree(msg);
|
||||
}
|
||||
|
||||
public void sendServerErrorResponse(Socket outSocket, Throwable ex) throws Exception {
|
||||
var err = new ServerErrorMessage(UUID.randomUUID(), ex);
|
||||
ObjectNode json = JacksonHelper.newMapper().valueToTree(err);
|
||||
var msg = JsonNodeFactory.instance.objectNode();
|
||||
msg.set("xPipeServerError", json);
|
||||
|
||||
var mapper = JacksonHelper.newMapper().disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
|
||||
var gen = mapper.createGenerator(outSocket.getOutputStream());
|
||||
gen.writeTree(msg);
|
||||
}
|
||||
|
||||
private <T extends RequestMessage> T parseRequest(JsonNode header) throws Exception {
|
||||
ObjectNode content = (ObjectNode) header.required("xPipeMessage");
|
||||
|
||||
var type = content.required("type").textValue();
|
||||
var phase = content.required("phase").textValue();
|
||||
if (!phase.equals("request")) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
content.remove("type");
|
||||
content.remove("phase");
|
||||
|
||||
var prov = MessageProviders.byId(type);
|
||||
if (prov.isEmpty()) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
var reader = JacksonHelper.newMapper().readerFor(prov.get().getRequestClass());
|
||||
return reader.readValue(content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package io.xpipe.beacon.socket;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class Sockets {
|
||||
|
||||
public static final byte[] BODY_SEPARATOR = "\n\n".getBytes(StandardCharsets.UTF_8);
|
||||
private static final String DEBUG_PROP = "io.xpipe.beacon.debugOutput";
|
||||
|
||||
public static boolean debugEnabled() {
|
||||
if (System.getProperty(DEBUG_PROP) != null) {
|
||||
return Boolean.parseBoolean(System.getProperty(DEBUG_PROP));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import io.xpipe.app.core.BeaconProvider;
|
||||
import io.xpipe.beacon.BeaconProviderImpl;
|
||||
import io.xpipe.beacon.message.MessageProvider;
|
||||
import io.xpipe.beacon.message.impl.*;
|
||||
|
||||
module io.xpipe.beacon {
|
||||
exports io.xpipe.beacon;
|
||||
exports io.xpipe.beacon.message;
|
||||
exports io.xpipe.beacon.message.impl;
|
||||
requires org.slf4j;
|
||||
requires org.slf4j.simple;
|
||||
|
||||
requires com.fasterxml.jackson.core;
|
||||
requires com.fasterxml.jackson.databind;
|
||||
requires com.fasterxml.jackson.module.paramnames;
|
||||
requires io.xpipe.core;
|
||||
|
||||
opens io.xpipe.beacon;
|
||||
opens io.xpipe.beacon.message;
|
||||
opens io.xpipe.beacon.message.impl;
|
||||
exports io.xpipe.beacon.socket;
|
||||
opens io.xpipe.beacon.socket;
|
||||
|
||||
requires org.apache.commons.lang;
|
||||
|
||||
uses MessageProvider;
|
||||
provides MessageProvider with ListCollectionsExchange, ListEntriesExchange, ReadTableDataExchange, VersionExchange, StatusExchange, ModeExchange, ReadTableInfoExchange;
|
||||
}
|
||||
Reference in New Issue
Block a user