From 4a4a2fea7ec45ea0667e34743874bfeba336b620 Mon Sep 17 00:00:00 2001 From: crschnick Date: Sat, 25 Jan 2025 22:19:55 +0000 Subject: [PATCH] Merge branch 'session-control' into 15-release --- .../app/comp/base/IntegratedTextAreaComp.java | 31 ++++ .../ext/SingletonSessionStoreProvider.java | 2 +- .../java/io/xpipe/app/util/Validators.java | 2 +- .../io/xpipe/core/process/ShellControl.java | 4 + .../io/xpipe/core/process/ShellScript.java | 9 ++ .../core/store/NetworkTunnelSessionChain.java | 9 +- .../java/io/xpipe/core/store/Session.java | 2 +- .../io/xpipe/core/util/CoreJacksonModule.java | 21 +++ .../service/AbstractServiceGroupStore.java | 2 +- .../base/service/CustomServiceGroupStore.java | 5 +- .../base/service/ServiceControlSession.java | 53 +++++++ .../ext/base/service/ServiceControlStore.java | 50 ++++++ .../service/ServiceControlStoreProvider.java | 143 ++++++++++++++++++ ext/base/src/main/java/module-info.java | 1 + 14 files changed, 326 insertions(+), 8 deletions(-) create mode 100644 core/src/main/java/io/xpipe/core/process/ShellScript.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/service/ServiceControlSession.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/service/ServiceControlStore.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/service/ServiceControlStoreProvider.java diff --git a/app/src/main/java/io/xpipe/app/comp/base/IntegratedTextAreaComp.java b/app/src/main/java/io/xpipe/app/comp/base/IntegratedTextAreaComp.java index 0708a6001..5f0151308 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/IntegratedTextAreaComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/IntegratedTextAreaComp.java @@ -2,10 +2,17 @@ package io.xpipe.app.comp.base; import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.CompStructure; +import io.xpipe.app.ext.ShellStore; +import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.FileOpener; +import io.xpipe.core.process.ShellScript; +import io.xpipe.core.process.ShellStoreState; +import io.xpipe.core.store.StatefulDataStore; import javafx.application.Platform; +import javafx.beans.binding.Bindings; import javafx.beans.property.Property; +import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; import javafx.scene.control.TextArea; import javafx.scene.layout.AnchorPane; @@ -20,6 +27,30 @@ import java.nio.file.Files; public class IntegratedTextAreaComp extends Comp { + public static IntegratedTextAreaComp script(ObservableValue> host, Property value) { + var string = new SimpleStringProperty(value.getValue() != null ? value.getValue().getValue() : null); + string.addListener((observable, oldValue, newValue) -> { + value.setValue(newValue != null ? new ShellScript(newValue) : null); + }); + var i = new IntegratedTextAreaComp( + string, + false, + "script", + Bindings.createStringBinding( + () -> { + return host.getValue() != null && host.getValue().getStore() instanceof StatefulDataStore sd && + sd.getState() instanceof ShellStoreState sss && sss.getShellDialect() != null + ? sss.getShellDialect() + .getScriptFileEnding() + : "sh"; + }, + host)); + i.minHeight(60); + i.prefHeight(60); + i.maxHeight(60); + return i; + } + private final Property value; private final boolean lazy; private final String identifier; diff --git a/app/src/main/java/io/xpipe/app/ext/SingletonSessionStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/SingletonSessionStoreProvider.java index d7478f4e1..de8f06777 100644 --- a/app/src/main/java/io/xpipe/app/ext/SingletonSessionStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/SingletonSessionStoreProvider.java @@ -59,7 +59,7 @@ public interface SingletonSessionStoreProvider extends DataStoreProvider { return new SystemStateComp(Bindings.createObjectBinding( () -> { SingletonSessionStore s = w.getEntry().getStore().asNeeded(); - if (!s.isSessionEnabled()) { + if (!s.isSessionEnabled() || (s.isSessionEnabled() && !s.isSessionRunning())) { return SystemStateComp.State.OTHER; } diff --git a/app/src/main/java/io/xpipe/app/util/Validators.java b/app/src/main/java/io/xpipe/app/util/Validators.java index 3308dbf82..2e5cc4ed9 100644 --- a/app/src/main/java/io/xpipe/app/util/Validators.java +++ b/app/src/main/java/io/xpipe/app/util/Validators.java @@ -9,7 +9,7 @@ import java.util.List; public class Validators { - public static void isType(DataStoreEntryRef ref, Class c) throws ValidationException { + public static void isType(DataStoreEntryRef ref, Class c) throws ValidationException { if (ref != null && !c.isAssignableFrom(ref.getStore().getClass())) { throw new ValidationException("Value must be an instance of " + c.getSimpleName()); } diff --git a/core/src/main/java/io/xpipe/core/process/ShellControl.java b/core/src/main/java/io/xpipe/core/process/ShellControl.java index 23d69e40b..c066a857b 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellControl.java +++ b/core/src/main/java/io/xpipe/core/process/ShellControl.java @@ -251,6 +251,10 @@ public interface ShellControl extends ProcessControl { return command(CommandBuilder.ofFunction(shellProcessControl -> command)); } + default CommandControl command(ShellScript command) { + return command(CommandBuilder.of().add(command.getValue())); + } + default CommandControl command(Consumer builder) { var b = CommandBuilder.of(); builder.accept(b); diff --git a/core/src/main/java/io/xpipe/core/process/ShellScript.java b/core/src/main/java/io/xpipe/core/process/ShellScript.java new file mode 100644 index 000000000..87bb2371a --- /dev/null +++ b/core/src/main/java/io/xpipe/core/process/ShellScript.java @@ -0,0 +1,9 @@ +package io.xpipe.core.process; + +import lombok.Value; + +@Value +public class ShellScript { + + String value; +} diff --git a/core/src/main/java/io/xpipe/core/store/NetworkTunnelSessionChain.java b/core/src/main/java/io/xpipe/core/store/NetworkTunnelSessionChain.java index 186fb56ac..db4cf34c3 100644 --- a/core/src/main/java/io/xpipe/core/store/NetworkTunnelSessionChain.java +++ b/core/src/main/java/io/xpipe/core/store/NetworkTunnelSessionChain.java @@ -27,8 +27,13 @@ public class NetworkTunnelSessionChain extends NetworkTunnelSession { } @Override - public boolean isRunning() { - return sessions.stream().allMatch(session -> session.isRunning()); + public boolean isRunning() throws Exception { + for (NetworkTunnelSession session : sessions) { + if (!session.isRunning()) { + return false; + } + } + return true; } @Override diff --git a/core/src/main/java/io/xpipe/core/store/Session.java b/core/src/main/java/io/xpipe/core/store/Session.java index 8e02e38c1..0351a49d2 100644 --- a/core/src/main/java/io/xpipe/core/store/Session.java +++ b/core/src/main/java/io/xpipe/core/store/Session.java @@ -16,7 +16,7 @@ public abstract class Session implements AutoCloseable { }; } - public abstract boolean isRunning(); + public abstract boolean isRunning() throws Exception; public abstract void start() throws Exception; diff --git a/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java b/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java index e081bb6d6..76547f569 100644 --- a/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java +++ b/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java @@ -7,6 +7,7 @@ import io.xpipe.core.dialog.HeaderElement; import io.xpipe.core.process.OsType; import io.xpipe.core.process.ShellDialect; import io.xpipe.core.process.ShellDialects; +import io.xpipe.core.process.ShellScript; import io.xpipe.core.store.FilePath; import io.xpipe.core.store.StorePath; @@ -65,12 +66,32 @@ public class CoreJacksonModule extends SimpleModule { addDeserializer(OsType.Local.class, new OsTypeLocalDeserializer()); addDeserializer(OsType.Any.class, new OsTypeAnyDeserializer()); + addSerializer(ShellScript.class, new ShellScriptSerializer()); + addDeserializer(ShellScript.class, new ShellScriptDeserializer()); + context.setMixInAnnotations(Throwable.class, ThrowableTypeMixIn.class); context.addSerializers(_serializers); context.addDeserializers(_deserializers); } + public static class ShellScriptSerializer extends JsonSerializer { + + @Override + public void serialize(ShellScript value, JsonGenerator jgen, SerializerProvider provider) + throws IOException { + jgen.writeString(value.getValue()); + } + } + + public static class ShellScriptDeserializer extends JsonDeserializer { + + @Override + public ShellScript deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return new ShellScript(p.getValueAsString()); + } + } + public static class StorePathSerializer extends JsonSerializer { @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStore.java index 51b47c4f4..13d936e09 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStore.java @@ -19,7 +19,7 @@ import lombok.experimental.SuperBuilder; @ToString public abstract class AbstractServiceGroupStore implements DataStore, GroupStore { - DataStoreEntryRef parent; + DataStoreEntryRef parent; @Override public void checkComplete() throws Throwable { diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceGroupStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceGroupStore.java index 993bbbe73..c1861ed23 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceGroupStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceGroupStore.java @@ -1,6 +1,7 @@ package io.xpipe.ext.base.service; import io.xpipe.app.util.Validators; +import io.xpipe.core.store.DataStore; import io.xpipe.core.store.NetworkTunnelStore; import com.fasterxml.jackson.annotation.JsonTypeName; @@ -13,11 +14,11 @@ import lombok.extern.jackson.Jacksonized; @JsonTypeName("customServiceGroup") @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) -public class CustomServiceGroupStore extends AbstractServiceGroupStore { +public class CustomServiceGroupStore extends AbstractServiceGroupStore { @Override public void checkComplete() throws Throwable { super.checkComplete(); - Validators.isType(getParent(), NetworkTunnelStore.class); + Validators.isType(getParent(), DataStore.class); } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceControlSession.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceControlSession.java new file mode 100644 index 000000000..f98d61101 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceControlSession.java @@ -0,0 +1,53 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.core.process.CommandBuilder; +import io.xpipe.core.process.ElevationFunction; +import io.xpipe.core.process.ShellControl; +import io.xpipe.core.store.Session; +import io.xpipe.core.store.SessionListener; +import io.xpipe.core.util.FailableSupplier; +import lombok.Getter; + +@Getter +public class ServiceControlSession extends Session { + + private final ServiceControlStore store; + + protected ServiceControlSession(SessionListener listener, ServiceControlStore store) { + super(listener); + this.store = store; + } + + private ElevationFunction elevationFunction() { + return store.isElevated() ? ElevationFunction.elevated("service") : ElevationFunction.none(); + } + + public void start() throws Exception { + if (isRunning()) { + listener.onStateChange(true); + return; + } + + var session = store.getHost().getStore().getOrStartSession(); + var builder = session.getShellDialect().launchAsnyc(CommandBuilder.of().add(store.getStartScript().getValue())); + session.command(builder).elevated(elevationFunction()).execute(); + listener.onStateChange(true); + } + + public boolean isRunning() throws Exception { + var session = store.getHost().getStore().getOrStartSession(); + var r = session.command(store.getStatusScript()).elevated(elevationFunction()).executeAndCheck(); + return r; + } + + public void stop() throws Exception { + if (!isRunning()) { + listener.onStateChange(false); + return; + } + + var session = store.getHost().getStore().getOrStartSession(); + session.command(store.getStopScript()).elevated(elevationFunction()).execute(); + listener.onStateChange(false); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceControlStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceControlStore.java new file mode 100644 index 000000000..b3cb062c8 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceControlStore.java @@ -0,0 +1,50 @@ +package io.xpipe.ext.base.service; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.ext.ShellStore; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.HostHelper; +import io.xpipe.app.util.LicenseProvider; +import io.xpipe.core.process.ShellScript; +import io.xpipe.app.util.Validators; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.NetworkTunnelSession; +import io.xpipe.core.store.NetworkTunnelStore; +import io.xpipe.core.store.SingletonSessionStore; +import io.xpipe.ext.base.store.StartableStore; +import io.xpipe.ext.base.store.StoppableStore; +import lombok.Value; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@SuperBuilder +@Value +@JsonTypeName("serviceControl") +@Jacksonized +public class ServiceControlStore implements SingletonSessionStore, DataStore { + + DataStoreEntryRef host; + ShellScript startScript; + ShellScript stopScript; + ShellScript statusScript; + boolean elevated; + + @Override + public void checkComplete() throws Throwable { + Validators.nonNull(getHost()); + Validators.nonNull(getStartScript()); + Validators.nonNull(getStopScript()); + Validators.nonNull(getStatusScript()); + } + + @Override + public ServiceControlSession newSession() throws Exception { + return new ServiceControlSession(running -> {}, this); + } + + @Override + public Class getSessionClass() { + return ServiceControlSession.class; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceControlStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceControlStoreProvider.java new file mode 100644 index 000000000..2be29e473 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceControlStoreProvider.java @@ -0,0 +1,143 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.comp.Comp; +import io.xpipe.app.comp.base.IntegratedTextAreaComp; +import io.xpipe.app.comp.store.*; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.ext.*; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.util.DataStoreFormatter; +import io.xpipe.app.util.OptionsBuilder; +import io.xpipe.app.util.ShellStoreFormat; +import io.xpipe.core.process.ShellDialect; +import io.xpipe.core.store.DataStore; +import io.xpipe.ext.base.script.ScriptStore; +import io.xpipe.ext.base.script.SimpleScriptStore; +import javafx.beans.binding.Bindings; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; + +import java.util.ArrayList; +import java.util.List; + +public class ServiceControlStoreProvider implements SingletonSessionStoreProvider, DataStoreProvider { + + public String displayName(DataStoreEntry entry) { + var s = (ServiceControlStore) entry.getStore(); + String n = entry.getName(); + return n + " (" + DataStorage.get().getStoreEntryDisplayName(s.getHost().get()) + ")"; + } + + @Override + public DataStoreUsageCategory getUsageCategory() { + return DataStoreUsageCategory.TUNNEL; + } + + @Override + public DataStoreEntry getSyntheticParent(DataStoreEntry store) { + ServiceControlStore s = store.getStore().asNeeded(); + return DataStorage.get() + .getOrCreateNewSyntheticEntry( + s.getHost().get(), + "Services", + CustomServiceGroupStore.builder().parent(s.getHost()).build()); + } + + @Override + public String summaryString(StoreEntryWrapper wrapper) { + ServiceControlStore s = wrapper.getEntry().getStore().asNeeded(); + return DataStoreFormatter.toApostropheName(s.getHost().get()) + " service control"; + } + + @Override + public ObservableValue informationString(StoreSection section) { + ServiceControlStore s = section.getWrapper().getEntry().getStore().asNeeded(); + return Bindings.createStringBinding( + () -> { + var state = s.isSessionRunning() + ? AppI18n.get("active") + : s.isSessionEnabled() ? AppI18n.get("starting") : AppI18n.get("inactive"); + return new ShellStoreFormat(null, state).format(); + }, + section.getWrapper().getCache(), + AppPrefs.get().language()); + } + + + @Override + public GuiDialog guiDialog(DataStoreEntry entry, Property store) { + ServiceControlStore st = store.getValue().asNeeded(); + var host = new SimpleObjectProperty<>(st.getHost()); + var start = new SimpleObjectProperty<>(st.getStartScript()); + var stop = new SimpleObjectProperty<>(st.getStopScript()); + var status = new SimpleObjectProperty<>(st.getStatusScript()); + var elevated = new SimpleBooleanProperty(st.isElevated()); + return new OptionsBuilder() + .nameAndDescription("serviceHost") + .addComp( + new StoreChoiceComp<>( + StoreChoiceComp.Mode.OTHER, + entry, + host, + ShellStore.class, + null, + StoreViewState.get().getAllConnectionsCategory()), + host) + .nonNull() + .nameAndDescription("serviceStartScript") + .addComp(IntegratedTextAreaComp.script(host,start), start) + .nonNull() + .nameAndDescription("serviceStopScript") + .addComp(IntegratedTextAreaComp.script(host, stop), stop) + .nonNull() + .nameAndDescription("serviceStatusScript") + .addComp(IntegratedTextAreaComp.script(host, status), status) + .nonNull() + .nameAndDescription("serviceElevated") + .addToggle(elevated) + .bind( + () -> { + return ServiceControlStore.builder() + .host(host.get()) + .startScript(start.get()) + .stopScript(stop.get()) + .statusScript(status.get()) + .elevated(elevated.get()) + .build(); + }, + store) + .buildDialog(); + } + + @Override + public String getDisplayIconFileName(DataStore store) { + return "base:service_icon.svg"; + } + + @Override + public List getPossibleNames() { + return List.of("serviceControl"); + } + + @Override + public List> getStoreClasses() { + return List.of(ServiceControlStore.class); + } + + @Override + public DataStore defaultStore() { + return ServiceControlStore.builder().build(); + } + + @Override + public DataStoreCreationCategory getCreationCategory() { + return DataStoreCreationCategory.SERVICE; + } + +} diff --git a/ext/base/src/main/java/module-info.java b/ext/base/src/main/java/module-info.java index 71da56a68..ba4576cbf 100644 --- a/ext/base/src/main/java/module-info.java +++ b/ext/base/src/main/java/module-info.java @@ -98,6 +98,7 @@ open module io.xpipe.ext.base { CustomServiceStoreProvider, MappedServiceStoreProvider, FixedServiceStoreProvider, + ServiceControlStoreProvider, SimpleScriptStoreProvider, DesktopApplicationStoreProvider, LocalIdentityStoreProvider,