From c3c415bb5abc4efc043f50f86df34f0473ce0d23 Mon Sep 17 00:00:00 2001 From: crschnick Date: Mon, 20 Oct 2025 14:02:56 +0000 Subject: [PATCH] Rework for host hierarchies --- .../io/xpipe/app/ext/HostAddressStore.java | 8 -- .../io/xpipe/app/storage/DataStoreEntry.java | 19 +-- app/src/main/java/module-info.java | 1 - .../AbstractHostCreationActionProvider.java | 54 +++++++++ .../{store => host}/AbstractHostStore.java | 11 +- .../AbstractHostStoreProvider.java | 37 +++++- .../base/host/AbstractHostTransformStore.java | 13 +++ .../ext/base/host}/HostAddressChoice.java | 3 +- .../ext/base/host}/HostAddressChoiceComp.java | 2 +- .../base/host/HostAddressGatewayStore.java | 9 ++ .../HostAddressIdentityStore.java | 3 +- .../xpipe/ext/base/host/HostAddressStore.java | 9 ++ .../HostAddressSwitchBranchProvider.java | 3 +- .../base/host}/HostAddressSwitchStore.java | 4 +- .../base/host}/HostAddressTunnelStore.java | 4 +- .../ext/base/{store => host}/HostStore.java | 6 +- .../AbstractServiceGroupStoreProvider.java | 1 + .../base/service/AbstractServiceStore.java | 110 ++++++++++++------ .../service/AbstractServiceStoreProvider.java | 15 ++- .../ext/base/service/CustomServiceStore.java | 28 ++++- .../service/CustomServiceStoreProvider.java | 68 +++++++++-- .../ext/base/service/FixedServiceStore.java | 10 ++ .../base/service/ServiceAddressRotation.java | 9 +- ext/base/src/main/java/module-info.java | 7 ++ lang/strings/translations_en.properties | 11 +- 25 files changed, 355 insertions(+), 90 deletions(-) delete mode 100644 app/src/main/java/io/xpipe/app/ext/HostAddressStore.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/host/AbstractHostCreationActionProvider.java rename ext/base/src/main/java/io/xpipe/ext/base/{store => host}/AbstractHostStore.java (75%) rename ext/base/src/main/java/io/xpipe/ext/base/{store => host}/AbstractHostStoreProvider.java (58%) create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/host/AbstractHostTransformStore.java rename {app/src/main/java/io/xpipe/app/ext => ext/base/src/main/java/io/xpipe/ext/base/host}/HostAddressChoice.java (96%) rename {app/src/main/java/io/xpipe/app/ext => ext/base/src/main/java/io/xpipe/ext/base/host}/HostAddressChoiceComp.java (99%) create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressGatewayStore.java rename ext/base/src/main/java/io/xpipe/ext/base/{store => host}/HostAddressIdentityStore.java (67%) create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressStore.java rename {app/src/main/java/io/xpipe/app/hub/action/impl => ext/base/src/main/java/io/xpipe/ext/base/host}/HostAddressSwitchBranchProvider.java (97%) rename {app/src/main/java/io/xpipe/app/ext => ext/base/src/main/java/io/xpipe/ext/base/host}/HostAddressSwitchStore.java (74%) rename {app/src/main/java/io/xpipe/app/ext => ext/base/src/main/java/io/xpipe/ext/base/host}/HostAddressTunnelStore.java (54%) rename ext/base/src/main/java/io/xpipe/ext/base/{store => host}/HostStore.java (50%) diff --git a/app/src/main/java/io/xpipe/app/ext/HostAddressStore.java b/app/src/main/java/io/xpipe/app/ext/HostAddressStore.java deleted file mode 100644 index 2c6e79fa7..000000000 --- a/app/src/main/java/io/xpipe/app/ext/HostAddressStore.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.xpipe.app.ext; - -import java.util.Optional; - -public interface HostAddressStore extends DataStore { - - HostAddress getHostAddress(); -} diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java index c08b27cdb..912b5ec5f 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java @@ -673,14 +673,19 @@ public class DataStoreEntry extends StorageElement { return; } - var newComplete = newStore.isComplete(); - if (!newComplete) { - var changed = !Objects.equals(store, newStore) || validity != Validity.INCOMPLETE; - validity = Validity.INCOMPLETE; - store = newStore; - if (changed) { - notifyUpdate(false, false); + try { + var newComplete = newStore.isComplete(); + if (!newComplete) { + var changed = !Objects.equals(store, newStore) || validity != Validity.INCOMPLETE; + validity = Validity.INCOMPLETE; + store = newStore; + if (changed) { + notifyUpdate(false, false); + } + return; } + } catch (Exception e) { + ErrorEventFactory.fromThrowable(e).handle(); return; } diff --git a/app/src/main/java/module-info.java b/app/src/main/java/module-info.java index f99a99f3f..b16b93159 100644 --- a/app/src/main/java/module-info.java +++ b/app/src/main/java/module-info.java @@ -128,7 +128,6 @@ open module io.xpipe.app { provides ActionProvider with SetupToolActionProvider, XPipeUrlProvider, - HostAddressSwitchBranchProvider, OpenHubMenuLeafProvider, EditHubLeafProvider, CloneHubLeafProvider, diff --git a/ext/base/src/main/java/io/xpipe/ext/base/host/AbstractHostCreationActionProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/host/AbstractHostCreationActionProvider.java new file mode 100644 index 000000000..3c8f49f0d --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/host/AbstractHostCreationActionProvider.java @@ -0,0 +1,54 @@ +package io.xpipe.ext.base.host; + +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.hub.action.HubLeafProvider; +import io.xpipe.app.hub.action.StoreAction; +import io.xpipe.app.hub.comp.StoreViewState; +import io.xpipe.app.platform.LabelGraphic; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.FileOpener; +import javafx.beans.value.ObservableValue; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +public class AbstractHostCreationActionProvider implements HubLeafProvider { + + @Override + public ObservableValue getName(DataStoreEntryRef store) { + return AppI18n.observable("abstractHostConvert"); + } + + @Override + public LabelGraphic getIcon(DataStoreEntryRef store) { + return new LabelGraphic.IconGraphic("mdi2c-cog-transfer-outline"); + } + + @Override + public Class getApplicableClass() { + return AbstractHostTransformStore.class; + } + + @Override + public boolean isApplicable(DataStoreEntryRef o) { + return o.getStore().canConvertToAbstractHost(); + } + + @Jacksonized + @SuperBuilder + public static class Action extends StoreAction { + + @Override + public void executeImpl() throws Exception { + var d = ref.getStore(); + var ah = d.createAbstractHostStore(); + var entry = DataStorage.get().addStoreIfNotPresent(ref.get().getName(), ah); + entry.setExpanded(true); + var newStore = d.withNewParent(entry.ref()); + DataStorage.get().updateEntryStore(ref.get(), newStore); + entry.setChildrenCache(null); + StoreViewState.get().triggerStoreListUpdate(); + } + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/AbstractHostStore.java b/ext/base/src/main/java/io/xpipe/ext/base/host/AbstractHostStore.java similarity index 75% rename from ext/base/src/main/java/io/xpipe/ext/base/store/AbstractHostStore.java rename to ext/base/src/main/java/io/xpipe/ext/base/host/AbstractHostStore.java index 03b61e890..a43e68986 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/store/AbstractHostStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/host/AbstractHostStore.java @@ -1,10 +1,9 @@ -package io.xpipe.ext.base.store; +package io.xpipe.ext.base.host; import com.fasterxml.jackson.annotation.JsonTypeName; import io.xpipe.app.ext.*; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.Validators; -import io.xpipe.ext.base.identity.IdentityValue; import lombok.ToString; import lombok.Value; import lombok.experimental.SuperBuilder; @@ -15,9 +14,10 @@ import lombok.extern.jackson.Jacksonized; @SuperBuilder @Jacksonized @JsonTypeName("abstractHost") -public class AbstractHostStore implements DataStore, HostAddressStore { +public class AbstractHostStore implements DataStore, HostAddressStore, HostAddressGatewayStore { String host; + DataStoreEntryRef gateway; @Override public void checkComplete() throws Throwable { @@ -28,4 +28,9 @@ public class AbstractHostStore implements DataStore, HostAddressStore { public HostAddress getHostAddress() { return HostAddress.of(host); } + + @Override + public DataStoreEntryRef getGateway() { + return gateway; + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/AbstractHostStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/host/AbstractHostStoreProvider.java similarity index 58% rename from ext/base/src/main/java/io/xpipe/ext/base/store/AbstractHostStoreProvider.java rename to ext/base/src/main/java/io/xpipe/ext/base/host/AbstractHostStoreProvider.java index ecaa336c0..928ef33f7 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/store/AbstractHostStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/host/AbstractHostStoreProvider.java @@ -1,14 +1,15 @@ -package io.xpipe.ext.base.store; +package io.xpipe.ext.base.host; import io.xpipe.app.comp.Comp; +import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.*; import io.xpipe.app.hub.comp.*; import io.xpipe.app.platform.OptionsBuilder; import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.storage.DataStoreEntry; +import javafx.beans.binding.Bindings; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; import lombok.SneakyThrows; @@ -36,16 +37,46 @@ public class AbstractHostStoreProvider implements DataStoreProvider { return DataStoreUsageCategory.GROUP; } + @Override + public ObservableValue informationString(StoreSection section) { + return Bindings.createStringBinding( + () -> { + var all = section.getAllChildren().getList(); + var shown = section.getShownChildren().getList(); + if (shown.size() == 0) { + return null; + } + + var string = all.size() == shown.size() ? all.size() : shown.size() + "/" + all.size(); + return all.size() > 0 + ? (all.size() == 1 ? AppI18n.get("abstractHostHasConnection", string) : AppI18n.get("abstractHostHasConnections", string)) + : AppI18n.get("abstractHostNoConnections"); + }, + section.getShownChildren().getList(), + section.getAllChildren().getList(), + AppI18n.activeLanguage()); + } + @SneakyThrows @Override public GuiDialog guiDialog(DataStoreEntry entry, Property store) { AbstractHostStore st = store.getValue().asNeeded(); - Property host = new SimpleObjectProperty<>(st.getHost()); + var host = new SimpleObjectProperty<>(st.getHost()); + var gateway = new SimpleObjectProperty<>(st.getGateway()); return new OptionsBuilder() .nameAndDescription("abstractHostAddress") .addString(host) + .nonNull() + .nameAndDescription("abstractHostGateway") + .addComp(new StoreChoiceComp<>(StoreChoiceComp.Mode.PROXY, + entry, + gateway, + NetworkTunnelStore.class, + ref -> true, + StoreViewState.get().getAllConnectionsCategory() + ), gateway) .bind( () -> { return AbstractHostStore.builder() diff --git a/ext/base/src/main/java/io/xpipe/ext/base/host/AbstractHostTransformStore.java b/ext/base/src/main/java/io/xpipe/ext/base/host/AbstractHostTransformStore.java new file mode 100644 index 000000000..5df264ca6 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/host/AbstractHostTransformStore.java @@ -0,0 +1,13 @@ +package io.xpipe.ext.base.host; + +import io.xpipe.app.ext.DataStore; +import io.xpipe.app.storage.DataStoreEntryRef; + +public interface AbstractHostTransformStore extends DataStore { + + boolean canConvertToAbstractHost(); + + AbstractHostStore createAbstractHostStore(); + + AbstractHostTransformStore withNewParent(DataStoreEntryRef newParent); +} diff --git a/app/src/main/java/io/xpipe/app/ext/HostAddressChoice.java b/ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressChoice.java similarity index 96% rename from app/src/main/java/io/xpipe/app/ext/HostAddressChoice.java rename to ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressChoice.java index 24b23d550..2f2155306 100644 --- a/app/src/main/java/io/xpipe/app/ext/HostAddressChoice.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressChoice.java @@ -1,5 +1,6 @@ -package io.xpipe.app.ext; +package io.xpipe.ext.base.host; +import io.xpipe.app.ext.HostAddress; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.platform.OptionsBuilder; diff --git a/app/src/main/java/io/xpipe/app/ext/HostAddressChoiceComp.java b/ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressChoiceComp.java similarity index 99% rename from app/src/main/java/io/xpipe/app/ext/HostAddressChoiceComp.java rename to ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressChoiceComp.java index 32ebfef22..f8de74627 100644 --- a/app/src/main/java/io/xpipe/app/ext/HostAddressChoiceComp.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressChoiceComp.java @@ -1,4 +1,4 @@ -package io.xpipe.app.ext; +package io.xpipe.ext.base.host; import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.CompStructure; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressGatewayStore.java b/ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressGatewayStore.java new file mode 100644 index 000000000..00357aecf --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressGatewayStore.java @@ -0,0 +1,9 @@ +package io.xpipe.ext.base.host; + +import io.xpipe.app.ext.NetworkTunnelStore; +import io.xpipe.app.storage.DataStoreEntryRef; + +public interface HostAddressGatewayStore extends HostAddressStore { + + DataStoreEntryRef getGateway(); +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/HostAddressIdentityStore.java b/ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressIdentityStore.java similarity index 67% rename from ext/base/src/main/java/io/xpipe/ext/base/store/HostAddressIdentityStore.java rename to ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressIdentityStore.java index 06c67e60c..8b00d0e97 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/store/HostAddressIdentityStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressIdentityStore.java @@ -1,6 +1,5 @@ -package io.xpipe.ext.base.store; +package io.xpipe.ext.base.host; -import io.xpipe.app.ext.HostAddressStore; import io.xpipe.ext.base.identity.IdentityValue; public interface HostAddressIdentityStore extends HostAddressStore { diff --git a/ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressStore.java b/ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressStore.java new file mode 100644 index 000000000..d5a470d09 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressStore.java @@ -0,0 +1,9 @@ +package io.xpipe.ext.base.host; + +import io.xpipe.app.ext.DataStore; +import io.xpipe.app.ext.HostAddress; + +public interface HostAddressStore extends DataStore { + + HostAddress getHostAddress(); +} diff --git a/app/src/main/java/io/xpipe/app/hub/action/impl/HostAddressSwitchBranchProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressSwitchBranchProvider.java similarity index 97% rename from app/src/main/java/io/xpipe/app/hub/action/impl/HostAddressSwitchBranchProvider.java rename to ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressSwitchBranchProvider.java index 156e15cc2..b073c5c67 100644 --- a/app/src/main/java/io/xpipe/app/hub/action/impl/HostAddressSwitchBranchProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressSwitchBranchProvider.java @@ -1,7 +1,6 @@ -package io.xpipe.app.hub.action.impl; +package io.xpipe.ext.base.host; import io.xpipe.app.core.AppI18n; -import io.xpipe.app.ext.HostAddressSwitchStore; import io.xpipe.app.hub.action.HubBranchProvider; import io.xpipe.app.hub.action.HubLeafProvider; import io.xpipe.app.hub.action.HubMenuItemProvider; diff --git a/app/src/main/java/io/xpipe/app/ext/HostAddressSwitchStore.java b/ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressSwitchStore.java similarity index 74% rename from app/src/main/java/io/xpipe/app/ext/HostAddressSwitchStore.java rename to ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressSwitchStore.java index cbddf0570..e2babae15 100644 --- a/app/src/main/java/io/xpipe/app/ext/HostAddressSwitchStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressSwitchStore.java @@ -1,4 +1,6 @@ -package io.xpipe.app.ext; +package io.xpipe.ext.base.host; + +import io.xpipe.app.ext.HostAddress; import java.util.Optional; diff --git a/app/src/main/java/io/xpipe/app/ext/HostAddressTunnelStore.java b/ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressTunnelStore.java similarity index 54% rename from app/src/main/java/io/xpipe/app/ext/HostAddressTunnelStore.java rename to ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressTunnelStore.java index b7d92c5c5..5cfa66b5c 100644 --- a/app/src/main/java/io/xpipe/app/ext/HostAddressTunnelStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/host/HostAddressTunnelStore.java @@ -1,4 +1,6 @@ -package io.xpipe.app.ext; +package io.xpipe.ext.base.host; + +import io.xpipe.app.ext.NetworkTunnelStore; public interface HostAddressTunnelStore extends HostAddressStore, NetworkTunnelStore { diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/HostStore.java b/ext/base/src/main/java/io/xpipe/ext/base/host/HostStore.java similarity index 50% rename from ext/base/src/main/java/io/xpipe/ext/base/store/HostStore.java rename to ext/base/src/main/java/io/xpipe/ext/base/host/HostStore.java index 09761ab53..2b6284746 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/store/HostStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/host/HostStore.java @@ -1,12 +1,10 @@ -package io.xpipe.ext.base.store; +package io.xpipe.ext.base.host; import io.xpipe.app.ext.DataStore; import io.xpipe.app.ext.HostAddress; -import io.xpipe.app.ext.NetworkTunnelStore; -import io.xpipe.app.ext.ShellStore; import io.xpipe.ext.base.identity.IdentityValue; -public interface HostStore extends DataStore, ShellStore, NetworkTunnelStore { +public interface HostStore extends DataStore { HostAddress getHostAddress(); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStoreProvider.java index 5906171d9..78e558a2d 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStoreProvider.java @@ -5,6 +5,7 @@ import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.DataStore; import io.xpipe.app.ext.DataStoreProvider; import io.xpipe.app.ext.DataStoreUsageCategory; +import io.xpipe.app.ext.NetworkTunnelStore; import io.xpipe.app.hub.comp.*; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStore.java index c27df12c2..6964a0f6d 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStore.java @@ -15,42 +15,15 @@ import lombok.Getter; import lombok.ToString; import lombok.experimental.SuperBuilder; -@SuperBuilder +@SuperBuilder(toBuilder = true) @Getter @EqualsAndHashCode @ToString public abstract class AbstractServiceStore implements SingletonSessionStore, DataStore { - private final Integer remotePort; - private final Integer localPort; - private final ServiceProtocolType serviceProtocolType; - - public abstract DataStoreEntryRef getHost(); - - public boolean licenseRequired() { - return true; - } - - - @Override - public void checkComplete() throws Throwable { - Validators.nonNull(getHost()); - NetworkTunnelStore.checkTunnelable(getHost()); - Validators.nonNull(remotePort); - Validators.nonNull(serviceProtocolType); - } - - public String getOpenTargetUrl() { - return ServiceAddressRotation.getRotatedAddress(this); - } - - public boolean requiresTunnel() { - if (getHost() == null) { - return false; - } - - if (!getHost().getStore().isLocallyTunnelable()) { - var parent = getHost().getStore().getNetworkParent(); + public static boolean requiresTunnel(NetworkTunnelStore t) { + if (!t.isLocallyTunnelable()) { + var parent = t.getNetworkParent(); if (!(parent instanceof NetworkTunnelStore nts)) { return false; } @@ -58,11 +31,78 @@ public abstract class AbstractServiceStore implements SingletonSessionStore getGateway(); + + public abstract DataStoreEntryRef getHost(); + + public boolean licenseRequired() { + return true; + } + + @Override + public void checkComplete() throws Throwable { + Validators.nonNull(remotePort); + Validators.nonNull(serviceProtocolType); + if (getHost() != null) { + getHost().checkComplete(); + } else { + Validators.nonNull(getAddress()); + } + } + + public String getOpenTargetUrl() { + return ServiceAddressRotation.getRotatedAddress(this); + } + + public boolean requiresTunnel() { + if (getHost() == null || !(getHost().getStore() instanceof NetworkTunnelStore t)) { + return false; + } + + if (!t.isLocallyTunnelable()) { + var parent = t.getNetworkParent(); + if (!(parent instanceof NetworkTunnelStore nts)) { + return false; + } + + return nts.requiresTunnel(); + } + + return t.requiresTunnel(); } @Override public NetworkTunnelSession newSession() { + if (!(getHost().getStore() instanceof NetworkTunnelStore t)) { + return null; + } + var f = LicenseProvider.get().getFeature("services"); if (licenseRequired() && !f.isSupported()) { var active = DataStorage.get().getStoreEntries().stream() @@ -78,13 +118,13 @@ public abstract class AbstractServiceStore implements SingletonSessionStore s) { var abs = (AbstractServiceStore) s; - return abs.getHost() == null - || !abs.getHost().getStore().requiresTunnel() - || !abs.getHost().getStore().isLocallyTunnelable(); + if (abs.getHost() != null && (!(abs.getHost().getStore() instanceof NetworkTunnelStore t) + || !t.requiresTunnel() + || !t.isLocallyTunnelable())) { + return false; + } + + if (abs.getHost() == null && (abs.getGateway() == null || + !abs.getGateway().getStore().isLocallyTunnelable() || !abs.getGateway().getStore().requiresTunnel())) { + return false; + } + + return true; } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStore.java index 918c3959a..5b71e3717 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStore.java @@ -1,22 +1,44 @@ package io.xpipe.ext.base.service; +import io.xpipe.app.ext.DataStore; import io.xpipe.app.ext.NetworkTunnelStore; import io.xpipe.app.storage.DataStoreEntryRef; import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.ext.base.host.AbstractHostStore; +import io.xpipe.ext.base.host.AbstractHostTransformStore; +import io.xpipe.ext.base.host.HostAddressStore; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; -@SuperBuilder +@SuperBuilder(toBuilder = true) @Getter @Jacksonized @JsonTypeName("customService") @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) -public final class CustomServiceStore extends AbstractServiceStore { +public final class CustomServiceStore extends AbstractServiceStore implements AbstractHostTransformStore { - private final DataStoreEntryRef host; + private final DataStoreEntryRef host; + private final String address; + private final DataStoreEntryRef gateway; + + + @Override + public boolean canConvertToAbstractHost() { + return host == null; + } + + @Override + public AbstractHostStore createAbstractHostStore() { + return AbstractHostStore.builder().host(address).gateway(gateway).build(); + } + + @Override + public AbstractHostTransformStore withNewParent(DataStoreEntryRef newParent) { + return toBuilder().address(null).gateway(null).host(newParent.asNeeded()).build(); + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStoreProvider.java index 7378ca596..d2db5cd19 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStoreProvider.java @@ -5,18 +5,44 @@ import io.xpipe.app.ext.DataStoreCreationCategory; import io.xpipe.app.ext.GuiDialog; import io.xpipe.app.ext.NetworkTunnelStore; import io.xpipe.app.hub.comp.StoreChoiceComp; +import io.xpipe.app.hub.comp.StoreComboChoiceComp; import io.xpipe.app.hub.comp.StoreViewState; +import io.xpipe.app.platform.BindingsHelper; import io.xpipe.app.platform.OptionsBuilder; +import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.ext.base.host.AbstractHostStore; +import io.xpipe.ext.base.host.HostAddressStore; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; import java.util.List; public class CustomServiceStoreProvider extends AbstractServiceStoreProvider { + @Override + public DataStoreEntry getSyntheticParent(DataStoreEntry store) { + var c = (CustomServiceStore) store.getStore(); + if (c.getHost() == null || c.getHost().getStore() instanceof AbstractHostStore) { + return null; + } + + return super.getSyntheticParent(store); + } + + @Override + public DataStoreEntry getDisplayParent(DataStoreEntry store) { + var c = (CustomServiceStore) store.getStore(); + if (c.getHost() != null && c.getHost().getStore() instanceof AbstractHostStore) { + return c.getHost().get(); + } + + return super.getDisplayParent(store); + } + @Override public int getOrderPriority() { return -1; @@ -30,20 +56,42 @@ public class CustomServiceStoreProvider extends AbstractServiceStoreProvider { @Override public GuiDialog guiDialog(DataStoreEntry entry, Property store) { CustomServiceStore st = store.getValue().asNeeded(); - var host = new SimpleObjectProperty<>(st.getHost()); + + var comboHost = new SimpleObjectProperty<>(StoreComboChoiceComp.ComboValue.of( + st.getAddress(), + st.getHost() + )); + var gateway = new SimpleObjectProperty<>(st.getGateway()); + var hideGateway = BindingsHelper.map(comboHost, c -> c == null || c.getRef() != null); + var localPort = new SimpleObjectProperty<>(st.getLocalPort()); var remotePort = new SimpleObjectProperty<>(st.getRemotePort()); var serviceProtocolType = new SimpleObjectProperty<>(st.getServiceProtocolType()); + + var hostChoice = new StoreComboChoiceComp<>( + hostStore -> hostStore.getHostAddress().get(), + entry, + comboHost, + NetworkTunnelStore.class, + n -> n.getStore() instanceof AbstractHostStore || + (n.getStore() instanceof NetworkTunnelStore t && t.isLocallyTunnelable()), + StoreViewState.get().getAllConnectionsCategory() + ); + var gatewayChoice = new StoreChoiceComp<>( + StoreChoiceComp.Mode.PROXY, + entry, + gateway, + NetworkTunnelStore.class, + ref -> !ref.get().equals(DataStorage.get().local()), + StoreViewState.get().getAllConnectionsCategory()); + var q = new OptionsBuilder() .nameAndDescription("serviceHost") - .addComp( - StoreChoiceComp.other( - host, - NetworkTunnelStore.class, - n -> n.getStore().isLocallyTunnelable(), - StoreViewState.get().getAllConnectionsCategory()), - host) + .addComp(hostChoice, comboHost) .nonNull() + .nameAndDescription("gateway") + .addComp(gatewayChoice, gateway) + .hide(hideGateway) .nameAndDescription("serviceRemotePort") .addInteger(remotePort) .nonNull() @@ -54,7 +102,9 @@ public class CustomServiceStoreProvider extends AbstractServiceStoreProvider { .bind( () -> { return CustomServiceStore.builder() - .host(host.get()) + .address(comboHost.get() != null ? comboHost.get().getManualHost() : null) + .host(comboHost.get() != null ? comboHost.get().getRef() : null) + .gateway(gateway.get()) .localPort(localPort.get()) .remotePort(remotePort.get()) .serviceProtocolType(serviceProtocolType.get()) diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStore.java index 146a38a96..904205878 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStore.java @@ -26,6 +26,16 @@ public class FixedServiceStore extends AbstractServiceStore implements FixedChil private final DataStoreEntryRef host; private final DataStoreEntryRef displayParent; + @Override + public String getAddress() { + return "localhost"; + } + + @Override + public DataStoreEntryRef getGateway() { + return null; + } + @Override public DataStoreEntryRef getHost() { return host; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceAddressRotation.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceAddressRotation.java index 945b9f807..f08e2e003 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceAddressRotation.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceAddressRotation.java @@ -27,10 +27,11 @@ public class ServiceAddressRotation { public static String getRotatedAddress(AbstractServiceStore serviceStore) { var s = serviceStore.getSession(); if (s == null) { - var host = serviceStore.getHost().getStore().getTunnelHostName() != null - ? serviceStore.getHost().getStore().getTunnelHostName() - : "localhost"; - return getRotatedLocalhost(host + ":" + serviceStore.getRemotePort()); + var address = serviceStore.getAddress(); + if (address == null) { + address = "localhost"; + } + return getRotatedLocalhost(address + ":" + serviceStore.getRemotePort()); } return getRotatedLocalhost("localhost:" + s.getLocalPort()); diff --git a/ext/base/src/main/java/module-info.java b/ext/base/src/main/java/module-info.java index 23954dcc8..5ceffb603 100644 --- a/ext/base/src/main/java/module-info.java +++ b/ext/base/src/main/java/module-info.java @@ -2,6 +2,9 @@ import io.xpipe.app.action.ActionProvider; import io.xpipe.app.ext.DataStorageExtensionProvider; import io.xpipe.app.ext.DataStoreProvider; import io.xpipe.ext.base.desktop.DesktopApplicationStoreProvider; +import io.xpipe.ext.base.host.AbstractHostCreationActionProvider; +import io.xpipe.ext.base.host.AbstractHostStoreProvider; +import io.xpipe.ext.base.host.HostAddressSwitchBranchProvider; import io.xpipe.ext.base.identity.*; import io.xpipe.ext.base.script.*; import io.xpipe.ext.base.service.*; @@ -14,6 +17,7 @@ open module io.xpipe.ext.base { exports io.xpipe.ext.base.service; exports io.xpipe.ext.base.identity; exports io.xpipe.ext.base.identity.ssh; + exports io.xpipe.ext.base.host; requires java.desktop; requires io.xpipe.core; @@ -28,8 +32,11 @@ open module io.xpipe.ext.base { requires atlantafx.base; requires com.sun.jna.platform; requires com.sun.jna; + requires javafx.base; provides ActionProvider with + AbstractHostCreationActionProvider, + HostAddressSwitchBranchProvider, LocalIdentityConvertHubLeafProvider, RunBackgroundScriptActionProvider, RunHubBatchScriptActionProvider, diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index 44c94a85a..f5f788fe4 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -726,7 +726,7 @@ serviceLocalPortDescription=The local port to forward to, otherwise a random one serviceRemotePort=Remote port serviceRemotePortDescription=The port on which the service is running on serviceHost=Service host -serviceHostDescription=The host the service is running on +serviceHostDescription=The host entry or manual address of the server on which the service is running on openWebsite=Open website customServiceGroup.displayName=Service group customServiceGroup.displayDescription=Group multiple services into one category @@ -741,7 +741,8 @@ fixedServiceGroup.displayDescription=List the available services on a system mappedService.displayName=Service mappedService.displayDescription=Interact with a service exposed by a container customService.displayName=Service -customService.displayDescription=Automatically tunnel a remote service port to your local machine +#force +customService.displayDescription=Automatically open or tunnel a remote service port on your local machine fixedService.displayName=Service fixedService.displayDescription=Use a predefined service noServices=No available services @@ -1665,5 +1666,11 @@ abstractHost.displayName=Abstract host abstractHost.displayDescription=Create an entry for a host that does not support shell connections abstractHostAddress=Host address abstractHostAddressDescription=The address of the host +abstractHostGateway=Gateway +abstractHostGatewayDescription=The optional gateway system through which to reach this host +abstractHostConvert=Convert to abstract host +abstractHostNoConnections=No available connections +abstractHostHasConnections=$COUNT$ available connections +abstractHostHasConnection=$COUNT$ available connection largeFileWarningTitle=Large file edit largeFileWarningContent=The file you want to edit is quite large with $SIZE$. Do you really want to open this file in your text editor?