mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-04-23 08:00:56 -04:00
Merge branch 'session-control' into 15-release
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.xpipe.core.process;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
public class ShellScript {
|
||||
|
||||
String value;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -98,6 +98,7 @@ open module io.xpipe.ext.base {
|
||||
CustomServiceStoreProvider,
|
||||
MappedServiceStoreProvider,
|
||||
FixedServiceStoreProvider,
|
||||
ServiceControlStoreProvider,
|
||||
SimpleScriptStoreProvider,
|
||||
DesktopApplicationStoreProvider,
|
||||
LocalIdentityStoreProvider,
|
||||
|
||||
Reference in New Issue
Block a user