Merge branch 'session-control' into 15-release

This commit is contained in:
crschnick
2025-01-25 22:19:55 +00:00
parent 56a03c4558
commit 4a4a2fea7e
14 changed files with 326 additions and 8 deletions

View File

@@ -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<IntegratedTextAreaComp.Structure> {
public static IntegratedTextAreaComp script(ObservableValue<DataStoreEntryRef<ShellStore>> host, Property<ShellScript> 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<String> value;
private final boolean lazy;
private final String identifier;

View File

@@ -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;
}

View File

@@ -9,7 +9,7 @@ import java.util.List;
public class Validators {
public static <T extends DataStore> void isType(DataStoreEntryRef<T> ref, Class<T> c) throws ValidationException {
public static <T extends DataStore> void isType(DataStoreEntryRef<? extends T> ref, Class<T> c) throws ValidationException {
if (ref != null && !c.isAssignableFrom(ref.getStore().getClass())) {
throw new ValidationException("Value must be an instance of " + c.getSimpleName());
}

View File

@@ -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<CommandBuilder> builder) {
var b = CommandBuilder.of();
builder.accept(b);

View File

@@ -0,0 +1,9 @@
package io.xpipe.core.process;
import lombok.Value;
@Value
public class ShellScript {
String value;
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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<ShellScript> {
@Override
public void serialize(ShellScript value, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
jgen.writeString(value.getValue());
}
}
public static class ShellScriptDeserializer extends JsonDeserializer<ShellScript> {
@Override
public ShellScript deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return new ShellScript(p.getValueAsString());
}
}
public static class StorePathSerializer extends JsonSerializer<StorePath> {
@Override

View File

@@ -19,7 +19,7 @@ import lombok.experimental.SuperBuilder;
@ToString
public abstract class AbstractServiceGroupStore<T extends DataStore> implements DataStore, GroupStore<T> {
DataStoreEntryRef<T> parent;
DataStoreEntryRef<? extends T> parent;
@Override
public void checkComplete() throws Throwable {

View File

@@ -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<NetworkTunnelStore> {
public class CustomServiceGroupStore extends AbstractServiceGroupStore<DataStore> {
@Override
public void checkComplete() throws Throwable {
super.checkComplete();
Validators.isType(getParent(), NetworkTunnelStore.class);
Validators.isType(getParent(), DataStore.class);
}
}

View File

@@ -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);
}
}

View File

@@ -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<ServiceControlSession>, DataStore {
DataStoreEntryRef<ShellStore> 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;
}
}

View File

@@ -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<String> 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<DataStore> 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<String> getPossibleNames() {
return List.of("serviceControl");
}
@Override
public List<Class<?>> getStoreClasses() {
return List.of(ServiceControlStore.class);
}
@Override
public DataStore defaultStore() {
return ServiceControlStore.builder().build();
}
@Override
public DataStoreCreationCategory getCreationCategory() {
return DataStoreCreationCategory.SERVICE;
}
}

View File

@@ -98,6 +98,7 @@ open module io.xpipe.ext.base {
CustomServiceStoreProvider,
MappedServiceStoreProvider,
FixedServiceStoreProvider,
ServiceControlStoreProvider,
SimpleScriptStoreProvider,
DesktopApplicationStoreProvider,
LocalIdentityStoreProvider,