Rework validation and state management

This commit is contained in:
crschnick
2024-09-19 13:02:37 +00:00
parent cab55594fb
commit 0ed7b2fcc9
20 changed files with 225 additions and 75 deletions

View File

@@ -21,7 +21,7 @@ public class ConnectionAddExchangeImpl extends ConnectionAddExchange {
try {
DataStorage.get().addStoreEntryInProgress(entry);
if (msg.getValidate()) {
entry.validateOrThrow();
entry.validateOrThrow(true);
}
} catch (Throwable ex) {
if (ex instanceof ValidationException) {

View File

@@ -17,7 +17,7 @@ public class ConnectionRefreshExchangeImpl extends ConnectionRefreshExchange {
if (e.getStore() instanceof FixedHierarchyStore) {
DataStorage.get().refreshChildren(e, true);
} else {
e.validateOrThrow();
e.validateOrThrow(true);
}
return Response.builder().build();
}

View File

@@ -1,5 +1,6 @@
package io.xpipe.app.comp.store;
import atlantafx.base.controls.Spacer;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.DialogComp;
import io.xpipe.app.comp.base.ErrorOverlayComp;
@@ -20,8 +21,8 @@ import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.*;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.ValidationContext;
import io.xpipe.core.util.ValidationException;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
@@ -33,8 +34,6 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import atlantafx.base.controls.Spacer;
import lombok.AccessLevel;
import lombok.experimental.FieldDefaults;
import net.synedra.validatorfx.GraphicDecorationStackPane;
@@ -42,14 +41,13 @@ import net.synedra.validatorfx.GraphicDecorationStackPane;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class StoreCreationComp extends DialogComp {
Stage window;
BiConsumer<DataStoreEntry, Boolean> consumer;
CreationConsumer consumer;
Property<DataStoreProvider> provider;
ObjectProperty<DataStore> store;
Predicate<DataStoreProvider> filter;
@@ -67,7 +65,7 @@ public class StoreCreationComp extends DialogComp {
public StoreCreationComp(
Stage window,
BiConsumer<DataStoreEntry, Boolean> consumer,
CreationConsumer consumer,
Property<DataStoreProvider> provider,
ObjectProperty<DataStore> store,
Predicate<DataStoreProvider> filter,
@@ -165,7 +163,7 @@ public class StoreCreationComp extends DialogComp {
e.getProvider(),
e.getStore(),
v -> true,
(newE, validated) -> {
(newE, context, validated) -> {
ThreadHelper.runAsync(() -> {
if (!DataStorage.get().getStoreEntries().contains(e)) {
DataStorage.get().addStoreEntryIfNotPresent(newE);
@@ -193,7 +191,7 @@ public class StoreCreationComp extends DialogComp {
base != null ? DataStoreProviders.byStore(base) : null,
base,
dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory()),
(e, validated) -> {
(e, context, validated) -> {
try {
DataStorage.get().addStoreEntryIfNotPresent(e);
if (validated
@@ -201,7 +199,7 @@ public class StoreCreationComp extends DialogComp {
&& AppPrefs.get()
.openConnectionSearchWindowOnConnectionCreation()
.get()) {
ScanAlert.showAsync(e);
ScanAlert.showAsync(e, context);
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
@@ -211,12 +209,17 @@ public class StoreCreationComp extends DialogComp {
null);
}
private static interface CreationConsumer {
void consume(DataStoreEntry entry, ValidationContext<?> validationContext, boolean validated);
}
private static void show(
String initialName,
DataStoreProvider provider,
DataStore s,
Predicate<DataStoreProvider> filter,
BiConsumer<DataStoreEntry, Boolean> con,
CreationConsumer con,
boolean staticDisplay,
DataStoreEntry existingEntry) {
var prop = new SimpleObjectProperty<>(provider);
@@ -247,7 +250,7 @@ public class StoreCreationComp extends DialogComp {
return List.of(
new ButtonComp(AppI18n.observable("skip"), null, () -> {
if (showInvalidConfirmAlert()) {
commit(false);
commit(null, false);
} else {
finish();
}
@@ -287,7 +290,7 @@ public class StoreCreationComp extends DialogComp {
// We didn't change anything
if (existingEntry != null && existingEntry.getStore().equals(store.getValue())) {
commit(false);
commit(null, false);
return;
}
@@ -317,8 +320,8 @@ public class StoreCreationComp extends DialogComp {
try (var b = new BooleanScope(busy).start()) {
DataStorage.get().addStoreEntryInProgress(entry.getValue());
entry.getValue().validateOrThrow();
commit(true);
var context = entry.getValue().validateOrThrow(false);
commit(context, true);
} catch (Throwable ex) {
if (ex instanceof ValidationException) {
ErrorEvent.expected(ex);
@@ -403,14 +406,14 @@ public class StoreCreationComp extends DialogComp {
.createRegion();
}
private void commit(boolean validated) {
private void commit(ValidationContext<?> validationContext, boolean validated) {
if (finished.get()) {
return;
}
finished.setValue(true);
if (entry.getValue() != null) {
consumer.accept(entry.getValue(), validated);
consumer.consume(entry.getValue(), validationContext, validated);
}
PlatformThread.runLaterIfNeeded(() -> {

View File

@@ -22,7 +22,7 @@ public class StoreCreationMenu {
automatically.setGraphic(new FontIcon("mdi2e-eye-plus-outline"));
automatically.textProperty().bind(AppI18n.observable("addAutomatically"));
automatically.setOnAction(event -> {
ScanAlert.showAsync(null);
ScanAlert.showAsync(null, null);
event.consume();
});
menu.getItems().add(automatically);

View File

@@ -55,7 +55,7 @@ public class StoreIconChoiceDialogComp extends SimpleComp {
var dialog = new DialogComp() {
@Override
protected void finish() {
entry.setIcon(selected.get() != null ? selected.getValue().getIconName() : null);
entry.setIcon(selected.get() != null ? selected.getValue().getIconName() : null, true);
dialogStage.close();
}

View File

@@ -39,7 +39,7 @@ public class StoreIntroComp extends SimpleComp {
var scanButton = new Button(null, new FontIcon("mdi2m-magnify"));
scanButton.textProperty().bind(AppI18n.observable("detectConnections"));
scanButton.setOnAction(event -> ScanAlert.showAsync(DataStorage.get().local()));
scanButton.setOnAction(event -> ScanAlert.showAsync(DataStorage.get().local(), null));
scanButton.setDefaultButton(true);
var scanPane = new StackPane(scanButton);
scanPane.setAlignment(Pos.CENTER);

View File

@@ -87,7 +87,6 @@ public class BaseMode extends OperationMode {
AppDataLock.unlock();
BlobManager.reset();
FileBridge.reset();
// Shut down server last to keep a non-daemon thread running
AppBeaconServer.reset();
TrackEvent.info("Background mode shutdown finished");
}

View File

@@ -293,17 +293,27 @@ public abstract class OperationMode {
inShutdown = true;
OperationMode.inShutdownHook = inShutdownHook;
try {
if (CURRENT != null) {
CURRENT.finalTeardown();
// Keep a non-daemon thread running
var thread = ThreadHelper.createPlatformThread("shutdown", false, () -> {
try {
if (CURRENT != null) {
CURRENT.finalTeardown();
}
CURRENT = null;
} catch (Throwable t) {
ErrorEvent.fromThrowable(t).term().handle();
OperationMode.halt(1);
}
CURRENT = null;
} catch (Throwable t) {
ErrorEvent.fromThrowable(t).term().handle();
OperationMode.halt(hasError ? 1 : 0);
});
thread.start();
try {
thread.join();
} catch (InterruptedException ignored) {
OperationMode.halt(1);
}
OperationMode.halt(hasError ? 1 : 0);
}
// public static synchronized void reload() {

View File

@@ -6,6 +6,7 @@ import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.resources.SystemIcons;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
@@ -200,18 +201,23 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
button.apply(struc -> {
struc.get().setMaxWidth(2000);
struc.get().setAlignment(Pos.CENTER_LEFT);
Comp<?> graphic = new PrettySvgComp(
Comp<?> graphic = PrettyImageHelper.ofFixedSize(
Bindings.createStringBinding(
() -> {
if (selected.getValue() == null) {
return null;
}
return selected.getValue()
.get()
.getProvider()
.getDisplayIconFileName(
selected.getValue().getStore());
if (selected.getValue().get().getIcon() == null) {
return selected.getValue()
.get()
.getProvider()
.getDisplayIconFileName(
selected.getValue().getStore());
}
SystemIcons.load();
return "app:system/" + selected.getValue().get().getIcon() + ".svg";
},
selected),
16,

View File

@@ -0,0 +1,35 @@
package io.xpipe.app.resources;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl;
import lombok.EqualsAndHashCode;
import lombok.Value;
@Value
@EqualsAndHashCode(callSuper=true)
public class FileAutoSystemIcon extends SystemIcon {
OsType.Any osType;
String file;
public FileAutoSystemIcon(String iconName, String displayName, OsType.Any osType, String file) {
super(iconName, displayName);
this.osType = osType;
this.file = file;
}
@Override
public boolean isApplicable(ShellControl sc) throws Exception {
if (sc.getOsType() != osType) {
return false;
}
var abs = sc.getShellDialect().evaluateExpression(sc, file).readStdoutIfPossible();
if (abs.isEmpty()) {
return false;
}
return sc.getShellDialect().createFileExistsCommand(sc, abs.get()).executeAndCheck() ||
sc.getShellDialect().directoryExists(sc, abs.get()).executeAndCheck();
}
}

View File

@@ -1,5 +1,6 @@
package io.xpipe.app.resources;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.process.ShellStoreState;
@@ -33,7 +34,8 @@ public class SystemIcons {
shellStoreState.getShellDialect() == ShellDialects.PFSENSE;
}
},
new ContainerAutoSystemIcon("file-browser", "File Browser", name -> name.contains("filebrowser"))
new ContainerAutoSystemIcon("file-browser", "File Browser", name -> name.contains("filebrowser")),
new FileAutoSystemIcon("syncthing", "Syncthing", OsType.LINUX, "~/.local/state/syncthing")
);
private static final List<SystemIcon> SYSTEM_ICONS = new ArrayList<>();

View File

@@ -188,7 +188,7 @@ public class DataStoreEntry extends StorageElement {
var icon = SystemIcons.detectForStore(store);
if (icon.isPresent()) {
setIcon(icon.get().getIconName());
setIcon(icon.get().getIconName(), true);
}
}
@@ -370,7 +370,11 @@ public class DataStoreEntry extends StorageElement {
return (T) storePersistentState;
}
public void setIcon(String icon) {
public void setIcon(String icon, boolean force) {
if (this.icon != null && !force) {
return;
}
var changed = !Objects.equals(this.icon, icon);
this.icon = icon;
if (changed) {
@@ -493,15 +497,24 @@ public class DataStoreEntry extends StorageElement {
dirty = true;
}
public void validate() {
public <T extends ValidationContext<?>> void validate() {
try {
validateOrThrow();
validateOrThrow(true);
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).handle();
}
}
public void validateOrThrow() throws Throwable {
public <T extends ValidationContext<?>> void validate(T context) {
try {
validateOrThrow(context);
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).handle();
}
}
@SuppressWarnings("unchecked")
public <T extends ValidationContext<?>> void validateOrThrow(T context) throws Throwable {
if (store == null) {
return;
}
@@ -509,8 +522,8 @@ public class DataStoreEntry extends StorageElement {
try {
store.checkComplete();
incrementBusyCounter();
if (store instanceof ValidatableStore l) {
l.validate();
if (store instanceof ValidatableStore<?> l) {
((ValidatableStore<T>) l).validate(context);
} else if (store instanceof FixedHierarchyStore h) {
childrenCache = h.listChildren(this).stream()
.map(DataStoreEntryRef::get)
@@ -521,6 +534,40 @@ public class DataStoreEntry extends StorageElement {
}
}
@SuppressWarnings("unchecked")
public <T> ValidationContext<?> validateOrThrow(boolean close) throws Throwable {
if (store == null) {
return null;
}
try {
store.checkComplete();
incrementBusyCounter();
if (store instanceof ValidatableStore<?> l) {
ValidationContext<T> context = (ValidationContext<T>) l.createContext();
try {
((ValidatableStore<ValidationContext<T>>) l).validate(context);
} catch (Throwable t) {
context.close();
throw t;
}
if (close) {
context.close();
}
return context;
} else if (store instanceof FixedHierarchyStore h) {
childrenCache = h.listChildren(this).stream()
.map(DataStoreEntryRef::get)
.collect(Collectors.toSet());
return null;
} else {
return null;
}
} finally {
decrementBusyCounter();
}
}
public void refreshStore() {
if (validity == Validity.LOAD_FAILED) {
return;

View File

@@ -16,6 +16,8 @@ import io.xpipe.core.process.ShellStoreState;
import io.xpipe.core.process.ShellTtyState;
import io.xpipe.core.store.ShellStore;
import io.xpipe.core.store.ShellValidationContext;
import io.xpipe.core.store.ValidationContext;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
@@ -34,7 +36,7 @@ import static javafx.scene.layout.Priority.ALWAYS;
public class ScanAlert {
public static void showAsync(DataStoreEntry entry) {
public static void showAsync(DataStoreEntry entry, ValidationContext<?> context) {
ThreadHelper.runAsync(() -> {
var showForCon = entry == null
|| (entry.getStore() instanceof ShellStore
@@ -42,12 +44,12 @@ public class ScanAlert {
|| shellStoreState.getTtyState() == null
|| shellStoreState.getTtyState() == ShellTtyState.NONE));
if (showForCon) {
showForShellStore(entry);
showForShellStore(entry, (ShellValidationContext) context);
}
});
}
public static void showForShellStore(DataStoreEntry initial) {
public static void showForShellStore(DataStoreEntry initial, ShellValidationContext context) {
show(initial, (DataStoreEntry entry, ShellControl sc) -> {
if (!sc.canHaveSubshells()) {
return null;
@@ -76,15 +78,16 @@ public class ScanAlert {
}
}
return applicable;
});
}, context);
}
private static void show(
DataStoreEntry initialStore,
BiFunction<DataStoreEntry, ShellControl, List<ScanProvider.ScanOperation>> applicable) {
BiFunction<DataStoreEntry, ShellControl, List<ScanProvider.ScanOperation>> applicable,
ShellValidationContext shellValidationContext) {
DialogComp.showWindow(
"scanAlertTitle",
stage -> new Dialog(stage, initialStore != null ? initialStore.ref() : null, applicable));
stage -> new Dialog(stage, initialStore != null ? initialStore.ref() : null, applicable, shellValidationContext));
}
private static class Dialog extends DialogComp {
@@ -96,16 +99,18 @@ public class ScanAlert {
private final ListProperty<ScanProvider.ScanOperation> selected =
new SimpleListProperty<>(FXCollections.observableArrayList());
private final BooleanProperty busy = new SimpleBooleanProperty();
private ShellControl shellControl;
private ShellValidationContext shellValidationContext;
private Dialog(
Stage window,
DataStoreEntryRef<ShellStore> entry,
BiFunction<DataStoreEntry, ShellControl, List<ScanProvider.ScanOperation>> applicable) {
BiFunction<DataStoreEntry, ShellControl, List<ScanProvider.ScanOperation>> applicable, ShellValidationContext shellValidationContext
) {
this.window = window;
this.initialStore = entry;
this.entry = new SimpleObjectProperty<>(entry);
this.applicable = applicable;
this.shellValidationContext = shellValidationContext;
}
@Override
@@ -138,7 +143,7 @@ public class ScanAlert {
}
// Previous scan operation could have exited the shell
shellControl.start();
shellValidationContext.get().start();
try {
a.getScanner().run();
@@ -148,10 +153,8 @@ public class ScanAlert {
}
});
} finally {
if (shellControl != null) {
shellControl.close();
}
shellControl = null;
shellValidationContext.close();
shellValidationContext = null;
}
});
}
@@ -198,15 +201,13 @@ public class ScanAlert {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
if (shellControl != null) {
shellControl.close();
shellControl = null;
if (shellValidationContext != null) {
shellValidationContext.close();
shellValidationContext = null;
}
shellControl = newValue.getStore().control();
shellControl.withoutLicenseCheck();
shellControl.start();
var a = applicable.apply(entry.get().get(), shellControl);
shellValidationContext = new ShellValidationContext(newValue.getStore().control().withoutLicenseCheck().start());
var a = applicable.apply(entry.get().get(), shellValidationContext.get());
Platform.runLater(() -> {
if (a == null) {

View File

@@ -9,7 +9,7 @@ See below on how to do this.
By default, no categories are set to shared so that you have explicit control on what connections to commit.
To have your connections of a category put inside your git repository,
you need to click on the `⚙️` icon (when hovering over the category)
you either need to right-click the category or click on the `⚙️` icon when hovering over the category
in your `Connections` tab under the category overview on the left side.
Then click on `Add to git repository` to sync the category and connections to your git repository.
This will add all shareable connections to the git repository.

View File

@@ -3,7 +3,7 @@ package io.xpipe.core.store;
import io.xpipe.core.process.ProcessControl;
import io.xpipe.core.process.ShellControl;
public interface ShellStore extends DataStore, FileSystemStore, ValidatableStore {
public interface ShellStore extends DataStore, FileSystemStore, ValidatableStore<ShellValidationContext> {
@Override
default FileSystem createFileSystem() {
@@ -17,12 +17,16 @@ public interface ShellStore extends DataStore, FileSystemStore, ValidatableStore
ShellControl control();
@Override
default void validate() throws Exception {
var c = control();
default void validate(ShellValidationContext context) throws Exception {
var c = context.get();
if (!isInStorage()) {
c.withoutLicenseCheck();
}
try (ShellControl pc = c.start()) {}
}
@Override
default ShellValidationContext createContext() throws Exception {
return new ShellValidationContext(control().start());
}
}

View File

@@ -0,0 +1,22 @@
package io.xpipe.core.store;
import io.xpipe.core.process.ShellControl;
import lombok.Value;
@Value
public class ShellValidationContext implements ValidationContext<ShellControl> {
ShellControl shellControl;
@Override
public ShellControl get() {
return shellControl;
}
@Override
public void close() {
try {
shellControl.close();
} catch (Exception ignored) {}
}
}

View File

@@ -1,6 +1,6 @@
package io.xpipe.core.store;
public interface ValidatableStore extends DataStore {
public interface ValidatableStore<T extends ValidationContext<?>> extends DataStore {
/**
* Performs a validation of this data store.
@@ -18,5 +18,9 @@ public interface ValidatableStore extends DataStore {
*
* @throws Exception if any part of the validation went wrong
*/
default void validate() throws Exception {}
default void validate(T context) throws Exception {}
default T createContext() throws Exception {
return null;
}
}

View File

@@ -0,0 +1,8 @@
package io.xpipe.core.store;
public interface ValidationContext<T> {
T get();
void close();
}

View File

@@ -70,7 +70,7 @@ public class ScanStoreAction implements ActionProvider {
@Override
public void execute() {
if (entry == null || entry.getStore() instanceof ShellStore) {
ScanAlert.showForShellStore(entry);
ScanAlert.showForShellStore(entry, null);
}
}
}

View File

@@ -10,13 +10,14 @@ import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreUsageCategory;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.resources.SystemIcons;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.DataStoreFormatter;
import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.core.process.ShellStoreState;
import io.xpipe.core.store.ShellStore;
import io.xpipe.ext.base.script.ScriptStore;
import javafx.beans.property.BooleanProperty;
import javafx.beans.value.ObservableValue;
@@ -29,11 +30,19 @@ public interface ShellStoreProvider extends DataStoreProvider {
public void execute() throws Exception {
var replacement = ProcessControlProvider.get().replace(entry.ref());
ShellStore store = replacement.getStore().asNeeded();
var control = ScriptStore.controlWithDefaultScripts(store.control());
control.onInit(sc -> {
if (entry.getStorePersistentState() instanceof ShellStoreState shellStoreState && shellStoreState.getShellDialect() == null) {
var found = SystemIcons.detectForSystem(sc);
if (found.isPresent()) {
entry.setIcon(found.get().getIconName(), false);
}
}
});
TerminalLauncher.open(
replacement.get(),
DataStorage.get().getStoreEntryDisplayName(replacement.get()),
null,
ScriptStore.controlWithDefaultScripts(store.control()));
null, control);
}
};
}